gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/tus.go (about) 1 package renter 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "sync" 12 "time" 13 14 "github.com/tus/tusd/pkg/handler" 15 "gitlab.com/NebulousLabs/errors" 16 "gitlab.com/NebulousLabs/fastrand" 17 "gitlab.com/SkynetLabs/skyd/build" 18 "gitlab.com/SkynetLabs/skyd/skymodules" 19 "gitlab.com/SkynetLabs/skyd/skymodules/renter/filesystem" 20 "go.sia.tech/siad/crypto" 21 "go.sia.tech/siad/modules" 22 "go.sia.tech/siad/persist" 23 ) 24 25 var ( 26 // PruneTUSUploadTimeout is the time of inactivity after which a 27 // skynetTUSUpload is pruned from skynetTUSUploader. Inactivity refers to 28 // the time passed since WriteChunk was called. 29 PruneTUSUploadTimeout = build.Select(build.Var{ 30 Dev: 5 * time.Minute, 31 Standard: 24 * time.Hour, // 24 hours to give plenty of time to finish an upload 32 Testing: 10 * time.Second, 33 }).(time.Duration) 34 35 // PruneTUSUploadInterval is the time that passes between pruning attempts. 36 // The smaller the interval, the smaller the batches of uploads we prune at 37 // a time. 38 PruneTUSUploadInterval = build.Select(build.Var{ 39 Dev: time.Minute, 40 Standard: time.Hour, 41 Testing: time.Second, 42 }).(time.Duration) 43 ) 44 45 // ErrTUSUploadInterrupted is returned if the upload seemingly succeeded but 46 // didn't actually upload a full chunk. 47 var ErrTUSUploadInterrupted = errors.New("tus upload was interrupted - please retry") 48 49 type ( 50 // skynetTUSUploader implements multiple TUS interfaces for skynet uploads 51 // allowing for resumable uploads. 52 skynetTUSUploader struct { 53 staticUploadStore skymodules.SkynetTUSUploadStore 54 55 ongoingUploads map[string]*ongoingTUSUpload 56 staticRenter *Renter 57 mu sync.Mutex 58 } 59 60 // ongoingTUSUpload implements multiple TUS interfaces for uploads. 61 ongoingTUSUpload struct { 62 staticUpload skymodules.SkynetTUSUpload 63 staticUploader *skynetTUSUploader 64 65 fileNode *filesystem.FileNode 66 closed bool 67 mu sync.Mutex 68 } 69 ) 70 71 // TusSkyfileMetadata is a helper which constructs metadata for tus uploads give 72 // some inputs. 73 func TusSkyfileMetadata(fileName string, fileType string, size uint64, mode os.FileMode) skymodules.SkyfileMetadata { 74 return skymodules.SkyfileMetadata{ 75 Filename: fileName, 76 Length: size, 77 Mode: mode, 78 Subfiles: skymodules.SkyfileSubfiles{ 79 fileName: skymodules.SkyfileSubfileMetadata{ 80 Filename: fileName, 81 ContentType: fileType, 82 Len: size, 83 Offset: 0, 84 }, 85 }, 86 } 87 } 88 89 // newSkynetTUSUploader creates a new uploader. 90 func newSkynetTUSUploader(renter *Renter, tus skymodules.SkynetTUSUploadStore) *skynetTUSUploader { 91 return &skynetTUSUploader{ 92 staticUploadStore: tus, 93 staticRenter: renter, 94 ongoingUploads: make(map[string]*ongoingTUSUpload), 95 } 96 } 97 98 // SkynetTUSUploader returns the renter's uploader for registering in the API. 99 func (r *Renter) SkynetTUSUploader() skymodules.SkynetTUSDataStore { 100 return r.staticSkynetTUSUploader 101 } 102 103 // Close closes the underlying upload store. 104 func (stu *skynetTUSUploader) Close() error { 105 return stu.staticUploadStore.Close() 106 } 107 108 // AsConcatableUpload implements the ConcaterDataStore interface. 109 func (stu *skynetTUSUploader) AsConcatableUpload(upload handler.Upload) handler.ConcatableUpload { 110 return upload.(*ongoingTUSUpload) 111 } 112 113 // NewLock implements the handler.Locker interface by passing on the call to the 114 // upload storage backend. 115 func (stu *skynetTUSUploader) NewLock(id string) (handler.Lock, error) { 116 return stu.staticUploadStore.NewLock(id) 117 } 118 119 // NewUpload creates a new upload from fileinfo. 120 func (stu *skynetTUSUploader) NewUpload(ctx context.Context, info handler.FileInfo) (handler.Upload, error) { 121 // Create the upload object. 122 info.ID = persist.UID() 123 124 // Get a siapath. 125 sp := skymodules.RandomSkynetFilePath() 126 127 // Get the filename from either the metadata or path. 128 fileName := sp.Name() 129 fileNameMD, fileNameFound := info.MetaData["filename"] 130 if fileNameFound { 131 fileName = fileNameMD 132 } 133 fileType := info.MetaData["filetype"] 134 135 // Create the skyfile upload params. 136 sup := skymodules.SkyfileUploadParameters{ 137 SiaPath: sp, 138 Filename: fileName, 139 BaseChunkRedundancy: SkyfileDefaultBaseChunkRedundancy, 140 } 141 142 // Create metadata. 143 var sm skymodules.SkyfileMetadata 144 var smBytes []byte 145 var err error 146 smStr, customSM := info.MetaData["skyfilemetadata"] 147 if customSM { 148 // Check if the client provided custom metadata. If they do, this will 149 // completely overwrite any other filename or filetype they might have 150 // provided and use that instead0. 151 smBytes = []byte(smStr) 152 err = json.Unmarshal(smBytes, &sm) 153 if err != nil { 154 return nil, errors.AddContext(err, "failed to unmarshal custom metadata") 155 } 156 } else { 157 // Use regular metadata. 158 sm = TusSkyfileMetadata(sup.Filename, fileType, uint64(info.Size), sup.Mode) 159 smBytes, err = json.Marshal(sm) 160 if err != nil { 161 return nil, errors.AddContext(err, "failed to marhsal metadata") 162 } 163 } 164 165 // Check whether it's valid. 166 err = skymodules.ValidateSkyfileMetadata(sm) 167 if err != nil { 168 return nil, errors.AddContext(err, "invalid metadata") 169 } 170 171 // Set the upload params to 'force' to allow overwriting the fileNode. 172 sup.Force = true 173 174 // Create the upload. 175 upload, err := stu.managedCreateUpload(info, sp, fileName, SkyfileDefaultBaseChunkRedundancy, skymodules.RenterDefaultDataPieces, skymodules.RenterDefaultParityPieces, smBytes, crypto.TypePlain) 176 if err != nil { 177 return nil, errors.AddContext(err, "failed to save new upload") 178 } 179 return upload, nil 180 } 181 182 // managedCreateUpload creates a new ongoing upload. 183 func (stu *skynetTUSUploader) managedCreateUpload(fi handler.FileInfo, sp skymodules.SiaPath, fileName string, baseChunkRedundancy uint8, fanoutDataPieces, fanoutParityPieces int, smBytes []byte, ct crypto.CipherType) (*ongoingTUSUpload, error) { 184 ctx := stu.staticRenter.tg.StopCtx() 185 upload, err := stu.staticUploadStore.CreateUpload(ctx, fi, sp, fileName, baseChunkRedundancy, fanoutDataPieces, fanoutParityPieces, smBytes, ct) 186 if err != nil { 187 return nil, errors.AddContext(err, "upload store failed to create new upload") 188 } 189 stu.mu.Lock() 190 defer stu.mu.Unlock() 191 ou := &ongoingTUSUpload{ 192 staticUpload: upload, 193 staticUploader: stu, 194 } 195 stu.ongoingUploads[fi.ID] = ou 196 return ou, nil 197 } 198 199 // GetUpload returns an existing upload. 200 func (stu *skynetTUSUploader) GetUpload(ctx context.Context, id string) (handler.Upload, error) { 201 // Get the upload from the db first. 202 upload, err := stu.staticUploadStore.GetUpload(ctx, id) 203 if err != nil { 204 return nil, err 205 } 206 // Search for ongoing upload. 207 stu.mu.Lock() 208 defer stu.mu.Unlock() 209 ou, exists := stu.ongoingUploads[id] 210 if exists { 211 // Only swap out the upload we just fetched. 212 ou.staticUpload = upload 213 return ou, nil 214 } 215 216 // If it doesn't exist create one. 217 ou = &ongoingTUSUpload{ 218 staticUpload: upload, 219 staticUploader: stu, 220 } 221 stu.ongoingUploads[id] = ou 222 return ou, nil 223 } 224 225 // Skylink returns the skylink for the upload with the given ID. 226 func (stu *skynetTUSUploader) Skylink(id string) (skymodules.Skylink, bool) { 227 u, err := stu.staticUploadStore.GetUpload(stu.staticRenter.tg.StopCtx(), id) 228 if err != nil { 229 return skymodules.Skylink{}, false 230 } 231 return u.GetSkylink() 232 } 233 234 // Close closes the upload and underlying filenode. 235 func (u *ongoingTUSUpload) Close() error { 236 return u.managedClose() 237 } 238 239 // tryUploadSmallFile checks if the file to upload and its metadata fit within a 240 // single sector. It returns true or false depending on whether the file is 241 // small and any buffered data. 242 func (u *ongoingTUSUpload) tryUploadSmallFile(ctx context.Context, reader io.Reader) ([]byte, bool, error) { 243 // Get skyfile metadata. 244 fi, err := u.staticUpload.GetInfo(ctx) 245 if err != nil { 246 return nil, false, err 247 } 248 smBytes, err := u.staticUpload.SkyfileMetadata(ctx) 249 if err != nil { 250 return nil, false, err 251 } 252 253 // Check if the file is indeed a small file. 254 headerSize := uint64(skymodules.SkyfileLayoutSize + len(smBytes)) 255 if uint64(fi.Size)+headerSize > modules.SectorSize { 256 return nil, false, nil 257 } 258 259 // Download the data and save it. 260 buf := make([]byte, fi.Size) 261 _, err = io.ReadFull(reader, buf) 262 return buf, true, err 263 } 264 265 // WriteChunk writes the chunk to the provided offset. 266 func (u *ongoingTUSUpload) WriteChunk(ctx context.Context, offset int64, src io.Reader) (_ int64, err error) { 267 u.mu.Lock() 268 defer u.mu.Unlock() 269 uploader := u.staticUploader 270 271 // Get fileinfo from upload. 272 fi, err := u.staticUpload.GetInfo(ctx) 273 if err != nil { 274 return 0, errors.AddContext(err, "failed to get fileinfo") 275 } 276 277 // Fetch the upload params. 278 sup, up, err := u.staticUpload.UploadParams(ctx) 279 if err != nil { 280 return 0, errors.AddContext(err, "failed to fetch upload params") 281 } 282 283 // Make sure to queue a bubble for the new chunk at the end. 284 dir, err := up.SiaPath.Dir() 285 if err != nil { 286 return 0, errors.AddContext(err, "failed to get parent folder's siapath") 287 } 288 u.staticUploader.staticRenter.staticDirUpdateBatcher.callQueueDirUpdate(dir) 289 290 // If the offset is 0, we try to determine if the upload is large or small. 291 var isSmall bool 292 if offset == 0 { 293 var smallFileData []byte 294 smallFileData, isSmall, err = u.tryUploadSmallFile(ctx, src) 295 if err != nil { 296 return 0, err 297 } 298 // If it is a small file we are done. Unless it's a partial 299 // upload. In that case we still upload it the same way as a 300 // large upload to receive a fanout. 301 if isSmall && !fi.IsPartial { 302 // Finish the small upload. 303 smBytes, err := u.staticUpload.SkyfileMetadata(ctx) 304 if err != nil { 305 return 0, errors.AddContext(err, "failed to fetch smBytes") 306 } 307 skylink, err := u.finishUploadSmall(ctx, sup, smBytes, smallFileData) 308 if err != nil { 309 return 0, err 310 } 311 err = u.staticUpload.CommitFinishUpload(ctx, skylink) 312 if err != nil { 313 return 0, errors.AddContext(err, "failed to finish small upload") 314 } 315 return int64(len(smallFileData)), nil 316 } 317 // Otherwise we add the data to a reader to be uploaded as a 318 // large file. 319 src = io.MultiReader(bytes.NewReader(smallFileData), src) 320 } 321 322 // If we get to this point with a small file, something is wrong. 323 // Theoretically this is not possible but return an error for extra safety. 324 if isSmall && !fi.IsPartial { 325 return 0, errors.New("can't upload another chunk to a small file upload") 326 } 327 328 // Upload is a large upload - we need a filenode for the fanout. If it 329 // doesn't exist yet, create it. 330 if u.fileNode == nil { 331 u.fileNode, err = uploader.staticRenter.managedInitFileNode(up, 0) 332 if err != nil { 333 return 0, err 334 } 335 } 336 fileNode := u.fileNode 337 ec := fileNode.ErasureCode() 338 339 // Sanity check offset. 340 // NOTE: If the offset is not chunk aligned, it means that a previous call 341 // to WriteChunk read an incomplete chunk from src and padded it. After 342 // uploading a padded chunk, we can't upload more chunks. That's why the 343 // client needs to make sure that the chunkSize they use is aligned with the 344 // chunkSize of the skyfile's fanout. 345 deps := u.staticUploader.staticRenter.staticDeps 346 if offset%int64(fileNode.ChunkSize()) != 0 { 347 err := fmt.Errorf("offset is not chunk aligned - make sure chunkSize is set to a multiple of %v for these upload params", fileNode.ChunkSize()) 348 if build.Release == "testing" { 349 // In test builds we want to be aware of this. 350 build.Critical(err) 351 } 352 return 0, err 353 } 354 355 // Simulate unstable connection. 356 if deps.Disrupt("TUSUnstable") { 357 // 50% chance that write fails 358 if fastrand.Intn(2) == 0 { 359 return 0, errors.New("TUSUnstable") 360 } 361 } 362 363 // Upload. 364 cr := NewFanoutChunkReader(src, ec, fileNode.MasterKey()) 365 var chunks []*unfinishedUploadChunk 366 chunks, n, err := uploader.staticRenter.callUploadStreamFromReaderWithFileNodeNoBlock(ctx, fileNode, cr, offset) 367 368 // Simulate loss of connection one byte early. 369 if deps.Disrupt("TUSConnectionDropped") { 370 n-- 371 err = nil 372 } 373 374 // If less than a full chunk was uploaded, we expect the file to be done. If 375 // that's not the case, the connection was closed early. That means the chunk 376 // was incorrectly padded and needs to be removed again by shrinking the siafile 377 // by one chunk before the user can retry the upload. 378 if n%int64(fileNode.ChunkSize()) != 0 && fi.Offset+n != fi.Size { 379 shrinkErr := fileNode.Shrink(uint64(fi.Offset) / fileNode.ChunkSize()) 380 if shrinkErr != nil { 381 return 0, shrinkErr 382 } 383 // Make sure that we return an error if none was returned by the 384 // upload. That way the client will know to retry. This usually 385 // happens if we reach a timeout in the reverse proxy. 386 err = errors.Compose(err, ErrTUSUploadInterrupted) 387 } 388 // In case of any error, return early. 389 if err != nil { 390 return 0, err 391 } 392 393 // Wait for chunks to become available. 394 for _, chunk := range chunks { 395 select { 396 case <-ctx.Done(): 397 return 0, errors.New("reached timeout before chunks became available") 398 case <-chunk.staticAvailableChan: 399 } 400 if chunk.err != nil { 401 return 0, errors.AddContext(err, "failed to upload chunk") 402 } 403 } 404 return n, u.staticUpload.CommitWriteChunk(ctx, fi.Offset+n, time.Now(), isSmall, cr.Fanout()) 405 } 406 407 // GetInfo returns the file info. 408 func (u *ongoingTUSUpload) GetInfo(ctx context.Context) (handler.FileInfo, error) { 409 return u.staticUpload.GetInfo(ctx) 410 } 411 412 // GetReader returns a reader for the upload. 413 // NOTE: This is part of the core upload interface but doesn't seem to be 414 // required for uploads to work. It is not necessary for this to work on 415 // incomplete uploads and it's recommended to implement this for completed 416 // uploads. 417 func (u *ongoingTUSUpload) GetReader(ctx context.Context) (io.Reader, error) { 418 return bytes.NewReader([]byte{}), handler.ErrNotImplemented 419 } 420 421 // finishUploadLarge handles finishing up a large upload. 422 func (u *ongoingTUSUpload) finishUploadLarge(ctx context.Context, fanout []byte, sup skymodules.SkyfileUploadParameters, fi handler.FileInfo, masterKey crypto.CipherKey, ec skymodules.ErasureCoder, smBytes []byte, concat bool) (skymodules.Skylink, error) { 423 r := u.staticUploader.staticRenter 424 // Sanity check fanout length 425 expectedLength := skymodules.ExpectedFanoutBytesLen(uint64(fi.Size), ec.MinPieces(), ec.NumPieces()-ec.MinPieces(), masterKey.Type()) 426 if len(fanout) != int(expectedLength) { 427 return skymodules.Skylink{}, fmt.Errorf("can't finish large upload - invalid fanout length: got %v expected %v", len(fanout), expectedLength) 428 } 429 skylink, err := r.managedCreateSkylinkRawMD(ctx, sup, smBytes, fanout, uint64(fi.Size), masterKey, ec) 430 if err != nil { 431 return skymodules.Skylink{}, errors.AddContext(err, "failed to create skylink for large upload") 432 } 433 if concat { 434 // Pin the large file if this is finishing up a concatenated 435 // upload. We don't need to do this for small uploads because 436 // they are always uploaded in one go. Large uploads can be 437 // spread across multiple servers. 438 // NOTE: We do this lazily since we know all the data has already been 439 // uploaded recently. We also skip the base sector upload because that 440 // just happened in managedCreateSkylinkRawMD. 441 err = u.staticUploader.staticRenter.managedPinSkylink(ctx, skylink, skymodules.SkyfileUploadParameters{ 442 BaseChunkRedundancy: SkyfileDefaultBaseChunkRedundancy, 443 SiaPath: skymodules.RandomSkynetFilePath(), 444 }, defaultNewStreamTimeout, skymodules.DefaultSkynetPricePerMS, true, true) 445 if err != nil { 446 return skymodules.Skylink{}, errors.AddContext(err, "failed to pin large upload") 447 } 448 } 449 return skylink, nil 450 } 451 452 // finishUploadSmall handles finishing up a small upload. 453 func (u *ongoingTUSUpload) finishUploadSmall(ctx context.Context, sup skymodules.SkyfileUploadParameters, smBytes, smallUploadData []byte) (skylink skymodules.Skylink, err error) { 454 r := u.staticUploader.staticRenter 455 return r.managedUploadSkyfileSmallFile(ctx, sup, smBytes, smallUploadData) 456 } 457 458 // FinishUpload is called when the upload is done. 459 func (u *ongoingTUSUpload) FinishUpload(ctx context.Context) (err error) { 460 // Close upload when done. 461 defer func() { 462 err = errors.Compose(err, u.Close()) 463 }() 464 465 u.mu.Lock() 466 defer u.mu.Unlock() 467 468 // If the upload is a partial upload we are done. We don't need to 469 // upload the metadata or create a skylink. 470 fi, err := u.staticUpload.GetInfo(ctx) 471 if err != nil { 472 return errors.AddContext(err, "failed to fetch fileinfo") 473 } 474 if fi.IsPartial { 475 return nil 476 } 477 // If the upload is a small file upload with >0 size we are done because 478 // it was already finalised in WriteChunk. 479 if u.fileNode == nil && fi.Size > 0 { 480 return nil 481 } 482 483 // Finish the large or 0-byte upload. 484 sup, _, err := u.staticUpload.UploadParams(ctx) 485 if err != nil { 486 return errors.AddContext(err, "failed to fetch upload params") 487 } 488 smBytes, err := u.staticUpload.SkyfileMetadata(ctx) 489 if err != nil { 490 return errors.AddContext(err, "failed to fetch smBytes") 491 } 492 fanout, err := u.staticUpload.Fanout(ctx) 493 if err != nil { 494 return errors.AddContext(err, "failed to fetch fanout") 495 } 496 497 // If the upload is 0-byte, WriteChunk is skipped. So we need to finish 498 // the upload here. 499 var skylink skymodules.Skylink 500 if fi.Size == 0 { 501 skylink, err = u.finishUploadSmall(ctx, sup, smBytes, []byte{}) 502 } else { 503 skylink, err = u.finishUploadLarge(ctx, fanout, sup, fi, u.fileNode.MasterKey(), u.fileNode.ErasureCode(), smBytes, false) 504 } 505 if err != nil { 506 return errors.AddContext(err, "failed to finish upload") 507 } 508 return u.staticUpload.CommitFinishUpload(ctx, skylink) 509 } 510 511 // managedClose closes the upload and underlying filenode. 512 func (u *ongoingTUSUpload) managedClose() error { 513 u.mu.Lock() 514 defer u.mu.Unlock() 515 if u.closed { 516 return nil 517 } 518 u.closed = true 519 // For large files we need to close the additional fileNode. 520 if u.fileNode != nil { 521 return u.fileNode.Close() 522 } 523 return nil 524 } 525 526 // threadedPruneTUSUploads periodically cleans up the uploads launched by the 527 // TUS endpoints. 528 func (r *Renter) threadedPruneTUSUploads() { 529 if r.staticDeps.Disrupt("TUSNoPrune") { 530 return // disable pruning 531 } 532 ticker := time.NewTicker(PruneTUSUploadInterval) 533 for { 534 select { 535 case <-r.tg.StopChan(): 536 return // shutdown 537 case <-ticker.C: 538 } 539 toDelete, err := r.staticSkynetTUSUploader.staticUploadStore.ToPrune(r.tg.StopCtx()) 540 if err != nil { 541 r.staticLog.Print("Failed to get TUS uploads for pruning", err) 542 } 543 544 // Delete files. 545 var prunedIDs []string 546 for _, upload := range toDelete { 547 uploadID, sp, err := upload.PruneInfo(r.tg.StopCtx()) 548 if err != nil { 549 r.staticLog.Print("WARN: failed to fetch prune info from upload", err) 550 continue 551 } 552 553 // Delete on disk. We don't care if the extended siapath 554 // didn't exist but we print some loging if the regular 555 // deletion failed. 556 spFanout, err := sp.AddSuffixStr(skymodules.ExtendedSuffix) 557 if err != nil { 558 r.staticLog.Critical("Failed to append ExtededSuffix to SiaPath", err) 559 } 560 if err := r.DeleteFile(sp); err != nil { 561 r.staticLog.Printf("WARN: failed to delete SiaPath %v: %v", sp.String(), err) 562 } 563 if err := r.DeleteFile(spFanout); err != nil && !errors.Contains(err, filesystem.ErrNotExist) { 564 r.staticLog.Printf("WARN: failed to delete extended SiaPath %v: %v", sp.String(), err) 565 } 566 567 // Remember the ID to later prune it from the store. 568 prunedIDs = append(prunedIDs, uploadID) 569 570 // Delete from ongoing uploads if it exists. 571 r.staticSkynetTUSUploader.mu.Lock() 572 ongoingUpload, exists := r.staticSkynetTUSUploader.ongoingUploads[uploadID] 573 if exists { 574 delete(r.staticSkynetTUSUploader.ongoingUploads, uploadID) 575 err = ongoingUpload.Close() 576 if err != nil { 577 r.staticLog.Critical("failed to close ongoing upload", err) 578 } 579 } 580 r.staticSkynetTUSUploader.mu.Unlock() 581 } 582 583 // Prune from the store. 584 if len(prunedIDs) > 0 { 585 err = r.staticSkynetTUSUploader.staticUploadStore.Prune(r.tg.StopCtx(), prunedIDs) 586 if err != nil { 587 r.staticLog.Printf("WARN: failed to prune %v uploads from store: %v", len(prunedIDs), err) 588 } 589 } 590 } 591 } 592 593 // TUSPreUploadCreateCallback is called before creating an upload. It is used to 594 // dynamically check the maximum size of the user's upload according to a set 595 // header field. 596 func TUSPreUploadCreateCallback(hook handler.HookEvent) error { 597 // Sanity check that the size is not deferred. 598 if hook.Upload.SizeIsDeferred { 599 err := errors.New("uploads with deferred size are not supported") 600 return handler.NewHTTPError(err, http.StatusBadRequest) 601 } 602 // Get user's max upload size from request. 603 maxSizeStr := hook.HTTPRequest.Header.Get("SkynetMaxUploadSize") 604 if maxSizeStr == "" { 605 err := errors.New("SkynetMaxUploadSize header is missing") 606 return handler.NewHTTPError(err, http.StatusBadRequest) 607 } 608 var maxSize int64 609 _, err := fmt.Sscan(maxSizeStr, &maxSize) 610 if err != nil { 611 err = errors.AddContext(err, "failed to parse SkynetMaxUploadSize") 612 return handler.NewHTTPError(err, http.StatusBadRequest) 613 } 614 // Check upload size against max size. 615 if hook.Upload.Size > maxSize { 616 err = fmt.Errorf("upload exceeds maximum size: %v > %v", hook.Upload.Size, maxSize) 617 return handler.NewHTTPError(err, http.StatusRequestEntityTooLarge) 618 } 619 return nil 620 } 621 622 // ConcatUploads implements the handler.ConcatableUpload interface. It combines 623 // the provided partial uploads into a single one. 624 func (u *ongoingTUSUpload) ConcatUploads(ctx context.Context, partialUploads []handler.Upload) error { 625 // Get fileinfo. 626 fi, err := u.GetInfo(ctx) 627 if err != nil { 628 return errors.AddContext(err, "failed to fetch fileinfo") 629 } 630 631 // All partial uploads should be marked as such. 632 for _, pu := range partialUploads { 633 fi, err := pu.GetInfo(ctx) 634 if err != nil { 635 return errors.AddContext(err, "failed to fetch fileinfo") 636 } 637 if !fi.IsPartial { 638 return errors.New("can't concat non-partial uploads") 639 } 640 } 641 642 // Concatenate the uploads by combining their fanouts. Concatenated 643 // uploads may never consist of small uploads except for the last 644 // upload. 645 pu := partialUploads[0].(*ongoingTUSUpload) 646 sup, fup, err := u.staticUpload.UploadParams(ctx) 647 if err != nil { 648 return err 649 } 650 chunkSize := skymodules.ChunkSize(fup.CipherType, uint64(fup.ErasureCode.MinPieces())) 651 ec := fup.ErasureCode 652 masterKey := fup.CipherKey 653 var fanout []byte 654 for i := range partialUploads { 655 fi, err := pu.GetInfo(ctx) 656 if err != nil { 657 return errors.AddContext(err, "failed to get partial upload's fileinfo") 658 } 659 isSmall := fi.Size%int64(chunkSize) != 0 660 if i < len(partialUploads)-1 && isSmall { 661 return errors.New("only last upload is allowed to be small") 662 } 663 pu := partialUploads[i].(*ongoingTUSUpload) 664 _, fup, err := pu.staticUpload.UploadParams(ctx) 665 if err != nil { 666 return errors.AddContext(err, "failed to get partial upload's upload params") 667 } 668 if fup.ErasureCode.Identifier() != ec.Identifier() { 669 return errors.New("all partial uploads need to use the same erasure coding") 670 } 671 if fup.CipherType != masterKey.Type() { 672 return errors.New("all masterkeys need to have the same type") 673 } 674 if !bytes.Equal(fup.CipherKey.Key(), masterKey.Key()) { 675 return errors.New("all masterkeys need to be the same") 676 } 677 partialFanout, err := pu.staticUpload.Fanout(ctx) 678 if err != nil { 679 return errors.AddContext(err, "failed to fetch fanout of partial upload") 680 } 681 fanout = append(fanout, partialFanout...) 682 } 683 smBytes, err := u.staticUpload.SkyfileMetadata(ctx) 684 if err != nil { 685 return err 686 } 687 sup.SiaPath = skymodules.RandomSkynetFilePath() 688 skylink, err := u.finishUploadLarge(ctx, fanout, sup, fi, masterKey, ec, smBytes, true) 689 if err != nil { 690 return err 691 } 692 693 // Make sure the updates in mongo are atomic. 694 return u.staticUploader.staticUploadStore.WithTransaction(ctx, func(sctx context.Context) error { 695 // Upon success, we mark the partial uploads as complete to prevent them 696 // from being pruned. 697 for i := range partialUploads { 698 // Commit the partial upload as complete as well. 699 pu = partialUploads[i].(*ongoingTUSUpload) 700 err := pu.staticUpload.CommitFinishUpload(sctx, skylink) 701 if err != nil { 702 return errors.AddContext(err, "failed to commit partial upload") 703 } 704 } 705 return errors.AddContext(u.staticUpload.CommitFinishUpload(sctx, skylink), "failed to commit concatenated upload") 706 }) 707 }