fix(upload): isolate resumable uploads with a per-request temp path - #3159
Merged
Conversation
- Stage each file_asset upload at a random <uuid4>.tmp instead of a filename-derived one, so concurrent same-name uploads and stale attempts can't clobber or bleed into each other - Return an opaque upload_id the client echoes via X-Upload-Id to reassemble a resumable (Content-Range) upload into one file - Reject a malformed X-Upload-Id (path-traversal guard) with 400 - Open chunks with O_CREAT (no O_TRUNC) to close the check-then-wb race Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR fixes a concurrency and cross-attempt corruption risk in the file_asset upload endpoint by removing the deterministic per-filename temp path and introducing a per-upload session identifier (upload_id) for resumable (Content-Range) uploads.
Changes:
- Stage uploads to a random
<upload_id>.tmpfile underassetdirand returnupload_idto allow resumable chunk reassembly without filename-derived temp paths. - Use
os.open(..., O_CREAT)(without truncation) for chunk writes to avoid theisfile()-then-open('wb')clobber race. - Add/adjust tests to validate isolation across same-name uploads, truncation behavior on “shrink”, and malformed upload id rejection.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/anthias_server/api/views/mixins.py |
Implements per-upload temp path isolation via upload_id, header-based resumable session binding, and safer file open semantics for chunked uploads. |
src/anthias_server/api/tests/test_v1_endpoints.py |
Updates chunked upload test flow to use upload_id and adds coverage for isolation + malformed id handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Addresses Copilot review on the resumable-upload isolation change.
- Ignore X-Upload-Id on single-shot (non-range) uploads: that path
uses open('wb'), so an echoed id could truncate another session's
in-progress .tmp. Without a range, always mint a fresh id.
- Normalize the echoed id's case before the traversal guard.
- Assert response status before reading .data in the upload tests.
- Add a regression test that X-Upload-Id is ignored without a range.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Addresses second Copilot review round. - Open resumable chunks with 0o666 (umask-masked) + O_CLOEXEC so permissions and fd-inheritance match the builtin open() used on the non-range path, instead of hard-coding 0o644. - Reword the upload-id comment: the check is a 32-hex-char traversal guard, not a UUIDv4 version/variant validation. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Issues Fixed
Fixes #3135
Description
The
file_assetupload endpoint staged every upload at a deterministic temp path derived from the filename (uuid5(NAMESPACE_URL, filename).hex + '.tmp'), so two uploads of the same name shared one.tmpwith no per-upload isolation. That allowed a concurrency race (two chunk requests both openingwband clobbering each other) and cross-attempt bleed (a stale.tmpfrom an interrupted upload reused by a later one).This adds per-upload session isolation:
<uuid4>.tmp— never derived from the filename — so concurrent same-name uploads and stale attempts land in separate files.upload_id; a resumable (Content-Range) client echoes it via theX-Upload-Idheader so every chunk reassembles into the same file. A malformed id is rejected with 400 (path-traversal guard).os.open(..., O_CREAT)(noO_TRUNC) to close theisfile()-then-open('wb')race.Orphaned
.tmpfiles are already reaped by the hourlycleanup()Celery task (the random names still match its*.tmpsweep), and the single-POST browser path already used a randomuuid4name, so no client changes are needed for the common case.Checklist