github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/node/api/renter.go (about)

     1  package api
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"path/filepath"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"SiaPrime/build"
    14  	"SiaPrime/modules"
    15  	"SiaPrime/modules/renter"
    16  	"SiaPrime/types"
    17  	"gitlab.com/NebulousLabs/errors"
    18  
    19  	"github.com/julienschmidt/httprouter"
    20  )
    21  
    22  var (
    23  	// recommendedHosts is the number of hosts that the renter will form
    24  	// contracts with if the value is not specified explicitly in the call to
    25  	// SetSettings.
    26  	recommendedHosts = build.Select(build.Var{
    27  		Standard: uint64(50),
    28  		Dev:      uint64(2),
    29  		Testing:  uint64(4),
    30  	}).(uint64)
    31  
    32  	// requiredHosts specifies the minimum number of hosts that must be set in
    33  	// the renter settings for the renter settings to be valid. This minimum is
    34  	// there to prevent users from shooting themselves in the foot.
    35  	requiredHosts = build.Select(build.Var{
    36  		Standard: uint64(20),
    37  		Dev:      uint64(1),
    38  		Testing:  uint64(1),
    39  	}).(uint64)
    40  
    41  	// requiredParityPieces specifies the minimum number of parity pieces that
    42  	// must be used when uploading a file. This minimum exists to prevent users
    43  	// from shooting themselves in the foot.
    44  	requiredParityPieces = build.Select(build.Var{
    45  		Standard: int(12),
    46  		Dev:      int(0),
    47  		Testing:  int(0),
    48  	}).(int)
    49  
    50  	// requiredRedundancy specifies the minimum redundancy that will be
    51  	// accepted by the renter when uploading a file. This minimum exists to
    52  	// prevent users from shooting themselves in the foot.
    53  	requiredRedundancy = build.Select(build.Var{
    54  		Standard: float64(2),
    55  		Dev:      float64(1),
    56  		Testing:  float64(1),
    57  	}).(float64)
    58  
    59  	// requiredRenewWindow establishes the minimum allowed renew window for the
    60  	// renter settings. This minimum is here to prevent users from shooting
    61  	// themselves in the foot.
    62  	requiredRenewWindow = build.Select(build.Var{
    63  		Standard: types.BlockHeight(288),
    64  		Dev:      types.BlockHeight(1),
    65  		Testing:  types.BlockHeight(1),
    66  	}).(types.BlockHeight)
    67  )
    68  
    69  type (
    70  	// RenterGET contains various renter metrics.
    71  	RenterGET struct {
    72  		Settings         modules.RenterSettings     `json:"settings"`
    73  		FinancialMetrics modules.ContractorSpending `json:"financialmetrics"`
    74  		CurrentPeriod    types.BlockHeight          `json:"currentperiod"`
    75  	}
    76  
    77  	// RenterContract represents a contract formed by the renter.
    78  	RenterContract struct {
    79  		// Amount of contract funds that have been spent on downloads.
    80  		DownloadSpending types.Currency `json:"downloadspending"`
    81  		// Block height that the file contract ends on.
    82  		EndHeight types.BlockHeight `json:"endheight"`
    83  		// Fees paid in order to form the file contract.
    84  		Fees types.Currency `json:"fees"`
    85  		// Public key of the host the contract was formed with.
    86  		HostPublicKey types.SiaPublicKey `json:"hostpublickey"`
    87  		// ID of the file contract.
    88  		ID types.FileContractID `json:"id"`
    89  		// A signed transaction containing the most recent contract revision.
    90  		LastTransaction types.Transaction `json:"lasttransaction"`
    91  		// Address of the host the file contract was formed with.
    92  		NetAddress modules.NetAddress `json:"netaddress"`
    93  		// Remaining funds left for the renter to spend on uploads & downloads.
    94  		RenterFunds types.Currency `json:"renterfunds"`
    95  		// Size of the file contract, which is typically equal to the number of
    96  		// bytes that have been uploaded to the host.
    97  		Size uint64 `json:"size"`
    98  		// Block height that the file contract began on.
    99  		StartHeight types.BlockHeight `json:"startheight"`
   100  		// Amount of contract funds that have been spent on storage.
   101  		StorageSpending types.Currency `json:"storagespending"`
   102  		// DEPRECATED: This is the exact same value as StorageSpending, but it has
   103  		// incorrect capitalization. This was fixed in 1.3.2, but this field is kept
   104  		// to preserve backwards compatibility on clients who depend on the
   105  		// incorrect capitalization. This field will be removed in the future, so
   106  		// clients should switch to the StorageSpending field (above) with the
   107  		// correct lowercase name.
   108  		StorageSpendingDeprecated types.Currency `json:"StorageSpending"`
   109  		// Total cost to the wallet of forming the file contract.
   110  		TotalCost types.Currency `json:"totalcost"`
   111  		// Amount of contract funds that have been spent on uploads.
   112  		UploadSpending types.Currency `json:"uploadspending"`
   113  		// Signals if contract is good for uploading data
   114  		GoodForUpload bool `json:"goodforupload"`
   115  		// Signals if contract is good for a renewal
   116  		GoodForRenew bool `json:"goodforrenew"`
   117  	}
   118  
   119  	// RenterContracts contains the renter's contracts.
   120  	RenterContracts struct {
   121  		Contracts         []RenterContract `json:"contracts"`
   122  		ActiveContracts   []RenterContract `json:"activecontracts"`
   123  		InactiveContracts []RenterContract `json:"inactivecontracts"`
   124  		ExpiredContracts  []RenterContract `json:"expiredcontracts"`
   125  	}
   126  
   127  	// RenterDownloadQueue contains the renter's download queue.
   128  	RenterDownloadQueue struct {
   129  		Downloads []DownloadInfo `json:"downloads"`
   130  	}
   131  
   132  	// RenterFile lists the file queried.
   133  	RenterFile struct {
   134  		File modules.FileInfo `json:"file"`
   135  	}
   136  
   137  	// RenterFiles lists the files known to the renter.
   138  	RenterFiles struct {
   139  		Files []modules.FileInfo `json:"files"`
   140  	}
   141  
   142  	// RenterLoad lists files that were loaded into the renter.
   143  	RenterLoad struct {
   144  		FilesAdded []string `json:"filesadded"`
   145  	}
   146  
   147  	// RenterPricesGET lists the data that is returned when a GET call is made
   148  	// to /renter/prices.
   149  	RenterPricesGET struct {
   150  		modules.RenterPriceEstimation
   151  		modules.Allowance
   152  	}
   153  
   154  	// RenterShareASCII contains an ASCII-encoded .sia file.
   155  	RenterShareASCII struct {
   156  		ASCIIsia string `json:"asciisia"`
   157  	}
   158  
   159  	// DownloadInfo contains all client-facing information of a file.
   160  	DownloadInfo struct {
   161  		Destination     string `json:"destination"`     // The destination of the download.
   162  		DestinationType string `json:"destinationtype"` // Can be "file", "memory buffer", or "http stream".
   163  		Filesize        uint64 `json:"filesize"`        // DEPRECATED. Same as 'Length'.
   164  		Length          uint64 `json:"length"`          // The length requested for the download.
   165  		Offset          uint64 `json:"offset"`          // The offset within the siafile requested for the download.
   166  		SiaPath         string `json:"siapath"`         // The siapath of the file used for the download.
   167  
   168  		Completed            bool      `json:"completed"`            // Whether or not the download has completed.
   169  		EndTime              time.Time `json:"endtime"`              // The time when the download fully completed.
   170  		Error                string    `json:"error"`                // Will be the empty string unless there was an error.
   171  		Received             uint64    `json:"received"`             // Amount of data confirmed and decoded.
   172  		StartTime            time.Time `json:"starttime"`            // The time when the download was started.
   173  		StartTimeUnix        int64     `json:"starttimeunix"`        // The time when the download was started in unix format.
   174  		TotalDataTransferred uint64    `json:"totaldatatransferred"` // The total amount of data transferred, including negotiation, overdrive etc.
   175  	}
   176  )
   177  
   178  // renterHandlerGET handles the API call to /renter.
   179  func (api *API) renterHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   180  	settings := api.renter.Settings()
   181  	periodStart := api.renter.CurrentPeriod()
   182  	WriteJSON(w, RenterGET{
   183  		Settings:         settings,
   184  		FinancialMetrics: api.renter.PeriodSpending(),
   185  		CurrentPeriod:    periodStart,
   186  	})
   187  }
   188  
   189  // renterHandlerPOST handles the API call to set the Renter's settings.
   190  func (api *API) renterHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   191  	// Get the existing settings
   192  	settings := api.renter.Settings()
   193  
   194  	// Scan the allowance amount. (optional parameter)
   195  	if f := req.FormValue("funds"); f != "" {
   196  		funds, ok := scanAmount(f)
   197  		if !ok {
   198  			WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest)
   199  			return
   200  		}
   201  		settings.Allowance.Funds = funds
   202  	}
   203  	// Scan the number of hosts to use. (optional parameter)
   204  	if h := req.FormValue("hosts"); h != "" {
   205  		var hosts uint64
   206  		if _, err := fmt.Sscan(h, &hosts); err != nil {
   207  			WriteError(w, Error{"unable to parse hosts: " + err.Error()}, http.StatusBadRequest)
   208  			return
   209  		} else if hosts != 0 && hosts < requiredHosts {
   210  			WriteError(w, Error{fmt.Sprintf("insufficient number of hosts, need at least %v but have %v", recommendedHosts, hosts)}, http.StatusBadRequest)
   211  		} else {
   212  			settings.Allowance.Hosts = hosts
   213  		}
   214  	} else if settings.Allowance.Hosts == 0 {
   215  		// Sane defaults if host haven't been set before.
   216  		settings.Allowance.Hosts = recommendedHosts
   217  	}
   218  	// Scan the period. (optional parameter)
   219  	if p := req.FormValue("period"); p != "" {
   220  		var period types.BlockHeight
   221  		if _, err := fmt.Sscan(p, &period); err != nil {
   222  			WriteError(w, Error{"unable to parse period: " + err.Error()}, http.StatusBadRequest)
   223  			return
   224  		}
   225  		settings.Allowance.Period = types.BlockHeight(period)
   226  	} else if settings.Allowance.Period == 0 {
   227  		WriteError(w, Error{"period needs to be set if it hasn't been set before"}, http.StatusBadRequest)
   228  		return
   229  	}
   230  	// Scan the renew window. (optional parameter)
   231  	if rw := req.FormValue("renewwindow"); rw != "" {
   232  		var renewWindow types.BlockHeight
   233  		if _, err := fmt.Sscan(rw, &renewWindow); err != nil {
   234  			WriteError(w, Error{"unable to parse renewwindow: " + err.Error()}, http.StatusBadRequest)
   235  			return
   236  		} else if renewWindow != 0 && types.BlockHeight(renewWindow) < requiredRenewWindow {
   237  			WriteError(w, Error{fmt.Sprintf("renew window is too small, must be at least %v blocks but have %v blocks", requiredRenewWindow, renewWindow)}, http.StatusBadRequest)
   238  			return
   239  		} else {
   240  			settings.Allowance.RenewWindow = types.BlockHeight(renewWindow)
   241  		}
   242  	} else if settings.Allowance.RenewWindow == 0 {
   243  		// Sane defaults if renew window hasn't been set before.
   244  		settings.Allowance.RenewWindow = settings.Allowance.Period / 2
   245  	}
   246  	// Scan the download speed limit. (optional parameter)
   247  	if d := req.FormValue("maxdownloadspeed"); d != "" {
   248  		var downloadSpeed int64
   249  		if _, err := fmt.Sscan(d, &downloadSpeed); err != nil {
   250  			WriteError(w, Error{"unable to parse downloadspeed: " + err.Error()}, http.StatusBadRequest)
   251  			return
   252  		}
   253  		settings.MaxDownloadSpeed = downloadSpeed
   254  	}
   255  	// Scan the upload speed limit. (optional parameter)
   256  	if u := req.FormValue("maxuploadspeed"); u != "" {
   257  		var uploadSpeed int64
   258  		if _, err := fmt.Sscan(u, &uploadSpeed); err != nil {
   259  			WriteError(w, Error{"unable to parse uploadspeed: " + err.Error()}, http.StatusBadRequest)
   260  			return
   261  		}
   262  		settings.MaxUploadSpeed = uploadSpeed
   263  	}
   264  	// Scan the stream cache size. (optional parameter)
   265  	if dcs := req.FormValue("streamcachesize"); dcs != "" {
   266  		var streamCacheSize uint64
   267  		if _, err := fmt.Sscan(dcs, &streamCacheSize); err != nil {
   268  			WriteError(w, Error{"unable to parse streamcachesize: " + err.Error()}, http.StatusBadRequest)
   269  			return
   270  		}
   271  		settings.StreamCacheSize = streamCacheSize
   272  	}
   273  	// Set the settings in the renter.
   274  	err := api.renter.SetSettings(settings)
   275  	if err != nil {
   276  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   277  		return
   278  	}
   279  	WriteSuccess(w)
   280  }
   281  
   282  // renterContractCancelHandler handles the API call to cancel a specific Renter contract.
   283  func (api *API) renterContractCancelHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   284  	var fcid types.FileContractID
   285  	if err := fcid.LoadString(req.FormValue("id")); err != nil {
   286  		WriteError(w, Error{"unable to parse id:" + err.Error()}, http.StatusBadRequest)
   287  		return
   288  	}
   289  	err := api.renter.CancelContract(fcid)
   290  	if err != nil {
   291  		WriteError(w, Error{"unable to cancel contract:" + err.Error()}, http.StatusBadRequest)
   292  		return
   293  	}
   294  	WriteSuccess(w)
   295  }
   296  
   297  // renterContractsHandler handles the API call to request the Renter's
   298  // contracts.
   299  //
   300  // Active contracts are contracts that the renter is actively using to store
   301  // data and can upload, download, and renew
   302  //
   303  // Inactive contracts are contracts that are not currently being used by the
   304  // renter because they are !goodForRenew, but have endheights that are in the
   305  // future so could potentially become active again
   306  //
   307  // Expired contracts are contracts who's endheights are in the past
   308  func (api *API) renterContractsHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   309  	// Parse flags
   310  	inactive, err := scanBool(req.FormValue("inactive"))
   311  	if err != nil {
   312  		WriteError(w, Error{"unable to parse inactive:" + err.Error()}, http.StatusBadRequest)
   313  		return
   314  	}
   315  	expired, err := scanBool(req.FormValue("expired"))
   316  	if err != nil {
   317  		WriteError(w, Error{"unable to parse expired:" + err.Error()}, http.StatusBadRequest)
   318  		return
   319  	}
   320  
   321  	// Get current block height for reference
   322  	blockHeight := api.cs.Height()
   323  
   324  	// Get active contracts
   325  	contracts := []RenterContract{}
   326  	activeContracts := []RenterContract{}
   327  	inactiveContracts := []RenterContract{}
   328  	expiredContracts := []RenterContract{}
   329  	for _, c := range api.renter.Contracts() {
   330  		var size uint64
   331  		if len(c.Transaction.FileContractRevisions) != 0 {
   332  			size = c.Transaction.FileContractRevisions[0].NewFileSize
   333  		}
   334  
   335  		// Fetch host address
   336  		var netAddress modules.NetAddress
   337  		hdbe, exists := api.renter.Host(c.HostPublicKey)
   338  		if exists {
   339  			netAddress = hdbe.NetAddress
   340  		}
   341  
   342  		// Fetch utilities for contract
   343  		var goodForUpload bool
   344  		var goodForRenew bool
   345  		if utility, ok := api.renter.ContractUtility(c.HostPublicKey); ok {
   346  			goodForUpload = utility.GoodForUpload
   347  			goodForRenew = utility.GoodForRenew
   348  		}
   349  		contract := RenterContract{
   350  			DownloadSpending:          c.DownloadSpending,
   351  			EndHeight:                 c.EndHeight,
   352  			Fees:                      c.TxnFee.Add(c.SiafundFee).Add(c.ContractFee),
   353  			GoodForUpload:             goodForUpload,
   354  			GoodForRenew:              goodForRenew,
   355  			HostPublicKey:             c.HostPublicKey,
   356  			ID:                        c.ID,
   357  			LastTransaction:           c.Transaction,
   358  			NetAddress:                netAddress,
   359  			RenterFunds:               c.RenterFunds,
   360  			Size:                      size,
   361  			StartHeight:               c.StartHeight,
   362  			StorageSpending:           c.StorageSpending,
   363  			StorageSpendingDeprecated: c.StorageSpending,
   364  			TotalCost:                 c.TotalCost,
   365  			UploadSpending:            c.UploadSpending,
   366  		}
   367  		if goodForRenew {
   368  			activeContracts = append(activeContracts, contract)
   369  		} else if inactive && !goodForRenew {
   370  			inactiveContracts = append(inactiveContracts, contract)
   371  		}
   372  		contracts = append(contracts, contract)
   373  	}
   374  
   375  	// Get expired contracts
   376  	if expired || inactive {
   377  		for _, c := range api.renter.OldContracts() {
   378  			var size uint64
   379  			if len(c.Transaction.FileContractRevisions) != 0 {
   380  				size = c.Transaction.FileContractRevisions[0].NewFileSize
   381  			}
   382  
   383  			// Fetch host address
   384  			var netAddress modules.NetAddress
   385  			hdbe, exists := api.renter.Host(c.HostPublicKey)
   386  			if exists {
   387  				netAddress = hdbe.NetAddress
   388  			}
   389  
   390  			// Fetch utilities for contract
   391  			var goodForUpload bool
   392  			var goodForRenew bool
   393  			if utility, ok := api.renter.ContractUtility(c.HostPublicKey); ok {
   394  				goodForUpload = utility.GoodForUpload
   395  				goodForRenew = utility.GoodForRenew
   396  			}
   397  
   398  			contract := RenterContract{
   399  				DownloadSpending:          c.DownloadSpending,
   400  				EndHeight:                 c.EndHeight,
   401  				Fees:                      c.TxnFee.Add(c.SiafundFee).Add(c.ContractFee),
   402  				GoodForUpload:             goodForUpload,
   403  				GoodForRenew:              goodForRenew,
   404  				HostPublicKey:             c.HostPublicKey,
   405  				ID:                        c.ID,
   406  				LastTransaction:           c.Transaction,
   407  				NetAddress:                netAddress,
   408  				RenterFunds:               c.RenterFunds,
   409  				Size:                      size,
   410  				StartHeight:               c.StartHeight,
   411  				StorageSpending:           c.StorageSpending,
   412  				StorageSpendingDeprecated: c.StorageSpending,
   413  				TotalCost:                 c.TotalCost,
   414  				UploadSpending:            c.UploadSpending,
   415  			}
   416  			if expired && c.EndHeight < blockHeight {
   417  				expiredContracts = append(expiredContracts, contract)
   418  			} else if inactive && c.EndHeight >= blockHeight {
   419  				inactiveContracts = append(inactiveContracts, contract)
   420  			}
   421  		}
   422  	}
   423  
   424  	WriteJSON(w, RenterContracts{
   425  		Contracts:         contracts,
   426  		ActiveContracts:   activeContracts,
   427  		InactiveContracts: inactiveContracts,
   428  		ExpiredContracts:  expiredContracts,
   429  	})
   430  }
   431  
   432  // renterClearDownloadsHandler handles the API call to request to clear the download queue.
   433  func (api *API) renterClearDownloadsHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   434  	var afterTime time.Time
   435  	beforeTime := types.EndOfTime
   436  	beforeStr, afterStr := req.FormValue("before"), req.FormValue("after")
   437  	if beforeStr != "" {
   438  		beforeInt, err := strconv.ParseInt(beforeStr, 10, 64)
   439  		if err != nil {
   440  			WriteError(w, Error{"parsing integer value for parameter `before` failed: " + err.Error()}, http.StatusBadRequest)
   441  			return
   442  		}
   443  		beforeTime = time.Unix(0, beforeInt)
   444  	}
   445  	if afterStr != "" {
   446  		afterInt, err := strconv.ParseInt(afterStr, 10, 64)
   447  		if err != nil {
   448  			WriteError(w, Error{"parsing integer value for parameter `after` failed: " + err.Error()}, http.StatusBadRequest)
   449  			return
   450  		}
   451  		afterTime = time.Unix(0, afterInt)
   452  	}
   453  
   454  	err := api.renter.ClearDownloadHistory(afterTime, beforeTime)
   455  	if err != nil {
   456  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   457  		return
   458  	}
   459  	WriteSuccess(w)
   460  }
   461  
   462  // renterDownloadsHandler handles the API call to request the download queue.
   463  func (api *API) renterDownloadsHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
   464  	var downloads []DownloadInfo
   465  	for _, di := range api.renter.DownloadHistory() {
   466  		downloads = append(downloads, DownloadInfo{
   467  			Destination:     di.Destination,
   468  			DestinationType: di.DestinationType,
   469  			Filesize:        di.Length,
   470  			Length:          di.Length,
   471  			Offset:          di.Offset,
   472  			SiaPath:         di.SiaPath,
   473  
   474  			Completed:            di.Completed,
   475  			EndTime:              di.EndTime,
   476  			Error:                di.Error,
   477  			Received:             di.Received,
   478  			StartTime:            di.StartTime,
   479  			StartTimeUnix:        di.StartTimeUnix,
   480  			TotalDataTransferred: di.TotalDataTransferred,
   481  		})
   482  	}
   483  	WriteJSON(w, RenterDownloadQueue{
   484  		Downloads: downloads,
   485  	})
   486  }
   487  
   488  // renterLoadHandler handles the API call to load a '.sia' file.
   489  func (api *API) renterLoadHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   490  	source, err := url.QueryUnescape(req.FormValue("source"))
   491  	if err != nil {
   492  		WriteError(w, Error{"failed to unescape the source path"}, http.StatusBadRequest)
   493  		return
   494  	}
   495  	if !filepath.IsAbs(source) {
   496  		WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest)
   497  		return
   498  	}
   499  
   500  	files, err := api.renter.LoadSharedFiles(source)
   501  	if err != nil {
   502  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   503  		return
   504  	}
   505  
   506  	WriteJSON(w, RenterLoad{FilesAdded: files})
   507  }
   508  
   509  // renterLoadAsciiHandler handles the API call to load a '.sia' file
   510  // in ASCII form.
   511  func (api *API) renterLoadASCIIHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   512  	files, err := api.renter.LoadSharedFilesASCII(req.FormValue("asciisia"))
   513  	if err != nil {
   514  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   515  		return
   516  	}
   517  
   518  	WriteJSON(w, RenterLoad{FilesAdded: files})
   519  }
   520  
   521  // renterRenameHandler handles the API call to rename a file entry in the
   522  // renter.
   523  func (api *API) renterRenameHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   524  	newSiaPath, err := url.QueryUnescape(req.FormValue("newsiapath"))
   525  	if err != nil {
   526  		WriteError(w, Error{"failed to unescape newsiapath"}, http.StatusBadRequest)
   527  		return
   528  	}
   529  	err = api.renter.RenameFile(strings.TrimPrefix(ps.ByName("siapath"), "/"), newSiaPath)
   530  	if err != nil {
   531  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   532  		return
   533  	}
   534  	WriteSuccess(w)
   535  }
   536  
   537  // renterFileHandler handles GET requests to the /renter/file/:siapath API endpoint.
   538  func (api *API) renterFileHandlerGET(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   539  	file, err := api.renter.File(strings.TrimPrefix(ps.ByName("siapath"), "/"))
   540  	if err != nil {
   541  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   542  		return
   543  	}
   544  	WriteJSON(w, RenterFile{
   545  		File: file,
   546  	})
   547  }
   548  
   549  // renterFileHandler handles POST requests to the /renter/file/:siapath API endpoint.
   550  func (api *API) renterFileHandlerPOST(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   551  	newTrackingPath := req.FormValue("trackingpath")
   552  
   553  	// Handle changing the tracking path of a file.
   554  	if newTrackingPath != "" {
   555  		siapath := strings.TrimPrefix(ps.ByName("siapath"), "/")
   556  		if err := api.renter.SetFileTrackingPath(siapath, newTrackingPath); err != nil {
   557  			WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest)
   558  			return
   559  		}
   560  	}
   561  	WriteSuccess(w)
   562  }
   563  
   564  // renterFilesHandler handles the API call to list all of the files.
   565  func (api *API) renterFilesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
   566  	WriteJSON(w, RenterFiles{
   567  		Files: api.renter.FileList(),
   568  	})
   569  }
   570  
   571  // renterPricesHandler reports the expected costs of various actions given the
   572  // renter settings and the set of available hosts.
   573  func (api *API) renterPricesHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   574  	allowance := modules.Allowance{}
   575  	// Scan the allowance amount. (optional parameter)
   576  	if f := req.FormValue("funds"); f != "" {
   577  		funds, ok := scanAmount(f)
   578  		if !ok {
   579  			WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest)
   580  			return
   581  		}
   582  		allowance.Funds = funds
   583  	}
   584  	// Scan the number of hosts to use. (optional parameter)
   585  	if h := req.FormValue("hosts"); h != "" {
   586  		var hosts uint64
   587  		if _, err := fmt.Sscan(h, &hosts); err != nil {
   588  			WriteError(w, Error{"unable to parse hosts: " + err.Error()}, http.StatusBadRequest)
   589  			return
   590  		} else if hosts != 0 && hosts < requiredHosts {
   591  			WriteError(w, Error{fmt.Sprintf("insufficient number of hosts, need at least %v but have %v", recommendedHosts, hosts)}, http.StatusBadRequest)
   592  		} else {
   593  			allowance.Hosts = hosts
   594  		}
   595  	}
   596  	// Scan the period. (optional parameter)
   597  	if p := req.FormValue("period"); p != "" {
   598  		var period types.BlockHeight
   599  		if _, err := fmt.Sscan(p, &period); err != nil {
   600  			WriteError(w, Error{"unable to parse period: " + err.Error()}, http.StatusBadRequest)
   601  			return
   602  		}
   603  		allowance.Period = types.BlockHeight(period)
   604  	}
   605  	// Scan the renew window. (optional parameter)
   606  	if rw := req.FormValue("renewwindow"); rw != "" {
   607  		var renewWindow types.BlockHeight
   608  		if _, err := fmt.Sscan(rw, &renewWindow); err != nil {
   609  			WriteError(w, Error{"unable to parse renewwindow: " + err.Error()}, http.StatusBadRequest)
   610  			return
   611  		} else if renewWindow != 0 && types.BlockHeight(renewWindow) < requiredRenewWindow {
   612  			WriteError(w, Error{fmt.Sprintf("renew window is too small, must be at least %v blocks but have %v blocks", requiredRenewWindow, renewWindow)}, http.StatusBadRequest)
   613  			return
   614  		} else {
   615  			allowance.RenewWindow = types.BlockHeight(renewWindow)
   616  		}
   617  	}
   618  
   619  	// Check for partially set allowance, which can happen since hosts and renew
   620  	// window can be optional fields. Checking here instead of assigning values
   621  	// above so that an empty allowance can still be submitted
   622  	if !reflect.DeepEqual(allowance, modules.Allowance{}) {
   623  		if allowance.Funds.Cmp(types.ZeroCurrency) == 0 {
   624  			WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `funds` parameter left empty")}, http.StatusBadRequest)
   625  			return
   626  		}
   627  		if allowance.Period == 0 {
   628  			WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `period` parameter left empty")}, http.StatusBadRequest)
   629  			return
   630  		}
   631  		if allowance.Hosts == 0 {
   632  			WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `hosts` parameter left empty")}, http.StatusBadRequest)
   633  			return
   634  		}
   635  		if allowance.RenewWindow == 0 {
   636  			WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `renewwindow` parameter left empty")}, http.StatusBadRequest)
   637  			return
   638  		}
   639  	}
   640  
   641  	estimate, a, err := api.renter.PriceEstimation(allowance)
   642  	if err != nil {
   643  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   644  		return
   645  	}
   646  	WriteJSON(w, RenterPricesGET{
   647  		RenterPriceEstimation: estimate,
   648  		Allowance:             a,
   649  	})
   650  }
   651  
   652  // renterDeleteHandler handles the API call to delete a file entry from the
   653  // renter.
   654  func (api *API) renterDeleteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   655  	err := api.renter.DeleteFile(strings.TrimPrefix(ps.ByName("siapath"), "/"))
   656  	if err != nil {
   657  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   658  		return
   659  	}
   660  
   661  	WriteSuccess(w)
   662  }
   663  
   664  // renterDownloadHandler handles the API call to download a file.
   665  func (api *API) renterDownloadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   666  	params, err := parseDownloadParameters(w, req, ps)
   667  	if err != nil {
   668  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   669  		return
   670  	}
   671  	if params.Async {
   672  		err = api.renter.DownloadAsync(params)
   673  	} else {
   674  		err = api.renter.Download(params)
   675  	}
   676  	if err != nil {
   677  		WriteError(w, Error{"download failed: " + err.Error()}, http.StatusInternalServerError)
   678  		return
   679  	}
   680  	if params.Httpwriter == nil {
   681  		// `httpresp=true` causes writes to w before this line is run, automatically
   682  		// adding `200 Status OK` code to response. Calling this results in a
   683  		// multiple calls to WriteHeaders() errors.
   684  		WriteSuccess(w)
   685  		return
   686  	}
   687  }
   688  
   689  // renterDownloadAsyncHandler handles the API call to download a file asynchronously.
   690  func (api *API) renterDownloadAsyncHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   691  	req.ParseForm()
   692  	req.Form.Set("async", "true")
   693  	api.renterDownloadHandler(w, req, ps)
   694  }
   695  
   696  // parseDownloadParameters parses the download parameters passed to the
   697  // /renter/download endpoint. Validation of these parameters is done by the
   698  // renter.
   699  func parseDownloadParameters(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (modules.RenterDownloadParameters, error) {
   700  	destination, err := url.QueryUnescape(req.FormValue("destination"))
   701  	if err != nil {
   702  		return modules.RenterDownloadParameters{}, errors.AddContext(err, "failed to unescape the destination")
   703  	}
   704  
   705  	// The offset and length in bytes.
   706  	offsetparam := req.FormValue("offset")
   707  	lengthparam := req.FormValue("length")
   708  
   709  	// Determines whether the response is written to response body.
   710  	httprespparam := req.FormValue("httpresp")
   711  
   712  	// Determines whether to return on completion of download or straight away.
   713  	// If httprespparam is present, this parameter is ignored.
   714  	asyncparam := req.FormValue("async")
   715  
   716  	// Parse the offset and length parameters.
   717  	var offset, length uint64
   718  	if len(offsetparam) > 0 {
   719  		_, err := fmt.Sscan(offsetparam, &offset)
   720  		if err != nil {
   721  			return modules.RenterDownloadParameters{}, errors.AddContext(err, "could not decode the offset as uint64")
   722  		}
   723  	}
   724  	if len(lengthparam) > 0 {
   725  		_, err := fmt.Sscan(lengthparam, &length)
   726  		if err != nil {
   727  			return modules.RenterDownloadParameters{}, errors.AddContext(err, "could not decode the offset as uint64")
   728  		}
   729  	}
   730  
   731  	// Parse the httpresp parameter.
   732  	httpresp, err := scanBool(httprespparam)
   733  	if err != nil {
   734  		return modules.RenterDownloadParameters{}, errors.AddContext(err, "httpresp parameter could not be parsed")
   735  	}
   736  
   737  	// Parse the async parameter.
   738  	async, err := scanBool(asyncparam)
   739  	if err != nil {
   740  		return modules.RenterDownloadParameters{}, errors.AddContext(err, "async parameter could not be parsed")
   741  	}
   742  
   743  	siapath := strings.TrimPrefix(ps.ByName("siapath"), "/") // Sia file name.
   744  
   745  	dp := modules.RenterDownloadParameters{
   746  		Destination: destination,
   747  		Async:       async,
   748  		Length:      length,
   749  		Offset:      offset,
   750  		SiaPath:     siapath,
   751  	}
   752  	if httpresp {
   753  		dp.Httpwriter = w
   754  	}
   755  
   756  	return dp, nil
   757  }
   758  
   759  // renterShareHandler handles the API call to create a '.sia' file that
   760  // shares a set of file.
   761  func (api *API) renterShareHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   762  	destination, err := url.QueryUnescape(req.FormValue("destination"))
   763  	if err != nil {
   764  		WriteError(w, Error{"failed to unescape the destination path"}, http.StatusBadRequest)
   765  		return
   766  	}
   767  	// Check that the destination path is absolute.
   768  	if !filepath.IsAbs(destination) {
   769  		WriteError(w, Error{"destination must be an absolute path"}, http.StatusBadRequest)
   770  		return
   771  	}
   772  
   773  	err = api.renter.ShareFiles(strings.Split(req.FormValue("siapaths"), ","), destination)
   774  	if err != nil {
   775  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   776  		return
   777  	}
   778  
   779  	WriteSuccess(w)
   780  }
   781  
   782  // renterShareAsciiHandler handles the API call to return a '.sia' file
   783  // in ascii form.
   784  func (api *API) renterShareASCIIHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   785  	ascii, err := api.renter.ShareFilesASCII(strings.Split(req.FormValue("siapaths"), ","))
   786  	if err != nil {
   787  		WriteError(w, Error{err.Error()}, http.StatusBadRequest)
   788  		return
   789  	}
   790  	WriteJSON(w, RenterShareASCII{
   791  		ASCIIsia: ascii,
   792  	})
   793  }
   794  
   795  // renterStreamHandler handles downloads from the /renter/stream endpoint
   796  func (api *API) renterStreamHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   797  	siaPath := strings.TrimPrefix(ps.ByName("siapath"), "/")
   798  	fileName, streamer, err := api.renter.Streamer(siaPath)
   799  	if err != nil {
   800  		WriteError(w, Error{fmt.Sprintf("failed to create download streamer: %v", err)},
   801  			http.StatusInternalServerError)
   802  		return
   803  	}
   804  	http.ServeContent(w, req, fileName, time.Time{}, streamer)
   805  }
   806  
   807  // renterUploadHandler handles the API call to upload a file.
   808  func (api *API) renterUploadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
   809  	source, err := url.QueryUnescape(req.FormValue("source"))
   810  	if err != nil {
   811  		WriteError(w, Error{"failed to unescape the source path"}, http.StatusBadRequest)
   812  		return
   813  	}
   814  	if !filepath.IsAbs(source) {
   815  		WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest)
   816  		return
   817  	}
   818  
   819  	// Check whether the erasure coding parameters have been supplied.
   820  	var ec modules.ErasureCoder
   821  	if req.FormValue("datapieces") != "" || req.FormValue("paritypieces") != "" {
   822  		// Check that both values have been supplied.
   823  		if req.FormValue("datapieces") == "" || req.FormValue("paritypieces") == "" {
   824  			WriteError(w, Error{"must provide both the datapieces parameter and the paritypieces parameter if specifying erasure coding parameters"}, http.StatusBadRequest)
   825  			return
   826  		}
   827  
   828  		// Parse the erasure coding parameters.
   829  		var dataPieces, parityPieces int
   830  		_, err := fmt.Sscan(req.FormValue("datapieces"), &dataPieces)
   831  		if err != nil {
   832  			WriteError(w, Error{"unable to read parameter 'datapieces': " + err.Error()}, http.StatusBadRequest)
   833  			return
   834  		}
   835  		_, err = fmt.Sscan(req.FormValue("paritypieces"), &parityPieces)
   836  		if err != nil {
   837  			WriteError(w, Error{"unable to read parameter 'paritypieces': " + err.Error()}, http.StatusBadRequest)
   838  			return
   839  		}
   840  
   841  		// Verify that sane values for parityPieces and redundancy are being
   842  		// supplied.
   843  		if parityPieces < requiredParityPieces {
   844  			WriteError(w, Error{fmt.Sprintf("a minimum of %v parity pieces is required, but %v parity pieces requested", parityPieces, requiredParityPieces)}, http.StatusBadRequest)
   845  			return
   846  		}
   847  		redundancy := float64(dataPieces+parityPieces) / float64(dataPieces)
   848  		if float64(dataPieces+parityPieces)/float64(dataPieces) < requiredRedundancy {
   849  			WriteError(w, Error{fmt.Sprintf("a redundancy of %.2f is required, but redundancy of %.2f supplied", redundancy, requiredRedundancy)}, http.StatusBadRequest)
   850  			return
   851  		}
   852  
   853  		// Create the erasure coder.
   854  		ec, err = renter.NewRSCode(dataPieces, parityPieces)
   855  		if err != nil {
   856  			WriteError(w, Error{"unable to encode file using the provided parameters: " + err.Error()}, http.StatusBadRequest)
   857  			return
   858  		}
   859  	}
   860  
   861  	// Call the renter to upload the file.
   862  	err = api.renter.Upload(modules.FileUploadParams{
   863  		Source:      source,
   864  		SiaPath:     strings.TrimPrefix(ps.ByName("siapath"), "/"),
   865  		ErasureCode: ec,
   866  	})
   867  	if err != nil {
   868  		WriteError(w, Error{"upload failed: " + err.Error()}, http.StatusInternalServerError)
   869  		return
   870  	}
   871  	WriteSuccess(w)
   872  }