Skip to content

Commit e66948e

Browse files
aaron-blondeau-dosedplewis
authored andcommitted
Add support for providing file upload progress. (#373)
* Add support for providing file upload progress. * clean up * improve documentation
1 parent 686ce43 commit e66948e

File tree

5 files changed

+95
-15
lines changed

5 files changed

+95
-15
lines changed

src/ParseFile.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111
/* global File */
1212
import CoreManager from './CoreManager';
13+
import type { FullOptions } from './RESTController';
1314

1415
type Base64 = { base64: string };
1516
type FileData = Array<number> | Base64 | File;
@@ -157,9 +158,14 @@ class ParseFile {
157158
/**
158159
* Saves the file to the Parse cloud.
159160
* @param {Object} options
161+
* * Valid options are:<ul>
162+
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
163+
* be used for this request.
164+
* <li>progress: In Browser only, callback for upload progress
165+
* </ul>
160166
* @return {Promise} Promise that is resolved when the save finishes.
161167
*/
162-
save(options?: { useMasterKey?: boolean, success?: any, error?: any }) {
168+
save(options?: FullOptions) {
163169
options = options || {};
164170
var controller = CoreManager.getFileController();
165171
if (!this._previousSave) {
@@ -236,7 +242,7 @@ class ParseFile {
236242
}
237243

238244
var DefaultController = {
239-
saveFile: function(name: string, source: FileSource) {
245+
saveFile: function(name: string, source: FileSource, options?: FullOptions) {
240246
if (source.format !== 'file') {
241247
throw new Error('saveFile can only be used with File-type sources.');
242248
}
@@ -254,10 +260,10 @@ var DefaultController = {
254260
url += '/';
255261
}
256262
url += 'files/' + name;
257-
return CoreManager.getRESTController().ajax('POST', url, source.file, headers).then(res=>res.response)
263+
return CoreManager.getRESTController().ajax('POST', url, source.file, headers, options).then(res=>res.response)
258264
},
259265

260-
saveBase64: function(name: string, source: FileSource, options?: { useMasterKey?: boolean, success?: any, error?: any }) {
266+
saveBase64: function(name: string, source: FileSource, options?: FullOptions) {
261267
if (source.format !== 'base64') {
262268
throw new Error('saveBase64 can only be used with Base64-type sources.');
263269
}

src/RESTController.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type RequestOptions = {
1818
installationId?: string;
1919
batchSize?: number;
2020
include?: any;
21+
progress?: any;
2122
};
2223

2324
export type FullOptions = {
@@ -26,6 +27,7 @@ export type FullOptions = {
2627
useMasterKey?: boolean;
2728
sessionToken?: string;
2829
installationId?: string;
30+
progress?: any;
2931
};
3032

3133
var XHR = null;
@@ -42,7 +44,7 @@ if (typeof XDomainRequest !== 'undefined' &&
4244
useXDomainRequest = true;
4345
}
4446

45-
function ajaxIE9(method: string, url: string, data: any) {
47+
function ajaxIE9(method: string, url: string, data: any, options?: FullOptions) {
4648
return new Promise((resolve, reject) => {
4749
var xdr = new XDomainRequest();
4850
xdr.onload = function() {
@@ -66,16 +68,20 @@ function ajaxIE9(method: string, url: string, data: any) {
6668
};
6769
reject(fakeResponse);
6870
};
69-
xdr.onprogress = function() { };
71+
xdr.onprogress = function() {
72+
if(options && typeof options.progress === 'function') {
73+
options.progress(xdr.responseText);
74+
}
75+
};
7076
xdr.open(method, url);
7177
xdr.send(data);
7278
});
7379
}
7480

7581
const RESTController = {
76-
ajax(method: string, url: string, data: any, headers?: any) {
82+
ajax(method: string, url: string, data: any, headers?: any, options?: FullOptions) {
7783
if (useXDomainRequest) {
78-
return ajaxIE9(method, url, data, headers);
84+
return ajaxIE9(method, url, data, headers, options);
7985
}
8086

8187
var res, rej;
@@ -141,7 +147,28 @@ const RESTController = {
141147
' (NodeJS ' + process.versions.node + ')';
142148
}
143149

150+
if(options && typeof options.progress === 'function') {
151+
if (xhr.upload) {
152+
xhr.upload.addEventListener('progress', (oEvent) => {
153+
if (oEvent.lengthComputable) {
154+
options.progress(oEvent.loaded / oEvent.total);
155+
} else {
156+
options.progress(null);
157+
}
158+
});
159+
} else if (xhr.addEventListener) {
160+
xhr.addEventListener('progress', (oEvent) => {
161+
if (oEvent.lengthComputable) {
162+
options.progress(oEvent.loaded / oEvent.total);
163+
} else {
164+
options.progress(null);
165+
}
166+
});
167+
}
168+
}
169+
144170
xhr.open(method, url, true);
171+
145172
for (var h in headers) {
146173
xhr.setRequestHeader(h, headers[h]);
147174
}
@@ -225,8 +252,7 @@ const RESTController = {
225252
}
226253

227254
var payloadString = JSON.stringify(payload);
228-
229-
return RESTController.ajax(method, url, payloadString).then(({ response }) => {
255+
return RESTController.ajax(method, url, payloadString, {}, options).then(({ response }) => {
230256
return response;
231257
});
232258
}).catch(function(response: { responseText: string }) {

src/__tests__/ParseFile-test.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66
* LICENSE file in the root directory of this source tree. An additional grant
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*/
9-
9+
/* global File */
1010
jest.autoMockOff();
1111

1212
const ParseFile = require('../ParseFile').default;
1313
const CoreManager = require('../CoreManager');
1414

1515
function generateSaveMock(prefix) {
16-
return function(name) {
16+
return function(name, payload, options) {
17+
if (options && typeof options.progress === 'function') {
18+
options.progress(0.5);
19+
}
1720
return Promise.resolve({
1821
name: name,
19-
url: prefix + name
22+
url: prefix + name,
2023
});
2124
};
2225
}
@@ -183,6 +186,22 @@ describe('ParseFile', () => {
183186
expect(a.equals(b)).toBe(false);
184187
expect(b.equals(a)).toBe(false);
185188
});
189+
190+
it('reports progress during save when source is a File', () => {
191+
const file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt"));
192+
193+
const options = {
194+
progress: function(){}
195+
};
196+
jest.spyOn(options, 'progress');
197+
198+
return file.save(options).then(function(f) {
199+
expect(options.progress).toHaveBeenCalledWith(0.5);
200+
expect(f).toBe(file);
201+
expect(f.name()).toBe('progress.txt');
202+
expect(f.url()).toBe('http://files.parsetfss.com/a/progress.txt');
203+
});
204+
});
186205
});
187206

188207
describe('FileController', () => {

src/__tests__/RESTController-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,31 @@ describe('RESTController', () => {
376376
'Cannot use the Master Key, it has not been provided.'
377377
);
378378
});
379+
380+
it('reports upload progress of the AJAX request when callback is provided', (done) => {
381+
const xhr = mockXHR([{ status: 200, response: { success: true }}], {
382+
addEventListener: (name, callback) => {
383+
if(name === "progress") {
384+
callback({
385+
lengthComputable: true,
386+
loaded: 5,
387+
total: 10
388+
});
389+
}
390+
}
391+
});
392+
RESTController._setXHR(xhr);
393+
394+
const options = {
395+
progress: function(){}
396+
};
397+
jest.spyOn(options, 'progress');
398+
399+
RESTController.ajax('POST', 'files/upload.txt', {}, {}, options).then(({ response, status }) => {
400+
expect(options.progress).toHaveBeenCalledWith(0.5);
401+
expect(response).toEqual({ success: true });
402+
expect(status).toBe(200);
403+
done();
404+
});
405+
});
379406
});

src/__tests__/test_helpers/mockXHR.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* { status: ..., response: ... }
1515
* where status is a HTTP status number and result is a JSON object to pass
1616
* alongside it.
17+
* `upload` can be provided to mock the XMLHttpRequest.upload property.
1718
*/
18-
function mockXHR(results) {
19+
function mockXHR(results, upload) {
1920
const XHR = function() { };
2021
let attempts = 0;
2122
XHR.prototype = {
@@ -27,7 +28,8 @@ function mockXHR(results) {
2728
this.readyState = 4;
2829
attempts++;
2930
this.onreadystatechange();
30-
}
31+
},
32+
upload: upload
3133
};
3234
return XHR;
3335
}

0 commit comments

Comments
 (0)