Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow File adapter to create file with specific locations or dynamic filenames #9557

Open
wants to merge 9 commits into
base: alpha
Choose a base branch
from
107 changes: 107 additions & 0 deletions spec/FilesController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
8 changes: 5 additions & 3 deletions src/Adapters/Files/FilesAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<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)
*/
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
*
Expand Down
9 changes: 6 additions & 3 deletions src/Controllers/FilesController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down