github.com/dmmcquay/sia@v1.3.1-0.20180712220038-9f8d535311b9/cmd/siac/rentercmd.go (about)

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