Skip to content

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

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

Open
wants to merge 18 commits into
base: alpha
Choose a base branch
from

Conversation

AdrianCurtin
Copy link

@AdrianCurtin AdrianCurtin commented Jan 16, 2025

Allow file adapter to override file name and location + give createFile access to config to allow getFileLocation calls internally

File adapter may specify a different filename or location during creation which should be returned after content storage.

Example, adapter timestamps upload filenames. getFileLocation may result in a slightly different url than the one used to create the file.

In the case where the url returned is identical this is not a problem. Additionally file adapter could call getFile location internally if it wanted to (and should probably have access to config for that reason).

In principle, if user wanted to name every file uploaded in an ordinal fashion (eg 1.jpg,2.jpg,3.jpg), they should allowed to do this.

As a separate improvement, this allows the url to be assigned during createFile as well (to prevent extra calls if the url is already known). Passing config to createFile allows getFileLocation to be called internally). Otherwise if url is not assigned, then getFileLocation can be called to retrieve it with the updated filename.

If url is not provided and filename is unchanged by createFile, then this code will not alter current functionality

Pull Request

Issue

Closes: #9556

Approach

  1. Allow file location and filename as generated to be overwritten by adapter.createFile if the file adapter desires
  2. Pass config as additional argument to fileadapter so that the adapter can call getFileLocation during createFile if desired (optional)

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)
  • Add security check

Summary by CodeRabbit

  • New Features

    • File uploads now use adapter-provided filename and URL when supplied; otherwise fall back to the original filename and a generated URL. Adapters can receive additional configuration to influence upload behavior.
  • Documentation

    • Adapter docs updated to describe enhanced return values and new config parameter.
  • Tests

    • Added tests covering adapter return scenarios: both values, none, only URL, and only filename.

Copy link

parse-github-assistant bot commented Jan 16, 2025

Thanks for opening this pull request!

@AdrianCurtin AdrianCurtin changed the title Allow File adapter to create file with specific locations Feature: Allow File adapter to create file with specific locations or dynamic filenames Jan 16, 2025
@AdrianCurtin AdrianCurtin changed the title Feature: Allow File adapter to create file with specific locations or dynamic filenames feat: Allow File adapter to create file with specific locations or dynamic filenames Jan 16, 2025
Copy link
Member

@mtrezza mtrezza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for this?

@AdrianCurtin
Copy link
Author

@mtrezza
Tests added

@AdrianCurtin
Copy link
Author

@mtrezza
Adjusted cloud code createFileSpy and mock adapter in tests, if you can re-run

@mtrezza mtrezza requested a review from a team February 7, 2025 01:19
@mtrezza
Copy link
Member

mtrezza commented Mar 7, 2025

@AdrianCurtin Thank you for picking this up again, I've restarted the CI, let's see...

* @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility
* @param {Config} config - (Optional) server configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the whole Parse Server config be passed into the adapter? Or am I misreading this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrezza
Allowing the server config to be passed to the createFile would allow a couple of things.

  1. files can be named based on serer attributes (ex: generateKey could store log files with the app id)
  2. getFileLocation can be called within createFile, either to test or check that the file has been created (especially if getFileLocation depends on the server config which it always has access to)

There may be other situations in which the server config may be useful apart from customizing file paths and running getFileLocation, but I don't see why createFile shouldn't have access to it if getFileLocation does.

If this was a smaller project I would probably have refactored adapter.createFile to use config as the first argument, but that had the potential to be fairly breaking and maybe its nicer for some people to use X.createFile('filename')

In any case tacking it at the end was the compromise I made.

In the current S3-adapter PR, this only allows getFileLocation to run inside and I didn't pass the parameters to generateKey.

I would say though that removing that expectation and removing getFileLocation from the adapter would make the changes more explicit, but then [location] = createFile would not always match the call from getFileLocation (if it depended in any way on server parameters)

Copy link
Member

@mtrezza mtrezza Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is passing the server config to the adapter l a good approach? It creates a dependency on a specific config structure. If that structure changes, it could break the adapter, right? Or is the config already used in other parts of the adapter?

Copy link
Author

@AdrianCurtin AdrianCurtin Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrezza

The file location is based on the server config as is, so when you call getFileLocation you need the server config.
You only need the mount and the app id though for most getFileLocation calls, but the adapter gets the full server config in those cases.

So we can't actually know where the file ends up without the server config unfortunately (if it ends up being different from the getFileLocation)

We can opt instead for the full server config to pass through but that was making the tests unhappy a little and I haven't seen any instances of getFileLocation use more than just those parameters.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the basic idea:

FilesController.createFile returns url (location) and filename

File location is determined by the file name and the server configuration

