Skip to content

Commit 59feabc

Browse files
authored
New: Chunked uploads (#15)
- Files >50MB will be uploaded with the chunked uploads API by default - The 'chunked' Content Uploader option can be set to false to disable chunked uploads - Chunk upload failures due to rate limiting or network errors will be retried until success
1 parent 482287e commit 59feabc

11 files changed

Lines changed: 794 additions & 73 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"react-modal": "^2.0.2",
8080
"react-tether": "^0.5.7",
8181
"react-virtualized": "^9.8.0",
82+
"rusha": "^0.8.6",
8283
"whatwg-fetch": "^2.0.3"
8384
},
8485
"devDependencies": {

src/api/Chunk.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* @flow
3+
* @file Represents a chunked part of a file - used by the chunked upload API
4+
* @author Box
5+
*/
6+
7+
import noop from 'lodash.noop';
8+
import Base from './Base';
9+
import type { StringMap } from '../flowTypes';
10+
11+
const UPLOAD_RETRY_INTERVAL_MS = 1000;
12+
13+
class Chunk extends Base {
14+
cancelled: boolean;
15+
chunk: ?Object;
16+
data: Object = {};
17+
progress: number = 0;
18+
retry: number;
19+
uploadHeaders: StringMap;
20+
uploadUrl: string;
21+
successCallback: Function;
22+
errorCallback: Function;
23+
progressCallback: Function;
24+
25+
/**
26+
* Returns file part associated with this chunk.
27+
*
28+
* @return {Object}
29+
*/
30+
getPart() {
31+
return this.data.part;
32+
}
33+
34+
/**
35+
* Setup chunk for uploading.
36+
*
37+
* @param {Object} options
38+
* @param {string} options.sessionId - ID of upload session that this chunk belongs to
39+
* @param {Blob} options.part - Chunk blob
40+
* @param {number} options.offset = Chunk offset
41+
* @param {string} options.sha1 - Chunk sha1
42+
* @param {number} options.totalSize - Total size of file that this chunk belongs to
43+
* @param {Function} [options.successCallback] - Chunk upload success callback
44+
* @param {Function} [options.errorCallback] - Chunk upload error callback
45+
* @param {Function} [options.progressCallback] - Chunk upload progress callback
46+
* @return {Promise}
47+
*/
48+
setup({
49+
sessionId,
50+
part,
51+
offset,
52+
sha1,
53+
totalSize,
54+
successCallback = noop,
55+
errorCallback = noop,
56+
progressCallback = noop
57+
}: {
58+
sessionId: string,
59+
part: Blob,
60+
offset: number,
61+
sha1: string,
62+
totalSize: number,
63+
successCallback?: Function,
64+
errorCallback?: Function,
65+
progressCallback?: Function
66+
}): void {
67+
this.uploadUrl = `${this.uploadHost}/api/2.0/files/upload_sessions/${sessionId}`;
68+
69+
// Calculate range
70+
const rangeStart = offset;
71+
let rangeEnd = offset + part.size - 1;
72+
if (rangeEnd > totalSize - 1) {
73+
rangeEnd = totalSize - 1;
74+
}
75+
76+
this.uploadHeaders = {
77+
'Content-Type': 'application/octet-stream',
78+
Digest: `SHA=${sha1}}`,
79+
'Content-Range': `bytes ${rangeStart}-${rangeEnd}/${totalSize}`
80+
};
81+
82+
this.chunk = part;
83+
this.successCallback = successCallback;
84+
this.errorCallback = errorCallback;
85+
this.progressCallback = progressCallback;
86+
}
87+
88+
/**
89+
* Uploads this chunk via the API. Will retry on network failures.
90+
*
91+
* @returns {void}
92+
*/
93+
upload(): void {
94+
if (this.isDestroyed()) {
95+
this.chunk = null;
96+
return;
97+
}
98+
99+
this.xhr.uploadFile({
100+
url: this.uploadUrl,
101+
data: this.chunk,
102+
headers: this.uploadHeaders,
103+
method: 'PUT',
104+
successHandler: (data) => {
105+
this.progress = 1;
106+
this.data = data;
107+
this.chunk = null;
108+
this.successCallback(data);
109+
},
110+
errorHandler: (err) => {
111+
// If there's an error code and it's not 429 from rate limiting, fail the upload
112+
if (err.code && err.code !== 429) {
113+
this.cancel();
114+
this.errorCallback(err);
115+
116+
// Retry on other failures since these are likely to be network errors
117+
} else {
118+
this.retry = setTimeout(() => this.upload(), UPLOAD_RETRY_INTERVAL_MS);
119+
}
120+
},
121+
progressHandler: this.progressCallback
122+
});
123+
}
124+
125+
/**
126+
* Cancels upload for this chunk.
127+
*
128+
* @returns {void}
129+
*/
130+
cancel(): void {
131+
if (this.xhr && typeof this.xhr.abort === 'function') {
132+
this.xhr.abort();
133+
}
134+
135+
clearTimeout(this.retry);
136+
this.chunk = null;
137+
this.data = {};
138+
this.destroy();
139+
}
140+
141+
/**
142+
* Returns progress. Progress goes from 0-1.
143+
*
144+
* @return {number} Progress from 0-1
145+
*/
146+
getProgress(): number {
147+
return this.progress;
148+
}
149+
150+
/**
151+
* Set progress.
152+
*
153+
* @param {number} progress - Numerical progress
154+
*/
155+
setProgress(progress: number): void {
156+
this.progress = progress;
157+
}
158+
}
159+
160+
export default Chunk;

0 commit comments

Comments
 (0)