github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/cmd/spc/rentercmd.go (about)

     1  package main
     2  
     3  // TODO: If you run siac from a non-existent directory, the abs() function does
     4  // not handle this very gracefully.
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/tabwriter"
    14  	"time"
    15  
    16  	"github.com/spf13/cobra"
    17  	"gitlab.com/NebulousLabs/errors"
    18  
    19  	"SiaPrime/modules"
    20  	"SiaPrime/node/api"
    21  	"SiaPrime/types"
    22  )
    23  
    24  var (
    25  	renterAllowanceCancelCmd = &cobra.Command{
    26  		Use:   "cancel",
    27  		Short: "Cancel the current allowance",
    28  		Long:  "Cancel the current allowance, which controls how much money is spent on file contracts.",
    29  		Run:   wrap(renterallowancecancelcmd),
    30  	}
    31  
    32  	renterAllowanceCmd = &cobra.Command{
    33  		Use:   "allowance",
    34  		Short: "View the current allowance",
    35  		Long:  "View the current allowance, which controls how much money is spent on file contracts.",
    36  		Run:   wrap(renterallowancecmd),
    37  	}
    38  
    39  	renterCmd = &cobra.Command{
    40  		Use:   "renter",
    41  		Short: "Perform renter actions",
    42  		Long:  "Upload, download, rename, delete, load, or share files.",
    43  		Run:   wrap(rentercmd),
    44  	}
    45  
    46  	renterContractsCmd = &cobra.Command{
    47  		Use:   "contracts",
    48  		Short: "View the Renter's contracts",
    49  		Long:  "View the contracts that the Renter has formed with hosts.",
    50  		Run:   wrap(rentercontractscmd),
    51  	}
    52  
    53  	renterContractsViewCmd = &cobra.Command{
    54  		Use:   "view [contract-id]",
    55  		Short: "View details of the specified contract",
    56  		Long:  "View all details available of the specified contract.",
    57  		Run:   wrap(rentercontractsviewcmd),
    58  	}
    59  
    60  	renterDownloadsCmd = &cobra.Command{
    61  		Use:   "downloads",
    62  		Short: "View the download queue",
    63  		Long:  "View the list of files currently downloading.",
    64  		Run:   wrap(renterdownloadscmd),
    65  	}
    66  
    67  	renterFilesDeleteCmd = &cobra.Command{
    68  		Use:     "delete [path]",
    69  		Aliases: []string{"rm"},
    70  		Short:   "Delete a file",
    71  		Long:    "Delete a file. Does not delete the file on disk.",
    72  		Run:     wrap(renterfilesdeletecmd),
    73  	}
    74  
    75  	renterFilesDownloadCmd = &cobra.Command{
    76  		Use:   "download [path] [destination]",
    77  		Short: "Download a file",
    78  		Long:  "Download a previously-uploaded file to a specified destination.",
    79  		Run:   wrap(renterfilesdownloadcmd),
    80  	}
    81  
    82  	renterFilesListCmd = &cobra.Command{
    83  		Use:     "list",
    84  		Aliases: []string{"ls"},
    85  		Short:   "List the status of all files",
    86  		Long:    "List the status of all files known to the renter on the SiaPrime network.",
    87  		Run:     wrap(renterfileslistcmd),
    88  	}
    89  
    90  	renterFilesRenameCmd = &cobra.Command{
    91  		Use:     "rename [path] [newpath]",
    92  		Aliases: []string{"mv"},
    93  		Short:   "Rename a file",
    94  		Long:    "Rename a file.",
    95  		Run:     wrap(renterfilesrenamecmd),
    96  	}
    97  
    98  	renterFilesUploadCmd = &cobra.Command{
    99  		Use:   "upload [source] [path]",
   100  		Short: "Upload a file",
   101  		Long:  "Upload a file to [path] on the SiaPrime network.",
   102  		Run:   wrap(renterfilesuploadcmd),
   103  	}
   104  
   105  	renterPricesCmd = &cobra.Command{
   106  		Use:   "prices [amount] [period] [hosts] [renew window]",
   107  		Short: "Display the price of storage and bandwidth",
   108  		Long: `Display the estimated prices of storing files, retrieving files, and creating a set of contracts.
   109  
   110  An allowance can be provided for a more accurate estimate, if no allowance is provided the current set allowance will be used,
   111  and if no allowance is set an allowance of 500SC, 12w period, 50 hosts, and 4w renew window will be used.`,
   112  		Run: renterpricescmd,
   113  	}
   114  
   115  	renterSetAllowanceCmd = &cobra.Command{
   116  		Use:   "setallowance [amount] [period] [hosts] [renew window]",
   117  		Short: "Set the allowance",
   118  		Long: `Set the amount of money that can be spent over a given period.
   119  
   120  amount is given in currency units (SCP, KS, etc.)
   121  
   122  period is given in either blocks (b), hours (h), days (d), or weeks (w). A
   123  block is approximately 10 minutes, so one hour is six blocks, a day is 144
   124  blocks, and a week is 1008 blocks.
   125  
   126  The SiaPrime renter module spreads data across more than one SiaPrime server computer
   127  or "host". The "hosts" parameter for the setallowance command determines
   128  how many different hosts the renter will spread the data across.
   129  
   130  Allowance can be automatically renewed periodically. If the current
   131  blockheight + the renew window >= the end height the contract,
   132  then the contract is renewed automatically.
   133  
   134  Note that setting the allowance will cause siad to immediately begin forming
   135  contracts! You should only set the allowance once you are fully synced and you
   136  have a reasonable number (>30) of hosts in your hostdb.`,
   137  		Run: rentersetallowancecmd,
   138  	}
   139  
   140  	renterUploadsCmd = &cobra.Command{
   141  		Use:   "uploads",
   142  		Short: "View the upload queue",
   143  		Long:  "View the list of files currently uploading.",
   144  		Run:   wrap(renteruploadscmd),
   145  	}
   146  )
   147  
   148  // abs returns the absolute representation of a path.
   149  // TODO: bad things can happen if you run siac from a non-existent directory.
   150  // Implement some checks to catch this problem.
   151  func abs(path string) string {
   152  	abspath, err := filepath.Abs(path)
   153  	if err != nil {
   154  		return path
   155  	}
   156  	return abspath
   157  }
   158  
   159  // rentercmd displays the renter's financial metrics and lists the files it is
   160  // tracking.
   161  func rentercmd() {
   162  	rg, err := httpClient.RenterGet()
   163  	if err != nil {
   164  		die("Could not get renter info:", err)
   165  	}
   166  	fm := rg.FinancialMetrics
   167  	totalSpent := fm.ContractFees.Add(fm.UploadSpending).
   168  		Add(fm.DownloadSpending).Add(fm.StorageSpending)
   169  
   170  	fmt.Printf(`Renter Info:
   171    Allowance:`)
   172  
   173  	if rg.Settings.Allowance.Funds.IsZero() {
   174  		fmt.Printf("\n    No current allowance.\n")
   175  	} else {
   176  		fmt.Printf(`       %v
   177    Spent Funds:     %v
   178    Unspent Funds:   %v
   179  `, currencyUnits(rg.Settings.Allowance.Funds),
   180  			currencyUnits(totalSpent), currencyUnits(fm.Unspent))
   181  	}
   182  
   183  	// also list files
   184  	renterfileslistcmd()
   185  }
   186  
   187  // renteruploadscmd is the handler for the command `siac renter uploads`.
   188  // Lists files currently uploading.
   189  func renteruploadscmd() {
   190  	rf, err := httpClient.RenterFilesGet()
   191  	if err != nil {
   192  		die("Could not get upload queue:", err)
   193  	}
   194  
   195  	// TODO: add a --history flag to the uploads command to mirror the --history
   196  	//       flag in the downloads command. This hasn't been done yet because the
   197  	//       call to /renter/files includes files that have been shared with you,
   198  	//       not just files you've uploaded.
   199  
   200  	// Filter out files that have been uploaded.
   201  	var filteredFiles []modules.FileInfo
   202  	for _, fi := range rf.Files {
   203  		if !fi.Available {
   204  			filteredFiles = append(filteredFiles, fi)
   205  		}
   206  	}
   207  	if len(filteredFiles) == 0 {
   208  		fmt.Println("No files are uploading.")
   209  		return
   210  	}
   211  	fmt.Println("Uploading", len(filteredFiles), "files:")
   212  	for _, file := range filteredFiles {
   213  		fmt.Printf("%13s  %s (uploading, %0.2f%%)\n", filesizeUnits(int64(file.Filesize)), file.SiaPath, file.UploadProgress)
   214  	}
   215  }
   216  
   217  // renterdownloadscmd is the handler for the command `siac renter downloads`.
   218  // Lists files currently downloading, and optionally previously downloaded
   219  // files if the -H or --history flag is specified.
   220  func renterdownloadscmd() {
   221  	queue, err := httpClient.RenterDownloadsGet()
   222  	if err != nil {
   223  		die("Could not get download queue:", err)
   224  	}
   225  	// Filter out files that have been downloaded.
   226  	var downloading []api.DownloadInfo
   227  	for _, file := range queue.Downloads {
   228  		if !file.Completed {
   229  			downloading = append(downloading, file)
   230  		}
   231  	}
   232  	if len(downloading) == 0 {
   233  		fmt.Println("No files are downloading.")
   234  	} else {
   235  		fmt.Println("Downloading", len(downloading), "files:")
   236  		for _, file := range downloading {
   237  			fmt.Printf("%s: %5.1f%% %s -> %s\n", file.StartTime.Format("Jan 02 03:04 PM"), 100*float64(file.Received)/float64(file.Filesize), file.SiaPath, file.Destination)
   238  		}
   239  	}
   240  	if !renterShowHistory {
   241  		return
   242  	}
   243  	fmt.Println()
   244  	// Filter out files that are downloading.
   245  	var downloaded []api.DownloadInfo
   246  	for _, file := range queue.Downloads {
   247  		if file.Completed {
   248  			downloaded = append(downloaded, file)
   249  		}
   250  	}
   251  	if len(downloaded) == 0 {
   252  		fmt.Println("No files downloaded.")
   253  	} else {
   254  		fmt.Println("Downloaded", len(downloaded), "files:")
   255  		for _, file := range downloaded {
   256  			fmt.Printf("%s: %s -> %s\n", file.StartTime.Format("Jan 02 03:04 PM"), file.SiaPath, file.Destination)
   257  		}
   258  	}
   259  }
   260  
   261  // renterallowancecmd displays the current allowance.
   262  func renterallowancecmd() {
   263  	rg, err := httpClient.RenterGet()
   264  	if err != nil {
   265  		die("Could not get allowance:", err)
   266  	}
   267  	allowance := rg.Settings.Allowance
   268  
   269  	// Show allowance info
   270  	fmt.Printf(`Allowance:
   271  	Amount:       %v
   272  	Period:       %v blocks
   273  	Renew Window: %v blocks
   274  	Hosts:        %v
   275  `, currencyUnits(allowance.Funds), allowance.Period, allowance.RenewWindow, allowance.Hosts)
   276  
   277  	// Show spending detail
   278  	fm := rg.FinancialMetrics
   279  	totalSpent := fm.ContractFees.Add(fm.UploadSpending).
   280  		Add(fm.DownloadSpending).Add(fm.StorageSpending)
   281  	// Calculate unspent allocated
   282  	unspentAllocated := types.ZeroCurrency
   283  	if fm.TotalAllocated.Cmp(totalSpent) >= 0 {
   284  		unspentAllocated = fm.TotalAllocated.Sub(totalSpent)
   285  	}
   286  	// Calculate unspent unallocated
   287  	unspentUnallocated := types.ZeroCurrency
   288  	if fm.Unspent.Cmp(unspentAllocated) >= 0 {
   289  		unspentUnallocated = fm.Unspent.Sub(unspentAllocated)
   290  	}
   291  
   292  	fmt.Printf(`
   293  Spending:
   294    Current Period Spending:`)
   295  
   296  	if rg.Settings.Allowance.Funds.IsZero() {
   297  		fmt.Printf("\n    No current period spending.\n")
   298  	} else {
   299  		fmt.Printf(`
   300      Spent Funds:     %v
   301        Storage:       %v
   302        Upload:        %v
   303        Download:      %v
   304        Fees:          %v
   305      Unspent Funds:   %v
   306        Allocated:     %v
   307        Unallocated:   %v
   308  `, currencyUnits(totalSpent), currencyUnits(fm.StorageSpending),
   309  			currencyUnits(fm.UploadSpending), currencyUnits(fm.DownloadSpending),
   310  			currencyUnits(fm.ContractFees), currencyUnits(fm.Unspent),
   311  			currencyUnits(unspentAllocated), currencyUnits(unspentUnallocated))
   312  	}
   313  
   314  	fmt.Printf("\n  Previous Spending:")
   315  	if fm.PreviousSpending.IsZero() && fm.WithheldFunds.IsZero() {
   316  		fmt.Printf("\n    No previous spending.\n\n")
   317  	} else {
   318  		fmt.Printf(` %v
   319      Withheld Funds:  %v
   320      Release Block:   %v
   321  
   322  `, currencyUnits(fm.PreviousSpending), currencyUnits(fm.WithheldFunds), fm.ReleaseBlock)
   323  	}
   324  }
   325  
   326  // renterallowancecancelcmd cancels the current allowance.
   327  func renterallowancecancelcmd() {
   328  	fmt.Println(`Canceling your allowance will disable uploading new files,
   329  repairing existing files, and renewing existing files. All files will cease
   330  to be accessible after a short period of time.`)
   331  again:
   332  	fmt.Print("Do you want to continue? [y/n] ")
   333  	var resp string
   334  	fmt.Scanln(&resp)
   335  	switch strings.ToLower(resp) {
   336  	case "y", "yes":
   337  		// continue below
   338  	case "n", "no":
   339  		return
   340  	default:
   341  		goto again
   342  	}
   343  	err := httpClient.RenterCancelAllowance()
   344  	if err != nil {
   345  		die("error canceling allowance:", err)
   346  	}
   347  	fmt.Println("Allowance canceled.")
   348  }
   349  
   350  // rentersetallowancecmd allows the user to set the allowance.
   351  // the first two parameters, amount and period, are required.
   352  // the second two parameters are optional:
   353  //    hosts                 integer number of hosts
   354  //    renewperiod           how many blocks between renewals
   355  func rentersetallowancecmd(cmd *cobra.Command, args []string) {
   356  	if len(args) < 2 || len(args) > 4 {
   357  		cmd.UsageFunc()(cmd)
   358  		os.Exit(exitCodeUsage)
   359  	}
   360  	hastings, err := parseCurrency(args[0])
   361  	if err != nil {
   362  		die("Could not parse amount:", err)
   363  	}
   364  	blocks, err := parsePeriod(args[1])
   365  	if err != nil {
   366  		die("Could not parse period:", err)
   367  	}
   368  	allowance := modules.Allowance{}
   369  	_, err = fmt.Sscan(hastings, &allowance.Funds)
   370  	if err != nil {
   371  		die("Could not parse amount:", err)
   372  	}
   373  
   374  	_, err = fmt.Sscan(blocks, &allowance.Period)
   375  	if err != nil {
   376  		die("Could not parse period:", err)
   377  	}
   378  	if len(args) > 2 {
   379  		hosts, err := strconv.Atoi(args[2])
   380  		if err != nil {
   381  			die("Could not parse host count")
   382  		}
   383  		allowance.Hosts = uint64(hosts)
   384  	}
   385  	if len(args) > 3 {
   386  		renewWindow, err := parsePeriod(args[3])
   387  		if err != nil {
   388  			die("Could not parse renew window")
   389  		}
   390  		_, err = fmt.Sscan(renewWindow, &allowance.RenewWindow)
   391  		if err != nil {
   392  			die("Could not parse renew window:", err)
   393  		}
   394  	}
   395  	err = httpClient.RenterPostAllowance(allowance)
   396  	if err != nil {
   397  		die("Could not set allowance:", err)
   398  	}
   399  	fmt.Println("Allowance updated.")
   400  }
   401  
   402  // byValue sorts contracts by their value in siacoins, high to low. If two
   403  // contracts have the same value, they are sorted by their host's address.
   404  type byValue []api.RenterContract
   405  
   406  func (s byValue) Len() int      { return len(s) }
   407  func (s byValue) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   408  func (s byValue) Less(i, j int) bool {
   409  	cmp := s[i].RenterFunds.Cmp(s[j].RenterFunds)
   410  	if cmp == 0 {
   411  		return s[i].NetAddress < s[j].NetAddress
   412  	}
   413  	return cmp > 0
   414  }
   415  
   416  // rentercontractscmd is the handler for the comand `siac renter contracts`.
   417  // It lists the Renter's contracts.
   418  func rentercontractscmd() {
   419  	rc, err := httpClient.RenterInactiveContractsGet()
   420  	if err != nil {
   421  		die("Could not get contracts:", err)
   422  	}
   423  
   424  	fmt.Println("Active Contracts:")
   425  	if len(rc.ActiveContracts) == 0 {
   426  		fmt.Println("  No active contracts.")
   427  	} else {
   428  		// Display Active Contracts
   429  		sort.Sort(byValue(rc.ActiveContracts))
   430  		var activeTotalStored uint64
   431  		var activeTotalRemaining, activeTotalSpent, activeTotalFees types.Currency
   432  		for _, c := range rc.ActiveContracts {
   433  			activeTotalStored += c.Size
   434  			activeTotalRemaining = activeTotalRemaining.Add(c.RenterFunds)
   435  			activeTotalSpent = activeTotalSpent.Add(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees))
   436  			activeTotalFees = activeTotalFees.Add(c.Fees)
   437  		}
   438  		fmt.Printf(`  Number of Contracts:  %v
   439    Total stored:         %s
   440    Total Remaining:      %v
   441    Total Spent:          %v
   442    Total Fees:           %v
   443  
   444  `, len(rc.ActiveContracts), filesizeUnits(int64(activeTotalStored)),
   445  			currencyUnits(activeTotalRemaining), currencyUnits(activeTotalSpent), currencyUnits(activeTotalFees))
   446  		w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   447  		fmt.Fprintln(w, "  Host\tRemaining Funds\tSpent Funds\tSpent Fees\tData\tEnd Height\tID\tGoodForUpload\tGoodForRenew")
   448  		for _, c := range rc.ActiveContracts {
   449  			address := c.NetAddress
   450  			if address == "" {
   451  				address = "Host Removed"
   452  			}
   453  			fmt.Fprintf(w, "  %v\t%8s\t%8s\t%8s\t%v\t%v\t%v\t%v\t%v\n",
   454  				address,
   455  				currencyUnits(c.RenterFunds),
   456  				currencyUnits(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees)),
   457  				currencyUnits(c.Fees),
   458  				filesizeUnits(int64(c.Size)),
   459  				c.EndHeight,
   460  				c.ID,
   461  				c.GoodForUpload,
   462  				c.GoodForRenew)
   463  		}
   464  		w.Flush()
   465  	}
   466  
   467  	fmt.Println("\nInactive Contracts:")
   468  	if len(rc.InactiveContracts) == 0 {
   469  		fmt.Println("  No inactive contracts.")
   470  	} else {
   471  		// Display Inactive Contracts
   472  		sort.Sort(byValue(rc.InactiveContracts))
   473  		var inactiveTotalStored uint64
   474  		var inactiveTotalRemaining, inactiveTotalSpent, inactiveTotalFees types.Currency
   475  		for _, c := range rc.InactiveContracts {
   476  			inactiveTotalStored += c.Size
   477  			inactiveTotalRemaining = inactiveTotalRemaining.Add(c.RenterFunds)
   478  			inactiveTotalSpent = inactiveTotalSpent.Add(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees))
   479  			inactiveTotalFees = inactiveTotalFees.Add(c.Fees)
   480  		}
   481  
   482  		fmt.Printf(`
   483    Number of Contracts:  %v
   484    Total stored:         %s
   485    Total Remaining:      %v
   486    Total Spent:          %v
   487    Total Fees:           %v
   488  
   489  `, len(rc.InactiveContracts), filesizeUnits(int64(inactiveTotalStored)), currencyUnits(inactiveTotalRemaining), currencyUnits(inactiveTotalSpent), currencyUnits(inactiveTotalFees))
   490  		w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   491  		fmt.Fprintln(w, "  Host\tRemaining Funds\tSpent Funds\tSpent Fees\tData\tEnd Height\tID\tGoodForUpload\tGoodForRenew")
   492  		for _, c := range rc.InactiveContracts {
   493  			address := c.NetAddress
   494  			if address == "" {
   495  				address = "Host Removed"
   496  			}
   497  			fmt.Fprintf(w, "  %v\t%8s\t%8s\t%8s\t%v\t%v\t%v\t%v\t%v\n",
   498  				address,
   499  				currencyUnits(c.RenterFunds),
   500  				currencyUnits(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees)),
   501  				currencyUnits(c.Fees),
   502  				filesizeUnits(int64(c.Size)),
   503  				c.EndHeight,
   504  				c.ID,
   505  				c.GoodForUpload,
   506  				c.GoodForRenew)
   507  		}
   508  		w.Flush()
   509  	}
   510  
   511  	if renterAllContracts {
   512  		fmt.Println("\nExpired Contracts:")
   513  		rce, err := httpClient.RenterExpiredContractsGet()
   514  		if err != nil {
   515  			die("Could not get expired contracts:", err)
   516  		}
   517  		if len(rce.ExpiredContracts) == 0 {
   518  			fmt.Println("  No expired contracts.")
   519  		} else {
   520  			sort.Sort(byValue(rce.ExpiredContracts))
   521  			var expiredTotalStored uint64
   522  			var expiredTotalWithheld, expiredTotalSpent, expiredTotalFees types.Currency
   523  			for _, c := range rce.ExpiredContracts {
   524  				expiredTotalStored += c.Size
   525  				expiredTotalWithheld = expiredTotalWithheld.Add(c.RenterFunds)
   526  				expiredTotalSpent = expiredTotalSpent.Add(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees))
   527  				expiredTotalFees = expiredTotalFees.Add(c.Fees)
   528  			}
   529  			fmt.Printf(`
   530  	Number of Contracts:  %v
   531  	Total stored:         %9s
   532  	Total Remaining:      %v
   533  	Total Spent:          %v
   534  	Total Fees:           %v
   535  			
   536  	`, len(rce.ExpiredContracts), filesizeUnits(int64(expiredTotalStored)), currencyUnits(expiredTotalWithheld), currencyUnits(expiredTotalSpent), currencyUnits(expiredTotalFees))
   537  			w := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0)
   538  			fmt.Fprintln(w, "  Host\tWithheld Funds\tSpent Funds\tSpent Fees\tData\tEnd Height\tID\tGoodForUpload\tGoodForRenew")
   539  			for _, c := range rce.ExpiredContracts {
   540  				address := c.NetAddress
   541  				if address == "" {
   542  					address = "Host Removed"
   543  				}
   544  				fmt.Fprintf(w, "  %v\t%8s\t%8s\t%8s\t%v\t%v\t%v\t%v\t%v\n",
   545  					address,
   546  					currencyUnits(c.RenterFunds),
   547  					currencyUnits(c.TotalCost.Sub(c.RenterFunds).Sub(c.Fees)),
   548  					currencyUnits(c.Fees),
   549  					filesizeUnits(int64(c.Size)),
   550  					c.EndHeight,
   551  					c.ID,
   552  					c.GoodForUpload,
   553  					c.GoodForRenew)
   554  			}
   555  			w.Flush()
   556  		}
   557  	}
   558  }
   559  
   560  // rentercontractsviewcmd is the handler for the command `siac renter contracts <id>`.
   561  // It lists details of a specific contract.
   562  func rentercontractsviewcmd(cid string) {
   563  	rc, err := httpClient.RenterInactiveContractsGet()
   564  	if err != nil {
   565  		die("Could not get contract details: ", err)
   566  	}
   567  	rce, err := httpClient.RenterExpiredContractsGet()
   568  	if err != nil {
   569  		die("Could not get expired contract details: ", err)
   570  	}
   571  
   572  	contracts := append(rc.ActiveContracts, rc.InactiveContracts...)
   573  	contracts = append(contracts, rce.ExpiredContracts...)
   574  
   575  	for _, rc := range contracts {
   576  		if rc.ID.String() == cid {
   577  			hostInfo, err := httpClient.HostDbHostsGet(rc.HostPublicKey)
   578  			if err != nil {
   579  				die("Could not fetch details of host: ", err)
   580  			}
   581  			fmt.Printf(`
   582  Contract %v
   583    Host: %v (Public Key: %v)
   584  
   585    Start Height: %v
   586    End Height:   %v
   587  
   588    Total cost:        %v (Fees: %v)
   589    Funds Allocated:   %v
   590    Upload Spending:   %v
   591    Storage Spending:  %v
   592    Download Spending: %v
   593    Remaining Funds:   %v
   594  
   595    File Size: %v
   596  `, rc.ID, rc.NetAddress, rc.HostPublicKey.String(), rc.StartHeight, rc.EndHeight,
   597  				currencyUnits(rc.TotalCost),
   598  				currencyUnits(rc.Fees),
   599  				currencyUnits(rc.TotalCost.Sub(rc.Fees)),
   600  				currencyUnits(rc.UploadSpending),
   601  				currencyUnits(rc.StorageSpending),
   602  				currencyUnits(rc.DownloadSpending),
   603  				currencyUnits(rc.RenterFunds),
   604  				filesizeUnits(int64(rc.Size)))
   605  
   606  			printScoreBreakdown(&hostInfo)
   607  			return
   608  		}
   609  	}
   610  
   611  	fmt.Println("Contract not found")
   612  }
   613  
   614  // renterfilesdeletecmd is the handler for the command `siac renter delete [path]`.
   615  // Removes the specified path from the Sia network.
   616  func renterfilesdeletecmd(path string) {
   617  	err := httpClient.RenterDeletePost(path)
   618  	if err != nil {
   619  		die("Could not delete file:", err)
   620  	}
   621  	fmt.Println("Deleted", path)
   622  }
   623  
   624  // renterfilesdownloadcmd is the handler for the comand `siac renter download [path] [destination]`.
   625  // Downloads a path from the Sia network to the local specified destination.
   626  func renterfilesdownloadcmd(path, destination string) {
   627  	destination = abs(destination)
   628  
   629  	// Queue the download. An error will be returned if the queueing failed, but
   630  	// the call will return before the download has completed. The call is made
   631  	// as an async call.
   632  	err := httpClient.RenterDownloadFullGet(path, destination, true)
   633  	if err != nil {
   634  		die("Download could not be started:", err)
   635  	}
   636  
   637  	// If the download is async, report success.
   638  	if renterDownloadAsync {
   639  		fmt.Printf("Queued Download '%s' to %s.\n", path, abs(destination))
   640  		return
   641  	}
   642  
   643  	// If the download is blocking, display progress as the file downloads.
   644  	err = downloadprogress(path, destination)
   645  	if err != nil {
   646  		die("\nDownload could not be completed:", err)
   647  	}
   648  	fmt.Printf("\nDownloaded '%s' to '%s'.\n", path, abs(destination))
   649  }
   650  
   651  // bandwidthUnit takes bps (bits per second) as an argument and converts
   652  // them into a more human-readable string with a unit.
   653  func bandwidthUnit(bps uint64) string {
   654  	units := []string{"Bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps", "Ybps"}
   655  	mag := uint64(1)
   656  	unit := ""
   657  	for _, unit = range units {
   658  		if bps < 1e3*mag {
   659  			break
   660  		} else if unit != units[len(units)-1] {
   661  			// don't want to perform this multiply on the last iter; that
   662  			// would give us 1.235 Ybps instead of 1235 Ybps
   663  			mag *= 1e3
   664  		}
   665  
   666  	}
   667  	return fmt.Sprintf("%.2f %s", float64(bps)/float64(mag), unit)
   668  }
   669  
   670  // downloadprogress will display the progress of the provided download to the
   671  // user, and return an error when the download is finished.
   672  func downloadprogress(siapath, destination string) error {
   673  	start := time.Now()
   674  
   675  	// helper type used for measurements.
   676  	type measurement struct {
   677  		progress uint64
   678  		time     time.Time
   679  	}
   680  
   681  	// initialize measurementswith a first measurement of 0 progress.
   682  	measurements := []measurement{{
   683  		progress: 0,
   684  		time:     time.Now(),
   685  	}}
   686  	for range time.Tick(OutputRefreshRate) {
   687  		// Get the list of downloads.
   688  		queue, err := httpClient.RenterDownloadsGet()
   689  		if err != nil {
   690  			continue // benign
   691  		}
   692  
   693  		// Search for the download in the list of downloads.
   694  		var d api.DownloadInfo
   695  		found := false
   696  		for _, d = range queue.Downloads {
   697  			if d.SiaPath == siapath && d.Destination == destination {
   698  				found = true
   699  				break
   700  			}
   701  		}
   702  		// If the download has not appeared in the queue yet, either continue or
   703  		// give up.
   704  		if !found {
   705  			if time.Since(start) > RenterDownloadTimeout {
   706  				return errors.New("Unable to find download in queue")
   707  			}
   708  			continue
   709  		}
   710  
   711  		// Check whether the file has completed or otherwise errored out.
   712  		if d.Error != "" {
   713  			return errors.New(d.Error)
   714  		}
   715  		if d.Completed {
   716  			return nil
   717  		}
   718  
   719  		// Add the current progress to the measurements.
   720  		measurements = append(measurements, measurement{
   721  			progress: d.Received,
   722  			time:     time.Now(),
   723  		})
   724  
   725  		// Shrink the measurements to only contain measurements from within the
   726  		// SpeedEstimationWindow.
   727  		for len(measurements) > 2 && measurements[len(measurements)-1].time.Sub(measurements[0].time) > SpeedEstimationWindow {
   728  			measurements = measurements[1:]
   729  		}
   730  
   731  		// Compute the progress and timespan between the first and last
   732  		// measurement to get the speed.
   733  		received := float64(measurements[len(measurements)-1].progress - measurements[0].progress)
   734  		timespan := measurements[len(measurements)-1].time.Sub(measurements[0].time)
   735  		speed := bandwidthUnit(uint64((received * 8) / timespan.Seconds()))
   736  
   737  		// Compuate the percentage of completion and time elapsed since the
   738  		// start of the download.
   739  		pct := 100 * float64(d.Received) / float64(d.Filesize)
   740  		elapsed := time.Since(d.StartTime)
   741  		elapsed -= elapsed % time.Second // round to nearest second
   742  
   743  		// Update the progress for the user.
   744  		fmt.Printf("\rDownloading... %5.1f%% of %v, %v elapsed, %s    ", pct, filesizeUnits(int64(d.Filesize)), elapsed, speed)
   745  	}
   746  
   747  	// This code is unreachable, but the compiler requires this to be here.
   748  	return errors.New("ERROR: download progress reached code that should not be reachable")
   749  }
   750  
   751  // bySiaPath implements sort.Interface for [] modules.FileInfo based on the
   752  // SiaPath field.
   753  type bySiaPath []modules.FileInfo
   754  
   755  func (s bySiaPath) Len() int           { return len(s) }
   756  func (s bySiaPath) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   757  func (s bySiaPath) Less(i, j int) bool { return s[i].SiaPath < s[j].SiaPath }
   758  
   759  // renterfileslistcmd is the handler for the command `siac renter list`.
   760  // Lists files known to the renter on the network.
   761  func renterfileslistcmd() {
   762  	var rf api.RenterFiles
   763  	rf, err := httpClient.RenterFilesGet()
   764  	if err != nil {
   765  		die("Could not get file list:", err)
   766  	}
   767  	if len(rf.Files) == 0 {
   768  		fmt.Println("No files have been uploaded.")
   769  		return
   770  	}
   771  	fmt.Print("Tracking ", len(rf.Files), " files:")
   772  	var totalStored uint64
   773  	for _, file := range rf.Files {
   774  		totalStored += file.Filesize
   775  	}
   776  	fmt.Printf(" %9s\n", filesizeUnits(int64(totalStored)))
   777  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   778  	if renterListVerbose {
   779  		fmt.Fprintln(w, "  File size\tAvailable\tUploaded\tProgress\tRedundancy\tRenewing\tOn Disk\tRecoverable\tSia path")
   780  	}
   781  	sort.Sort(bySiaPath(rf.Files))
   782  	for _, file := range rf.Files {
   783  		fmt.Fprintf(w, "  %9s", filesizeUnits(int64(file.Filesize)))
   784  		if renterListVerbose {
   785  			availableStr := yesNo(file.Available)
   786  			renewingStr := yesNo(file.Renewing)
   787  			redundancyStr := fmt.Sprintf("%.2f", file.Redundancy)
   788  			if file.Redundancy == -1 {
   789  				redundancyStr = "-"
   790  			}
   791  			uploadProgressStr := fmt.Sprintf("%.2f%%", file.UploadProgress)
   792  			if file.UploadProgress == -1 {
   793  				uploadProgressStr = "-"
   794  			}
   795  			onDiskStr := yesNo(file.OnDisk)
   796  			recoverableStr := yesNo(file.Recoverable)
   797  			fmt.Fprintf(w, "\t%s\t%9s\t%8s\t%10s\t%s\t%s\t%s", availableStr, filesizeUnits(int64(file.UploadedBytes)), uploadProgressStr, redundancyStr, renewingStr, onDiskStr, recoverableStr)
   798  		}
   799  		fmt.Fprintf(w, "\t%s", file.SiaPath)
   800  		if !renterListVerbose && !file.Available {
   801  			fmt.Fprintf(w, " (uploading, %0.2f%%)", file.UploadProgress)
   802  		}
   803  		fmt.Fprintln(w, "")
   804  	}
   805  	w.Flush()
   806  }
   807  
   808  // renterfilesrenamecmd is the handler for the command `siac renter rename [path] [newpath]`.
   809  // Renames a file on the Sia network.
   810  func renterfilesrenamecmd(path, newpath string) {
   811  	err := httpClient.RenterRenamePost(path, newpath)
   812  	if err != nil {
   813  		die("Could not rename file:", err)
   814  	}
   815  	fmt.Printf("Renamed %s to %s\n", path, newpath)
   816  }
   817  
   818  // renterfilesuploadcmd is the handler for the command `siac renter upload
   819  // [source] [path]`. Uploads the [source] file to [path] on the Sia network.
   820  // If [source] is a directory, all files inside it will be uploaded and named
   821  // relative to [path].
   822  func renterfilesuploadcmd(source, path string) {
   823  	stat, err := os.Stat(source)
   824  	if err != nil {
   825  		die("Could not stat file or folder:", err)
   826  	}
   827  
   828  	if stat.IsDir() {
   829  		// folder
   830  		var files []string
   831  		err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
   832  			if err != nil {
   833  				fmt.Println("Warning: skipping file:", err)
   834  				return nil
   835  			}
   836  			if info.IsDir() {
   837  				return nil
   838  			}
   839  			files = append(files, path)
   840  			return nil
   841  		})
   842  		if err != nil {
   843  			die("Could not read folder:", err)
   844  		} else if len(files) == 0 {
   845  			die("Nothing to upload.")
   846  		}
   847  		for _, file := range files {
   848  			fpath, _ := filepath.Rel(source, file)
   849  			fpath = filepath.Join(path, fpath)
   850  			fpath = filepath.ToSlash(fpath)
   851  			err = httpClient.RenterUploadDefaultPost(abs(file), fpath)
   852  			if err != nil {
   853  				die("Could not upload file:", err)
   854  			}
   855  		}
   856  		fmt.Printf("Uploaded %d files into '%s'.\n", len(files), path)
   857  	} else {
   858  		// single file
   859  		err = httpClient.RenterUploadDefaultPost(abs(source), path)
   860  		if err != nil {
   861  			die("Could not upload file:", err)
   862  		}
   863  		fmt.Printf("Uploaded '%s' as '%s'.\n", abs(source), path)
   864  	}
   865  }
   866  
   867  // renterpricescmd is the handler for the command `siac renter prices`, which
   868  // displays the prices of various storage operations. The user can submit an
   869  // allowance to have the estimate reflect those settings or the user can submit
   870  // nothing
   871  func renterpricescmd(cmd *cobra.Command, args []string) {
   872  	allowance := modules.Allowance{}
   873  
   874  	if len(args) != 0 && len(args) != 4 {
   875  		cmd.UsageFunc()(cmd)
   876  		os.Exit(exitCodeUsage)
   877  	}
   878  	if len(args) > 0 {
   879  		hastings, err := parseCurrency(args[0])
   880  		if err != nil {
   881  			die("Could not parse amount:", err)
   882  		}
   883  		blocks, err := parsePeriod(args[1])
   884  		if err != nil {
   885  			die("Could not parse period:", err)
   886  		}
   887  		_, err = fmt.Sscan(hastings, &allowance.Funds)
   888  		if err != nil {
   889  			die("Could not set allowance funds:", err)
   890  		}
   891  
   892  		_, err = fmt.Sscan(blocks, &allowance.Period)
   893  		if err != nil {
   894  			die("Could not set allowance period:", err)
   895  		}
   896  		hosts, err := strconv.Atoi(args[2])
   897  		if err != nil {
   898  			die("Could not parse host count")
   899  		}
   900  		allowance.Hosts = uint64(hosts)
   901  		renewWindow, err := parsePeriod(args[3])
   902  		if err != nil {
   903  			die("Could not parse renew window")
   904  		}
   905  		_, err = fmt.Sscan(renewWindow, &allowance.RenewWindow)
   906  		if err != nil {
   907  			die("Could not set allowance renew window:", err)
   908  		}
   909  	}
   910  
   911  	rpg, err := httpClient.RenterPricesGet(allowance)
   912  	if err != nil {
   913  		die("Could not read the renter prices:", err)
   914  	}
   915  	periodFactor := uint64(rpg.Allowance.Period / types.BlockHeight(4032))
   916  
   917  	// Display Estimate
   918  	fmt.Println("Renter Prices (estimated):")
   919  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   920  	fmt.Fprintln(w, "\tFees for Creating a Set of Contracts:\t", currencyUnits(rpg.FormContracts))
   921  	fmt.Fprintln(w, "\tDownload 1 TB:\t", currencyUnits(rpg.DownloadTerabyte))
   922  	fmt.Fprintln(w, "\tStore 1 TB for 1 Month:\t", currencyUnits(rpg.StorageTerabyteMonth))
   923  	fmt.Fprintln(w, "\tStore 1 TB for Allowance Period:\t", currencyUnits(rpg.StorageTerabyteMonth.Mul64(periodFactor)))
   924  	fmt.Fprintln(w, "\tUpload 1 TB:\t", currencyUnits(rpg.UploadTerabyte))
   925  	w.Flush()
   926  
   927  	// Display allowance used for estimate
   928  	fmt.Println("\nAllowance used for estimate:")
   929  	fmt.Fprintln(w, "\tFunds:\t", currencyUnits(rpg.Allowance.Funds))
   930  	fmt.Fprintln(w, "\tPeriod:\t", rpg.Allowance.Period)
   931  	fmt.Fprintln(w, "\tHosts:\t", rpg.Allowance.Hosts)
   932  	fmt.Fprintln(w, "\tRenew Window:\t", rpg.Allowance.RenewWindow)
   933  	w.Flush()
   934  }