Skip to content

Commit ba63504

Browse files
committed
feat: Allow File adapter to create file with specific locations or dynamic filenames
1 parent 315e157 commit ba63504

4 files changed

Lines changed: 128 additions & 10 deletions

File tree

spec/CloudCode.spec.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4144,11 +4144,13 @@ describe('saveFile hooks', () => {
41444144
foo: 'bar',
41454145
},
41464146
};
4147+
41474148
expect(createFileSpy).toHaveBeenCalledWith(
41484149
jasmine.any(String),
41494150
newData,
41504151
'text/plain',
4151-
newOptions
4152+
newOptions,
4153+
jasmine.objectContaining({ applicationId: 'test', mount: Parse.serverURL })
41524154
);
41534155
});
41544156

@@ -4176,11 +4178,13 @@ describe('saveFile hooks', () => {
41764178
foo: 'bar',
41774179
},
41784180
};
4181+
41794182
expect(createFileSpy).toHaveBeenCalledWith(
41804183
jasmine.any(String),
41814184
newData,
41824185
newContentType,
4183-
newOptions
4186+
newOptions,
4187+
jasmine.objectContaining({ applicationId: 'test', mount: Parse.serverURL })
41844188
);
41854189
const expectedFileName = 'donald_duck.pdf';
41864190
expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length);
@@ -4206,11 +4210,13 @@ describe('saveFile hooks', () => {
42064210
metadata: { foo: 'bar' },
42074211
tags: { bar: 'foo' },
42084212
};
4213+
42094214
expect(createFileSpy).toHaveBeenCalledWith(
42104215
jasmine.any(String),
42114216
jasmine.any(Buffer),
42124217
'text/plain',
4213-
options
4218+
options,
4219+
jasmine.objectContaining({ applicationId: 'test', mount: Parse.serverURL })
42144220
);
42154221
});
42164222

spec/FilesController.spec.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,102 @@ describe('FilesController', () => {
218218
expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null);
219219
done();
220220
});
221+
222+
it('should return filename and url when adapter returns both', async () => {
223+
const config = Config.get(Parse.applicationId);
224+
const adapterWithReturn = { ...mockAdapter };
225+
adapterWithReturn.createFile = () => {
226+
return Promise.resolve({
227+
name: 'newFilename.txt',
228+
url: 'http://example.com/newFilename.txt'
229+
});
230+
};
231+
adapterWithReturn.getFileLocation = () => {
232+
return Promise.resolve('http://example.com/file.txt');
233+
};
234+
const controllerWithReturn = new FilesController(adapterWithReturn, null, { preserveFileName: true });
235+
236+
const result = await controllerWithReturn.createFile(
237+
config,
238+
'originalFile.txt',
239+
'data',
240+
'text/plain'
241+
);
242+
243+
expect(result.name).toBe('newFilename.txt');
244+
expect(result.url).toBe('http://example.com/newFilename.txt');
245+
});
246+
247+
it('should use original filename and generate url when adapter returns nothing', async () => {
248+
const config = Config.get(Parse.applicationId);
249+
const adapterWithoutReturn = { ...mockAdapter };
250+
adapterWithoutReturn.createFile = () => {
251+
return Promise.resolve();
252+
};
253+
adapterWithoutReturn.getFileLocation = (config, filename) => {
254+
return Promise.resolve(`http://example.com/${filename}`);
255+
};
256+
257+
const controllerWithoutReturn = new FilesController(adapterWithoutReturn, null, { preserveFileName: true });
258+
const result = await controllerWithoutReturn.createFile(
259+
config,
260+
'originalFile.txt',
261+
'data',
262+
'text/plain',
263+
{}
264+
);
265+
266+
expect(result.name).toBe('originalFile.txt');
267+
expect(result.url).toBe('http://example.com/originalFile.txt');
268+
});
269+
270+
it('should use original filename when adapter returns only url', async () => {
271+
const config = Config.get(Parse.applicationId);
272+
const adapterWithOnlyURL = { ...mockAdapter };
273+
adapterWithOnlyURL.createFile = () => {
274+
return Promise.resolve({
275+
url: 'http://example.com/partialFile.txt'
276+
});
277+
};
278+
adapterWithOnlyURL.getFileLocation = () => {
279+
return Promise.resolve('http://example.com/file.txt');
280+
};
281+
282+
const controllerWithPartial = new FilesController(adapterWithOnlyURL, null, { preserveFileName: true });
283+
const result = await controllerWithPartial.createFile(
284+
config,
285+
'originalFile.txt',
286+
'data',
287+
'text/plain',
288+
{}
289+
);
290+
291+
expect(result.name).toBe('originalFile.txt');
292+
expect(result.url).toBe('http://example.com/partialFile.txt');
293+
});
294+
295+
it('should use adapter filename and generate url when adapter returns only filename', async () => {
296+
const config = Config.get(Parse.applicationId);
297+
const adapterWithOnlyFilename = { ...mockAdapter };
298+
adapterWithOnlyFilename.createFile = () => {
299+
return Promise.resolve({
300+
name: 'newname.txt'
301+
});
302+
};
303+
adapterWithOnlyFilename.getFileLocation = (config, filename) => {
304+
return Promise.resolve(`http://example.com/${filename}`);
305+
};
306+
307+
const controllerWithOnlyFilename = new FilesController(adapterWithOnlyFilename, null, { preserveFileName: true });
308+
const result = await controllerWithOnlyFilename.createFile(
309+
config,
310+
'originalFile.txt',
311+
'data',
312+
'text/plain',
313+
{}
314+
);
315+
316+
expect(result.name).toBe('newname.txt');
317+
expect(result.url).toBe('http://example.com/newname.txt');
318+
});
221319
});

