Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/s3-store-remove-incomplete-part.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tus/s3-store": patch
---

Delete the incomplete part object (`<id>.part`) when removing an upload so terminating an upload that has a pending sub-minimum-size chunk no longer leaves an orphaned object in the bucket.
2 changes: 1 addition & 1 deletion packages/s3-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ export class S3Store extends DataStore {
await this.client.deleteObjects({
Bucket: this.bucket,
Delete: {
Objects: [{Key: id}, {Key: this.infoKey(id)}],
Objects: [{Key: id}, {Key: this.infoKey(id)}, {Key: this.partKey(id, true)}],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Failed part deletion reports success

When S3 rejects only the .part deletion, remove still succeeds. deleteObjects reports per-object failures in Errors, leaving the part behind.

Learn more

S3 can return HTTP 200 from a multi-object delete even when it could not delete one requested key. Its response lists failed keys in Errors. The new .part deletion shares a request with the upload and info objects, but remove discards that response. A denied .part deletion therefore leaves the incomplete object behind after termination succeeds.

Example: A bucket policy permits deleting sample and sample.info but denies deleting sample.part. Terminating sample removes the first two objects and returns success, while sample.part remains.

Recommended fix: Inspect DeleteObjectsCommandOutput.Errors in remove and reject if any requested deletion failed; add a stubbed partial-success response to the termination tests.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ferhatelmas ferhatelmas Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

correct but existing design, we are just doing best effort and still better than nothing for most cases

},
})

Expand Down
20 changes: 20 additions & 0 deletions packages/s3-store/src/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,26 @@ describe('S3DataStore', () => {
await assert.rejects(store.remove(id), {status_code: 404})
})

it('should delete the incomplete part when removing an upload', async function () {
const store = this.datastore as S3Store
const incompleteSize = 1024
const upload = new Upload({
id: shared.testId('remove-incomplete-part'),
size: incompleteSize * 2,
offset: 0,
})

await store.create(upload)
await store.write(Readable.from(Buffer.alloc(incompleteSize)), upload.id, 0)
// @ts-expect-error protected method
assert.equal(await store.getIncompletePartSize(upload.id), incompleteSize)

await store.remove(upload.id)

// @ts-expect-error protected method
assert.equal(await store.getIncompletePartSize(upload.id), undefined)
})

for (const [S3Error, completed] of [
[NoSuchUpload, true],
[NoSuchKey, true],
Expand Down
Loading