gitlab.com/SkynetLabs/skyd@v1.6.9/cmd/skyc/rentercmd_helpers.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math/big"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"text/tabwriter"
    14  	"time"
    15  
    16  	"gitlab.com/NebulousLabs/errors"
    17  	"gitlab.com/SkynetLabs/skyd/build"
    18  	"gitlab.com/SkynetLabs/skyd/node/api"
    19  	"gitlab.com/SkynetLabs/skyd/skymodules"
    20  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem"
    21  	"go.sia.tech/siad/modules"
    22  	"go.sia.tech/siad/types"
    23  )
    24  
    25  var (
    26  	// errIncorrectNumArgs is the error returned if there is an incorrect number
    27  	// of arguments
    28  	errIncorrectNumArgs = errors.New("incorrect number of arguments")
    29  )
    30  
    31  // byDirectoryInfo implements sort.Interface for []directoryInfo based on the
    32  // SiaPath field.
    33  type byDirectoryInfo []directoryInfo
    34  
    35  func (s byDirectoryInfo) Len() int      { return len(s) }
    36  func (s byDirectoryInfo) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    37  func (s byDirectoryInfo) Less(i, j int) bool {
    38  	return s[i].dir.SiaPath.String() < s[j].dir.SiaPath.String()
    39  }
    40  
    41  // bySiaPathFile implements sort.Interface for [] skymodules.FileInfo based on the
    42  // SiaPath field.
    43  type bySiaPathFile []skymodules.FileInfo
    44  
    45  func (s bySiaPathFile) Len() int           { return len(s) }
    46  func (s bySiaPathFile) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
    47  func (s bySiaPathFile) Less(i, j int) bool { return s[i].SiaPath.String() < s[j].SiaPath.String() }
    48  
    49  // bySiaPathDir implements sort.Interface for [] skymodules.DirectoryInfo based on the
    50  // SiaPath field.
    51  type bySiaPathDir []skymodules.DirectoryInfo
    52  
    53  func (s bySiaPathDir) Len() int           { return len(s) }
    54  func (s bySiaPathDir) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
    55  func (s bySiaPathDir) Less(i, j int) bool { return s[i].SiaPath.String() < s[j].SiaPath.String() }
    56  
    57  // byValue sorts contracts by their value in siacoins, high to low. If two
    58  // contracts have the same value, they are sorted by their host's address.
    59  type byValue []api.RenterContract
    60  
    61  func (s byValue) Len() int      { return len(s) }
    62  func (s byValue) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    63  func (s byValue) Less(i, j int) bool {
    64  	cmp := s[i].RenterFunds.Cmp(s[j].RenterFunds)
    65  	if cmp == 0 {
    66  		return s[i].NetAddress < s[j].NetAddress
    67  	}
    68  	return cmp > 0
    69  }
    70  
    71  // directoryInfo is a helper struct that contains the skymodules.DirectoryInfo for
    72  // a directory, the skymodules.FileInfo for all the directory's files, and the
    73  // skymodules.DirectoryInfo for all the subdirs.
    74  type directoryInfo struct {
    75  	dir     skymodules.DirectoryInfo
    76  	files   []skymodules.FileInfo
    77  	subDirs []skymodules.DirectoryInfo
    78  }
    79  
    80  // progressMeasurement is a helper type used for measuring the progress of
    81  // a download.
    82  type progressMeasurement struct {
    83  	progress uint64
    84  	time     time.Time
    85  }
    86  
    87  // trackedFile is a helper struct for tracking files related to downloads
    88  type trackedFile struct {
    89  	siaPath skymodules.SiaPath
    90  	dst     string
    91  }
    92  
    93  // contractStats is a helper function to pull information out of the renter
    94  // contracts to be displayed
    95  func contractStats(contracts []api.RenterContract) (size uint64, spent, remaining, fees types.Currency) {
    96  	for _, c := range contracts {
    97  		size += c.Size
    98  		remaining = remaining.Add(c.RenterFunds)
    99  		fees = fees.Add(c.Fees)
   100  		// Negative Currency Check
   101  		var contractTotalSpent types.Currency
   102  		if c.TotalCost.Cmp(c.RenterFunds.Add(c.Fees)) < 0 {
   103  			contractTotalSpent = c.RenterFunds.Add(c.Fees)
   104  		} else {
   105  			contractTotalSpent = c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees)
   106  		}
   107  		spent = spent.Add(contractTotalSpent)
   108  	}
   109  	return
   110  }
   111  
   112  // downloadDir downloads the dir at the specified siaPath to the specified
   113  // location. It returns all the files for which a download was initialized as
   114  // tracked files and the ones which were ignored as skipped. Errors are composed
   115  // into a single error.
   116  func downloadDir(siaPath skymodules.SiaPath, destination string) (tfs []trackedFile, skipped []string, totalSize uint64, err error) {
   117  	// Get dir info.
   118  	rd, err := httpClient.RenterDirRootGet(siaPath)
   119  	if err != nil {
   120  		err = errors.AddContext(err, "failed to get dir info")
   121  		return
   122  	}
   123  	// Create destination on disk.
   124  	if err = os.MkdirAll(destination, 0750); err != nil {
   125  		err = errors.AddContext(err, "failed to create destination dir")
   126  		return
   127  	}
   128  	// Download files.
   129  	for _, file := range rd.Files {
   130  		// Skip files that already exist.
   131  		dst := filepath.Join(destination, file.SiaPath.Name())
   132  		if _, err = os.Stat(dst); err == nil {
   133  			skipped = append(skipped, dst)
   134  			continue
   135  		} else if !os.IsNotExist(err) {
   136  			err = errors.AddContext(err, "failed to get file stats")
   137  			return
   138  		}
   139  		// Download file.
   140  		totalSize += file.Filesize
   141  		_, err = httpClient.RenterDownloadFullGet(file.SiaPath, dst, true, true)
   142  		if err != nil {
   143  			err = errors.AddContext(err, "Failed to start download")
   144  			return
   145  		}
   146  		// Append file to tracked files.
   147  		tfs = append(tfs, trackedFile{
   148  			siaPath: file.SiaPath,
   149  			dst:     dst,
   150  		})
   151  	}
   152  	// If the download isn't recursive we are done.
   153  	if !renterDownloadRecursive {
   154  		return
   155  	}
   156  	// Call downloadDir on all subdirs.
   157  	for i := 1; i < len(rd.Directories); i++ {
   158  		subDir := rd.Directories[i]
   159  		rtfs, rskipped, totalSubSize, rerr := downloadDir(subDir.SiaPath, filepath.Join(destination, subDir.SiaPath.Name()))
   160  		tfs = append(tfs, rtfs...)
   161  		skipped = append(skipped, rskipped...)
   162  		totalSize += totalSubSize
   163  		err = errors.Compose(err, rerr)
   164  	}
   165  	return
   166  }
   167  
   168  // downloadProgress will display the progress of the provided files and return a
   169  // slice of DownloadInfos for failed downloads.
   170  func downloadProgress(tfs []trackedFile) []api.DownloadInfo {
   171  	// Nothing to do if no files are tracked.
   172  	if len(tfs) == 0 {
   173  		return nil
   174  	}
   175  	start := time.Now()
   176  
   177  	// Create a map of all tracked files for faster lookups and also a measurement
   178  	// map which is initialized with 0 progress for all tracked files.
   179  	tfsMap := make(map[skymodules.SiaPath]trackedFile)
   180  	measurements := make(map[skymodules.SiaPath][]progressMeasurement)
   181  	for _, tf := range tfs {
   182  		tfsMap[tf.siaPath] = tf
   183  		measurements[tf.siaPath] = []progressMeasurement{{
   184  			progress: 0,
   185  			time:     time.Now(),
   186  		}}
   187  	}
   188  	// Periodically print measurements until download is done.
   189  	completed := make(map[string]struct{})
   190  	errMap := make(map[string]api.DownloadInfo)
   191  	failedDownloads := func() (fd []api.DownloadInfo) {
   192  		for _, di := range errMap {
   193  			fd = append(fd, di)
   194  		}
   195  		return
   196  	}
   197  	for range time.Tick(OutputRefreshRate) {
   198  		// Get the list of downloads.
   199  		rdg, err := httpClient.RenterDownloadsRootGet()
   200  		if err != nil {
   201  			continue // benign
   202  		}
   203  		// Create a map of downloads for faster lookups. To get unique keys we use
   204  		// siaPath + destination as the key.
   205  		queue := make(map[string]api.DownloadInfo)
   206  		for _, d := range rdg.Downloads {
   207  			key := d.SiaPath.String() + d.Destination
   208  			if _, exists := queue[key]; !exists {
   209  				queue[key] = d
   210  			}
   211  		}
   212  		// Clear terminal.
   213  		clearStr := fmt.Sprint("\033[H\033[2J")
   214  		// Take new measurements for each tracked file.
   215  		progressStr := clearStr
   216  		for tfIdx, tf := range tfs {
   217  			// Search for the download in the list of downloads.
   218  			mapKey := tf.siaPath.String() + tf.dst
   219  			d, found := queue[mapKey]
   220  			m, exists := measurements[tf.siaPath]
   221  			if !exists {
   222  				die("Measurement missing for tracked file. This should never happen.")
   223  			}
   224  			// If the download has not appeared in the queue yet, either continue or
   225  			// give up.
   226  			if !found {
   227  				if time.Since(start) > RenterDownloadTimeout {
   228  					die("Unable to find download in queue. This should never happen.")
   229  				}
   230  				continue
   231  			}
   232  			// Check whether the file has completed or otherwise errored out.
   233  			if d.Error != "" {
   234  				errMap[mapKey] = d
   235  			}
   236  			if d.Completed {
   237  				completed[mapKey] = struct{}{}
   238  				// Check if all downloads are done.
   239  				if len(completed) == len(tfs) {
   240  					return failedDownloads()
   241  				}
   242  				continue
   243  			}
   244  			// Add the current progress to the measurements.
   245  			m = append(m, progressMeasurement{
   246  				progress: d.Received,
   247  				time:     time.Now(),
   248  			})
   249  			// Shrink the measurements to only contain measurements from within the
   250  			// SpeedEstimationWindow.
   251  			for len(m) > 2 && m[len(m)-1].time.Sub(m[0].time) > SpeedEstimationWindow {
   252  				m = m[1:]
   253  			}
   254  			// Update measurements in the map.
   255  			measurements[tf.siaPath] = m
   256  			// Compute the progress and timespan between the first and last
   257  			// measurement to get the speed.
   258  			received := float64(m[len(m)-1].progress - m[0].progress)
   259  			timespan := m[len(m)-1].time.Sub(m[0].time)
   260  			speed := bandwidthUnit(uint64((received * 8) / timespan.Seconds()))
   261  
   262  			// Compuate the percentage of completion and time elapsed since the
   263  			// start of the download.
   264  			pct := 100 * float64(d.Received) / float64(d.Filesize)
   265  			elapsed := time.Since(d.StartTime)
   266  			elapsed -= elapsed % time.Second // round to nearest second
   267  
   268  			progressLine := fmt.Sprintf("Downloading %v... %5.1f%% of %v, %v elapsed, %s    ", tf.siaPath.String(), pct, modules.FilesizeUnits(d.Filesize), elapsed, speed)
   269  			if tfIdx < len(tfs)-1 {
   270  				progressStr += fmt.Sprintln(progressLine)
   271  			} else {
   272  				progressStr += fmt.Sprint(progressLine)
   273  			}
   274  		}
   275  		fmt.Print(progressStr)
   276  		progressStr = clearStr
   277  	}
   278  	// This code is unreachable, but the compiler requires this to be here.
   279  	return nil
   280  }
   281  
   282  // fileHealthBreakdown returns a percentage breakdown of the renter's files'
   283  // healths and the number of stuck files
   284  func fileHealthBreakdown(dirs []directoryInfo, printLostFiles bool) ([]float64, int, error) {
   285  	// Check for nil input
   286  	if len(dirs) == 0 {
   287  		return nil, 0, errors.New("No Directories Found")
   288  	}
   289  
   290  	// Note: we are manually counting the number of files here since the
   291  	// aggregate fields in the directory could be incorrect due to delays in the
   292  	// health loop. This is OK since we have to iterate over all the files
   293  	// anyways.
   294  	var total, fullHealth, greater75, greater50, greater25, greater0, lost float64
   295  	var numStuck int
   296  	for _, dir := range dirs {
   297  		for _, file := range dir.files {
   298  			total++
   299  			if file.Stuck {
   300  				numStuck++
   301  			}
   302  			switch {
   303  			case file.MaxHealthPercent == 100:
   304  				fullHealth++
   305  			case file.MaxHealthPercent > 75:
   306  				greater75++
   307  			case file.MaxHealthPercent > 50:
   308  				greater50++
   309  			case file.MaxHealthPercent > 25:
   310  				greater25++
   311  			case file.MaxHealthPercent > 0 || file.OnDisk:
   312  				greater0++
   313  			case file.Lost:
   314  				lost++
   315  				if printLostFiles {
   316  					fmt.Println(file.SiaPath)
   317  				}
   318  			case !file.Finished:
   319  				// Nothing to report for unfinished files.
   320  			default:
   321  				return nil, 0, fmt.Errorf("unexpected file condition; Health %v, OnDisk %v, Lost %v, Finished %v", file.MaxHealthPercent, file.OnDisk, file.Lost, file.Finished)
   322  			}
   323  		}
   324  	}
   325  
   326  	// Print out total lost files
   327  	if printLostFiles {
   328  		fmt.Println()
   329  		fmt.Println(lost, "lost files found.")
   330  	}
   331  
   332  	// Check for no files uploaded
   333  	if total == 0 {
   334  		return nil, 0, errors.New("No Files Uploaded")
   335  	}
   336  
   337  	fullHealth = 100 * fullHealth / total
   338  	greater75 = 100 * greater75 / total
   339  	greater50 = 100 * greater50 / total
   340  	greater25 = 100 * greater25 / total
   341  	greater0 = 100 * greater0 / total
   342  	lost = 100 * lost / total
   343  
   344  	return []float64{fullHealth, greater75, greater50, greater25, greater0, lost}, numStuck, nil
   345  }
   346  
   347  // atomicTotalGetDirs is a helper for printing out the status of the getDir
   348  // function call.
   349  var atomicTotalGetDirs uint64
   350  
   351  // getDir returns the directory info for the directory at siaPath and its
   352  // subdirs, querying the root directory.
   353  func getDir(siaPath skymodules.SiaPath, root, recursive, verbose bool) (dirs []directoryInfo) {
   354  	// Query the directory
   355  	var rd api.RenterDirectory
   356  	var err error
   357  	if root {
   358  		rd, err = httpClient.RenterDirRootGet(siaPath)
   359  	} else {
   360  		rd, err = httpClient.RenterDirGet(siaPath)
   361  	}
   362  	if err != nil {
   363  		die("failed to get dir info:", err)
   364  	}
   365  
   366  	// Defer print status update
   367  	if verbose {
   368  		defer func() {
   369  			fmt.Printf("\r%v directories queried", atomic.AddUint64(&atomicTotalGetDirs, 1))
   370  		}()
   371  	}
   372  
   373  	// Split the directory and sub directory information
   374  	dir := rd.Directories[0]
   375  	subDirs := rd.Directories[1:]
   376  
   377  	// Append directory to dirs.
   378  	dirs = append(dirs, directoryInfo{
   379  		dir:     dir,
   380  		files:   rd.Files,
   381  		subDirs: subDirs,
   382  	})
   383  
   384  	// If -R isn't set, or there are no subDirs we are done.
   385  	if !recursive || len(subDirs) == 0 {
   386  		return
   387  	}
   388  
   389  	// Define number of workers for this call to use.
   390  	//
   391  	// NOTE: This is a recursive call so all subsequent calls will also have this
   392  	// many workers. While go routines themselves are cheap, we want to limit the
   393  	// chance of a panic due to too many open files.
   394  	//
   395  	// There will be numGetDirWorkers^maxDirectoryDepth go routines launched.
   396  	numGetDirWorkers := 5
   397  	var dirsMu sync.Mutex
   398  
   399  	// Create a siapath chan
   400  	siaPathChan := make(chan skymodules.SiaPath, numGetDirWorkers)
   401  
   402  	// Define getDirWorker function
   403  	getDirWorkerFunc := func(root, recursive bool) {
   404  		for siaPath := range siaPathChan {
   405  			subdirs := getDir(siaPath, root, recursive, verbose)
   406  			dirsMu.Lock()
   407  			dirs = append(dirs, subdirs...)
   408  			dirsMu.Unlock()
   409  		}
   410  	}
   411  
   412  	// Launch workers.
   413  	var wg sync.WaitGroup
   414  	for i := 0; i < numGetDirWorkers; i++ {
   415  		wg.Add(1)
   416  		go func(root, recursive bool) {
   417  			getDirWorkerFunc(root, recursive)
   418  			wg.Done()
   419  		}(root, recursive)
   420  	}
   421  
   422  	// Call getDir on subdirs.
   423  	for _, subDir := range subDirs {
   424  		siaPathChan <- subDir.SiaPath
   425  	}
   426  
   427  	close(siaPathChan)
   428  	wg.Wait()
   429  	return
   430  }
   431  
   432  // getDirSorted calls getDir and then sorts the response by siapath
   433  func getDirSorted(siaPath skymodules.SiaPath, root, recursive, verbose bool) []directoryInfo {
   434  	// Get Dirs
   435  	dirs := getDir(siaPath, root, recursive, verbose)
   436  
   437  	// Sort the directories and the files.
   438  	sort.Sort(byDirectoryInfo(dirs))
   439  	for i := 0; i < len(dirs); i++ {
   440  		sort.Sort(bySiaPathDir(dirs[i].subDirs))
   441  		sort.Sort(bySiaPathFile(dirs[i].files))
   442  	}
   443  	return dirs
   444  }
   445  
   446  // parseLSArgs is a helper that parses the arguments for renter ls and skynet ls
   447  // and returns the siapath.
   448  func parseLSArgs(args []string) (skymodules.SiaPath, error) {
   449  	var path string
   450  	switch len(args) {
   451  	case 0:
   452  		path = "."
   453  	case 1:
   454  		path = args[0]
   455  	default:
   456  		return skymodules.SiaPath{}, errIncorrectNumArgs
   457  	}
   458  	// Parse the input siapath.
   459  	var sp skymodules.SiaPath
   460  	var err error
   461  	if path == "." || path == "" || path == "/" {
   462  		sp = skymodules.RootSiaPath()
   463  	} else {
   464  		sp, err = skymodules.NewSiaPath(path)
   465  		if err != nil {
   466  			return skymodules.SiaPath{}, errors.AddContext(err, "could not parse siaPath")
   467  		}
   468  	}
   469  	return sp, nil
   470  }
   471  
   472  // printContractInfo is a helper function for printing the information about a
   473  // specific contract
   474  func printContractInfo(cid string, contracts []api.RenterContract) error {
   475  	for _, rc := range contracts {
   476  		if rc.ID.String() == cid {
   477  			var fundsAllocated types.Currency
   478  			if rc.TotalCost.Cmp(rc.Fees) > 0 {
   479  				fundsAllocated = rc.TotalCost.Sub(rc.Fees)
   480  			}
   481  			hostInfo, err := httpClient.HostDbHostsGet(rc.HostPublicKey)
   482  			if err != nil {
   483  				return fmt.Errorf("Could not fetch details of host: %v", err)
   484  			}
   485  			fmt.Printf(`
   486  Contract %v
   487  	Host: %v (Public Key: %v)
   488  	Host Version: %v
   489  
   490    Start Height: %v
   491    End Height:   %v
   492  
   493    Total cost:           %v (Fees: %v)
   494    Funds Allocated:      %v
   495    Upload Spending:      %v
   496    Storage Spending:     %v
   497    Download Spending:    %v
   498    FundAccount Spending: %v
   499    Maintenance Spending: %v
   500    Remaining Funds:      %v
   501  
   502    File Size: %v
   503  `, rc.ID, rc.NetAddress, rc.HostPublicKey.String(), rc.HostVersion, rc.StartHeight, rc.EndHeight,
   504  				currencyUnits(rc.TotalCost), currencyUnits(rc.Fees),
   505  				currencyUnits(fundsAllocated),
   506  				currencyUnits(rc.UploadSpending),
   507  				currencyUnits(rc.StorageSpending),
   508  				currencyUnits(rc.DownloadSpending),
   509  				currencyUnits(rc.FundAccountSpending),
   510  				currencyUnits(rc.MaintenanceSpending.Sum()),
   511  				currencyUnits(rc.RenterFunds),
   512  				modules.FilesizeUnits(rc.Size))
   513  
   514  			printScoreBreakdown(&hostInfo)
   515  			return nil
   516  		}
   517  	}
   518  
   519  	fmt.Println("Contract not found")
   520  	return nil
   521  }
   522  
   523  // printDirs is a helper for printing directoryInfos
   524  func printDirs(dirs []directoryInfo) error {
   525  	for _, dir := range dirs {
   526  		// Initialize a tab writer for the diretory
   527  		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   528  
   529  		// Print the Directory SiaPath
   530  		fmt.Fprintf(w, "%v/\n", dir.dir.SiaPath)
   531  
   532  		// Print SubDirs
   533  		for _, subDir := range dir.subDirs {
   534  			name := subDir.SiaPath.Name() + "/"
   535  			size := modules.FilesizeUnits(subDir.AggregateSize)
   536  			fmt.Fprintf(w, "  %v\t%9v\n", name, size)
   537  		}
   538  
   539  		// Print files
   540  		for _, file := range dir.files {
   541  			name := file.SiaPath.Name()
   542  			size := modules.FilesizeUnits(file.Filesize)
   543  			fmt.Fprintf(w, "  %v\t%9v\n", name, size)
   544  		}
   545  		fmt.Fprintln(w)
   546  
   547  		// Flush the writer
   548  		if err := w.Flush(); err != nil {
   549  			return errors.AddContext(err, "failed to flush writer")
   550  		}
   551  	}
   552  	return nil
   553  }
   554  
   555  // printDirsVerbose is a helper for verbose printing of directoryInfos
   556  func printDirsVerbose(dirs []directoryInfo) error {
   557  	for _, dir := range dirs {
   558  		// Create a tab writer for the directory
   559  		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   560  
   561  		// Print the Directory SiaPath
   562  		fmt.Fprintf(w, "%v/\n", dir.dir.SiaPath)
   563  
   564  		// Print SubDirs
   565  		fmt.Fprintf(w, "  Name\tFilesize\tAvailable\t Uploaded\tProgress\tRedundancy\tHealth\tStuck Health\tStuck\tRenewing\tOn Disk\tRecoverable\n")
   566  		for _, subDir := range dir.subDirs {
   567  			name := subDir.SiaPath.Name() + "/"
   568  			size := modules.FilesizeUnits(subDir.AggregateSize)
   569  			redundancyStr := fmt.Sprintf("%.2f", subDir.AggregateMinRedundancy)
   570  			if subDir.AggregateMinRedundancy == -1 {
   571  				redundancyStr = "-"
   572  			}
   573  			healthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(subDir.AggregateHealth))
   574  			stuckHealthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(subDir.AggregateStuckHealth))
   575  			stuckStr := yesNo(subDir.AggregateNumStuckChunks > 0)
   576  			fmt.Fprintf(w, "  %v\t%9v\t%9s\t%9s\t%8s\t%10s\t%7s\t%7s\t%5s\t%8s\t%7s\t%11s\n", name, size, "-", "-", "-", redundancyStr, healthStr, stuckHealthStr, stuckStr, "-", "-", "-")
   577  		}
   578  
   579  		// Print files
   580  		for _, file := range dir.files {
   581  			name := file.SiaPath.Name()
   582  			size := modules.FilesizeUnits(file.Filesize)
   583  			availStr := yesNo(file.Available)
   584  			bytesUploaded := modules.FilesizeUnits(file.UploadedBytes)
   585  			uploadStr := fmt.Sprintf("%.2f%%", file.UploadProgress)
   586  			if file.UploadProgress == -1 {
   587  				uploadStr = "-"
   588  			}
   589  			redundancyStr := fmt.Sprintf("%.2f", file.Redundancy)
   590  			if file.Redundancy == -1 {
   591  				redundancyStr = "-"
   592  			}
   593  
   594  			healthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(file.Health))
   595  			stuckHealthStr := fmt.Sprintf("%.2f%%", skymodules.HealthPercentage(file.StuckHealth))
   596  			stuckStr := yesNo(file.Stuck)
   597  			renewStr := yesNo(file.Renewing)
   598  			onDiskStr := yesNo(file.OnDisk)
   599  			recoverStr := yesNo(file.Recoverable)
   600  			fmt.Fprintf(w, "  %v\t%9v\t%9s\t%9s\t%8s\t%10s\t%7s\t%7s\t%5s\t%8s\t%7s\t%11s\n", name, size, availStr, bytesUploaded, uploadStr, redundancyStr, healthStr, stuckHealthStr, stuckStr, renewStr, onDiskStr, recoverStr)
   601  		}
   602  		fmt.Fprintln(w)
   603  
   604  		// Flush the writer
   605  		if err := w.Flush(); err != nil {
   606  			return errors.AddContext(err, "failed to flush writer")
   607  		}
   608  	}
   609  	return nil
   610  }
   611  
   612  // printSingleFile is a helper for printing information about a single file
   613  func printSingleFile(sp skymodules.SiaPath, root, skylinkCheck bool) (tryDir bool, err error) {
   614  	var rf api.RenterFile
   615  	if root {
   616  		rf, err = httpClient.RenterFileRootGet(sp)
   617  	} else {
   618  		rf, err = httpClient.RenterFileGet(sp)
   619  	}
   620  	if err == nil {
   621  		if skylinkCheck && len(rf.File.Skylinks) == 0 {
   622  			err = errors.New("File is not pinning any skylinks")
   623  			return
   624  		}
   625  		var data []byte
   626  		data, err = json.MarshalIndent(rf.File, "", "  ")
   627  		if err != nil {
   628  			return
   629  		}
   630  
   631  		fmt.Println()
   632  		fmt.Println(string(data))
   633  		fmt.Println()
   634  		return
   635  	} else if !strings.Contains(err.Error(), filesystem.ErrNotExist.Error()) {
   636  		err = fmt.Errorf("Error getting file %v: %v", sp.Name(), err)
   637  		return
   638  	}
   639  	tryDir = true
   640  	err = nil
   641  	return
   642  }
   643  
   644  // fundaccountdrift is a small helper that returns a big.Int representing the
   645  // drift that might have occurred between the money spent on funding the account
   646  // and the money that's actually accounted for
   647  func fundaccountdrift(fm skymodules.FinancialMetrics, ea skymodules.AccountSpending) *big.Int {
   648  	var drift *big.Int
   649  
   650  	funded := fm.FundAccountSpending
   651  	accountedFor := ea.Sum().Add(ea.Balance).Add(ea.Residue)
   652  	if funded.Cmp(accountedFor) >= 0 {
   653  		drift = funded.Sub(accountedFor).Big()
   654  	} else {
   655  		drift = accountedFor.Sub(funded).Big()
   656  		drift = drift.Neg(drift)
   657  	}
   658  
   659  	return drift
   660  }
   661  
   662  // currentperiodspending returns the spending breakdown for the given financial
   663  // metrics and exchange rate as a string
   664  func currentperiodspending(fm skymodules.FinancialMetrics, rate *types.ExchangeRate) string {
   665  	// Calculate breakdown
   666  	totalSpent, unspentAllocated, unspentUnallocated := fm.SpendingBreakdown()
   667  
   668  	// Calculate the aggregated account spending
   669  	var ea skymodules.AccountSpending
   670  	for _, as := range fm.EphemeralAccountSpending {
   671  		ea = ea.Add(as.AccountSpending)
   672  	}
   673  
   674  	// Calculate drift
   675  	balanceDrift := &ea.BalanceDrift
   676  	spendingDrift := fundaccountdrift(fm, ea)
   677  
   678  	// Calculate misc & repair
   679  	misc := ea.SnapshotDownloadsCost.Add(ea.SnapshotUploadsCost)
   680  	repair := ea.RepairDownloadsCost.Add(ea.RepairUploadsCost)
   681  
   682  	return fmt.Sprintf(`
   683      Spent Funds:               %v
   684        Storage:                 %v
   685        Upload:                  %v
   686        Download:                %v
   687        FundAccount:             %v (+%v residue)
   688          AccountBalanceCost:    %v
   689          Balance:               %v (%v drift)
   690          DownloadsCost:         %v
   691          MiscCost:              %v
   692          RegistryReadsCost:     %v
   693          RegistryWritesCost:    %v
   694          RepairsCost:           %v
   695          SubscriptionsCost:     %v
   696          UpdatePriceTableCost:  %v
   697          UploadsCost:           %v
   698          Drift:                 %v
   699        Maintenance:             %v
   700          AccountBalanceCost:    %v
   701          FundAccountCost:       %v
   702          UpdatePriceTableCost:  %v
   703        Fees:                    %v
   704          ContractFees:          %v
   705          SiafundFees:           %v
   706          TransactionFees:       %v
   707      Unspent Funds:             %v
   708        Allocated:               %v
   709        Unallocated:             %v
   710      Skynet Fee:                %v
   711  `, currencyUnitsWithExchangeRate(totalSpent, rate),
   712  		currencyUnitsWithExchangeRate(fm.StorageSpending, rate),
   713  		currencyUnitsWithExchangeRate(fm.UploadSpending, rate),
   714  		currencyUnitsWithExchangeRate(fm.DownloadSpending, rate),
   715  		currencyUnitsWithExchangeRate(fm.FundAccountSpending, rate), currencyUnitsWithExchangeRate(ea.Residue, rate),
   716  		currencyUnitsWithExchangeRate(ea.AccountBalanceCost, rate),
   717  		currencyUnitsWithExchangeRate(ea.Balance, rate),
   718  		bigIntToCurrencyUnitsWithExchangeRate(balanceDrift, rate),
   719  		currencyUnitsWithExchangeRate(ea.DownloadsCost, rate),
   720  		currencyUnitsWithExchangeRate(misc, rate),
   721  		currencyUnitsWithExchangeRate(ea.RegistryReadsCost, rate),
   722  		currencyUnitsWithExchangeRate(ea.RegistryWritesCost, rate),
   723  		currencyUnitsWithExchangeRate(repair, rate),
   724  		currencyUnitsWithExchangeRate(ea.SubscriptionsCost, rate),
   725  		currencyUnitsWithExchangeRate(ea.UpdatePriceTableCost, rate),
   726  		currencyUnitsWithExchangeRate(ea.UploadsCost, rate),
   727  		bigIntToCurrencyUnitsWithExchangeRate(spendingDrift, rate),
   728  		currencyUnitsWithExchangeRate(fm.MaintenanceSpending.Sum(), rate),
   729  		currencyUnitsWithExchangeRate(fm.MaintenanceSpending.AccountBalanceCost, rate),
   730  		currencyUnitsWithExchangeRate(fm.MaintenanceSpending.FundAccountCost, rate),
   731  		currencyUnitsWithExchangeRate(fm.MaintenanceSpending.UpdatePriceTableCost, rate),
   732  		currencyUnitsWithExchangeRate(fm.Fees.Sum(), rate),
   733  		currencyUnitsWithExchangeRate(fm.Fees.ContractFees, rate),
   734  		currencyUnitsWithExchangeRate(fm.Fees.SiafundFees, rate),
   735  		currencyUnitsWithExchangeRate(fm.Fees.TransactionFees, rate),
   736  		currencyUnitsWithExchangeRate(fm.Unspent, rate),
   737  		currencyUnitsWithExchangeRate(unspentAllocated, rate),
   738  		currencyUnitsWithExchangeRate(unspentUnallocated, rate),
   739  		currencyUnitsWithExchangeRate(fm.SkynetFee, rate))
   740  }
   741  
   742  // renewedContracts is a helper function to determine the number of renewed and to renew contracts
   743  func renewedContracts(contracts []api.RenterContract, endHeight types.BlockHeight) (renewed, toRenew uint64) {
   744  	for _, c := range contracts {
   745  		if c.EndHeight <= endHeight && c.GoodForRenew {
   746  			toRenew++
   747  		} else if c.EndHeight > endHeight {
   748  			renewed++
   749  		}
   750  	}
   751  	return
   752  }
   753  
   754  // renterallowancespending prints info about the current period spending
   755  // this also get called by 'skyc renter -v' which is why it's in its own
   756  // function
   757  func renterallowancespending(rg api.RenterGET) {
   758  	// Parse exchange rate
   759  	rate, err := types.ParseExchangeRate(build.ExchangeRate())
   760  	if err != nil {
   761  		fmt.Printf("Warning: ignoring exchange rate - %s\n", err)
   762  	}
   763  
   764  	// Print current period spending
   765  	fmt.Printf(`
   766  Spending:
   767    Current Period Spending:`)
   768  
   769  	if rg.Settings.Allowance.Funds.IsZero() {
   770  		fmt.Printf("\n    No current period spending.\n")
   771  	} else {
   772  		fmt.Print(currentperiodspending(rg.FinancialMetrics, rate))
   773  	}
   774  }
   775  
   776  // renterFilesAndContractSummary prints out a summary of what the renter is
   777  // storing
   778  func renterFilesAndContractSummary(verbose bool) error {
   779  	rf, err := httpClient.RenterDirRootGet(skymodules.RootSiaPath())
   780  	if errors.Contains(err, api.ErrAPICallNotRecognized) {
   781  		// Assume module is not loaded if status command is not recognized.
   782  		fmt.Printf("\n  Status: %s\n\n", moduleNotReadyStatus)
   783  		return nil
   784  	} else if err != nil {
   785  		return errors.AddContext(err, "unable to get root dir with RenterDirRootGet")
   786  	}
   787  	timeSinceHealthCheck := time.Since(rf.Directories[0].AggregateLastHealthCheckTime)
   788  
   789  	rc, err := httpClient.RenterDisabledContractsGet()
   790  	if err != nil {
   791  		return err
   792  	}
   793  	redundancyStr := fmt.Sprintf("%.2f", rf.Directories[0].AggregateMinRedundancy)
   794  	if rf.Directories[0].AggregateMinRedundancy == -1 {
   795  		redundancyStr = "-"
   796  	}
   797  	// Active Contracts are all good data
   798  	activeSize, _, _, _ := contractStats(rc.ActiveContracts)
   799  	// Passive Contracts are all good data
   800  	passiveSize, _, _, _ := contractStats(rc.PassiveContracts)
   801  
   802  	// Grab the RenewWindow Calculation from Skynet Stats
   803  	ss, err := httpClient.SkynetStatsGet()
   804  	if err != nil {
   805  		return err
   806  	}
   807  
   808  	// Renew Window Calculations
   809  	rg, err := httpClient.RenterGet()
   810  	if err != nil {
   811  		return err
   812  	}
   813  	cg, err := httpClient.ConsensusGet()
   814  	if err != nil {
   815  		return err
   816  	}
   817  	renewWindowStart := rg.NextPeriod - rg.Settings.Allowance.RenewWindow
   818  	var renewBlocksStr string
   819  	outsideOfRenewWindow := renewWindowStart > cg.Height
   820  	if outsideOfRenewWindow {
   821  		// renew window in the future
   822  		renewBlocksStr = fmt.Sprintf("%v Blocks until Renew", renewWindowStart-cg.Height)
   823  	} else {
   824  		// we are in the renew window
   825  		renewBlocksStr = fmt.Sprintf("%v Blocks Remaining in Renew Window", rg.NextPeriod-cg.Height)
   826  	}
   827  
   828  	// Check on renewal status
   829  	activeRenewed, activeToRenew := renewedContracts(rc.ActiveContracts, rg.NextPeriod)
   830  	passiveRenewed, passiveToRenew := renewedContracts(rc.PassiveContracts, rg.NextPeriod)
   831  	disabledRenewed, disabledToRenew := renewedContracts(rc.DisabledContracts, rg.NextPeriod)
   832  
   833  	// Sum totals, disabledRenewed and disabledToRenew are expected to be zero but including just
   834  	// in case there are some state discrepencies.
   835  	totalRenewed := activeRenewed + passiveRenewed + disabledRenewed
   836  	totalToRenew := activeToRenew + passiveToRenew + disabledToRenew
   837  
   838  	w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   839  	fmt.Fprint(w, "File Summary:\n")
   840  	fmt.Fprintf(w, "  Files:\t%v\n", rf.Directories[0].AggregateNumFiles)
   841  	fmt.Fprintf(w, "  Total Stored:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateSize))
   842  	fmt.Fprintf(w, "  Total Renewing Data:\t%v\n", modules.FilesizeUnits(activeSize+passiveSize))
   843  	fmt.Fprint(w, "Repair Status:\n")
   844  	fmt.Fprintf(w, "  Last Health Check:\t%.0fm\n", timeSinceHealthCheck.Minutes())
   845  	fmt.Fprintf(w, "  Repair Data Remaining:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateRepairSize))
   846  	fmt.Fprintf(w, "  Stuck Repair Remaining:\t%v\n", modules.FilesizeUnits(rf.Directories[0].AggregateStuckSize))
   847  	fmt.Fprintf(w, "  Stuck Chunks:\t%v\n", rf.Directories[0].AggregateNumStuckChunks)
   848  	fmt.Fprintf(w, "  Max Health:\t%v%%\n", rf.Directories[0].AggregateMaxHealthPercentage)
   849  	fmt.Fprintf(w, "  Min Redundancy:\t%v\n", redundancyStr)
   850  	fmt.Fprintf(w, "  Lost Files:\t%v\n", rf.Directories[0].AggregateNumLostFiles)
   851  	fmt.Fprint(w, "Contract Summary:\n")
   852  	fmt.Fprintf(w, "  Renew Window (days):\t%v\n", ss.RenewWindow)
   853  	// If the allowance isn't set, don't bother printing this renew window
   854  	// fields. The Above print out will tell the user that no renew window
   855  	// is set.
   856  	if !rg.Settings.Allowance.Funds.IsZero() {
   857  		fmt.Fprintf(w, "  Renew Window (blocks):\t%v\n", renewBlocksStr)
   858  		fmt.Fprintf(w, "  Current Period Start:\t%v\n", rg.CurrentPeriod)
   859  		fmt.Fprintf(w, "  Next Period Start:\t%v\n", rg.NextPeriod)
   860  		fmt.Fprintf(w, "  Renew Window Start:\t%v\n", renewWindowStart)
   861  		// Only print the contract renewal status if we are in the renew
   862  		// window. Otherwise the current contracts are what was renewed.
   863  		if !outsideOfRenewWindow {
   864  			fmt.Fprintf(w, "  Renewed Contracts:\t%v\n", totalRenewed)
   865  			if verbose {
   866  				fmt.Fprintf(w, "    Active:\t%v\n", activeRenewed)
   867  				fmt.Fprintf(w, "    Passive:\t%v\n", passiveRenewed)
   868  				fmt.Fprintf(w, "    Disabled:\t%v\n", disabledRenewed)
   869  			}
   870  			fmt.Fprintf(w, "  Contracts to Renew:\t%v\n", totalToRenew)
   871  			if verbose {
   872  				fmt.Fprintf(w, "    Active:\t%v\n", activeToRenew)
   873  				fmt.Fprintf(w, "    Passive:\t%v\n", passiveToRenew)
   874  				fmt.Fprintf(w, "    Disabled:\t%v\n", disabledToRenew)
   875  			}
   876  		}
   877  	}
   878  	fmt.Fprintf(w, "  Active Contracts:\t%v\n", len(rc.ActiveContracts))
   879  	fmt.Fprintf(w, "  Passive Contracts:\t%v\n", len(rc.PassiveContracts))
   880  	fmt.Fprintf(w, "  Disabled Contracts:\t%v\n", len(rc.DisabledContracts))
   881  	return w.Flush()
   882  }
   883  
   884  // renterFilesDownload downloads the file at the specified path from the Sia
   885  // network to the local specified destination.
   886  func renterFilesDownload(path, destination string) {
   887  	destination = abs(destination)
   888  	// Parse SiaPath.
   889  	siaPath, err := skymodules.NewSiaPath(path)
   890  	if err != nil {
   891  		die("Couldn't parse SiaPath:", err)
   892  	}
   893  	// If root is not set we need to rebase.
   894  	if !renterDownloadRoot {
   895  		siaPath, err = siaPath.Rebase(skymodules.RootSiaPath(), skymodules.UserFolder)
   896  		if err != nil {
   897  			die("Couldn't rebase SiaPath:", err)
   898  		}
   899  	}
   900  	// If the destination is a folder, download the file to that folder.
   901  	fi, err := os.Stat(destination)
   902  	if err == nil && fi.IsDir() {
   903  		destination = filepath.Join(destination, siaPath.Name())
   904  	}
   905  
   906  	// Queue the download. An error will be returned if the queueing failed, but
   907  	// the call will return before the download has completed. The call is made
   908  	// as an async call.
   909  	start := time.Now()
   910  	cancelID, err := httpClient.RenterDownloadFullGet(siaPath, destination, true, true)
   911  	if err != nil {
   912  		die("Download could not be started:", err)
   913  	}
   914  
   915  	// If the download is async, report success.
   916  	if renterDownloadAsync {
   917  		fmt.Printf("Queued Download '%s' to %s.\n", siaPath.String(), abs(destination))
   918  		fmt.Printf("ID to cancel download: '%v'\n", cancelID)
   919  		return
   920  	}
   921  
   922  	// If the download is blocking, display progress as the file downloads.
   923  	var file api.RenterFile
   924  	file, err = httpClient.RenterFileRootGet(siaPath)
   925  	if err != nil {
   926  		die("Error getting file after download has started:", err)
   927  	}
   928  
   929  	failedDownloads := downloadProgress([]trackedFile{{siaPath: siaPath, dst: destination}})
   930  	if len(failedDownloads) > 0 {
   931  		die("\nDownload could not be completed:", failedDownloads[0].Error)
   932  	}
   933  	fmt.Printf("\nDownloaded '%s' to '%s - %v in %v'.\n", path, abs(destination), modules.FilesizeUnits(file.File.Filesize), time.Since(start).Round(time.Millisecond))
   934  }
   935  
   936  // renterFileHealthSummary prints out a summary of the status of all the files
   937  // in the renter to track the progress of the files
   938  func renterFileHealthSummary(dirs []directoryInfo) {
   939  	percentages, numStuck, err := fileHealthBreakdown(dirs, false)
   940  	if err != nil {
   941  		die(err)
   942  	}
   943  
   944  	percentages = parsePercentages(percentages)
   945  
   946  	fmt.Println("File Health Summary")
   947  	w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   948  	fmt.Fprintf(w, "  %% At 100%%\t%v%%\n", percentages[0])
   949  	fmt.Fprintf(w, "  %% Between 75%% - 100%%\t%v%%\n", percentages[1])
   950  	fmt.Fprintf(w, "  %% Between 50%% - 75%%\t%v%%\n", percentages[2])
   951  	fmt.Fprintf(w, "  %% Between 25%% - 50%%\t%v%%\n", percentages[3])
   952  	fmt.Fprintf(w, "  %% Between 0%% - 25%%\t%v%%\n", percentages[4])
   953  	fmt.Fprintf(w, "  %% Lost\t%v%%\n", percentages[5])
   954  	fmt.Fprintf(w, "  Number of Stuck Files\t%v\n", numStuck)
   955  	if err := w.Flush(); err != nil {
   956  		die("failed to flush writer:", err)
   957  	}
   958  }
   959  
   960  // writeContracts is a helper function to display contracts
   961  func writeContracts(contracts []api.RenterContract) {
   962  	fmt.Println("  Number of Contracts:", len(contracts))
   963  	sort.Sort(byValue(contracts))
   964  	w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   965  	fmt.Fprintln(w, "  \nHost\tHost PubKey\tHost Version\tRemaining Funds\tSpent Funds\tSpent Fees\tData\tEnd Height\tContract ID\tGoodForUpload\tGoodForRenew\tGoodForRefresh\tBadContract")
   966  	for _, c := range contracts {
   967  		address := c.NetAddress
   968  		hostVersion := c.HostVersion
   969  		if address == "" {
   970  			address = "Host Removed"
   971  			hostVersion = ""
   972  		}
   973  		// Negative Currency Check
   974  		var contractTotalSpent types.Currency
   975  		if c.TotalCost.Cmp(c.RenterFunds.Add(c.Fees)) < 0 {
   976  			contractTotalSpent = c.RenterFunds.Add(c.Fees)
   977  		} else {
   978  			contractTotalSpent = c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees)
   979  		}
   980  		fmt.Fprintf(w, "  %v\t%v\t%v\t%8s\t%8s\t%8s\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n",
   981  			address,
   982  			c.HostPublicKey.String(),
   983  			hostVersion,
   984  			currencyUnits(c.RenterFunds),
   985  			currencyUnits(contractTotalSpent),
   986  			currencyUnits(c.Fees),
   987  			modules.FilesizeUnits(c.Size),
   988  			c.EndHeight,
   989  			c.ID,
   990  			c.GoodForUpload,
   991  			c.GoodForRenew,
   992  			c.GoodForRefresh,
   993  			c.BadContract)
   994  	}
   995  	if err := w.Flush(); err != nil {
   996  		die("failed to flush writer:", err)
   997  	}
   998  }
   999  
  1000  // writeWorkerDownloadInfo is a helper function for writing the download
  1001  // information to the tabwriter.
  1002  func writeWorkerDownloadInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) {
  1003  	// print summary
  1004  	fmt.Fprintf(w, "Worker Pool Summary \n")
  1005  	fmt.Fprintf(w, "  Total Workers: \t%v\n", rw.NumWorkers)
  1006  	fmt.Fprintf(w, "  Workers On HasSector Cooldown:\t%v\n", rw.TotalHasSectorCoolDown)
  1007  	fmt.Fprintf(w, "  Workers On Download Cooldown:\t%v\n", rw.TotalDownloadCoolDown)
  1008  
  1009  	// print header
  1010  	hostInfo := "Host PubKey"
  1011  	info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated"
  1012  	header := hostInfo + info
  1013  	fmt.Fprintln(w, "\nWorker Downloads Detail  \n\n"+header)
  1014  
  1015  	// print rows
  1016  	for _, worker := range rw.Workers {
  1017  		// Host Info
  1018  		fmt.Fprintf(w, "%v", worker.HostPubKey.String())
  1019  
  1020  		// Download Info
  1021  		fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n",
  1022  			worker.DownloadOnCoolDown,
  1023  			absDuration(worker.DownloadCoolDownTime),
  1024  			sanitizeErr(worker.DownloadCoolDownError),
  1025  			worker.DownloadQueueSize,
  1026  			worker.DownloadTerminated)
  1027  	}
  1028  }
  1029  
  1030  // writeWorkerRepairInfo is a helper function for writing the low prio download
  1031  // information (used for repairs) to the tabwriter.
  1032  func writeWorkerRepairInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) {
  1033  	// print summary
  1034  	fmt.Fprintf(w, "Worker Pool Summary \n")
  1035  	fmt.Fprintf(w, "  Total Workers: \t%v\n", rw.NumWorkers)
  1036  	fmt.Fprintf(w, "  Workers On Repair Cooldown:\t%v\n", rw.TotalLowPrioDownloadCoolDown)
  1037  
  1038  	// print header
  1039  	hostInfo := "Host PubKey"
  1040  	info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated"
  1041  	header := hostInfo + info
  1042  	fmt.Fprintln(w, "\nWorker Repair Detail  \n\n"+header)
  1043  
  1044  	// print rows
  1045  	for _, worker := range rw.Workers {
  1046  		// Host Info
  1047  		fmt.Fprintf(w, "%v", worker.HostPubKey.String())
  1048  
  1049  		// Download Info
  1050  		fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n",
  1051  			worker.LowPrioDownloadOnCoolDown,
  1052  			absDuration(worker.LowPrioDownloadCoolDownTime),
  1053  			sanitizeErr(worker.LowPrioDownloadCoolDownError),
  1054  			worker.LowPrioDownloadQueueSize,
  1055  			worker.LowPrioDownloadTerminated)
  1056  	}
  1057  }
  1058  
  1059  // writeWorkerMaintenanceInfo is a helper function for writing the maintenance
  1060  // state for every worker.
  1061  func writeWorkerMaintenanceInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) {
  1062  	// print summary
  1063  	fmt.Fprintf(w, "Worker Pool Summary \n")
  1064  	fmt.Fprintf(w, "  Total Workers: \t%v\n", rw.NumWorkers)
  1065  	fmt.Fprintf(w, "  Workers On Maintenance Cooldown:\t%v\n", rw.TotalMaintenanceCoolDown)
  1066  
  1067  	// print header
  1068  	hostInfo := "Host PubKey"
  1069  	info := "\tOn Cooldown\tCooldown Time\tLast Error"
  1070  	header := hostInfo + info
  1071  	fmt.Fprintln(w, "\nWorker Maintenance Detail  \n\n"+header)
  1072  
  1073  	// print rows
  1074  	for _, worker := range rw.Workers {
  1075  		// Host Info
  1076  		fmt.Fprintf(w, "%v", worker.HostPubKey.String())
  1077  
  1078  		// Download Info
  1079  		fmt.Fprintf(w, "\t%v\t%v\t%v\n",
  1080  			worker.MaintenanceOnCooldown,
  1081  			absDuration(worker.MaintenanceCoolDownTime),
  1082  			sanitizeErr(worker.MaintenanceCoolDownError))
  1083  	}
  1084  }
  1085  
  1086  // writeWorkerUploadInfo is a helper function for writing the upload information
  1087  // to the tabwriter.
  1088  func writeWorkerUploadInfo(w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) {
  1089  	// print summary
  1090  	fmt.Fprintf(w, "Worker Pool Summary \n")
  1091  	fmt.Fprintf(w, "  Total Workers: \t%v\n", rw.NumWorkers)
  1092  	fmt.Fprintf(w, "  Workers On Upload Cooldown:\t%v\n", rw.TotalUploadCoolDown)
  1093  
  1094  	// print header
  1095  	hostInfo := "Host PubKey"
  1096  	info := "\tOn Cooldown\tCooldown Time\tLast Error\tQueue\tTerminated"
  1097  	header := hostInfo + info
  1098  	fmt.Fprintln(w, "\nWorker Uploads Detail  \n\n"+header)
  1099  
  1100  	// print rows
  1101  	for _, worker := range rw.Workers {
  1102  		// Host Info
  1103  		fmt.Fprintf(w, "%v", worker.HostPubKey.String())
  1104  
  1105  		// Upload Info
  1106  		fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n",
  1107  			worker.UploadOnCoolDown,
  1108  			absDuration(worker.UploadCoolDownTime),
  1109  			sanitizeErr(worker.UploadCoolDownError),
  1110  			worker.UploadQueueSize,
  1111  			worker.UploadTerminated)
  1112  	}
  1113  }
  1114  
  1115  // writeWorkerReadUpdateRegistryInfo is a helper function for writing the read registry
  1116  // or update registry information to the tabwriter.
  1117  func writeWorkerReadUpdateRegistryInfo(read bool, w *tabwriter.Writer, rw skymodules.WorkerPoolStatus) {
  1118  	// print summary
  1119  	fmt.Fprintf(w, "Worker Pool Summary \n")
  1120  	fmt.Fprintf(w, "  Total Workers: \t%v\n", rw.NumWorkers)
  1121  	if read {
  1122  		fmt.Fprintf(w, "  Workers On ReadRegistry Cooldown:\t%v\n", rw.TotalDownloadCoolDown)
  1123  	} else {
  1124  		fmt.Fprintf(w, "  Workers On UpdateRegistry Cooldown:\t%v\n", rw.TotalUploadCoolDown)
  1125  	}
  1126  
  1127  	// print header
  1128  	hostInfo := "Host PubKey"
  1129  	info := "\tOn Cooldown\tCooldown Time\tLast Error\tLast Error Time\tQueue"
  1130  	header := hostInfo + info
  1131  	if read {
  1132  		fmt.Fprintln(w, "\nWorker ReadRegistry Detail  \n\n"+header)
  1133  	} else {
  1134  		fmt.Fprintln(w, "\nWorker UpdateRegistry Detail  \n\n"+header)
  1135  	}
  1136  
  1137  	// print rows
  1138  	for _, worker := range rw.Workers {
  1139  		// Host Info
  1140  		fmt.Fprintf(w, "%v", worker.HostPubKey.String())
  1141  
  1142  		// Qeue Info
  1143  		if read {
  1144  			status := worker.ReadRegistryJobsStatus
  1145  			fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n",
  1146  				status.OnCooldown,
  1147  				absDuration(time.Until(status.OnCooldownUntil)),
  1148  				sanitizeErr(status.RecentErr),
  1149  				status.RecentErrTime,
  1150  				status.JobQueueSize)
  1151  		} else {
  1152  			status := worker.UpdateRegistryJobsStatus
  1153  			fmt.Fprintf(w, "\t%v\t%v\t%v\t%v\t%v\n",
  1154  				status.OnCooldown,
  1155  				absDuration(time.Until(status.OnCooldownUntil)),
  1156  				sanitizeErr(status.RecentErr),
  1157  				status.RecentErrTime,
  1158  				status.JobQueueSize)
  1159  		}
  1160  	}
  1161  }