55 */
66
77import noop from 'lodash/noop' ;
8+ import isNaN from 'lodash/isNaN' ;
89import { getFileLastModifiedAsISONoMSIfPossible , getBoundedExpBackoffRetryDelay } from '../../utils/uploads' ;
910import { retryNumOfTimes } from '../../utils/function' ;
1011import { digest } from '../../utils/webcrypto' ;
@@ -14,8 +15,15 @@ import {
1415 DEFAULT_RETRY_DELAY_MS ,
1516 ERROR_CODE_UPLOAD_STORAGE_LIMIT_EXCEEDED ,
1617 HTTP_STATUS_CODE_FORBIDDEN ,
18+ MS_IN_S ,
1719} from '../../constants' ;
18- import MultiputPart , { PART_STATE_UPLOADED , PART_STATE_DIGEST_READY , PART_STATE_NOT_STARTED } from './MultiputPart' ;
20+ import MultiputPart , {
21+ PART_STATE_UPLOADED ,
22+ PART_STATE_UPLOADING ,
23+ PART_STATE_DIGEST_READY ,
24+ PART_STATE_COMPUTING_DIGEST ,
25+ PART_STATE_NOT_STARTED ,
26+ } from './MultiputPart' ;
1927import BaseMultiput from './BaseMultiput' ;
2028
2129// Constants used for specifying log event types.
@@ -50,6 +58,8 @@ class MultiputUpload extends BaseMultiput {
5058
5159 initialFileSize : number ;
5260
61+ isResumableUploadsEnabled : boolean ;
62+
5363 successCallback : Function ;
5464
5565 progressCallback : Function ;
@@ -70,6 +80,8 @@ class MultiputUpload extends BaseMultiput {
7080
7181 numPartsUploading : number ;
7282
83+ numResumeRetries : number ;
84+
7385 sessionEndpoints : Object ;
7486
7587 sessionId : string ;
@@ -115,6 +127,66 @@ class MultiputUpload extends BaseMultiput {
115127 this . partSize = 0 ;
116128 this . commitRetryCount = 0 ;
117129 this . clientId = null ;
130+ this . isResumableUploadsEnabled = false ;
131+ }
132+
133+ /**
134+ * Reset values for uploading process.
135+ */
136+ reset ( ) {
137+ this . parts = [ ] ;
138+ this . fileSha1 = null ;
139+ this . totalUploadedBytes = 0 ;
140+ this . numPartsNotStarted = 0 ; // # of parts yet to be processed
141+ this . numPartsDigestComputing = 0 ; // # of parts sent to the digest worker
142+ this . numPartsDigestReady = 0 ; // # of parts with digest finished that are waiting to be uploaded.
143+ this . numPartsUploading = 0 ; // # of parts with upload requests currently inflight
144+ this . numPartsUploaded = 0 ; // # of parts successfully uploaded
145+ this . firstUnuploadedPartIndex = 0 ; // Index of first part that hasn't been uploaded yet.
146+ this . createSessionNumRetriesPerformed = 0 ;
147+ this . partSize = 0 ;
148+ this . commitRetryCount = 0 ;
149+ }
150+
151+ /**
152+ * Set information about file being uploaded
153+ *
154+ *
155+ * @param {Object } options
156+ * @param {File } options.file
157+ * @param {string } options.folderId - Untyped folder id (e.g. no "folder_" prefix)
158+ * @param {string } [options.fileId] - Untyped file id (e.g. no "file_" prefix)
159+ * @param {string } options.sessionId
160+ * @param {Function } [options.errorCallback]
161+ * @param {Function } [options.progressCallback]
162+ * @param {Function } [options.successCallback]
163+ * @return {void }
164+ */
165+ setFileInfo ( {
166+ file,
167+ folderId,
168+ errorCallback,
169+ progressCallback,
170+ successCallback,
171+ overwrite = true ,
172+ fileId,
173+ } : {
174+ errorCallback ?: Function ,
175+ file : File ,
176+ fileId : ?string ,
177+ folderId : string ,
178+ overwrite ?: boolean ,
179+ progressCallback ?: Function ,
180+ successCallback ?: Function ,
181+ } ) : void {
182+ this. file = file ;
183+ this . fileName = this . file . name ;
184+ this . folderId = folderId ;
185+ this . errorCallback = errorCallback || noop ;
186+ this . progressCallback = progressCallback || noop ;
187+ this . successCallback = successCallback || noop ;
188+ this . overwrite = overwrite ;
189+ this . fileId = fileId ;
118190 }
119191
120192 /**
@@ -329,6 +401,174 @@ class MultiputUpload extends BaseMultiput {
329401 this . processNextParts ( ) ;
330402 }
331403
404+ /**
405+ * Resume uploading the given file
406+ *
407+ *
408+ * @param {Object } options
409+ * @param {File } options.file
410+ * @param {string } options.folderId - Untyped folder id (e.g. no "folder_" prefix)
411+ * @param {string } [options.fileId] - Untyped file id (e.g. no "file_" prefix)
412+ * @param {string } options.sessionId
413+ * @param {Function } [options.errorCallback]
414+ * @param {Function } [options.progressCallback]
415+ * @param {Function } [options.successCallback]
416+ * @return {void }
417+ */
418+ resume ( {
419+ file,
420+ folderId,
421+ errorCallback,
422+ progressCallback,
423+ sessionId,
424+ successCallback,
425+ overwrite = true ,
426+ fileId,
427+ } : {
428+ errorCallback ?: Function ,
429+ file : File ,
430+ fileId : ?string ,
431+ folderId : string ,
432+ overwrite ?: boolean ,
433+ progressCallback ?: Function ,
434+ sessionId : string ,
435+ successCallback ?: Function ,
436+ } ) : void {
437+ this . setFileInfo ( { file, folderId, errorCallback, progressCallback, successCallback, overwrite, fileId } ) ;
438+ this . sessionId = sessionId ;
439+
440+ if ( ! this . sha1Worker ) {
441+ this . sha1Worker = createWorker ( ) ;
442+ }
443+ this . sha1Worker . addEventListener ( 'message' , this . onWorkerMessage ) ;
444+
445+ this . getSessionInfo ( ) ;
446+ }
447+
448+ /**
449+ * Get session information from API.
450+ * Uses session info to commit a complete session or continue an in-progress session.
451+ *
452+ * @private
453+ * @return {void }
454+ */
455+ getSessionInfo = async ( ) : Promise < any > => {
456+ const uploadUrl = this . getBaseUploadUrl ( ) ;
457+ const sessionUrl = `${ uploadUrl } /files/upload_sessions/${ this . sessionId } ` ;
458+ try {
459+ const response = await this . xhr . get ( { url : sessionUrl } ) ;
460+ this . getSessionSuccessHandler ( response . data ) ;
461+ } catch ( error ) {
462+ this . getSessionErrorHandler ( error ) ;
463+ }
464+ } ;
465+
466+ /**
467+ * Handles a getSessionInfo success and either commits the session or continues to process
468+ * the parts that still need to be uploaded.
469+ *
470+ * @param response
471+ * @return {void }
472+ */
473+ getSessionSuccessHandler ( data : any ) : void {
474+ const { part_size , session_endpoints } = data ;
475+
476+ // Set session information gotten from API response
477+ this . partSize = part_size ;
478+ this . sessionEndpoints = {
479+ ...this . sessionEndpoints ,
480+ uploadPart : session_endpoints . upload_part ,
481+ listParts : session_endpoints . list_parts ,
482+ commit : session_endpoints . commit ,
483+ abort : session_endpoints . abort ,
484+ logEvent : session_endpoints . log_event ,
485+ } ;
486+
487+ // Reset uploading process for parts that were in progress when the upload failed
488+ let nextUploadIndex = this . firstUnuploadedPartIndex ;
489+ while ( this . numPartsUploading > 0 || this . numPartsDigestComputing > 0 ) {
490+ const part = this . parts [ nextUploadIndex ] ;
491+ if ( part && part . state === PART_STATE_UPLOADING ) {
492+ part . state = PART_STATE_DIGEST_READY ;
493+ part . numUploadRetriesPerformed = 0 ;
494+ part . timing = { } ;
495+ part . uploadedBytes = 0 ;
496+
497+ this . numPartsUploading -= 1 ;
498+ this . numPartsDigestReady += 1 ;
499+ } else if ( part && part . state === PART_STATE_COMPUTING_DIGEST ) {
500+ part . state = PART_STATE_NOT_STARTED ;
501+ part . numDigestRetriesPerformed = 0 ;
502+ part . timing = { } ;
503+
504+ this . numPartsDigestComputing -= 1 ;
505+ this . numPartsNotStarted += 1 ;
506+ }
507+ nextUploadIndex += 1 ;
508+ }
509+
510+ this . processNextParts ( ) ;
511+ }
512+
513+ /**
514+ * Handle error from getting upload session.
515+ * Restart uploads without valid sessions from the beginning of the upload process.
516+ *
517+ * @param error
518+ * @return {void }
519+ */
520+ getSessionErrorHandler ( error : Error ) : void {
521+ if ( this . isDestroyed ( ) ) {
522+ return ;
523+ }
524+
525+ const errorData = this . getErrorResponse ( error ) ;
526+ if ( this . numResumeRetries > this . config . retries ) {
527+ this . errorCallback ( errorData ) ;
528+ return ;
529+ }
530+
531+ if ( errorData && errorData . status === 429 ) {
532+ let retryAfterMs = DEFAULT_RETRY_DELAY_MS ;
533+ if ( errorData . headers ) {
534+ const retryAfterSec = parseInt (
535+ errorData . headers [ 'retry-after' ] || errorData . headers . get ( 'Retry-After' ) ,
536+ 10 ,
537+ ) ;
538+ if ( ! isNaN ( retryAfterSec ) ) {
539+ retryAfterMs = retryAfterSec * MS_IN_S ;
540+ }
541+ }
542+ this . retryTimeout = setTimeout ( this . getSessionInfo , retryAfterMs ) ;
543+ this . numResumeRetries += 1 ;
544+ } else if ( errorData && errorData . status >= 500 ) {
545+ this . retryTimeout = setTimeout ( this . getSessionInfo , 2 ** this . numResumeRetries * MS_IN_S ) ;
546+ this . numResumeRetries += 1 ;
547+ } else {
548+ // Restart upload process for errors resulting from invalid session
549+ this. parts . forEach ( part => {
550+ part . cancel ( ) ;
551+ } ) ;
552+ this . reset ( ) ;
553+
554+ // Abort session
555+ clearTimeout ( this . createSessionTimeout ) ;
556+ clearTimeout ( this . commitSessionTimeout ) ;
557+ this . abortSession ( ) ;
558+ // Restart the uploading process from the beginning
559+ const uploadOptions : Object = {
560+ file : this . file ,
561+ folderId : this . folderId ,
562+ errorCallback : this . errorCallback ,
563+ progressCallback : this . progressCallback ,
564+ successCallback : this . successCallback ,
565+ overwrite : this . overwrite ,
566+ fileId : this . fileId ,
567+ } ;
568+ this . upload ( uploadOptions ) ;
569+ }
570+ }
571+
332572 /**
333573 * Session error handler.
334574 * Retries the create session request or fails the upload.
@@ -340,7 +580,9 @@ class MultiputUpload extends BaseMultiput {
340580 * @return {Promise }
341581 */
342582 async sessionErrorHandler ( error : ?Error , logEventType : string , logMessage ?: string ) : Promise < any > {
343- this .destroy ( ) ;
583+ if ( ! this . isResumableUploadsEnabled ) {
584+ this . destroy ( ) ;
585+ }
344586 const errorData = this . getErrorResponse ( error ) ;
345587 this . errorCallback ( errorData ) ;
346588
@@ -358,10 +600,13 @@ class MultiputUpload extends BaseMultiput {
358600 this . config . retries,
359601 this . config . initialRetryDelayMs,
360602 ) ;
361-
362- this . abortSession ( ) ;
603+ if ( ! this . isResumableUploadsEnabled ) {
604+ this . abortSession( ) ;
605+ }
363606 } catch ( err ) {
364- this . abortSession ( ) ;
607+ if ( ! this . isResumableUploadsEnabled ) {
608+ this . abortSession( ) ;
609+ }
365610 }
366611 }
367612
0 commit comments