Skip to content

Fix deletion of partially-uploaded (pending) files#11655

Open
Copilot wants to merge 2 commits into1.9.xfrom
copilot/add-test-for-deleting-partially-uploaded-file
Open

Fix deletion of partially-uploaded (pending) files#11655
Copilot wants to merge 2 commits into1.9.xfrom
copilot/add-test-for-deleting-partially-uploaded-file

Conversation

Copy link
Contributor

Copilot AI commented Mar 26, 2026

Fixes: SER-1179

  • Fix abort() exception propagation for partially-uploaded files in Delete.php
  • Inject Log $log and capture exception as $e to surface unexpected abort() failures via $log->addTag('abortException', $e->getMessage())
  • Add test for deleting partially-uploaded file in StorageBase.php
  • Remove @ error-suppression operators from fopen/fread/fclose; add explicit assertion that file handle is valid
  • Add bucket cleanup at end of testDeletePartiallyUploadedFile to avoid leftover state across test runs

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

@github-actions
Copy link

github-actions bot commented Mar 26, 2026

🔄 PHP-Retry Summary

Flaky tests detected across commits:

Commit 898e8e2 - 3 flaky tests
Test Retries Total Time Details
UsageTest::testVectorsDBStats 1 10.14s Logs
LegacyCustomClientTest::testListIndexes 1 240.48s Logs
LegacyCustomServerTest::testAttributeResponseModels 1 242.87s Logs

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR fixes a server error that occurred when deleting a file whose chunked upload was cancelled before any bytes were written to disk. The root cause was that abort() on the Local storage device throws \Exception when the temporary chunk directory doesn't exist, and that exception propagated uncaught — leaving the pending DB document behind.

The fix wraps the abort() call in a try/catch (\Exception) so that a missing-chunks-on-disk scenario is treated as a clean deletion, allowing the DB document to be removed normally. A dedicated e2e test (testDeletePartiallyUploadedFile) validates the happy path by uploading only the first chunk of a multi-chunk file and asserting that the resulting pending file can be deleted successfully.

Key points:

  • The Local storage abort() implementation only throws \Exception for a missing directory, so the catch is correct for that adapter.
  • The catch (\Exception) is broad and will also swallow exceptions from cloud storage adapters (S3, B2, etc.) that may throw for transient reasons (network, credentials), potentially orphaning partial upload data on those providers without any log trace.
  • The new test does not clean up the bucket it creates, and uses error-suppressing @ operators for file I/O that could produce confusing failures if the test resource is ever missing.

Confidence Score: 4/5

Safe to merge for Local storage deployments; the broad exception catch warrants a follow-up for cloud storage adapters but is not a blocker.

The core fix is correct and well-targeted for the described bug. The exception type thrown by the Local device (\Exception) matches what is caught. The e2e test exercises the fixed path end-to-end. The one concrete concern — that catch (\Exception) silently swallows unexpected errors from cloud storage adapters — is a real edge case but not likely to cause problems in normal usage for the majority of deployments. P2 test hygiene issues do not affect merge safety.

src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php — review the exception catch breadth if cloud storage adapters are a primary concern.

Important Files Changed

Filename Overview
src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php Wraps abort() in a try/catch to gracefully handle cases where chunk data was never written to disk, allowing the pending DB document to be cleaned up; exception catch is correct but broad.
tests/e2e/Services/Storage/StorageBase.php New testDeletePartiallyUploadedFile test correctly exercises the fix by uploading only the first chunk and asserting the pending file can be deleted; minor: test bucket is never cleaned up, and file I/O uses suppressed-error @ operators.

Reviews (1): Last reviewed commit: "fix: allow deletion of partially-uploade..." | Re-trigger Greptile

Comment on lines +116 to +121
} catch (\Exception) {
// If the partial upload chunks are already gone from the device
// (e.g. the upload never wrote anything to disk), treat it as deleted
// so the pending file document can still be removed from the database.
$deviceDeleted = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Broad catch may silently orphan data on cloud storage backends

catch (\Exception) catches every exception thrown by abort(), not just the "directory doesn't exist" case. For the Local device this is safe — the only exception it throws is "File doesn't exist: …" when the tmp directory was never created. However, cloud storage adapters (S3, B2, etc.) can throw \Exception for transient reasons such as network timeouts, credential errors, or throttling. In those cases the multipart upload object may still exist on the provider's side, yet the code sets $deviceDeleted = true and removes the DB document, leaving orphaned (and potentially billable) partial-upload data with no DB reference to clean it up later.

Consider narrowing the catch to a known "not found" scenario, or at minimum logging the exception before swallowing it so unexpected failures surface in the error logs:

} catch (\Exception $e) {
    // Chunks were never written to disk / remote storage; safe to remove the document.
    // Log unexpected errors so they don't vanish silently.
    \Utopia\Logger\Logger::warning('abort() failed for partial upload, treating as deleted: ' . $e->getMessage());
    $deviceDeleted = true;
}

(Adjust to whatever logging facility is already used in this codebase.)

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot Apply this suggestion

Comment on lines +1149 to +1151

$this->assertEquals(404, $getResponse['headers']['status-code']);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Test bucket is never cleaned up

The bucket created at the start of testDeletePartiallyUploadedFile is not deleted at the end of the test. Other tests in StorageBase that create their own buckets (e.g. testDeleteBucketFile) also clean up the bucket afterward. Leaving this bucket behind could accumulate leftover state across test runs. Consider adding a bucket-deletion call after the 404 assertion.

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot Make sure to leanup bucket as suggested here

@github-actions
Copy link

✨ Benchmark results

  • Requests per second: 1,654
  • Requests with 200 status code: 297,748
  • P99 latency: 0.103929985

⚡ Benchmark Comparison

Metric This PR Latest version
RPS 1,654 1,098
200 297,748 197,678
P99 0.103929985 0.203358202

Copilot AI requested a review from Meldiron March 26, 2026 18:24
@blacksmith-sh
Copy link

blacksmith-sh bot commented Mar 26, 2026

Found 4 test failures on Blacksmith runners:

Failures

Test View Logs
› Tests\E2E\Services\TablesDB\TablesDBConsoleClientTest/
testEnforceCollectionAndDocumentPermissions
View Logs
› Tests\E2E\Services\TablesDB\Transactions\TablesDBTransactionsConsoleClientTest/
testOperationsOnNonExistentDocuments
View Logs
› Tests\E2E\Services\TablesDB\Transactions\TablesDBTransactionsCustomClientTest/
testBulkUpsert
View Logs
› Tests\E2E\Services\TablesDB\Transactions\TablesDBTransactionsCustomServerTest/
testBulkDelete
View Logs

Fix in Cursor

@Meldiron
Copy link
Contributor

@copilot Address two reviews above, I pinged you in both

Copy link
Contributor Author

Copilot AI commented Mar 26, 2026

@copilot Address two reviews above, I pinged you in both

Done in ecc76c7:

  • Delete.php: Inject Log $log, capture exception as $e, and call $log->addTag('abortException', $e->getMessage()) so unexpected failures surface in structured logs.
  • StorageBase.php: Removed @ error-suppression operators (added assertNotFalse on the file handle), and added bucket deletion at the end of testDeletePartiallyUploadedFile for proper cleanup.

// If the partial upload chunks are already gone from the device
// (e.g. the upload never wrote anything to disk), treat it as deleted
// so the pending file document can still be removed from the database.
$log->addTag('abortException', $e->getMessage());
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if we really want to add this...

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.

3 participants