Skip to content

Commit 8625ae2

Browse files
authored
File upload via uri (#749)
* Can upload files with url * use built-in packages * more tests * improve coverage * typo
1 parent 13ec8ff commit 8625ae2

File tree

3 files changed

+202
-2
lines changed

3 files changed

+202
-2
lines changed

integration/test/ParseFileTest.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const clear = require('./clear');
5+
const Parse = require('../../node');
6+
7+
describe('Parse.File', () => {
8+
beforeEach((done) => {
9+
Parse.initialize('integration');
10+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
11+
Parse.Storage._clear();
12+
clear().then(done).catch(done.fail);
13+
});
14+
15+
it('can save file with uri', async () => {
16+
// Try https
17+
const parseLogo = 'https://raw.githubusercontent.com/parse-community/parse-server/master/.github/parse-server-logo.png';
18+
const file1 = new Parse.File('parse-server-logo', { uri: parseLogo });
19+
await file1.save();
20+
21+
const object = new Parse.Object('TestObject');
22+
object.set('file1', file1);
23+
await object.save();
24+
25+
const query = new Parse.Query('TestObject');
26+
let result = await query.get(object.id);
27+
28+
assert.equal(file1.name(), result.get('file1').name());
29+
assert.equal(file1.url(), result.get('file1').url());
30+
31+
// Try http
32+
const file2 = new Parse.File('parse-server-logo', { uri: file1.url() });
33+
await file2.save();
34+
35+
object.set('file2', file2);
36+
await object.save();
37+
38+
result = await query.get(object.id);
39+
assert.equal(file2.url(), result.get('file2').url());
40+
});
41+
});

src/ParseFile.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
/* global File */
1212
import CoreManager from './CoreManager';
1313
import type { FullOptions } from './RESTController';
14+
const http = require('http');
15+
const https = require('https');
1416

1517
type Base64 = { base64: string };
1618
type FileData = Array<number> | Base64 | File;
@@ -22,6 +24,10 @@ export type FileSource = {
2224
format: 'base64';
2325
base64: string;
2426
type: string
27+
} | {
28+
format: 'uri';
29+
uri: string;
30+
type: string
2531
};
2632

2733
const dataUriRegexp =
@@ -65,7 +71,8 @@ class ParseFile {
6571
* @param data {Array} The data for the file, as either:
6672
* 1. an Array of byte value Numbers, or
6773
* 2. an Object like { base64: "..." } with a base64-encoded String.
68-
* 3. a File object selected with a file upload control. (3) only works
74+
* 3. an Object like { uri: "..." } with a uri String.
75+
* 4. a File object selected with a file upload control. (3) only works
6976
* in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+.
7077
* For example:
7178
* <pre>
@@ -102,6 +109,12 @@ class ParseFile {
102109
file: data,
103110
type: specifiedType
104111
};
112+
} else if (data && typeof data.uri === 'string' && data.uri !== undefined) {
113+
this._source = {
114+
format: 'uri',
115+
uri: data.uri,
116+
type: specifiedType
117+
};
105118
} else if (data && typeof data.base64 === 'string') {
106119
const base64 = data.base64;
107120
const commaIndex = base64.indexOf(',');
@@ -175,6 +188,12 @@ class ParseFile {
175188
this._url = res.url;
176189
return this;
177190
});
191+
} else if (this._source.format === 'uri') {
192+
this._previousSave = controller.saveUri(this._name, this._source, options).then((res) => {
193+
this._name = res.name;
194+
this._url = res.url;
195+
return this;
196+
});
178197
} else {
179198
this._previousSave = controller.saveBase64(this._name, this._source, options).then((res) => {
180199
this._name = res.name;
@@ -275,6 +294,40 @@ const DefaultController = {
275294
}
276295
const path = 'files/' + name;
277296
return CoreManager.getRESTController().request('POST', path, data, options);
297+
},
298+
299+
saveUri: function(name: string, source: FileSource, options?: FullOptions) {
300+
if (source.format !== 'uri') {
301+
throw new Error('saveUri can only be used with Uri-type sources.');
302+
}
303+
return this.download(source.uri).then((result) => {
304+
const newSource = {
305+
format: 'base64',
306+
base64: result.base64,
307+
type: result.contentType,
308+
};
309+
return this.saveBase64(name, newSource, options);
310+
});
311+
},
312+
313+
download: function(uri) {
314+
return new Promise((resolve, reject) => {
315+
let client = http;
316+
if (uri.indexOf('https') === 0) {
317+
client = https;
318+
}
319+
client.get(uri, (resp) => {
320+
resp.setEncoding('base64');
321+
let base64 = '';
322+
resp.on('data', (data) => base64 += data);
323+
resp.on('end', () => {
324+
resolve({
325+
base64,
326+
contentType: resp.headers['content-type'],
327+
});
328+
});
329+
}).on('error', reject);
330+
});
278331
}
279332
};
280333

src/__tests__/ParseFile-test.js

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
*/
99
/* global File */
1010
jest.autoMockOff();
11+
jest.mock('http');
12+
jest.mock('https');
1113

1214
const ParseFile = require('../ParseFile').default;
1315
const CoreManager = require('../CoreManager');
16+
const EventEmitter = require('../EventEmitter');
17+
18+
const mockHttp = require('http');
19+
const mockHttps = require('https');
1420

1521
function generateSaveMock(prefix) {
1622
return function(name, payload, options) {
@@ -30,7 +36,8 @@ describe('ParseFile', () => {
3036
beforeEach(() => {
3137
CoreManager.setFileController({
3238
saveFile: generateSaveMock('http://files.parsetfss.com/a/'),
33-
saveBase64: generateSaveMock('http://files.parsetfss.com/a/')
39+
saveBase64: generateSaveMock('http://files.parsetfss.com/a/'),
40+
saveUri: generateSaveMock('http://files.parsetfss.com/a/'),
3441
});
3542
});
3643

@@ -48,6 +55,12 @@ describe('ParseFile', () => {
4855
expect(file._source.type).toBe('image/png');
4956
});
5057

58+
it('can create files with file uri', () => {
59+
const file = new ParseFile('parse-image', { uri:'http://example.com/image.png' });
60+
expect(file._source.format).toBe('uri');
61+
expect(file._source.uri).toBe('http://example.com/image.png');
62+
});
63+
5164
it('can extract data type from base64 with data type containing a number', () => {
5265
const file = new ParseFile('parse.m4a', {
5366
base64: 'data:audio/m4a;base64,ParseA=='
@@ -129,6 +142,17 @@ describe('ParseFile', () => {
129142
});
130143
});
131144

145+
it('updates fields when saved with uri', () => {
146+
const file = new ParseFile('parse.png', { uri: 'https://example.com/image.png' });
147+
expect(file.name()).toBe('parse.png');
148+
expect(file.url()).toBe(undefined);
149+
return file.save().then(function(result) {
150+
expect(result).toBe(file);
151+
expect(result.name()).toBe('parse.png');
152+
expect(result.url()).toBe('http://files.parsetfss.com/a/parse.png');
153+
});
154+
});
155+
132156
it('generates a JSON representation', () => {
133157
const file = new ParseFile('parse.txt', { base64: 'ParseA==' });
134158
return file.save().then(function(result) {
@@ -245,4 +269,86 @@ describe('FileController', () => {
245269
expect(f.url()).toBe('https://files.parsetfss.com/a//api.parse.com/1/files/parse.txt');
246270
});
247271
});
272+
273+
it('saveUri without uri type', () => {
274+
try {
275+
defaultController.saveUri('name', { format: 'unknown' });
276+
} catch (error) {
277+
expect(error.message).toBe('saveUri can only be used with Uri-type sources.');
278+
}
279+
});
280+
281+
it('saveUri with uri type', async () => {
282+
const source = { format: 'uri', uri: 'https://example.com/image.png' };
283+
jest.spyOn(
284+
defaultController,
285+
'download'
286+
)
287+
.mockImplementationOnce(() => {
288+
return Promise.resolve({
289+
base64: 'ParseA==',
290+
contentType: 'image/png',
291+
});
292+
});
293+
294+
jest.spyOn(defaultController, 'saveBase64');
295+
await defaultController.saveUri('fileName', source, {});
296+
expect(defaultController.download).toHaveBeenCalledTimes(1);
297+
expect(defaultController.saveBase64).toHaveBeenCalledTimes(1);
298+
expect(defaultController.saveBase64.mock.calls[0]).toEqual([
299+
'fileName',
300+
{ format: 'base64', base64: 'ParseA==', type: 'image/png' },
301+
{}
302+
]);
303+
});
304+
305+
it('download with base64 http', async () => {
306+
const mockResponse = Object.create(EventEmitter.prototype);
307+
EventEmitter.call(mockResponse);
308+
mockResponse.setEncoding = function() {}
309+
mockResponse.headers = {
310+
'content-type': 'image/png'
311+
};
312+
const spy = jest.spyOn(mockHttp, 'get')
313+
.mockImplementationOnce((uri, cb) => {
314+
cb(mockResponse);
315+
mockResponse.emit('data', 'base64String');
316+
mockResponse.emit('end');
317+
return {
318+
on: function() {}
319+
};
320+
});
321+
322+
const data = await defaultController.download('http://example.com/image.png');
323+
expect(data.base64).toBe('base64String');
324+
expect(data.contentType).toBe('image/png');
325+
expect(mockHttp.get).toHaveBeenCalledTimes(1);
326+
expect(mockHttps.get).toHaveBeenCalledTimes(0);
327+
spy.mockRestore();
328+
});
329+
330+
it('download with base64 https', async () => {
331+
const mockResponse = Object.create(EventEmitter.prototype);
332+
EventEmitter.call(mockResponse);
333+
mockResponse.setEncoding = function() {}
334+
mockResponse.headers = {
335+
'content-type': 'image/png'
336+
};
337+
const spy = jest.spyOn(mockHttps, 'get')
338+
.mockImplementationOnce((uri, cb) => {
339+
cb(mockResponse);
340+
mockResponse.emit('data', 'base64String');
341+
mockResponse.emit('end');
342+
return {
343+
on: function() {}
344+
};
345+
});
346+
347+
const data = await defaultController.download('https://example.com/image.png');
348+
expect(data.base64).toBe('base64String');
349+
expect(data.contentType).toBe('image/png');
350+
expect(mockHttp.get).toHaveBeenCalledTimes(0);
351+
expect(mockHttps.get).toHaveBeenCalledTimes(1);
352+
spy.mockRestore();
353+
});
248354
});

0 commit comments

Comments
 (0)