Skip to content

Commit 093f889

Browse files
rosepiercemergify[bot]
authored andcommitted
feat(uploads): resume a single file upload (#1552)
* feat(uploads): resume a single file upload * fix: removing redundant check and corresponding test * fix: isnan updated for IE functionality * fix: updating concurrency check for recent commit changes
1 parent dbf115c commit 093f889

File tree

4 files changed

+484
-12
lines changed

4 files changed

+484
-12
lines changed

src/api/uploads/MultiputUpload.js

Lines changed: 250 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import noop from 'lodash/noop';
8+
import isNaN from 'lodash/isNaN';
89
import { getFileLastModifiedAsISONoMSIfPossible, getBoundedExpBackoffRetryDelay } from '../../utils/uploads';
910
import { retryNumOfTimes } from '../../utils/function';
1011
import { 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';
1927
import 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

Comments
 (0)