diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 30acf7d13c..2792290c58 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -218,4 +218,111 @@ describe('FilesController', () => { expect(gridFSAdapter.validateFilename(fileName)).not.toBe(null); done(); }); + + it('should return valid filename or url from createFile response when provided', async () => { + const config = Config.get(Parse.applicationId); + + // Test case 1: adapter returns new filename and url + const adapterWithReturn = { + createFile: () => { + return Promise.resolve({ + name: 'newfilename.txt', + url: 'http://new.url/newfilename.txt' + }); + }, + getFileLocation: () => { + return Promise.resolve('http://default.url/file.txt'); + }, + validateFilename: () => null + }; + + const controllerWithReturn = new FilesController(adapterWithReturn); + const result1 = await controllerWithReturn.createFile( + config, + 'originalfile.txt', + 'data', + 'text/plain' + ); + + expect(result1.name).toBe('newfilename.txt'); + expect(result1.url).toBe('http://new.url/newfilename.txt'); + + // Test case 2: adapter returns nothing, falling back to default behavior + const adapterWithoutReturn = { + createFile: () => { + return Promise.resolve(); + }, + getFileLocation: (config, filename) => { + return Promise.resolve(`http://default.url/${filename}`); + }, + validateFilename: () => null + }; + + const controllerWithoutReturn = new FilesController(adapterWithoutReturn); + const result2 = await controllerWithoutReturn.createFile( + config, + 'originalfile.txt', + 'data', + 'text/plain', + {}, + { preserveFileName: true } // To make filename predictable + ); + + expect(result2.name).toBe('originalfile.txt'); + expect(result2.url).toBe('http://default.url/originalfile.txt'); + + // Test case 3: adapter returns partial info (only url) + // This is a valid scenario, as the adapter may return a modified filename + // but may result in a mismatch between the filename and the resource URL + const adapterWithOnlyURL = { + createFile: () => { + return Promise.resolve({ + url: 'http://new.url/partialfile.txt' + }); + }, + getFileLocation: () => { + return Promise.resolve('http://default.url/file.txt'); + }, + validateFilename: () => null + }; + + const controllerWithPartial = new FilesController(adapterWithOnlyURL); + const result3 = await controllerWithPartial.createFile( + config, + 'originalfile.txt', + 'data', + 'text/plain', + {}, + { preserveFileName: true } // To make filename predictable + ); + + expect(result3.name).toBe('originalfile.txt'); + expect(result3.url).toBe('http://new.url/partialfile.txt'); // Technically, the resource does not need to match the filename + + // Test case 4: adapter returns only filename + const adapterWithOnlyFilename = { + createFile: () => { + return Promise.resolve({ + name: 'newname.txt' + }); + }, + getFileLocation: (config, filename) => { + return Promise.resolve(`http://default.url/${filename}`); + }, + validateFilename: () => null + }; + + const controllerWithOnlyFilename = new FilesController(adapterWithOnlyFilename); + const result4 = await controllerWithOnlyFilename.createFile( + config, + 'originalfile.txt', + 'data', + 'text/plain', + {}, + { preserveFileName: true } + ); + + expect(result4.name).toBe('newname.txt'); + expect(result4.url).toBe('http://default.url/newname.txt'); + }); }); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index f06c52df89..aaccb49903 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -31,12 +31,14 @@ export class FilesAdapter { * @discussion the contentType can be undefined if the controller was not able to determine it * @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only) * - tags: object containing key value pairs that will be stored with file - * - 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) + * - 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) * @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility + * @param {Config} config - (Optional) server configuration + * @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 * - * @return {Promise} a promise that should fail if the storage didn't succeed + * @return {Promise<{url?: string, name?: string, location?: string}>|Promise} 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) */ - createFile(filename: string, data, contentType: string, options: Object): Promise {} + createFile(filename: string, data, contentType: string, options: Object, config: Config): Promise {} /** Responsible for deleting the specified file * diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index a88c527b00..733b2544cd 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -29,10 +29,13 @@ export class FilesController extends AdaptableController { filename = randomHexString(32) + '_' + filename; } - const location = await this.adapter.getFileLocation(config, filename); - await this.adapter.createFile(filename, data, contentType, options); + const createResult = await this.adapter.createFile(filename, data, contentType, options, config); + filename = createResult?.name || filename; // if createFile returns a new filename, use it + + const url = createResult?.url || await this.adapter.getFileLocation(config, filename); // if createFile returns a new url, use it otherwise get the url from the adapter + return { - url: location, + url: url, name: filename, } }