diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 99ec4910d1..fbd6fd5324 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3687,11 +3687,13 @@ describe('saveFile hooks', () => { foo: 'bar', }, }; + const config = Config.get('test'); expect(createFileSpy).toHaveBeenCalledWith( jasmine.any(String), newData, 'text/plain', - newOptions + newOptions, + config ); }); @@ -3719,11 +3721,13 @@ describe('saveFile hooks', () => { foo: 'bar', }, }; + const config = Config.get('test'); expect(createFileSpy).toHaveBeenCalledWith( jasmine.any(String), newData, newContentType, - newOptions + newOptions, + config ); const expectedFileName = 'donald_duck.pdf'; expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length); @@ -3749,11 +3753,13 @@ describe('saveFile hooks', () => { metadata: { foo: 'bar' }, tags: { bar: 'foo' }, }; + const config = Config.get('test'); expect(createFileSpy).toHaveBeenCalledWith( jasmine.any(String), jasmine.any(Buffer), 'text/plain', - options + options, + config ); }); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 30acf7d13c..b0442e74b5 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -218,4 +218,99 @@ 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 = { ...mockAdapter }; + adapterWithReturn.createFile = () => { + return Promise.resolve({ + name: 'newFilename.txt', + url: 'http://new.url/newFilename.txt' + }); + }; + adapterWithReturn.getFileLocation = () => { + return Promise.resolve('http://default.url/file.txt'); + }; + const controllerWithReturn = new FilesController(adapterWithReturn, null, { preserveFileName: true }); + // preserveFileName is true to make filename behaviors predictable + 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 = { ...mockAdapter }; + adapterWithoutReturn.createFile = () => { + return Promise.resolve(); + }; + adapterWithoutReturn.getFileLocation = (config, filename) => { + return Promise.resolve(`http://default.url/${filename}`); + }; + + const controllerWithoutReturn = new FilesController(adapterWithoutReturn, null, { preserveFileName: true }); + const result2 = await controllerWithoutReturn.createFile( + config, + 'originalFile.txt', + 'data', + 'text/plain', + {} + ); + + 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 = { ...mockAdapter }; + adapterWithOnlyURL.createFile = () => { + return Promise.resolve({ + url: 'http://new.url/partialFile.txt' + }); + }; + adapterWithOnlyURL.getFileLocation = () => { + return Promise.resolve('http://default.url/file.txt'); + }; + + const controllerWithPartial = new FilesController(adapterWithOnlyURL, null, { preserveFileName: true }); + const result3 = await controllerWithPartial.createFile( + config, + 'originalFile.txt', + 'data', + 'text/plain', + {} + ); + + 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 = { ...mockAdapter }; + adapterWithOnlyFilename.createFile = () => { + return Promise.resolve({ + name: 'newname.txt' + }); + }; + adapterWithOnlyFilename.getFileLocation = (config, filename) => { + return Promise.resolve(`http://default.url/${filename}`); + }; + + const controllerWithOnlyFilename = new FilesController(adapterWithOnlyFilename, null, { preserveFileName: true }); + const result4 = await controllerWithOnlyFilename.createFile( + config, + 'originalFile.txt', + 'data', + 'text/plain', + {} + ); + + 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..32924d6b26 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, } }