gitlab.com/jokerrs1/Sia@v1.3.2/node/api/renter.go (about) 1 package api 2 3 import ( 4 "fmt" 5 "net/http" 6 "path/filepath" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/NebulousLabs/Sia/build" 12 "github.com/NebulousLabs/Sia/modules" 13 "github.com/NebulousLabs/Sia/modules/renter" 14 "github.com/NebulousLabs/Sia/types" 15 16 "github.com/julienschmidt/httprouter" 17 ) 18 19 var ( 20 // recommendedHosts is the number of hosts that the renter will form 21 // contracts with if the value is not specified explicitly in the call to 22 // SetSettings. 23 recommendedHosts = build.Select(build.Var{ 24 Standard: uint64(50), 25 Dev: uint64(2), 26 Testing: uint64(1), 27 }).(uint64) 28 29 // requiredHosts specifies the minimum number of hosts that must be set in 30 // the renter settings for the renter settings to be valid. This minimum is 31 // there to prevent users from shooting themselves in the foot. 32 requiredHosts = build.Select(build.Var{ 33 Standard: uint64(20), 34 Dev: uint64(1), 35 Testing: uint64(1), 36 }).(uint64) 37 38 // requiredParityPieces specifies the minimum number of parity pieces that 39 // must be used when uploading a file. This minimum exists to prevent users 40 // from shooting themselves in the foot. 41 requiredParityPieces = build.Select(build.Var{ 42 Standard: int(12), 43 Dev: int(0), 44 Testing: int(0), 45 }).(int) 46 47 // requiredRedundancy specifies the minimum redundancy that will be 48 // accepted by the renter when uploading a file. This minimum exists to 49 // prevent users from shooting themselves in the foot. 50 requiredRedundancy = build.Select(build.Var{ 51 Standard: float64(2), 52 Dev: float64(1), 53 Testing: float64(1), 54 }).(float64) 55 56 // requiredRenewWindow establishes the minimum allowed renew window for the 57 // renter settings. This minimum is here to prevent users from shooting 58 // themselves in the foot. 59 requiredRenewWindow = build.Select(build.Var{ 60 Standard: types.BlockHeight(288), 61 Dev: types.BlockHeight(1), 62 Testing: types.BlockHeight(1), 63 }).(types.BlockHeight) 64 ) 65 66 type ( 67 // RenterGET contains various renter metrics. 68 RenterGET struct { 69 Settings modules.RenterSettings `json:"settings"` 70 FinancialMetrics modules.ContractorSpending `json:"financialmetrics"` 71 CurrentPeriod types.BlockHeight `json:"currentperiod"` 72 } 73 74 // RenterContract represents a contract formed by the renter. 75 RenterContract struct { 76 // Amount of contract funds that have been spent on downloads. 77 DownloadSpending types.Currency `json:"downloadspending"` 78 // Block height that the file contract ends on. 79 EndHeight types.BlockHeight `json:"endheight"` 80 // Fees paid in order to form the file contract. 81 Fees types.Currency `json:"fees"` 82 // Public key of the host the contract was formed with. 83 HostPublicKey types.SiaPublicKey `json:"hostpublickey"` 84 // ID of the file contract. 85 ID types.FileContractID `json:"id"` 86 // A signed transaction containing the most recent contract revision. 87 LastTransaction types.Transaction `json:"lasttransaction"` 88 // Address of the host the file contract was formed with. 89 NetAddress modules.NetAddress `json:"netaddress"` 90 // Remaining funds left for the renter to spend on uploads & downloads. 91 RenterFunds types.Currency `json:"renterfunds"` 92 // Size of the file contract, which is typically equal to the number of 93 // bytes that have been uploaded to the host. 94 Size uint64 `json:"size"` 95 // Block height that the file contract began on. 96 StartHeight types.BlockHeight `json:"startheight"` 97 // Amount of contract funds that have been spent on storage. 98 StorageSpending types.Currency `json:"storagespending"` 99 // DEPRECATED: This is the exact same value as StorageSpending, but it has 100 // incorrect capitalization. This was fixed in 1.3.2, but this field is kept 101 // to preserve backwards compatibility on clients who depend on the 102 // incorrect capitalization. This field will be removed in the future, so 103 // clients should switch to the StorageSpending field (above) with the 104 // correct lowercase name. 105 StorageSpendingDeprecated types.Currency `json:"StorageSpending"` 106 // Total cost to the wallet of forming the file contract. 107 TotalCost types.Currency `json:"totalcost"` 108 // Amount of contract funds that have been spent on uploads. 109 UploadSpending types.Currency `json:"uploadspending"` 110 // Signals if contract is good for uploading data 111 GoodForUpload bool `json:"goodforupload"` 112 // Signals if contract is good for a renewal 113 GoodForRenew bool `json:"goodforrenew"` 114 } 115 116 // RenterContracts contains the renter's contracts. 117 RenterContracts struct { 118 Contracts []RenterContract `json:"contracts"` 119 } 120 121 // RenterDownloadQueue contains the renter's download queue. 122 RenterDownloadQueue struct { 123 Downloads []DownloadInfo `json:"downloads"` 124 } 125 126 // RenterFiles lists the files known to the renter. 127 RenterFiles struct { 128 Files []modules.FileInfo `json:"files"` 129 } 130 131 // RenterLoad lists files that were loaded into the renter. 132 RenterLoad struct { 133 FilesAdded []string `json:"filesadded"` 134 } 135 136 // RenterPricesGET lists the data that is returned when a GET call is made 137 // to /renter/prices. 138 RenterPricesGET struct { 139 modules.RenterPriceEstimation 140 } 141 142 // RenterShareASCII contains an ASCII-encoded .sia file. 143 RenterShareASCII struct { 144 ASCIIsia string `json:"asciisia"` 145 } 146 147 // DownloadInfo contains all client-facing information of a file. 148 DownloadInfo struct { 149 Destination string `json:"destination"` // The destination of the download. 150 DestinationType string `json:"destinationtype"` // Can be "file", "memory buffer", or "http stream". 151 Filesize uint64 `json:"filesize"` // DEPRECATED. Same as 'Length'. 152 Length uint64 `json:"length"` // The length requested for the download. 153 Offset uint64 `json:"offset"` // The offset within the siafile requested for the download. 154 SiaPath string `json:"siapath"` // The siapath of the file used for the download. 155 156 Completed bool `json:"completed"` // Whether or not the download has completed. 157 EndTime time.Time `json:"endtime"` // The time when the download fully completed. 158 Error string `json:"error"` // Will be the empty string unless there was an error. 159 Received uint64 `json:"received"` // Amount of data confirmed and decoded. 160 StartTime time.Time `json:"starttime"` // The time when the download was started. 161 TotalDataTransferred uint64 `json:"totaldatatransferred"` // The total amount of data transferred, including negotiation, overdrive etc. 162 } 163 ) 164 165 // renterHandlerGET handles the API call to /renter. 166 func (api *API) renterHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 167 settings := api.renter.Settings() 168 periodStart := api.renter.CurrentPeriod() 169 WriteJSON(w, RenterGET{ 170 Settings: settings, 171 FinancialMetrics: api.renter.PeriodSpending(), 172 CurrentPeriod: periodStart, 173 }) 174 } 175 176 // renterHandlerPOST handles the API call to set the Renter's settings. 177 func (api *API) renterHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 178 // Get the existing settings 179 settings := api.renter.Settings() 180 181 // Scan the allowance amount. (optional parameter) 182 if f := req.FormValue("funds"); f != "" { 183 funds, ok := scanAmount(f) 184 if !ok { 185 WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest) 186 return 187 } 188 settings.Allowance.Funds = funds 189 } 190 // Scan the number of hosts to use. (optional parameter) 191 if h := req.FormValue("hosts"); h != "" { 192 var hosts uint64 193 if _, err := fmt.Sscan(h, &hosts); err != nil { 194 WriteError(w, Error{"unable to parse hosts: " + err.Error()}, http.StatusBadRequest) 195 return 196 } else if hosts != 0 && hosts < requiredHosts { 197 WriteError(w, Error{fmt.Sprintf("insufficient number of hosts, need at least %v but have %v", recommendedHosts, hosts)}, http.StatusBadRequest) 198 } else { 199 settings.Allowance.Hosts = hosts 200 } 201 } else if settings.Allowance.Hosts == 0 { 202 // Sane defaults if host haven't been set before. 203 settings.Allowance.Hosts = recommendedHosts 204 } 205 // Scan the period. (optional parameter) 206 if p := req.FormValue("period"); p != "" { 207 var period types.BlockHeight 208 if _, err := fmt.Sscan(p, &period); err != nil { 209 WriteError(w, Error{"unable to parse period: " + err.Error()}, http.StatusBadRequest) 210 return 211 } 212 settings.Allowance.Period = types.BlockHeight(period) 213 } else if settings.Allowance.Period == 0 { 214 WriteError(w, Error{"period needs to be set if it hasn't been set before"}, http.StatusBadRequest) 215 return 216 } 217 // Scan the renew window. (optional parameter) 218 if rw := req.FormValue("renewwindow"); rw != "" { 219 var renewWindow types.BlockHeight 220 if _, err := fmt.Sscan(rw, &renewWindow); err != nil { 221 WriteError(w, Error{"unable to parse renewwindow: " + err.Error()}, http.StatusBadRequest) 222 return 223 } else if renewWindow != 0 && types.BlockHeight(renewWindow) < requiredRenewWindow { 224 WriteError(w, Error{fmt.Sprintf("renew window is too small, must be at least %v blocks but have %v blocks", requiredRenewWindow, renewWindow)}, http.StatusBadRequest) 225 return 226 } else { 227 settings.Allowance.RenewWindow = types.BlockHeight(renewWindow) 228 } 229 } else if settings.Allowance.RenewWindow == 0 { 230 // Sane defaults if renew window hasn't been set before. 231 settings.Allowance.RenewWindow = settings.Allowance.Period / 2 232 } 233 // Scan the download speed limit. (optional parameter) 234 if d := req.FormValue("maxdownloadspeed"); d != "" { 235 var downloadSpeed int64 236 if _, err := fmt.Sscan(d, &downloadSpeed); err != nil { 237 WriteError(w, Error{"unable to parse downloadspeed: " + err.Error()}, http.StatusBadRequest) 238 return 239 } 240 settings.MaxDownloadSpeed = downloadSpeed 241 } 242 // Scan the upload speed limit. (optional parameter) 243 if u := req.FormValue("maxuploadspeed"); u != "" { 244 var uploadSpeed int64 245 if _, err := fmt.Sscan(u, &uploadSpeed); err != nil { 246 WriteError(w, Error{"unable to parse uploadspeed: " + err.Error()}, http.StatusBadRequest) 247 return 248 } 249 settings.MaxUploadSpeed = uploadSpeed 250 } 251 // Set the settings in the renter. 252 err := api.renter.SetSettings(settings) 253 if err != nil { 254 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 255 return 256 } 257 WriteSuccess(w) 258 } 259 260 // renterContractsHandler handles the API call to request the Renter's contracts. 261 func (api *API) renterContractsHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { 262 contracts := []RenterContract{} 263 for _, c := range api.renter.Contracts() { 264 var size uint64 265 if len(c.Transaction.FileContractRevisions) != 0 { 266 size = c.Transaction.FileContractRevisions[0].NewFileSize 267 } 268 269 // Fetch host address 270 var netAddress modules.NetAddress 271 hdbe, exists := api.renter.Host(c.HostPublicKey) 272 if exists { 273 netAddress = hdbe.NetAddress 274 } 275 276 // Fetch utilities for contract 277 var goodForUpload bool 278 var goodForRenew bool 279 if utility, ok := api.renter.ContractUtility(c.ID); ok { 280 goodForUpload = utility.GoodForUpload 281 goodForRenew = utility.GoodForRenew 282 } 283 284 contracts = append(contracts, RenterContract{ 285 DownloadSpending: c.DownloadSpending, 286 EndHeight: c.EndHeight, 287 Fees: c.TxnFee.Add(c.SiafundFee).Add(c.ContractFee), 288 GoodForUpload: goodForUpload, 289 GoodForRenew: goodForRenew, 290 HostPublicKey: c.HostPublicKey, 291 ID: c.ID, 292 LastTransaction: c.Transaction, 293 NetAddress: netAddress, 294 RenterFunds: c.RenterFunds, 295 Size: size, 296 StartHeight: c.StartHeight, 297 StorageSpending: c.StorageSpending, 298 StorageSpendingDeprecated: c.StorageSpending, 299 TotalCost: c.TotalCost, 300 UploadSpending: c.UploadSpending, 301 }) 302 } 303 WriteJSON(w, RenterContracts{ 304 Contracts: contracts, 305 }) 306 } 307 308 // renterDownloadsHandler handles the API call to request the download queue. 309 func (api *API) renterDownloadsHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { 310 var downloads []DownloadInfo 311 for _, di := range api.renter.DownloadHistory() { 312 downloads = append(downloads, DownloadInfo{ 313 Destination: di.Destination, 314 DestinationType: di.DestinationType, 315 Filesize: di.Length, 316 Length: di.Length, 317 Offset: di.Offset, 318 SiaPath: di.SiaPath, 319 320 Completed: di.Completed, 321 EndTime: di.EndTime, 322 Error: di.Error, 323 Received: di.Received, 324 StartTime: di.StartTime, 325 TotalDataTransferred: di.TotalDataTransferred, 326 }) 327 } 328 // sort the downloads by newest first 329 sort.Slice(downloads, func(i, j int) bool { return downloads[i].StartTime.After(downloads[j].StartTime) }) 330 WriteJSON(w, RenterDownloadQueue{ 331 Downloads: downloads, 332 }) 333 } 334 335 // renterLoadHandler handles the API call to load a '.sia' file. 336 func (api *API) renterLoadHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 337 source := req.FormValue("source") 338 if !filepath.IsAbs(source) { 339 WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest) 340 return 341 } 342 343 files, err := api.renter.LoadSharedFiles(source) 344 if err != nil { 345 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 346 return 347 } 348 349 WriteJSON(w, RenterLoad{FilesAdded: files}) 350 } 351 352 // renterLoadAsciiHandler handles the API call to load a '.sia' file 353 // in ASCII form. 354 func (api *API) renterLoadASCIIHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 355 files, err := api.renter.LoadSharedFilesASCII(req.FormValue("asciisia")) 356 if err != nil { 357 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 358 return 359 } 360 361 WriteJSON(w, RenterLoad{FilesAdded: files}) 362 } 363 364 // renterRenameHandler handles the API call to rename a file entry in the 365 // renter. 366 func (api *API) renterRenameHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 367 err := api.renter.RenameFile(strings.TrimPrefix(ps.ByName("siapath"), "/"), req.FormValue("newsiapath")) 368 if err != nil { 369 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 370 return 371 } 372 373 WriteSuccess(w) 374 } 375 376 // renterFilesHandler handles the API call to list all of the files. 377 func (api *API) renterFilesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 378 WriteJSON(w, RenterFiles{ 379 Files: api.renter.FileList(), 380 }) 381 } 382 383 // renterPricesHandler reports the expected costs of various actions given the 384 // renter settings and the set of available hosts. 385 func (api *API) renterPricesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 386 WriteJSON(w, RenterPricesGET{ 387 RenterPriceEstimation: api.renter.PriceEstimation(), 388 }) 389 } 390 391 // renterDeleteHandler handles the API call to delete a file entry from the 392 // renter. 393 func (api *API) renterDeleteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 394 err := api.renter.DeleteFile(strings.TrimPrefix(ps.ByName("siapath"), "/")) 395 if err != nil { 396 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 397 return 398 } 399 400 WriteSuccess(w) 401 } 402 403 // renterDownloadHandler handles the API call to download a file. 404 func (api *API) renterDownloadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 405 params, err := parseDownloadParameters(w, req, ps) 406 if err != nil { 407 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 408 return 409 } 410 411 if params.Async { // Create goroutine if `async` param set. 412 // check for errors for 5 seconds to catch validation errors (no file with 413 // that path, invalid parameters, insufficient hosts, etc) 414 errchan := make(chan error) 415 go func() { 416 errchan <- api.renter.Download(params) 417 }() 418 select { 419 case err = <-errchan: 420 if err != nil { 421 WriteError(w, Error{"download failed: " + err.Error()}, http.StatusInternalServerError) 422 return 423 } 424 case <-time.After(time.Millisecond * 100): 425 } 426 } else { 427 err := api.renter.Download(params) 428 if err != nil { 429 WriteError(w, Error{"download failed: " + err.Error()}, http.StatusInternalServerError) 430 return 431 } 432 } 433 434 if params.Httpwriter == nil { 435 // `httpresp=true` causes writes to w before this line is run, automatically 436 // adding `200 Status OK` code to response. Calling this results in a 437 // multiple calls to WriteHeaders() errors. 438 WriteSuccess(w) 439 return 440 } 441 } 442 443 // renterDownloadAsyncHandler handles the API call to download a file asynchronously. 444 func (api *API) renterDownloadAsyncHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 445 req.ParseForm() 446 req.Form.Set("async", "true") 447 api.renterDownloadHandler(w, req, ps) 448 } 449 450 // parseDownloadParameters parses the download parameters passed to the 451 // /renter/download endpoint. Validation of these parameters is done by the 452 // renter. 453 func parseDownloadParameters(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (modules.RenterDownloadParameters, error) { 454 destination := req.FormValue("destination") 455 456 // The offset and length in bytes. 457 offsetparam := req.FormValue("offset") 458 lengthparam := req.FormValue("length") 459 460 // Determines whether the response is written to response body. 461 httprespparam := req.FormValue("httpresp") 462 463 // Determines whether to return on completion of download or straight away. 464 // If httprespparam is present, this parameter is ignored. 465 asyncparam := req.FormValue("async") 466 467 // Parse the offset and length parameters. 468 var offset, length uint64 469 if len(offsetparam) > 0 { 470 _, err := fmt.Sscan(offsetparam, &offset) 471 if err != nil { 472 return modules.RenterDownloadParameters{}, build.ExtendErr("could not decode the offset as uint64: ", err) 473 } 474 } 475 if len(lengthparam) > 0 { 476 _, err := fmt.Sscan(lengthparam, &length) 477 if err != nil { 478 return modules.RenterDownloadParameters{}, build.ExtendErr("could not decode the offset as uint64: ", err) 479 } 480 } 481 482 // Parse the httpresp parameter. 483 httpresp, err := scanBool(httprespparam) 484 if err != nil { 485 return modules.RenterDownloadParameters{}, build.ExtendErr("httpresp parameter could not be parsed", err) 486 } 487 488 // Parse the async parameter. 489 async, err := scanBool(asyncparam) 490 if err != nil { 491 return modules.RenterDownloadParameters{}, build.ExtendErr("async parameter could not be parsed", err) 492 } 493 494 siapath := strings.TrimPrefix(ps.ByName("siapath"), "/") // Sia file name. 495 496 dp := modules.RenterDownloadParameters{ 497 Destination: destination, 498 Async: async, 499 Length: length, 500 Offset: offset, 501 SiaPath: siapath, 502 } 503 if httpresp { 504 dp.Httpwriter = w 505 } 506 507 return dp, nil 508 } 509 510 // renterShareHandler handles the API call to create a '.sia' file that 511 // shares a set of file. 512 func (api *API) renterShareHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 513 destination := req.FormValue("destination") 514 // Check that the destination path is absolute. 515 if !filepath.IsAbs(destination) { 516 WriteError(w, Error{"destination must be an absolute path"}, http.StatusBadRequest) 517 return 518 } 519 520 err := api.renter.ShareFiles(strings.Split(req.FormValue("siapaths"), ","), destination) 521 if err != nil { 522 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 523 return 524 } 525 526 WriteSuccess(w) 527 } 528 529 // renterShareAsciiHandler handles the API call to return a '.sia' file 530 // in ascii form. 531 func (api *API) renterShareASCIIHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 532 ascii, err := api.renter.ShareFilesASCII(strings.Split(req.FormValue("siapaths"), ",")) 533 if err != nil { 534 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 535 return 536 } 537 WriteJSON(w, RenterShareASCII{ 538 ASCIIsia: ascii, 539 }) 540 } 541 542 // renterUploadHandler handles the API call to upload a file. 543 func (api *API) renterUploadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 544 source := req.FormValue("source") 545 if !filepath.IsAbs(source) { 546 WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest) 547 return 548 } 549 550 // Check whether the erasure coding parameters have been supplied. 551 var ec modules.ErasureCoder 552 if req.FormValue("datapieces") != "" || req.FormValue("paritypieces") != "" { 553 // Check that both values have been supplied. 554 if req.FormValue("datapieces") == "" || req.FormValue("paritypieces") == "" { 555 WriteError(w, Error{"must provide both the datapieces paramaeter and the paritypieces parameter if specifying erasure coding parameters"}, http.StatusBadRequest) 556 return 557 } 558 559 // Parse the erasure coding parameters. 560 var dataPieces, parityPieces int 561 _, err := fmt.Sscan(req.FormValue("datapieces"), &dataPieces) 562 if err != nil { 563 WriteError(w, Error{"unable to read parameter 'datapieces': " + err.Error()}, http.StatusBadRequest) 564 return 565 } 566 _, err = fmt.Sscan(req.FormValue("paritypieces"), &parityPieces) 567 if err != nil { 568 WriteError(w, Error{"unable to read parameter 'paritypieces': " + err.Error()}, http.StatusBadRequest) 569 return 570 } 571 572 // Verify that sane values for parityPieces and redundancy are being 573 // supplied. 574 if parityPieces < requiredParityPieces { 575 WriteError(w, Error{fmt.Sprintf("a minimum of %v parity pieces is required, but %v parity pieces requested", parityPieces, requiredParityPieces)}, http.StatusBadRequest) 576 return 577 } 578 redundancy := float64(dataPieces+parityPieces) / float64(dataPieces) 579 if float64(dataPieces+parityPieces)/float64(dataPieces) < requiredRedundancy { 580 WriteError(w, Error{fmt.Sprintf("a redundancy of %.2f is required, but redundancy of %.2f supplied", redundancy, requiredRedundancy)}, http.StatusBadRequest) 581 return 582 } 583 584 // Create the erasure coder. 585 ec, err = renter.NewRSCode(dataPieces, parityPieces) 586 if err != nil { 587 WriteError(w, Error{"unable to encode file using the provided parameters: " + err.Error()}, http.StatusBadRequest) 588 return 589 } 590 } 591 592 // Call the renter to upload the file. 593 err := api.renter.Upload(modules.FileUploadParams{ 594 Source: source, 595 SiaPath: strings.TrimPrefix(ps.ByName("siapath"), "/"), 596 ErasureCode: ec, 597 }) 598 if err != nil { 599 WriteError(w, Error{"upload failed: " + err.Error()}, http.StatusInternalServerError) 600 return 601 } 602 WriteSuccess(w) 603 }