gitlab.com/SiaPrime/SiaPrime@v1.4.1/node/api/renter.go (about) 1 package api 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/url" 9 "os" 10 "path/filepath" 11 "reflect" 12 "strconv" 13 "time" 14 15 "gitlab.com/SiaPrime/SiaPrime/modules/renter/contractor" 16 17 "github.com/julienschmidt/httprouter" 18 "gitlab.com/NebulousLabs/errors" 19 "gitlab.com/NebulousLabs/fastrand" 20 21 "gitlab.com/SiaPrime/SiaPrime/build" 22 "gitlab.com/SiaPrime/SiaPrime/crypto" 23 "gitlab.com/SiaPrime/SiaPrime/modules" 24 "gitlab.com/SiaPrime/SiaPrime/modules/renter/proto" 25 "gitlab.com/SiaPrime/SiaPrime/modules/renter/siafile" 26 "gitlab.com/SiaPrime/SiaPrime/types" 27 ) 28 29 var ( 30 // requiredHosts specifies the minimum number of hosts that must be set in 31 // the renter settings for the renter settings to be valid. This minimum is 32 // there to prevent users from shooting themselves in the foot. 33 requiredHosts = build.Select(build.Var{ 34 Standard: uint64(20), 35 Dev: uint64(1), 36 Testing: uint64(1), 37 }).(uint64) 38 39 // requiredParityPieces specifies the minimum number of parity pieces that 40 // must be used when uploading a file. This minimum exists to prevent users 41 // from shooting themselves in the foot. 42 requiredParityPieces = build.Select(build.Var{ 43 Standard: int(12), 44 Dev: int(0), 45 Testing: int(0), 46 }).(int) 47 48 // requiredRedundancy specifies the minimum redundancy that will be 49 // accepted by the renter when uploading a file. This minimum exists to 50 // prevent users from shooting themselves in the foot. 51 requiredRedundancy = build.Select(build.Var{ 52 Standard: float64(2), 53 Dev: float64(1), 54 Testing: float64(1), 55 }).(float64) 56 57 // requiredRenewWindow establishes the minimum allowed renew window for the 58 // renter settings. This minimum is here to prevent users from shooting 59 // themselves in the foot. 60 requiredRenewWindow = build.Select(build.Var{ 61 Standard: types.BlockHeight(288), 62 Dev: types.BlockHeight(1), 63 Testing: types.BlockHeight(1), 64 }).(types.BlockHeight) 65 66 //BackupKeySpecifier is the specifier used for deriving the secret used to 67 //encrypt a backup from the RenterSeed. 68 backupKeySpecifier = types.Specifier{'b', 'a', 'c', 'k', 'u', 'p', 'k', 'e', 'y'} 69 70 // errNeedBothDataAndParityPieces is the error returned when only one of the 71 // erasure coding parameters is set 72 errNeedBothDataAndParityPieces = errors.New("must provide both the datapieces parameter and the paritypieces parameter if specifying erasure coding parameters") 73 ) 74 75 type ( 76 // RenterGET contains various renter metrics. 77 RenterGET struct { 78 Settings modules.RenterSettings `json:"settings"` 79 FinancialMetrics modules.ContractorSpending `json:"financialmetrics"` 80 CurrentPeriod types.BlockHeight `json:"currentperiod"` 81 } 82 83 // RenterContract represents a contract formed by the renter. 84 RenterContract struct { 85 // Amount of contract funds that have been spent on downloads. 86 DownloadSpending types.Currency `json:"downloadspending"` 87 // Block height that the file contract ends on. 88 EndHeight types.BlockHeight `json:"endheight"` 89 // Fees paid in order to form the file contract. 90 Fees types.Currency `json:"fees"` 91 // Public key of the host the contract was formed with. 92 HostPublicKey types.SiaPublicKey `json:"hostpublickey"` 93 // HostVersion is the version of Sia that the host is running 94 HostVersion string `json:"hostversion"` 95 // ID of the file contract. 96 ID types.FileContractID `json:"id"` 97 // A signed transaction containing the most recent contract revision. 98 LastTransaction types.Transaction `json:"lasttransaction"` 99 // Address of the host the file contract was formed with. 100 NetAddress modules.NetAddress `json:"netaddress"` 101 // Remaining funds left for the renter to spend on uploads & downloads. 102 RenterFunds types.Currency `json:"renterfunds"` 103 // Size of the file contract, which is typically equal to the number of 104 // bytes that have been uploaded to the host. 105 Size uint64 `json:"size"` 106 // Block height that the file contract began on. 107 StartHeight types.BlockHeight `json:"startheight"` 108 // Amount of contract funds that have been spent on storage. 109 StorageSpending types.Currency `json:"storagespending"` 110 // DEPRECATED: This is the exact same value as StorageSpending, but it has 111 // incorrect capitalization. This was fixed in 1.3.2, but this field is kept 112 // to preserve backwards compatibility on clients who depend on the 113 // incorrect capitalization. This field will be removed in the future, so 114 // clients should switch to the StorageSpending field (above) with the 115 // correct lowercase name. 116 StorageSpendingDeprecated types.Currency `json:"StorageSpending"` 117 // Total cost to the wallet of forming the file contract. 118 TotalCost types.Currency `json:"totalcost"` 119 // Amount of contract funds that have been spent on uploads. 120 UploadSpending types.Currency `json:"uploadspending"` 121 // Signals if contract is good for uploading data 122 GoodForUpload bool `json:"goodforupload"` 123 // Signals if contract is good for a renewal 124 GoodForRenew bool `json:"goodforrenew"` 125 } 126 127 // RenterContracts contains the renter's contracts. 128 RenterContracts struct { 129 // Compatibility Fields 130 Contracts []RenterContract `json:"contracts"` 131 InactiveContracts []RenterContract `json:"inactivecontracts"` 132 133 // Current Fields 134 ActiveContracts []RenterContract `json:"activecontracts"` 135 PassiveContracts []RenterContract `json:"passivecontracts"` 136 RefreshedContracts []RenterContract `json:"refreshedcontracts"` 137 DisabledContracts []RenterContract `json:"disabledcontracts"` 138 ExpiredContracts []RenterContract `json:"expiredcontracts"` 139 ExpiredRefreshedContracts []RenterContract `json:"expiredrefreshedcontracts"` 140 RecoverableContracts []modules.RecoverableContract `json:"recoverablecontracts"` 141 } 142 143 // RenterDirectory lists the files and directories contained in the queried 144 // directory 145 RenterDirectory struct { 146 Directories []modules.DirectoryInfo `json:"directories"` 147 Files []modules.FileInfo `json:"files"` 148 } 149 150 // RenterDownloadQueue contains the renter's download queue. 151 RenterDownloadQueue struct { 152 Downloads []DownloadInfo `json:"downloads"` 153 } 154 155 // RenterFile lists the file queried. 156 RenterFile struct { 157 File modules.FileInfo `json:"file"` 158 } 159 160 // RenterFiles lists the files known to the renter. 161 RenterFiles struct { 162 Files []modules.FileInfo `json:"files"` 163 } 164 165 // RenterLoad lists files that were loaded into the renter. 166 RenterLoad struct { 167 FilesAdded []string `json:"filesadded"` 168 } 169 170 // RenterPricesGET lists the data that is returned when a GET call is made 171 // to /renter/prices. 172 RenterPricesGET struct { 173 modules.RenterPriceEstimation 174 modules.Allowance 175 } 176 // RenterRecoveryStatusGET returns information about potential contract 177 // recovery scans. 178 RenterRecoveryStatusGET struct { 179 ScanInProgress bool `json:"scaninprogress"` 180 ScannedHeight types.BlockHeight `json:"scannedheight"` 181 } 182 // RenterShareASCII contains an ASCII-encoded .sia file. 183 RenterShareASCII struct { 184 ASCIIsia string `json:"asciisia"` 185 } 186 187 // RenterUploadedBackup describes an uploaded backup. 188 RenterUploadedBackup struct { 189 Name string `json:"name"` 190 CreationDate types.Timestamp `json:"creationdate"` 191 Size uint64 `json:"size"` 192 UploadProgress float64 `json:"uploadprogress"` 193 } 194 195 // RenterBackupsGET lists the renter's uploaded backups, as well as the 196 // set of contracts storing all known backups. 197 RenterBackupsGET struct { 198 Backups []RenterUploadedBackup `json:"backups"` 199 SyncedHosts []types.SiaPublicKey `json:"syncedhosts"` 200 UnsyncedHosts []types.SiaPublicKey `json:"unsyncedhosts"` 201 } 202 203 // RenterUploadReadyGet lists the upload ready status of the renter 204 RenterUploadReadyGet struct { 205 // Ready indicates whether of not the renter is ready to successfully 206 // upload to full redundancy based on the erasure coding provided and 207 // the number of contracts 208 Ready bool `json:"ready"` 209 210 // Contract information 211 ContractsNeeded int `json:"contractsneeded"` 212 NumActiveContracts int `json:"numactivecontracts"` 213 214 // Erasure Coding information 215 DataPieces int `json:"datapieces"` 216 ParityPieces int `json:"paritypieces"` 217 } 218 219 // DownloadInfo contains all client-facing information of a file. 220 DownloadInfo struct { 221 Destination string `json:"destination"` // The destination of the download. 222 DestinationType string `json:"destinationtype"` // Can be "file", "memory buffer", or "http stream". 223 Filesize uint64 `json:"filesize"` // DEPRECATED. Same as 'Length'. 224 Length uint64 `json:"length"` // The length requested for the download. 225 Offset uint64 `json:"offset"` // The offset within the siafile requested for the download. 226 SiaPath modules.SiaPath `json:"siapath"` // The siapath of the file used for the download. 227 228 Completed bool `json:"completed"` // Whether or not the download has completed. 229 EndTime time.Time `json:"endtime"` // The time when the download fully completed. 230 Error string `json:"error"` // Will be the empty string unless there was an error. 231 Received uint64 `json:"received"` // Amount of data confirmed and decoded. 232 StartTime time.Time `json:"starttime"` // The time when the download was started. 233 StartTimeUnix int64 `json:"starttimeunix"` // The time when the download was started in unix format. 234 TotalDataTransferred uint64 `json:"totaldatatransferred"` // The total amount of data transferred, including negotiation, overdrive etc. 235 } 236 ) 237 238 // renterBackupsHandlerGET handles the API calls to /renter/backups. 239 func (api *API) renterBackupsHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 240 backups, syncedHosts, err := api.renter.UploadedBackups() 241 if err != nil { 242 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 243 return 244 } 245 var unsyncedHosts []types.SiaPublicKey 246 outer: 247 for _, c := range api.renter.Contracts() { 248 for _, h := range syncedHosts { 249 if c.HostPublicKey.String() == h.String() { 250 continue outer 251 } 252 } 253 unsyncedHosts = append(unsyncedHosts, c.HostPublicKey) 254 } 255 256 // if requested, fetch the backups stored on a specific host 257 if req.FormValue("host") != "" { 258 var hostKey types.SiaPublicKey 259 hostKey.LoadString(req.FormValue("host")) 260 if hostKey.Key == nil { 261 WriteError(w, Error{"invalid host public key"}, http.StatusBadRequest) 262 return 263 } 264 backups, err = api.renter.BackupsOnHost(hostKey) 265 if err != nil { 266 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 267 return 268 } 269 } 270 271 rups := make([]RenterUploadedBackup, len(backups)) 272 for i, b := range backups { 273 rups[i] = RenterUploadedBackup{ 274 Name: b.Name, 275 CreationDate: b.CreationDate, 276 Size: b.Size, 277 UploadProgress: b.UploadProgress, 278 } 279 } 280 WriteJSON(w, RenterBackupsGET{ 281 Backups: rups, 282 SyncedHosts: syncedHosts, 283 UnsyncedHosts: unsyncedHosts, 284 }) 285 } 286 287 // renterBackupsCreateHandlerPOST handles the API calls to /renter/backups/create 288 func (api *API) renterBackupsCreateHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 289 // Check that a name was specified. 290 name := req.FormValue("name") 291 if name == "" { 292 WriteError(w, Error{"name not specified"}, http.StatusBadRequest) 293 return 294 } 295 296 // Write the backup to a temporary file and delete it after uploading. 297 tmpDir, err := ioutil.TempDir("", "sia-backup") 298 if err != nil { 299 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 300 return 301 } 302 defer os.RemoveAll(tmpDir) 303 backupPath := filepath.Join(tmpDir, name) 304 305 // Get the wallet seed. 306 ws, _, err := api.wallet.PrimarySeed() 307 if err != nil { 308 WriteError(w, Error{"failed to get wallet's primary seed"}, http.StatusInternalServerError) 309 return 310 } 311 // Derive the renter seed and wipe the memory once we are done using it. 312 rs := proto.DeriveRenterSeed(ws) 313 defer fastrand.Read(rs[:]) 314 // Derive the secret and wipe it afterwards. 315 secret := crypto.HashAll(rs, backupKeySpecifier) 316 defer fastrand.Read(secret[:]) 317 // Create the backup. 318 if err := api.renter.CreateBackup(backupPath, secret[:32]); err != nil { 319 WriteError(w, Error{"failed to create backup: " + err.Error()}, http.StatusBadRequest) 320 return 321 } 322 // Upload the backup. 323 if err := api.renter.UploadBackup(backupPath, name); err != nil { 324 WriteError(w, Error{"failed to upload backup: " + err.Error()}, http.StatusBadRequest) 325 return 326 } 327 WriteSuccess(w) 328 } 329 330 // renterBackupsRestoreHandlerGET handles the API calls to /renter/backups/restore 331 func (api *API) renterBackupsRestoreHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 332 // Check that a name was specified. 333 name := req.FormValue("name") 334 if name == "" { 335 WriteError(w, Error{"name not specified"}, http.StatusBadRequest) 336 return 337 } 338 // Write the backup to a temporary file and delete it after loading. 339 tmpDir, err := ioutil.TempDir("", "sia-backup") 340 if err != nil { 341 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 342 return 343 } 344 defer os.RemoveAll(tmpDir) 345 backupPath := filepath.Join(tmpDir, name) 346 if err := api.renter.DownloadBackup(backupPath, name); err != nil { 347 WriteError(w, Error{"failed to download backup: " + err.Error()}, http.StatusBadRequest) 348 return 349 } 350 // Get the wallet seed. 351 ws, _, err := api.wallet.PrimarySeed() 352 if err != nil { 353 WriteError(w, Error{"failed to get wallet's primary seed"}, http.StatusInternalServerError) 354 return 355 } 356 // Derive the renter seed and wipe the memory once we are done using it. 357 rs := proto.DeriveRenterSeed(ws) 358 defer fastrand.Read(rs[:]) 359 // Derive the secret and wipe it afterwards. 360 secret := crypto.HashAll(rs, backupKeySpecifier) 361 defer fastrand.Read(secret[:]) 362 // Load the backup. 363 if err := api.renter.LoadBackup(backupPath, secret[:32]); err != nil { 364 WriteError(w, Error{"failed to load backup: " + err.Error()}, http.StatusBadRequest) 365 return 366 } 367 WriteSuccess(w) 368 } 369 370 // renterBackupHandlerPOST handles the API calls to /renter/backup 371 func (api *API) renterBackupHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 372 // Check that destination was specified. 373 dst := req.FormValue("destination") 374 if dst == "" { 375 WriteError(w, Error{"destination not specified"}, http.StatusBadRequest) 376 return 377 } 378 // The destination needs to be an absolute path. 379 if !filepath.IsAbs(dst) { 380 WriteError(w, Error{"destination must be an absolute path"}, http.StatusBadRequest) 381 return 382 } 383 // Get the wallet seed. 384 ws, _, err := api.wallet.PrimarySeed() 385 if err != nil { 386 WriteError(w, Error{"failed to get wallet's primary seed"}, http.StatusInternalServerError) 387 return 388 } 389 // Derive the renter seed and wipe the memory once we are done using it. 390 rs := proto.DeriveRenterSeed(ws) 391 defer fastrand.Read(rs[:]) 392 // Derive the secret and wipe it afterwards. 393 secret := crypto.HashAll(rs, backupKeySpecifier) 394 defer fastrand.Read(secret[:]) 395 // Create the backup. 396 if err := api.renter.CreateBackup(dst, secret[:32]); err != nil { 397 WriteError(w, Error{"failed to create backup: " + err.Error()}, http.StatusBadRequest) 398 return 399 } 400 WriteSuccess(w) 401 } 402 403 // renterBackupHandlerPOST handles the API calls to /renter/recoverbackup 404 func (api *API) renterLoadBackupHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 405 // Check that source was specified. 406 src := req.FormValue("source") 407 if src == "" { 408 WriteError(w, Error{"source not specified"}, http.StatusBadRequest) 409 return 410 } 411 // The source needs to be an absolute path. 412 if !filepath.IsAbs(src) { 413 WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest) 414 return 415 } 416 // Get the wallet seed. 417 ws, _, err := api.wallet.PrimarySeed() 418 if err != nil { 419 WriteError(w, Error{"failed to get wallet's primary seed"}, http.StatusInternalServerError) 420 return 421 } 422 // Derive the renter seed and wipe the memory once we are done using it. 423 rs := proto.DeriveRenterSeed(ws) 424 defer fastrand.Read(rs[:]) 425 // Derive the secret and wipe it afterwards. 426 secret := crypto.HashAll(rs, backupKeySpecifier) 427 defer fastrand.Read(secret[:]) 428 // Load the backup. 429 if err := api.renter.LoadBackup(src, secret[:32]); err != nil { 430 WriteError(w, Error{"failed to load backup: " + err.Error()}, http.StatusBadRequest) 431 return 432 } 433 WriteSuccess(w) 434 } 435 436 // parseErasureCodingParameters parses the supplied string values and creates 437 // an erasure coder. If values haven't been supplied it will fill in sane 438 // defaults. 439 func parseErasureCodingParameters(strDataPieces, strParityPieces string) (modules.ErasureCoder, error) { 440 // Parse data and parity pieces 441 dataPieces, parityPieces, err := parseDataAndParityPieces(strDataPieces, strParityPieces) 442 if err != nil { 443 return nil, err 444 } 445 446 // Check if data and parity pieces were set 447 if dataPieces == 0 && parityPieces == 0 { 448 return nil, nil 449 } 450 451 // Verify that sane values for parityPieces and redundancy are being 452 // supplied. 453 if parityPieces < requiredParityPieces { 454 err := fmt.Errorf("a minimum of %v parity pieces is required, but %v parity pieces requested", parityPieces, requiredParityPieces) 455 return nil, err 456 } 457 redundancy := float64(dataPieces+parityPieces) / float64(dataPieces) 458 if float64(dataPieces+parityPieces)/float64(dataPieces) < requiredRedundancy { 459 err := fmt.Errorf("a redundancy of %.2f is required, but redundancy of %.2f supplied", redundancy, requiredRedundancy) 460 return nil, err 461 } 462 463 // Create the erasure coder. 464 return siafile.NewRSSubCode(dataPieces, parityPieces, crypto.SegmentSize) 465 } 466 467 // parseDataAndParityPieces parse the numeric values for dataPieces and 468 // parityPieces from the input strings 469 func parseDataAndParityPieces(strDataPieces, strParityPieces string) (dataPieces, parityPieces int, err error) { 470 // Check that both values have been supplied. 471 if (strDataPieces == "") != (strParityPieces == "") { 472 return 0, 0, errNeedBothDataAndParityPieces 473 } 474 475 // Check for blank strings. 476 if strDataPieces == "" && strParityPieces == "" { 477 return 0, 0, nil 478 } 479 480 // Parse dataPieces and Parity Pieces. 481 _, err = fmt.Sscan(strDataPieces, &dataPieces) 482 if err != nil { 483 err = errors.AddContext(err, "unable to read parameter 'datapieces'") 484 return 0, 0, err 485 } 486 _, err = fmt.Sscan(strParityPieces, &parityPieces) 487 if err != nil { 488 err = errors.AddContext(err, "unable to read parameter 'paritypieces'") 489 return 0, 0, err 490 } 491 492 // Check that either both values are zero or neither are zero 493 if (dataPieces == 0) != (parityPieces == 0) { 494 return 0, 0, errNeedBothDataAndParityPieces 495 } 496 497 return dataPieces, parityPieces, nil 498 } 499 500 // renterHandlerGET handles the API call to /renter. 501 func (api *API) renterHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 502 WriteJSON(w, RenterGET{ 503 Settings: api.renter.Settings(), 504 FinancialMetrics: api.renter.PeriodSpending(), 505 CurrentPeriod: api.renter.CurrentPeriod(), 506 }) 507 } 508 509 // renterHandlerPOST handles the API call to set the Renter's settings. This API 510 // call handles multiple settings and so each setting is optional on it's own. 511 // Groups of settings, such as the allowance, have certain requirements if they 512 // are being set in which case certain fields are no longer optional. 513 func (api *API) renterHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 514 // Get the existing settings 515 settings := api.renter.Settings() 516 517 // Scan for all allowance fields 518 if f := req.FormValue("funds"); f != "" { 519 funds, ok := scanAmount(f) 520 if !ok { 521 WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest) 522 return 523 } 524 settings.Allowance.Funds = funds 525 } 526 if h := req.FormValue("hosts"); h != "" { 527 var hosts uint64 528 if _, err := fmt.Sscan(h, &hosts); err != nil { 529 WriteError(w, Error{"unable to parse hosts: " + err.Error()}, http.StatusBadRequest) 530 return 531 } else if hosts != 0 && hosts < requiredHosts { 532 WriteError(w, Error{fmt.Sprintf("insufficient number of hosts, need at least %v but have %v", requiredHosts, hosts)}, http.StatusBadRequest) 533 return 534 } 535 settings.Allowance.Hosts = hosts 536 } 537 if p := req.FormValue("period"); p != "" { 538 var period types.BlockHeight 539 if _, err := fmt.Sscan(p, &period); err != nil { 540 WriteError(w, Error{"unable to parse period: " + err.Error()}, http.StatusBadRequest) 541 return 542 } 543 settings.Allowance.Period = types.BlockHeight(period) 544 } 545 if rw := req.FormValue("renewwindow"); rw != "" { 546 var renewWindow types.BlockHeight 547 if _, err := fmt.Sscan(rw, &renewWindow); err != nil { 548 WriteError(w, Error{"unable to parse renewwindow: " + err.Error()}, http.StatusBadRequest) 549 return 550 } else if renewWindow != 0 && types.BlockHeight(renewWindow) < requiredRenewWindow { 551 WriteError(w, Error{fmt.Sprintf("renew window is too small, must be at least %v blocks but have %v blocks", requiredRenewWindow, renewWindow)}, http.StatusBadRequest) 552 return 553 } else if renewWindow == 0 && settings.Allowance.Period != 0 { 554 WriteError(w, Error{contractor.ErrAllowanceZeroWindow.Error()}, http.StatusBadRequest) 555 return 556 } else { 557 settings.Allowance.RenewWindow = types.BlockHeight(renewWindow) 558 } 559 } 560 if es := req.FormValue("expectedstorage"); es != "" { 561 var expectedStorage uint64 562 if _, err := fmt.Sscan(es, &expectedStorage); err != nil { 563 WriteError(w, Error{"unable to parse expectedStorage: " + err.Error()}, http.StatusBadRequest) 564 return 565 } 566 settings.Allowance.ExpectedStorage = expectedStorage 567 } 568 if euf := req.FormValue("expectedupload"); euf != "" { 569 var expectedUpload uint64 570 if _, err := fmt.Sscan(euf, &expectedUpload); err != nil { 571 WriteError(w, Error{"unable to parse expectedUpload: " + err.Error()}, http.StatusBadRequest) 572 return 573 } 574 settings.Allowance.ExpectedUpload = expectedUpload 575 } 576 if edf := req.FormValue("expecteddownload"); edf != "" { 577 var expectedDownload uint64 578 if _, err := fmt.Sscan(edf, &expectedDownload); err != nil { 579 WriteError(w, Error{"unable to parse expectedDownload: " + err.Error()}, http.StatusBadRequest) 580 return 581 } 582 settings.Allowance.ExpectedDownload = expectedDownload 583 } 584 if er := req.FormValue("expectedredundancy"); er != "" { 585 var expectedRedundancy float64 586 if _, err := fmt.Sscan(er, &expectedRedundancy); err != nil { 587 WriteError(w, Error{"unable to parse expectedRedundancy: " + err.Error()}, http.StatusBadRequest) 588 return 589 } 590 settings.Allowance.ExpectedRedundancy = expectedRedundancy 591 } 592 593 // Validate any allowance changes 594 if !reflect.DeepEqual(settings.Allowance, modules.Allowance{}) { 595 // Allowance has been set at least partially. Validate that all fields 596 // are set correctly 597 598 // If Funds is still 0 return an error since we need the user to set the funding initially 599 if settings.Allowance.Funds.IsZero() { 600 WriteError(w, Error{"funds needs to be set if it hasn't been set before"}, http.StatusBadRequest) 601 return 602 } 603 604 // If Period is still 0 return an error since we need the user to set the period initially 605 if settings.Allowance.Period == 0 { 606 WriteError(w, Error{"period needs to be set if it hasn't been set before"}, http.StatusBadRequest) 607 return 608 } 609 610 // If Hosts is still 0 set to the sane default 611 if settings.Allowance.Hosts == 0 { 612 settings.Allowance.Hosts = modules.DefaultAllowance.Hosts 613 } 614 615 // If Renew Window is still 0 set to the sane default 616 if settings.Allowance.RenewWindow == 0 { 617 settings.Allowance.RenewWindow = settings.Allowance.Period / 2 618 } 619 620 // If Expected Storage is still 0 set to the sane default 621 if settings.Allowance.ExpectedStorage == 0 { 622 settings.Allowance.ExpectedStorage = modules.DefaultAllowance.ExpectedStorage 623 } 624 625 // If Expected Upload is still 0 set to the sane default 626 if settings.Allowance.ExpectedUpload == 0 { 627 settings.Allowance.ExpectedUpload = modules.DefaultAllowance.ExpectedUpload 628 } 629 630 // If Expected Download is still 0 set to the sane default 631 if settings.Allowance.ExpectedDownload == 0 { 632 settings.Allowance.ExpectedDownload = modules.DefaultAllowance.ExpectedDownload 633 } 634 635 // If Expected Redundancy is still 0 set to the sane default 636 if settings.Allowance.ExpectedRedundancy == 0 { 637 settings.Allowance.ExpectedRedundancy = modules.DefaultAllowance.ExpectedRedundancy 638 } 639 } 640 641 // Scan the download speed limit. (optional parameter) 642 if d := req.FormValue("maxdownloadspeed"); d != "" { 643 var downloadSpeed int64 644 if _, err := fmt.Sscan(d, &downloadSpeed); err != nil { 645 WriteError(w, Error{"unable to parse downloadspeed: " + err.Error()}, http.StatusBadRequest) 646 return 647 } 648 settings.MaxDownloadSpeed = downloadSpeed 649 } 650 // Scan the upload speed limit. (optional parameter) 651 if u := req.FormValue("maxuploadspeed"); u != "" { 652 var uploadSpeed int64 653 if _, err := fmt.Sscan(u, &uploadSpeed); err != nil { 654 WriteError(w, Error{"unable to parse uploadspeed: " + err.Error()}, http.StatusBadRequest) 655 return 656 } 657 settings.MaxUploadSpeed = uploadSpeed 658 } 659 // Scan the checkforipviolation flag. 660 if ipc := req.FormValue("checkforipviolation"); ipc != "" { 661 var ipviolationcheck bool 662 if _, err := fmt.Sscan(ipc, &ipviolationcheck); err != nil { 663 WriteError(w, Error{"unable to parse checkforipviolation: " + err.Error()}, http.StatusBadRequest) 664 return 665 } 666 settings.IPViolationsCheck = ipviolationcheck 667 } 668 669 // Set the settings in the renter. 670 err := api.renter.SetSettings(settings) 671 if err != nil { 672 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 673 return 674 } 675 WriteSuccess(w) 676 } 677 678 // renterContractCancelHandler handles the API call to cancel a specific Renter contract. 679 func (api *API) renterContractCancelHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 680 var fcid types.FileContractID 681 if err := fcid.LoadString(req.FormValue("id")); err != nil { 682 WriteError(w, Error{"unable to parse id:" + err.Error()}, http.StatusBadRequest) 683 return 684 } 685 err := api.renter.CancelContract(fcid) 686 if err != nil { 687 WriteError(w, Error{"unable to cancel contract:" + err.Error()}, http.StatusBadRequest) 688 return 689 } 690 WriteSuccess(w) 691 } 692 693 // renterContractsHandler handles the API call to request the Renter's 694 // contracts. Active and renewed contracts are returned by default 695 // 696 // Contracts are returned for Compatibility and are the contracts returned from 697 // renter.Contracts() 698 // 699 // Inactive contracts are contracts that are not currently being used by the 700 // renter because they are !goodForRenew, but have endheights that are in the 701 // future so could potentially become active again 702 // 703 // Active contracts are contracts that the renter is actively using to store 704 // data and can upload, download, and renew. These contracts are GoodForUpload 705 // and GoodForRenew 706 // 707 // Refreshed contracts are contracts that are in the current period and were 708 // refreshed due to running out of funds. A new contract that replaced a 709 // refreshed contract can either be in Active or Disabled contracts. These 710 // contracts are broken out as to not double count the data recorded in the 711 // contract. 712 // 713 // Disabled Contracts are contracts that are no longer active as there are Not 714 // GoodForUpload and Not GoodForRenew but still have endheights in the current 715 // period. 716 // 717 // Expired contracts are contracts who's endheights are in the past. 718 // 719 // ExpiredRefreshed contracts are refreshed contracts who's endheights are in 720 // the past. 721 // 722 // Recoverable contracts are contracts of the renter that are recovered from the 723 // blockchain by using the renter's seed. 724 func (api *API) renterContractsHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 725 // Parse flags 726 var disabled, inactive, expired, recoverable bool 727 var err error 728 if s := req.FormValue("disabled"); s != "" { 729 disabled, err = scanBool(s) 730 if err != nil { 731 WriteError(w, Error{"unable to parse disabled:" + err.Error()}, http.StatusBadRequest) 732 return 733 } 734 } 735 if s := req.FormValue("inactive"); s != "" { 736 inactive, err = scanBool(s) 737 if err != nil { 738 WriteError(w, Error{"unable to parse inactive:" + err.Error()}, http.StatusBadRequest) 739 return 740 } 741 } 742 if s := req.FormValue("expired"); s != "" { 743 expired, err = scanBool(s) 744 if err != nil { 745 WriteError(w, Error{"unable to parse expired:" + err.Error()}, http.StatusBadRequest) 746 return 747 } 748 } 749 if s := req.FormValue("recoverable"); s != "" { 750 recoverable, err = scanBool(s) 751 if err != nil { 752 WriteError(w, Error{"unable to parse recoverable:" + err.Error()}, http.StatusBadRequest) 753 return 754 } 755 } 756 757 // Parse the renter's contracts into their appropriate categories 758 contracts := api.parseRenterContracts(disabled, inactive, expired) 759 760 // Get recoverable contracts 761 var recoverableContracts []modules.RecoverableContract 762 if recoverable { 763 recoverableContracts = api.renter.RecoverableContracts() 764 } 765 contracts.RecoverableContracts = recoverableContracts 766 767 WriteJSON(w, contracts) 768 } 769 770 // parseRenterContracts categorized the Renter's contracts from Contracts() and 771 // OldContracts(). 772 func (api *API) parseRenterContracts(disabled, inactive, expired bool) RenterContracts { 773 var rc RenterContracts 774 for _, c := range api.renter.Contracts() { 775 var size uint64 776 if len(c.Transaction.FileContractRevisions) != 0 { 777 size = c.Transaction.FileContractRevisions[0].NewFileSize 778 } 779 780 // Fetch host address 781 var netAddress modules.NetAddress 782 hdbe, exists := api.renter.Host(c.HostPublicKey) 783 if exists { 784 netAddress = hdbe.NetAddress 785 } 786 787 // Build the contract. 788 contract := RenterContract{ 789 DownloadSpending: c.DownloadSpending, 790 EndHeight: c.EndHeight, 791 Fees: c.TxnFee.Add(c.SiafundFee).Add(c.ContractFee), 792 GoodForUpload: c.Utility.GoodForUpload, 793 GoodForRenew: c.Utility.GoodForRenew, 794 HostPublicKey: c.HostPublicKey, 795 HostVersion: hdbe.Version, 796 ID: c.ID, 797 LastTransaction: c.Transaction, 798 NetAddress: netAddress, 799 RenterFunds: c.RenterFunds, 800 Size: size, 801 StartHeight: c.StartHeight, 802 StorageSpending: c.StorageSpending, 803 StorageSpendingDeprecated: c.StorageSpending, 804 TotalCost: c.TotalCost, 805 UploadSpending: c.UploadSpending, 806 } 807 808 // Determine contract status 809 refreshed := api.renter.RefreshedContract(c.ID) 810 active := c.Utility.GoodForUpload && c.Utility.GoodForRenew && !refreshed 811 passive := !c.Utility.GoodForUpload && c.Utility.GoodForRenew && !refreshed 812 disabledContract := disabled && !active && !passive && !refreshed 813 814 // A contract can either be active, passive, refreshed, or disabled 815 statusErr := active && passive && refreshed || active && refreshed || active && passive || passive && refreshed 816 if statusErr { 817 build.Critical("Contract has multiple status types, this should never happen") 818 } else if active { 819 rc.ActiveContracts = append(rc.ActiveContracts, contract) 820 } else if passive { 821 rc.PassiveContracts = append(rc.PassiveContracts, contract) 822 } else if refreshed { 823 rc.RefreshedContracts = append(rc.RefreshedContracts, contract) 824 } else if disabledContract { 825 rc.DisabledContracts = append(rc.DisabledContracts, contract) 826 } 827 828 // Record InactiveContracts and Contracts for compatibility 829 if !active && inactive { 830 rc.InactiveContracts = append(rc.InactiveContracts, contract) 831 } 832 rc.Contracts = append(rc.Contracts, contract) 833 } 834 835 // Get current block height for reference 836 currentPeriod := api.renter.CurrentPeriod() 837 for _, c := range api.renter.OldContracts() { 838 var size uint64 839 if len(c.Transaction.FileContractRevisions) != 0 { 840 size = c.Transaction.FileContractRevisions[0].NewFileSize 841 } 842 843 // Fetch host address 844 var netAddress modules.NetAddress 845 hdbe, exists := api.renter.Host(c.HostPublicKey) 846 if exists { 847 netAddress = hdbe.NetAddress 848 } 849 850 // Build contract 851 contract := RenterContract{ 852 DownloadSpending: c.DownloadSpending, 853 EndHeight: c.EndHeight, 854 Fees: c.TxnFee.Add(c.SiafundFee).Add(c.ContractFee), 855 GoodForUpload: c.Utility.GoodForUpload, 856 GoodForRenew: c.Utility.GoodForRenew, 857 HostPublicKey: c.HostPublicKey, 858 HostVersion: hdbe.Version, 859 ID: c.ID, 860 LastTransaction: c.Transaction, 861 NetAddress: netAddress, 862 RenterFunds: c.RenterFunds, 863 Size: size, 864 StartHeight: c.StartHeight, 865 StorageSpending: c.StorageSpending, 866 StorageSpendingDeprecated: c.StorageSpending, 867 TotalCost: c.TotalCost, 868 UploadSpending: c.UploadSpending, 869 } 870 871 // Determine contract status 872 refreshed := api.renter.RefreshedContract(c.ID) 873 currentPeriodContract := c.StartHeight >= currentPeriod 874 expiredContract := expired && !currentPeriodContract && !refreshed 875 expiredRefreshed := expired && !currentPeriodContract && refreshed 876 refreshedContract := refreshed && currentPeriodContract 877 disabledContract := disabled && !refreshed && currentPeriodContract 878 879 // A contract can only be refreshed, disabled, expired, or expired refreshed 880 if expiredContract { 881 rc.ExpiredContracts = append(rc.ExpiredContracts, contract) 882 } else if expiredRefreshed { 883 rc.ExpiredRefreshedContracts = append(rc.ExpiredRefreshedContracts, contract) 884 } else if refreshedContract { 885 rc.RefreshedContracts = append(rc.RefreshedContracts, contract) 886 } else if disabledContract { 887 rc.DisabledContracts = append(rc.DisabledContracts, contract) 888 } 889 890 // Record inactive contracts for compatibility 891 if inactive && currentPeriodContract { 892 rc.InactiveContracts = append(rc.InactiveContracts, contract) 893 } 894 } 895 896 return rc 897 } 898 899 // renterClearDownloadsHandler handles the API call to request to clear the download queue. 900 func (api *API) renterClearDownloadsHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 901 var afterTime time.Time 902 beforeTime := types.EndOfTime 903 beforeStr, afterStr := req.FormValue("before"), req.FormValue("after") 904 if beforeStr != "" { 905 beforeInt, err := strconv.ParseInt(beforeStr, 10, 64) 906 if err != nil { 907 WriteError(w, Error{"parsing integer value for parameter `before` failed: " + err.Error()}, http.StatusBadRequest) 908 return 909 } 910 beforeTime = time.Unix(0, beforeInt) 911 } 912 if afterStr != "" { 913 afterInt, err := strconv.ParseInt(afterStr, 10, 64) 914 if err != nil { 915 WriteError(w, Error{"parsing integer value for parameter `after` failed: " + err.Error()}, http.StatusBadRequest) 916 return 917 } 918 afterTime = time.Unix(0, afterInt) 919 } 920 921 err := api.renter.ClearDownloadHistory(afterTime, beforeTime) 922 if err != nil { 923 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 924 return 925 } 926 WriteSuccess(w) 927 } 928 929 // renterDownloadsHandler handles the API call to request the download queue. 930 func (api *API) renterDownloadsHandler(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { 931 var downloads []DownloadInfo 932 for _, di := range api.renter.DownloadHistory() { 933 downloads = append(downloads, DownloadInfo{ 934 Destination: di.Destination, 935 DestinationType: di.DestinationType, 936 Filesize: di.Length, 937 Length: di.Length, 938 Offset: di.Offset, 939 SiaPath: di.SiaPath, 940 941 Completed: di.Completed, 942 EndTime: di.EndTime, 943 Error: di.Error, 944 Received: di.Received, 945 StartTime: di.StartTime, 946 StartTimeUnix: di.StartTimeUnix, 947 TotalDataTransferred: di.TotalDataTransferred, 948 }) 949 } 950 WriteJSON(w, RenterDownloadQueue{ 951 Downloads: downloads, 952 }) 953 } 954 955 // renterRecoveryScanHandlerPOST handles the API call to /renter/recoveryscan. 956 func (api *API) renterRecoveryScanHandlerPOST(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 957 if err := api.renter.InitRecoveryScan(); err != nil { 958 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 959 return 960 } 961 WriteSuccess(w) 962 } 963 964 // renterRecoveryScanHandlerGET handles the API call to /renter/recoveryscan. 965 func (api *API) renterRecoveryScanHandlerGET(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 966 scanInProgress, height := api.renter.RecoveryScanStatus() 967 WriteJSON(w, RenterRecoveryStatusGET{ 968 ScanInProgress: scanInProgress, 969 ScannedHeight: height, 970 }) 971 } 972 973 // renterRenameHandler handles the API call to rename a file entry in the 974 // renter. 975 func (api *API) renterRenameHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 976 newSiaPathStr := req.FormValue("newsiapath") 977 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 978 if err != nil { 979 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 980 return 981 } 982 newSiaPath, err := modules.NewSiaPath(newSiaPathStr) 983 if err != nil { 984 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 985 return 986 } 987 err = api.renter.RenameFile(siaPath, newSiaPath) 988 if err != nil { 989 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 990 return 991 } 992 WriteSuccess(w) 993 } 994 995 // renterFileHandler handles GET requests to the /renter/file/:siapath API endpoint. 996 func (api *API) renterFileHandlerGET(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 997 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 998 if err != nil { 999 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1000 return 1001 } 1002 file, err := api.renter.File(siaPath) 1003 if err != nil { 1004 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1005 return 1006 } 1007 WriteJSON(w, RenterFile{ 1008 File: file, 1009 }) 1010 } 1011 1012 // renterFileHandler handles POST requests to the /renter/file/:siapath API endpoint. 1013 func (api *API) renterFileHandlerPOST(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1014 newTrackingPath := req.FormValue("trackingpath") 1015 stuck := req.FormValue("stuck") 1016 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1017 if err != nil { 1018 WriteError(w, Error{"unable to parse siapath" + err.Error()}, http.StatusBadRequest) 1019 return 1020 } 1021 // Handle changing the tracking path of a file. 1022 if newTrackingPath != "" { 1023 if err := api.renter.SetFileTrackingPath(siaPath, newTrackingPath); err != nil { 1024 WriteError(w, Error{fmt.Sprintf("unable set tracking path: %v", err)}, http.StatusBadRequest) 1025 return 1026 } 1027 } 1028 // Handle changing the 'stuck' status of a file. 1029 if stuck != "" { 1030 s, err := strconv.ParseBool(stuck) 1031 if err != nil { 1032 WriteError(w, Error{"unable to parse 'stuck' arg"}, http.StatusBadRequest) 1033 return 1034 } 1035 if err := api.renter.SetFileStuck(siaPath, s); err != nil { 1036 WriteError(w, Error{"failed to change file 'stuck' status: " + err.Error()}, http.StatusBadRequest) 1037 return 1038 } 1039 } 1040 WriteSuccess(w) 1041 } 1042 1043 // renterFilesHandler handles the API call to list all of the files. 1044 func (api *API) renterFilesHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { 1045 var c bool 1046 var err error 1047 if cached := req.FormValue("cached"); cached != "" { 1048 c, err = strconv.ParseBool(cached) 1049 if err != nil { 1050 WriteError(w, Error{"unable to parse 'cached' arg"}, http.StatusBadRequest) 1051 return 1052 } 1053 } 1054 files, err := api.renter.FileList(modules.RootSiaPath(), true, c) 1055 if err != nil { 1056 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1057 return 1058 } 1059 WriteJSON(w, RenterFiles{ 1060 Files: files, 1061 }) 1062 } 1063 1064 // renterPricesHandler reports the expected costs of various actions given the 1065 // renter settings and the set of available hosts. 1066 func (api *API) renterPricesHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1067 allowance := modules.Allowance{} 1068 // Scan the allowance amount. (optional parameter) 1069 if f := req.FormValue("funds"); f != "" { 1070 funds, ok := scanAmount(f) 1071 if !ok { 1072 WriteError(w, Error{"unable to parse funds"}, http.StatusBadRequest) 1073 return 1074 } 1075 allowance.Funds = funds 1076 } 1077 // Scan the number of hosts to use. (optional parameter) 1078 if h := req.FormValue("hosts"); h != "" { 1079 var hosts uint64 1080 if _, err := fmt.Sscan(h, &hosts); err != nil { 1081 WriteError(w, Error{"unable to parse hosts: " + err.Error()}, http.StatusBadRequest) 1082 return 1083 } else if hosts != 0 && hosts < requiredHosts { 1084 WriteError(w, Error{fmt.Sprintf("insufficient number of hosts, need at least %v but have %v", requiredHosts, hosts)}, http.StatusBadRequest) 1085 } else { 1086 allowance.Hosts = hosts 1087 } 1088 } 1089 // Scan the period. (optional parameter) 1090 if p := req.FormValue("period"); p != "" { 1091 var period types.BlockHeight 1092 if _, err := fmt.Sscan(p, &period); err != nil { 1093 WriteError(w, Error{"unable to parse period: " + err.Error()}, http.StatusBadRequest) 1094 return 1095 } 1096 allowance.Period = types.BlockHeight(period) 1097 } 1098 // Scan the renew window. (optional parameter) 1099 if rw := req.FormValue("renewwindow"); rw != "" { 1100 var renewWindow types.BlockHeight 1101 if _, err := fmt.Sscan(rw, &renewWindow); err != nil { 1102 WriteError(w, Error{"unable to parse renewwindow: " + err.Error()}, http.StatusBadRequest) 1103 return 1104 } else if renewWindow != 0 && types.BlockHeight(renewWindow) < requiredRenewWindow { 1105 WriteError(w, Error{fmt.Sprintf("renew window is too small, must be at least %v blocks but have %v blocks", requiredRenewWindow, renewWindow)}, http.StatusBadRequest) 1106 return 1107 } else { 1108 allowance.RenewWindow = types.BlockHeight(renewWindow) 1109 } 1110 } 1111 1112 // Check for partially set allowance, which can happen since hosts and renew 1113 // window can be optional fields. Checking here instead of assigning values 1114 // above so that an empty allowance can still be submitted 1115 if !reflect.DeepEqual(allowance, modules.Allowance{}) { 1116 if allowance.Funds.Cmp(types.ZeroCurrency) == 0 { 1117 WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `funds` parameter left empty")}, http.StatusBadRequest) 1118 return 1119 } 1120 if allowance.Period == 0 { 1121 WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `period` parameter left empty")}, http.StatusBadRequest) 1122 return 1123 } 1124 if allowance.Hosts == 0 { 1125 WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `hosts` parameter left empty")}, http.StatusBadRequest) 1126 return 1127 } 1128 if allowance.RenewWindow == 0 { 1129 WriteError(w, Error{fmt.Sprint("Allowance not set correctly, `renewwindow` parameter left empty")}, http.StatusBadRequest) 1130 return 1131 } 1132 } 1133 1134 estimate, a, err := api.renter.PriceEstimation(allowance) 1135 if err != nil { 1136 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1137 return 1138 } 1139 WriteJSON(w, RenterPricesGET{ 1140 RenterPriceEstimation: estimate, 1141 Allowance: a, 1142 }) 1143 } 1144 1145 // renterDeleteHandler handles the API call to delete a file entry from the 1146 // renter. 1147 func (api *API) renterDeleteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1148 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1149 if err != nil { 1150 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1151 return 1152 } 1153 err = api.renter.DeleteFile(siaPath) 1154 if err != nil { 1155 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1156 return 1157 } 1158 1159 WriteSuccess(w) 1160 } 1161 1162 // renterCancelDownloadHandler handles the API call to cancel a download. 1163 func (api *API) renterCancelDownloadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1164 // Get the id. 1165 id := req.FormValue("id") 1166 if id == "" { 1167 WriteError(w, Error{"id not specified"}, http.StatusBadRequest) 1168 return 1169 } 1170 // Get the download from the map and delete it. 1171 api.downloadMu.Lock() 1172 cancel, ok := api.downloads[id] 1173 delete(api.downloads, id) 1174 api.downloadMu.Unlock() 1175 if !ok { 1176 WriteError(w, Error{"download for id not found"}, http.StatusBadRequest) 1177 return 1178 } 1179 // Cancel download and delete it from the map. 1180 cancel() 1181 WriteSuccess(w) 1182 } 1183 1184 // renterDownloadHandler handles the API call to download a file. 1185 func (api *API) renterDownloadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1186 params, err := parseDownloadParameters(w, req, ps) 1187 if err != nil { 1188 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1189 return 1190 } 1191 if params.Async { 1192 var cancel func() 1193 id := hex.EncodeToString(fastrand.Bytes(16)) 1194 cancel, err = api.renter.DownloadAsync(params, func(_ error) error { 1195 api.downloadMu.Lock() 1196 delete(api.downloads, id) 1197 api.downloadMu.Unlock() 1198 return nil 1199 }) 1200 if err == nil { 1201 w.Header().Set("ID", id) 1202 api.downloadMu.Lock() 1203 api.downloads[id] = cancel 1204 api.downloadMu.Unlock() 1205 } 1206 } else { 1207 err = api.renter.Download(params) 1208 } 1209 if err != nil { 1210 WriteError(w, Error{"download failed: " + err.Error()}, http.StatusInternalServerError) 1211 return 1212 } 1213 if params.Httpwriter == nil { 1214 // `httpresp=true` causes writes to w before this line is run, automatically 1215 // adding `200 Status OK` code to response. Calling this results in a 1216 // multiple calls to WriteHeaders() errors. 1217 WriteSuccess(w) 1218 return 1219 } 1220 } 1221 1222 // renterDownloadAsyncHandler handles the API call to download a file asynchronously. 1223 func (api *API) renterDownloadAsyncHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1224 req.ParseForm() 1225 req.Form.Set("async", "true") 1226 api.renterDownloadHandler(w, req, ps) 1227 } 1228 1229 // parseDownloadParameters parses the download parameters passed to the 1230 // /renter/download endpoint. Validation of these parameters is done by the 1231 // renter. 1232 func parseDownloadParameters(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (modules.RenterDownloadParameters, error) { 1233 destination := req.FormValue("destination") 1234 1235 // The offset and length in bytes. 1236 offsetparam := req.FormValue("offset") 1237 lengthparam := req.FormValue("length") 1238 1239 // Determines whether the response is written to response body. 1240 httprespparam := req.FormValue("httpresp") 1241 1242 // Determines whether to return on completion of download or straight away. 1243 // If httprespparam is present, this parameter is ignored. 1244 asyncparam := req.FormValue("async") 1245 1246 // Parse the offset and length parameters. 1247 var offset, length uint64 1248 if len(offsetparam) > 0 { 1249 _, err := fmt.Sscan(offsetparam, &offset) 1250 if err != nil { 1251 return modules.RenterDownloadParameters{}, errors.AddContext(err, "could not decode the offset as uint64") 1252 } 1253 } 1254 if len(lengthparam) > 0 { 1255 _, err := fmt.Sscan(lengthparam, &length) 1256 if err != nil { 1257 return modules.RenterDownloadParameters{}, errors.AddContext(err, "could not decode the offset as uint64") 1258 } 1259 } 1260 1261 // Parse the httpresp parameter. 1262 httpresp, err := scanBool(httprespparam) 1263 if err != nil { 1264 return modules.RenterDownloadParameters{}, errors.AddContext(err, "httpresp parameter could not be parsed") 1265 } 1266 1267 // Parse the async parameter. 1268 async, err := scanBool(asyncparam) 1269 if err != nil { 1270 return modules.RenterDownloadParameters{}, errors.AddContext(err, "async parameter could not be parsed") 1271 } 1272 1273 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1274 if err != nil { 1275 return modules.RenterDownloadParameters{}, errors.AddContext(err, "error parsing the siapath") 1276 } 1277 1278 dp := modules.RenterDownloadParameters{ 1279 Destination: destination, 1280 Async: async, 1281 Length: length, 1282 Offset: offset, 1283 SiaPath: siaPath, 1284 } 1285 if httpresp { 1286 dp.Httpwriter = w 1287 } 1288 1289 return dp, nil 1290 } 1291 1292 // renterStreamHandler handles downloads from the /renter/stream endpoint 1293 func (api *API) renterStreamHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1294 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1295 if err != nil { 1296 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1297 return 1298 } 1299 fileName, streamer, err := api.renter.Streamer(siaPath) 1300 if err != nil { 1301 WriteError(w, Error{fmt.Sprintf("failed to create download streamer: %v", err)}, 1302 http.StatusInternalServerError) 1303 return 1304 } 1305 defer streamer.Close() 1306 http.ServeContent(w, req, fileName, time.Time{}, streamer) 1307 } 1308 1309 // renterUploadHandler handles the API call to upload a file. 1310 func (api *API) renterUploadHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1311 // Get the source path. 1312 source := req.FormValue("source") 1313 // Source must be absolute path. 1314 if !filepath.IsAbs(source) { 1315 WriteError(w, Error{"source must be an absolute path"}, http.StatusBadRequest) 1316 return 1317 } 1318 // Check whether existing file should be overwritten 1319 var err error 1320 force := false 1321 if f := req.FormValue("force"); f != "" { 1322 force, err = strconv.ParseBool(f) 1323 if err != nil { 1324 WriteError(w, Error{"unable to parse 'force' parameter: " + err.Error()}, http.StatusBadRequest) 1325 return 1326 } 1327 } 1328 // Parse the erasure coder. 1329 ec, err := parseErasureCodingParameters(req.FormValue("datapieces"), req.FormValue("paritypieces")) 1330 if err != nil { 1331 WriteError(w, Error{"unable to parse erasure code settings" + err.Error()}, http.StatusBadRequest) 1332 return 1333 } 1334 1335 // Call the renter to upload the file. 1336 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1337 if err != nil { 1338 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1339 return 1340 } 1341 err = api.renter.Upload(modules.FileUploadParams{ 1342 Source: source, 1343 SiaPath: siaPath, 1344 ErasureCode: ec, 1345 Force: force, 1346 }) 1347 if err != nil { 1348 WriteError(w, Error{"upload failed: " + err.Error()}, http.StatusInternalServerError) 1349 return 1350 } 1351 WriteSuccess(w) 1352 } 1353 1354 // renterUploadStreamHandler handles the API call to upload a file using a 1355 // stream. 1356 func (api *API) renterUploadStreamHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1357 // Parse the query params. 1358 queryForm, err := url.ParseQuery(req.URL.RawQuery) 1359 if err != nil { 1360 WriteError(w, Error{"failed to parse query params"}, http.StatusBadRequest) 1361 return 1362 } 1363 // Check whether existing file should be overwritten 1364 force := false 1365 if f := queryForm.Get("force"); f != "" { 1366 force, err = strconv.ParseBool(f) 1367 if err != nil { 1368 WriteError(w, Error{"unable to parse 'force' parameter: " + err.Error()}, http.StatusBadRequest) 1369 return 1370 } 1371 } 1372 // Check whether existing file should be repaired 1373 repair := false 1374 if r := queryForm.Get("repair"); r != "" { 1375 repair, err = strconv.ParseBool(r) 1376 if err != nil { 1377 WriteError(w, Error{"unable to parse 'repair' parameter: " + err.Error()}, http.StatusBadRequest) 1378 return 1379 } 1380 } 1381 // Parse the erasure coder. 1382 ec, err := parseErasureCodingParameters(queryForm.Get("datapieces"), queryForm.Get("paritypieces")) 1383 if err != nil && !repair { 1384 WriteError(w, Error{"unable to parse erasure code settings" + err.Error()}, http.StatusBadRequest) 1385 return 1386 } 1387 if repair && ec != nil { 1388 WriteError(w, Error{"can't provide erasure code settings when doing a repair"}, http.StatusBadRequest) 1389 return 1390 } 1391 1392 // Call the renter to upload the file. 1393 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1394 if err != nil { 1395 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1396 return 1397 } 1398 up := modules.FileUploadParams{ 1399 SiaPath: siaPath, 1400 ErasureCode: ec, 1401 Force: force, 1402 Repair: repair, 1403 } 1404 err = api.renter.UploadStreamFromReader(up, req.Body) 1405 if err != nil { 1406 WriteError(w, Error{"upload failed: " + err.Error()}, http.StatusInternalServerError) 1407 return 1408 } 1409 WriteSuccess(w) 1410 } 1411 1412 // renterValidateSiaPathHandler handles the API call that validates a siapath 1413 func (api *API) renterValidateSiaPathHandler(w http.ResponseWriter, _ *http.Request, ps httprouter.Params) { 1414 // Try and create a new siapath, this will validate the potential siapath 1415 _, err := modules.NewSiaPath(ps.ByName("siapath")) 1416 if err != nil { 1417 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1418 return 1419 } 1420 WriteSuccess(w) 1421 } 1422 1423 // renterDirHandlerGET handles the API call to query a directory 1424 func (api *API) renterDirHandlerGET(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1425 var siaPath modules.SiaPath 1426 var err error 1427 str := ps.ByName("siapath") 1428 if str == "" || str == "/" { 1429 siaPath = modules.RootSiaPath() 1430 } else { 1431 siaPath, err = modules.NewSiaPath(str) 1432 } 1433 if err != nil { 1434 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1435 return 1436 } 1437 directories, err := api.renter.DirList(siaPath) 1438 if err != nil { 1439 WriteError(w, Error{"failed to get directory contents:" + err.Error()}, http.StatusInternalServerError) 1440 return 1441 } 1442 files, err := api.renter.FileList(siaPath, false, true) 1443 if err != nil { 1444 WriteError(w, Error{"failed to get file infos:" + err.Error()}, http.StatusInternalServerError) 1445 return 1446 } 1447 WriteJSON(w, RenterDirectory{ 1448 Directories: directories, 1449 Files: files, 1450 }) 1451 return 1452 } 1453 1454 // renterDirHandlerPOST handles the API call to create, delete and rename a 1455 // directory 1456 func (api *API) renterDirHandlerPOST(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 1457 // Parse action 1458 action := req.FormValue("action") 1459 if action == "" { 1460 WriteError(w, Error{"you must set the action you wish to execute"}, http.StatusInternalServerError) 1461 return 1462 } 1463 siaPath, err := modules.NewSiaPath(ps.ByName("siapath")) 1464 if err != nil { 1465 WriteError(w, Error{err.Error()}, http.StatusBadRequest) 1466 return 1467 } 1468 if action == "create" { 1469 // Call the renter to create directory 1470 err := api.renter.CreateDir(siaPath) 1471 if err != nil { 1472 WriteError(w, Error{"failed to create directory: " + err.Error()}, http.StatusInternalServerError) 1473 return 1474 } 1475 WriteSuccess(w) 1476 return 1477 } 1478 if action == "delete" { 1479 err := api.renter.DeleteDir(siaPath) 1480 if err != nil { 1481 WriteError(w, Error{"failed to delete directory: " + err.Error()}, http.StatusInternalServerError) 1482 return 1483 } 1484 WriteSuccess(w) 1485 return 1486 } 1487 if action == "rename" { 1488 newSiaPath, err := modules.NewSiaPath(req.FormValue("newsiapath")) 1489 if err != nil { 1490 WriteError(w, Error{"failed to parse newsiapath: " + err.Error()}, http.StatusBadRequest) 1491 return 1492 } 1493 err = api.renter.RenameDir(siaPath, newSiaPath) 1494 if err != nil { 1495 WriteError(w, Error{"failed to rename directory: " + err.Error()}, http.StatusInternalServerError) 1496 return 1497 } 1498 WriteSuccess(w) 1499 return 1500 } 1501 1502 // Report that no calls were made 1503 WriteError(w, Error{"no calls were made, please check your submission and try again"}, http.StatusInternalServerError) 1504 return 1505 }