but adapter.createFile does not have access to the server config and therefore cannot return the stored file config if the filename or any other part has changed.

With this change, the expectation is that createFile might return an updated filename (if for instance generatekey was used to alter the filepath), and optionally, createFile could return the entire location (presumably by calling getFileLocation internally.

If createFile returns nothing (current implementation), the original filename will be returned and getFileLocation will be called using the original filename (after creation though instead of before). <- no breaking changes here

If a file adapter chooses to alter the filename, but does not internally implement getFileLocation and return a url, this version will simply call getFileLocation but with the revised filename so that it can be correctly found. Adapters would be responsible for returning altered filenames when they are changed and this is the current issue with the s3 adapter issue.

If a file adapter chooses to internally call getFileLocation or a similar function to return the URL, then we can simply return the location that was part of createFile instead of calling a separate function. (File adapters would be responsible for ensuring that the returned url would be equivalent to the getFileLocation url, again this is opt-in).

If a file adapter chose to alter the filename or some other aspect of file creation based on the server configuration (ex: s3 folders based on application id), it would be the file adapter's responsibility to ensure that the create file behavior was not brittle. However, this complexity is especially opt-in and would not be advised for anything beyond altering the filename if desired.

For most adapters, the best course of action here is to simply update them to return the filename as stored (if the adapter is one that might change the filename).

For some adapters the getFileLocation might simply be returning that file directly with some modification, or others like presigning might be more involved. Implementing this internally may result in performance increases (small likely) but also is not strictly necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrezza passing the whole config to an adapter don't feel strange to me, it allow a true inversion of control, yes it can break in case of config change, but a developer is responsible of testing its custom adapter basically

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please split this into separate tests, each one with a distinct title to describe what it's testing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also may want to take a look at the failing tests in the CI

Copy link
Author

@AdrianCurtin AdrianCurtin Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, sorry about the delay, @mtrezza lmk when you have a second to rerun the CI

…le access to config to allow getFileLocation calls internally

File adapter may specify a different filename or location during creation which should be returned after content storage.

Example, adapter timestamps upload filenames. getFileLocation may result in a slightly different url than the one used to create the file.

In the case where the url returned is identical this is not a problem. Additionally file adapter could call getFile location internally if it wanted to (and should probably have access to config for that reason)
but use updated filename in getFileLocation
Copy link

coderabbitai bot commented Aug 13, 2025

Warning

Rate limit exceeded

@AdrianCurtin has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 14 minutes and 47 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6114fbf and 028103a.

📒 Files selected for processing (1)
  • src/Controllers/FilesController.js (1 hunks)
📝 Walkthrough

Walkthrough

FilesAdapter.createFile gains a final config parameter; FilesController.createFile passes that config to the adapter, uses adapter-returned name to override filename and url to override location (falling back to getFileLocation), and tests were added/updated to cover adapter-return permutations and the new call signature.

Changes

Cohort / File(s) Summary
Tests: saveFile hooks and adapter signature
spec/CloudCode.spec.js
Updated tests to expect adapter.createFile called with 5 arguments, passing Config.get('test') as the 5th parameter.
Tests: FilesController createFile branching
spec/FilesController.spec.js
Added tests for adapter returning {name,url}, undefined, url only, or name only; verify final filename/url resolution and getFileLocation fallback.
Adapter interface update
src/Adapters/Files/FilesAdapter.js
Public API change: createFile(filename, data, contentType, options, config); JSDoc updated to document config and broadened return description.
Controller flow update
src/Controllers/FilesController.js
FilesController.createFile now calls adapter.createFile(..., config), uses createResult.name to override filename and createResult.url or getFileLocation(config, filename) for final URL.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant FilesController
  participant FilesAdapter

  Client->>FilesController: createFile(filename, data, contentType, options)
  FilesController->>FilesAdapter: createFile(filename, data, contentType, options, config)
  FilesAdapter-->>FilesController: { name?, url? } or undefined
  alt adapter provides name
    FilesController->>FilesController: filename = result.name
  end
  alt adapter provides url
    FilesController->>FilesController: url = result.url
  else
    FilesController->>FilesAdapter: getFileLocation(config, filename)
    FilesAdapter-->>FilesController: url
  end
  FilesController-->>Client: { url, name: filename }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Assessment against linked issues

Objective Addressed Explanation
Allow adapters to set filename and file location during createFile; ensure location reflects post-creation filename changes (#9556)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Typo/comment correction "sotred" -> "stored" (src/Adapters/Files/FilesAdapter.js) Documentation-only change unrelated to the issue objective.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Aug 13, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 13, 2025
…ation

Strip down the config variables that get sent along with the file
coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 13, 2025
@AdrianCurtin
Copy link
Author

@mtrezza

In order to get this to pass the CI checks, I limited the server config access to just app id, mount and the legacy filekey, since that's what it looks like all of the current file adapters are using. If something else is required we could add it or weaken the tests so that a less precise config could be checked. lmk what you think

Copy link

codecov bot commented Aug 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.01%. Comparing base (587abdd) to head (a0a1782).

Additional details and impacted files
@@           Coverage Diff           @@
##            alpha    #9557   +/-   ##
=======================================
  Coverage   93.01%   93.01%           
=======================================
  Files         187      187           
  Lines       15096    15098    +2     
  Branches      174      174           
=======================================
+ Hits        14041    14043    +2     
  Misses       1043     1043           
  Partials       12       12           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@@ -3727,11 +3734,18 @@ describe('saveFile hooks', () => {
foo: 'bar',
},
};
// Get the actual config values that will be used
const expectedConfig = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you import this from the test suite where it's defined?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it, but previously the checks were unhappy with the full config being passed through, I don't recall why off the top of my head

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it but i still stripped it down because previously the config had been modified through the process or something. We'll see if it passes CI, but Maybe the check just needs to be loosened here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't mind assisting,

when i revert to the config('test") i end up with these errors

`

  1. saveFile hooks beforeSave(Parse.File) should change values of uploaded file by editing fileObject directly
  • Expected spy createFile to have been called with:
    [ <jasmine.any(String)>, Buffer [ 1, 2, 3 ], 'text/plain', Object({ tags: Object({ tagA: 'some-tag' }), metadata: Object({ foo: 'bar' }) }), Object({ applicationId: 'test', mount: undefined, fileKey: 'test' }) ]
    but actual calls were:
    [ '7e441dcb3fc497b19592367415376ea1_popeye.txt', Buffer [ 1, 2, 3 ], 'text/plain', Object({ metadata: Object({ foo: 'bar' }), tags: Object({ tagA: 'some-tag' }) }), Object({ applicationId: 'test', mount: 'http://localhost:8378/1', fileKey: 'test' }) ].

Call 0:
Expected $[4].mount = 'http://localhost:8378/1' to equal undefined.

  1. saveFile hooks beforeSave(Parse.File) should contain metadata and tags saved from client
  • Expected spy createFile to have been called with:
    [ <jasmine.any(String)>, <jasmine.any(Buffer)>, 'text/plain', Object({ metadata: Object({ foo: 'bar' }), tags: Object({ bar: 'foo' }) }), Object({ applicationId: 'test', mount: undefined, fileKey: 'test' }) ]
    but actual calls were:
    [ '352ace761fac45c85ba556c85548888d_popeye.txt', Buffer [ 1, 2, 3 ], 'text/plain', Object({ metadata: Object({ foo: 'bar' }), tags: Object({ bar: 'foo' }) }), Object({ applicationId: 'test', mount: 'http://localhost:8378/1', fileKey: 'test' }) ].

Call 0:
Expected $[4].mount = 'http://localhost:8378/1' to equal undefined.

  1. saveFile hooks beforeSave(Parse.File) should change values by returning new fileObject
  • Expected spy createFile to have been called with:
    [ <jasmine.any(String)>, Buffer [ 4, 5, 6 ], 'application/pdf', Object({ tags: Object({ tagA: 'some-tag' }), metadata: Object({ foo: 'bar' }) }), Object({ applicationId: 'test', mount: undefined, fileKey: 'test' }) ]
    but actual calls were:
    [ 'b048f76b35f8628bf9336c5efdd0eb95_donald_duck.pdf', Buffer [ 4, 5, 6 ], 'application/pdf', Object({ metadata: Object({ foo: 'bar' }), tags: Object({ tagA: 'some-tag' }) }), Object({ applicationId: 'test', mount: 'http://localhost:8378/1', fileKey: 'test' }) ].

Call 0:
Expected $[4].mount = 'http://localhost:8378/1' to equal undefined.
`

coderabbitai[bot]
coderabbitai bot previously approved these changes Aug 14, 2025
Comment on lines +33 to +37
const basicServerConfig = {
applicationId: config.applicationId,
mount: config.mount,
fileKey: config.fileKey
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: why passing a partial config, we could land with inconsistencies, i think the whole config should be passed for a true inversion of control

* @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility
* @param {Config} config - (Optional) server configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtrezza passing the whole config to an adapter don't feel strange to me, it allow a true inversion of control, yes it can break in case of config change, but a developer is responsible of testing its custom adapter basically

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

File adapters cannot dynamically set file location or filename during file creation because location used may not yet exist
4 participants