diff --git a/lib/s3/s3.disk.ts b/lib/s3/s3.disk.ts index db17d34..6a26d2e 100644 --- a/lib/s3/s3.disk.ts +++ b/lib/s3/s3.disk.ts @@ -338,7 +338,18 @@ export class S3Disk extends StorageDisk { const signal = options?.signal; if (!this.batchDelete || keys.length === 1) { for (const key of keys) { - await this.send({ operation: 'DeleteObject', method: 'DELETE', key: this.prefix + key, signal }); + // S3 answers 204 for a missing key; Google Cloud Storage answers 404 NoSuchKey + await this.send({ + operation: 'DeleteObject', + method: 'DELETE', + key: this.prefix + key, + notFoundKey: key, + signal, + }).catch((error: unknown) => { + if (!(error instanceof StorageFileNotFoundError)) { + throw error; + } + }); } return; } diff --git a/tests/s3-responses.spec.ts b/tests/s3-responses.spec.ts index 17bf41b..0cf17bb 100644 --- a/tests/s3-responses.spec.ts +++ b/tests/s3-responses.spec.ts @@ -411,6 +411,16 @@ describe('S3Disk requests', () => { expect(calls.map((c) => `${c.method} ${c.url.pathname}${c.url.search}`)).toEqual(['DELETE /shop/only.txt']); }); + it('a DeleteObject that answers 404 NoSuchKey (Google Cloud Storage) is not an error', async () => { + const { disk: s3, calls } = disk(() => s3Error(404, 'NoSuchKey'), { batchDelete: false }); + await expect(s3.delete('gone.txt')).resolves.toBeUndefined(); + await expect(s3.delete(['a.txt', 'b.txt'])).resolves.toBeUndefined(); + expect(calls).toHaveLength(3); + + const noBucket = disk(() => s3Error(404, 'NoSuchBucket')); + await expect(noBucket.disk.delete('a.txt')).rejects.toMatchObject({ code: 'NoSuchBucket' }); + }); + it('deletes several keys in one quiet DeleteObjects with escaped, prefixed keys and a matching Content-MD5', async () => { const { disk: s3, calls } = disk(() => xml(''), { prefix: 'p/' }); await s3.delete(['a&b.txt', '.txt']);