diff --git a/packages/plugin-cloud-storage/src/hooks/afterChange.ts b/packages/plugin-cloud-storage/src/hooks/afterChange.ts index 866756e2d5a..2e4e2c3dde8 100644 --- a/packages/plugin-cloud-storage/src/hooks/afterChange.ts +++ b/packages/plugin-cloud-storage/src/hooks/afterChange.ts @@ -54,7 +54,17 @@ export const getAfterChangeHook = if (!req.context) { req.context = {} } - req.context.skipCloudStorage = true + + // Hold onto the object the flag is set on. The nested update below runs + // `createLocalReq`, which reassigns `req.context` to a fresh copy — so + // clearing `req.context.skipCloudStorage` afterwards would clear it off + // that copy and leave it set on the original. When a caller reuses one + // `context` across several Local API creates (the seed-script pattern), + // `createLocalReq` hands that same object to `req.context`, and the + // leftover flag makes every later upload return early: the document is + // written but no bytes reach storage. + const contextWithFlag = req.context + contextWithFlag.skipCloudStorage = true // Clear to prevent re-processing req.file = undefined @@ -70,7 +80,7 @@ export const getAfterChangeHook = req, }) } finally { - delete req.context.skipCloudStorage + delete contextWithFlag.skipCloudStorage } docWithMetadata = { ...doc, ...uploadMetadata } diff --git a/test/plugin-cloud-storage/int.spec.ts b/test/plugin-cloud-storage/int.spec.ts index 1d56040d5aa..10c937b77c9 100644 --- a/test/plugin-cloud-storage/int.spec.ts +++ b/test/plugin-cloud-storage/int.spec.ts @@ -323,6 +323,38 @@ describe('@payloadcms/plugin-cloud-storage', () => { expect(upload.url).toEqual(`/api/${mediaSlug}/file/${String(upload.filename)}`) }) + it('uploads every file when one context object is reused across creates', async () => { + // `createLocalReq` assigns this exact object to `req.context`, so + // anything a hook writes onto it survives into the next create. + const sharedContext: Record = {} + + const uploads = [] + + for (let i = 0; i < 3; i++) { + uploads.push( + await payload.create({ + collection: mediaSlug, + context: sharedContext, + data: {}, + filePath: path.resolve(dirname, '../uploads/image.png'), + }), + ) + } + + // The documents are written either way — only the bytes go missing. + for (const upload of uploads) { + await verifyUploads({ + client, + collectionSlug: mediaSlug, + payload, + TEST_BUCKET, + uploadId: upload.id, + }) + } + + expect(sharedContext).not.toHaveProperty('skipCloudStorage') + }) + it('can upload with prefix', async () => { const upload = await payload.create({ collection: mediaWithPrefixSlug,