diff --git a/public/i18n/locale-en.json b/public/i18n/locale-en.json index 1ec75d1..71aeea6 100644 --- a/public/i18n/locale-en.json +++ b/public/i18n/locale-en.json @@ -1,6 +1,7 @@ { "ERRORS": { "repository_job_cancelled": "The repository changed or was removed while processing.", + "storage_delete_failed": "Unable to remove cached files. Please try again later.", "unknown_error": "Unknown error, contact the admin.", "unreachable": "Anonymous GitHub is unreachable, contact the admin.", "request_error": "Unable to download the file, check your connection or contact the admin.", diff --git a/src/core/storage/S3.ts b/src/core/storage/S3.ts index 1113508..94fd1a9 100644 --- a/src/core/storage/S3.ts +++ b/src/core/storage/S3.ts @@ -102,7 +102,13 @@ export default class S3Storage extends StorageBase { // nothing to remove return; } - await this.client(200000).deleteObjects(params); + const result = await this.client(200000).deleteObjects(params); + if (result.Errors?.length) { + throw new AnonymousError("storage_delete_failed", { + httpStatus: 502, + object: result.Errors, + }); + } if (data.IsTruncated) { await this.rm(repoId, dir); diff --git a/test/production-regressions.test.js b/test/production-regressions.test.js index 73e370f..1e575ee 100644 --- a/test/production-regressions.test.js +++ b/test/production-regressions.test.js @@ -79,6 +79,21 @@ describe("production regressions", function () { }); } + function s3(client) { + stub(config, "S3_BUCKET", "test"); + const S3 = require("../src/core/storage/S3").default; + const storage = new S3(); storage.client = () => client; storage.repoPath = () => "repo"; + return storage; + } + it("rejects S3 per-object deletion failures", async function () { + const storage = s3({ + listObjectsV2: async () => ({ Contents: [{ Key: "repo/data" }] }), + deleteObjects: async () => ({ Errors: [{ Key: "repo/data", Code: "AccessDenied" }] }), + }); + try { await storage.rm("repo", "data"); throw new Error("expected rejection"); } + catch (error) { expect(error.message).to.equal("storage_delete_failed"); } + }); + for (const status of ["removing", "removed", "expiring", "expired"]) { it(`ignores delayed downloads for ${status} repositories`, async function () { stub(db, "connect", async () => {});