diff --git a/src/features/publications/server/publication-object-service.ts b/src/features/publications/server/publication-object-service.ts index 16d07d4ef..1a1276d9c 100644 --- a/src/features/publications/server/publication-object-service.ts +++ b/src/features/publications/server/publication-object-service.ts @@ -282,6 +282,21 @@ export async function planPublicationObjects(input: { kind: object.kind, sha256: claim.expectedSha256, }); + if (object.status === "linked") { + return { + objectId: object.id, + storageStatus: "linked" as const, + response: { + kind: object.kind, + sha256: object.sha256, + r2Key: object.r2Key, + status: "already_present" as const, + uploadUrl: null, + expiresAt: null, + requiredHeaders: headers, + }, + }; + } const verified = await verifyR2Object({ contentType: claim.expectedContentType, kind: object.kind, diff --git a/tests/unit/publication-object-service.test.ts b/tests/unit/publication-object-service.test.ts index 7ff272ead..36dfec0ed 100644 --- a/tests/unit/publication-object-service.test.ts +++ b/tests/unit/publication-object-service.test.ts @@ -194,6 +194,43 @@ describe("publication object completion", () => { }); }); + it("reuses a linked object's completed verification without reading R2", async () => { + const claim = { + expectedContentType: "text/plain", + expectedSha256: sha256OfAbc, + expectedSize: 3, + object: { + id: "object-1", + kind: "body_html" as const, + r2Key: `publications/body_html/sha256/ba/${sha256OfAbc}`, + sha256: sha256OfAbc, + status: "linked" as const, + }, + }; + mocks.batchFindUnique.mockResolvedValue({ objects: [claim] }); + + const planned = await planPublicationObjects({ + principal, + payload: { + batchId: "batch-linked", + objects: [{ kind: "body_html", sha256: sha256OfAbc }], + }, + }); + + expect(planned.objects).toEqual([ + expect.objectContaining({ + kind: "body_html", + sha256: sha256OfAbc, + status: "already_present", + uploadUrl: null, + }), + ]); + expect(mocks.getBucket).not.toHaveBeenCalled(); + expect(mocks.bucket.head).not.toHaveBeenCalled(); + expect(mocks.bucket.get).not.toHaveBeenCalled(); + expect(mocks.objectUpdateMany).not.toHaveBeenCalled(); + }); + it("plans a large request with one batch lookup and bounded R2 concurrency", async () => { const objectCount = 500; const claims = Array.from({ length: objectCount }, (_, index) => {