src/Adapters/Files/FilesAdapter.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ export class FilesAdapter {
3131
* @discussion the contentType can be undefined if the controller was not able to determine it
3232
* @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only)
3333
* - tags: object containing key value pairs that will be stored with file
34-
* - metadata: object containing key value pairs that will be sotred with file (https://docs.aws.amazon.com/AmazonS3/latest/user-guide/add-object-metadata.html)
34+
* - metadata: object containing key value pairs that will be stored with file (https://docs.aws.amazon.com/AmazonS3/latest/user-guide/add-object-metadata.html)
3535
* @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility
36+
* @param {Config} config - (Optional) server configuration
37+
* @discussion config may be passed to adapter to allow for more complex configuration and internal call of getFileLocation (if needed). This argument is not supported by all file adapters. Check the your adapter's documentation for compatibility
3638
*
37-
* @return {Promise} a promise that should fail if the storage didn't succeed
39+
* @return {Promise<{url?: string, name?: string, location?: string}>|Promise<undefined>} Either a plain promise that should fail if storage didn't succeed, or a promise resolving to an object containing url and/or an updated filename and/or location (if relevant)
3840
*/
39-
createFile(filename: string, data, contentType: string, options: Object): Promise {}
41+
createFile(filename: string, data, contentType: string, options: Object, config: Config): Promise {}
4042

4143
/** Whether this adapter supports receiving Readable streams in createFile().
4244
* If false (default), streams are buffered to a Buffer before being passed.

src/Controllers/FilesController.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,23 @@ export class FilesController extends AdaptableController {
4646
});
4747
}
4848

49-
const location = await this.adapter.getFileLocation(config, filename);
50-
await this.adapter.createFile(filename, data, contentType, options);
49+
// The adapter receives the server config so that it can derive a location
50+
// itself, and may report back a filename it changed and a url it already
51+
// resolved. An adapter that returns nothing keeps the previous behavior.
52+
const createResult = await this.adapter.createFile(
53+
filename,
54+
data,
55+
contentType,
56+
options,
57+
config
58+
);
59+
// The location has to be resolved after creation, not before, because the
60+
// adapter may have renamed the file.
61+
const name = createResult?.name || filename;
62+
const url = createResult?.url || (await this.adapter.getFileLocation(config, name));
5163
return {
52-
url: location,
53-
name: filename,
64+
url,
65+
name,
5466
}
5567
}
5668

0 commit comments

Comments
 (0)