Merge pull request #11655 from appwrite/copilot/add-test-for-deleting-partially-uploaded-file

Fix deletion of partially-uploaded (pending) files
This commit is contained in:
Matej Bačo authored and GitHub committed 2026-04-09 14:58:57 +02:00
commit 6dd63ee152
2 files changed
+103 -5

No files matched your search

@@ -79,7 +79,7 @@ class Delete extends Action
Device $deviceForFiles,
DeleteEvent $queueForDeletes,
Authorization $authorization,
User $user
User $user,
) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@@ -110,10 +110,17 @@ class Delete extends Action
$deviceDeleted = false;
if ($file->getAttribute('chunksTotal') !== $file->getAttribute('chunksUploaded')) {
$deviceDeleted = $deviceForFiles->abort(
$file->getAttribute('path'),
($file->getAttribute('metadata', [])['uploadId'] ?? '')
);
try {
$deviceDeleted = $deviceForFiles->abort(
$file->getAttribute('path'),
($file->getAttribute('metadata', [])['uploadId'] ?? '')
);
} catch (\Exception $e) {
// 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;
}
} else {
$deviceDeleted = $deviceForFiles->delete($file->getAttribute('path'));
}
@@ -1071,6 +1071,97 @@ trait StorageBase
$this->assertNotEmpty($preview['body']);
}
public function testDeletePartiallyUploadedFile(): void
{
// Create a bucket for this test
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => ID::unique(),
'name' => 'Test Bucket Partial Upload',
'fileSecurity' => true,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$bucketId = $bucket['body']['$id'];
// Simulate a partial (cancelled) chunked upload by sending only the first chunk
$source = __DIR__ . "/../../../resources/disk-a/large-file.mp4";
$totalSize = \filesize($source);
$chunkSize = 5 * 1024 * 1024; // 5MB chunks
$mimeType = mime_content_type($source);
$handle = fopen($source, "rb");
$this->assertNotFalse($handle, "Could not open test resource: $source");
$chunkData = fread($handle, $chunkSize);
fclose($handle);
$curlFile = new \CURLFile(
'data://' . $mimeType . ';base64,' . base64_encode($chunkData),
$mimeType,
'large-file.mp4'
);
// Send only the first chunk (bytes 0 to chunkSize-1 of totalSize)
$end = min($chunkSize - 1, $totalSize - 1);
$partialFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'content-range' => 'bytes 0-' . $end . '/' . $totalSize,
], $this->getHeaders()), [
'fileId' => ID::unique(),
'file' => $curlFile,
'permissions' => [
Permission::read(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $partialFile['headers']['status-code']);
$fileId = $partialFile['body']['$id'];
// Confirm the file is in a pending state (chunksTotal > chunksUploaded)
$this->assertGreaterThan(
$partialFile['body']['chunksUploaded'],
$partialFile['body']['chunksTotal'],
'File should be partially uploaded (pending)'
);
// Delete the partially-uploaded (pending) file — this should succeed
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
$this->assertEmpty($deleteResponse['body']);
// Confirm the file is gone
$getResponse = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $getResponse['headers']['status-code']);
// Clean up the test bucket
$deleteBucketResponse = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(204, $deleteBucketResponse['headers']['status-code']);
}
public function testDeleteBucketFile(): void
{
// Create a fresh file just for deletion testing (not using cache since we delete it)