@payloadcms/storage-r2 3.91.0-internal.c1569dc → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/adapter.d.ts +4 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js +68 -9
  5. package/dist/adapter.js.map +1 -1
  6. package/dist/client/R2ClientUploadHandler.d.ts +4 -5
  7. package/dist/client/R2ClientUploadHandler.d.ts.map +1 -1
  8. package/dist/client/R2ClientUploadHandler.js +10 -17
  9. package/dist/client/R2ClientUploadHandler.js.map +1 -1
  10. package/dist/getFile.d.ts +4 -3
  11. package/dist/getFile.d.ts.map +1 -1
  12. package/dist/getFile.js +23 -20
  13. package/dist/getFile.js.map +1 -1
  14. package/dist/handleMultiPartUpload.d.ts +2 -3
  15. package/dist/handleMultiPartUpload.d.ts.map +1 -1
  16. package/dist/handleMultiPartUpload.js +18 -16
  17. package/dist/handleMultiPartUpload.js.map +1 -1
  18. package/dist/index.d.ts +11 -14
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +54 -54
  21. package/dist/index.js.map +1 -1
  22. package/dist/types.d.ts +3 -6
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/types.js +5 -1
  25. package/dist/types.js.map +1 -1
  26. package/package.json +7 -5
  27. package/dist/client/R2ClientUploadHandler.spec.d.ts +0 -2
  28. package/dist/client/R2ClientUploadHandler.spec.d.ts.map +0 -1
  29. package/dist/client/R2ClientUploadHandler.spec.js +0 -76
  30. package/dist/client/R2ClientUploadHandler.spec.js.map +0 -1
  31. package/dist/handleMultiPartUpload.spec.d.ts +0 -2
  32. package/dist/handleMultiPartUpload.spec.d.ts.map +0 -1
  33. package/dist/handleMultiPartUpload.spec.js +0 -154
  34. package/dist/handleMultiPartUpload.spec.js.map +0 -1
  35. package/dist/staticHandler.spec.d.ts +0 -2
  36. package/dist/staticHandler.spec.d.ts.map +0 -1
  37. package/dist/staticHandler.spec.js +0 -60
  38. package/dist/staticHandler.spec.js.map +0 -1
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
3
+ Copyright (c) 2018-2026 Payload CMS, LLC <info@payloadcms.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
package/dist/adapter.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { Adapter, ClientUploadsConfig } from '@payloadcms/plugin-cloud-storage/types';
2
+ import type { R2StorageOptions } from './index.js';
2
3
  import type { R2Bucket } from './types.js';
3
4
  interface CreateR2AdapterArgs {
4
5
  bucket: R2Bucket;
5
6
  clientUploads?: ClientUploadsConfig;
7
+ collections: R2StorageOptions['collections'];
8
+ copyCredentials?: R2StorageOptions['copyCredentials'];
6
9
  useCompositePrefixes?: boolean;
7
10
  }
8
- export declare function createR2Adapter({ bucket, clientUploads, useCompositePrefixes, }: CreateR2AdapterArgs): Adapter;
11
+ export declare function createR2Adapter({ bucket, clientUploads, collections, copyCredentials, useCompositePrefixes, }: CreateR2AdapterArgs): Adapter;
9
12
  export {};
10
13
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,mBAAmB,EAEpB,MAAM,wCAAwC,CAAA;AAE/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAM1C,UAAU,mBAAmB;IAC3B,MAAM,EAAE,QAAQ,CAAA;IAChB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,EACb,oBAA4B,GAC7B,EAAE,mBAAmB,GAAG,OAAO,CAiC/B"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,mBAAmB,EAGpB,MAAM,wCAAwC,CAAA;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAO1C,UAAU,mBAAmB;IAC3B,MAAM,EAAE,QAAQ,CAAA;IAChB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IAC5C,eAAe,CAAC,EAAE,gBAAgB,CAAC,iBAAiB,CAAC,CAAA;IACrD,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,EACb,WAAW,EACX,eAAe,EACf,oBAA4B,GAC7B,EAAE,mBAAmB,GAAG,OAAO,CAuF/B"}
package/dist/adapter.js CHANGED
@@ -1,30 +1,89 @@
1
1
  import { deleteFile } from './deleteFile.js';
2
2
  import { getFile } from './getFile.js';
3
+ import { getHandleMultiPartUpload } from './handleMultiPartUpload.js';
3
4
  import { uploadFile } from './uploadFile.js';
4
- export function createR2Adapter({ bucket, clientUploads, useCompositePrefixes = false }) {
5
+ export function createR2Adapter({ bucket, clientUploads, collections, copyCredentials, useCompositePrefixes = false }) {
6
+ const access = typeof clientUploads === 'object' ? clientUploads.access : undefined;
7
+ const uploadInstructions = {
8
+ adminHandler: {
9
+ path: '@payloadcms/storage-r2/client#R2ClientUploadHandler'
10
+ },
11
+ enabled: Boolean(clientUploads),
12
+ endpoint: {
13
+ handler: getHandleMultiPartUpload({
14
+ access,
15
+ bucket,
16
+ collections,
17
+ useCompositePrefixes
18
+ }),
19
+ path: '/storage-r2-multi-part-upload'
20
+ },
21
+ generate: ({ filename, filesize, mimeType })=>({
22
+ name: 'uploadToR2',
23
+ type: 'dispatch',
24
+ file: {
25
+ filename,
26
+ mimeType,
27
+ size: filesize,
28
+ uploadReference: {}
29
+ }
30
+ }),
31
+ requiresUploadReceipt: true,
32
+ useInAdmin: true
33
+ };
34
+ const deleteStoredFile = ({ storageFilePath })=>deleteFile({
35
+ bucket,
36
+ storageFilePath
37
+ });
5
38
  return ({ collection, prefix = '' })=>({
6
39
  name: 'r2',
7
- clientUploads,
8
- requiresClientUploadReceipt: true,
9
- handleDelete: ({ storageFilePath })=>deleteFile({
10
- bucket,
11
- storageFilePath
12
- }),
40
+ uploadInstructions,
41
+ copyFile: async ({ from, req, to })=>{
42
+ if (!copyCredentials) {
43
+ throw new Error('R2 file copy requires S3 API credentials');
44
+ }
45
+ const [{ S3 }, { copyS3File }] = await Promise.all([
46
+ import('@aws-sdk/client-s3'),
47
+ import('@payloadcms/storage-s3/copy-file')
48
+ ]);
49
+ const client = new S3({
50
+ credentials: {
51
+ accessKeyId: copyCredentials.accessKeyId,
52
+ secretAccessKey: copyCredentials.secretAccessKey
53
+ },
54
+ endpoint: `https://${copyCredentials.accountId}.r2.cloudflarestorage.com`,
55
+ region: 'auto'
56
+ });
57
+ try {
58
+ await copyS3File({
59
+ bucket: copyCredentials.bucket,
60
+ client,
61
+ from,
62
+ req,
63
+ to
64
+ });
65
+ } finally{
66
+ client.destroy();
67
+ }
68
+ },
69
+ deleteFile: deleteStoredFile,
70
+ handleDelete: deleteStoredFile,
13
71
  handleUpload: ({ file, storageFilePath })=>uploadFile({
14
72
  bucket,
15
73
  buffer: file.buffer,
16
74
  mimeType: file.mimeType,
17
75
  storageFilePath
18
76
  }),
19
- staticHandler: (req, { doc, headers, params: { clientUploadContext, filename } })=>getFile({
77
+ staticHandler: (req, { doc, headers, params: { filename, operation, uploadReference } })=>getFile({
20
78
  bucket,
21
- clientUploadContext,
22
79
  collection,
23
80
  doc,
24
81
  filename,
25
82
  incomingHeaders: headers,
83
+ operation,
26
84
  prefix,
27
85
  req,
86
+ uploadReference,
28
87
  useCompositePrefixes
29
88
  })
30
89
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter.ts"],"sourcesContent":["import type {\n Adapter,\n ClientUploadsConfig,\n GeneratedAdapter,\n} from '@payloadcms/plugin-cloud-storage/types'\n\nimport type { R2Bucket } from './types.js'\n\nimport { deleteFile } from './deleteFile.js'\nimport { getFile } from './getFile.js'\nimport { uploadFile } from './uploadFile.js'\n\ninterface CreateR2AdapterArgs {\n bucket: R2Bucket\n clientUploads?: ClientUploadsConfig\n useCompositePrefixes?: boolean\n}\n\nexport function createR2Adapter({\n bucket,\n clientUploads,\n useCompositePrefixes = false,\n}: CreateR2AdapterArgs): Adapter {\n return ({ collection, prefix = '' }): GeneratedAdapter => ({\n name: 'r2',\n clientUploads,\n requiresClientUploadReceipt: true,\n\n handleDelete: ({ storageFilePath }) =>\n deleteFile({\n bucket,\n storageFilePath,\n }),\n\n handleUpload: ({ file, storageFilePath }) =>\n uploadFile({\n bucket,\n buffer: file.buffer,\n mimeType: file.mimeType,\n storageFilePath,\n }),\n\n staticHandler: (req, { doc, headers, params: { clientUploadContext, filename } }) =>\n getFile({\n bucket,\n clientUploadContext,\n collection,\n doc,\n filename,\n incomingHeaders: headers,\n prefix,\n req,\n useCompositePrefixes,\n }),\n })\n}\n"],"names":["deleteFile","getFile","uploadFile","createR2Adapter","bucket","clientUploads","useCompositePrefixes","collection","prefix","name","requiresClientUploadReceipt","handleDelete","storageFilePath","handleUpload","file","buffer","mimeType","staticHandler","req","doc","headers","params","clientUploadContext","filename","incomingHeaders"],"mappings":"AAQA,SAASA,UAAU,QAAQ,kBAAiB;AAC5C,SAASC,OAAO,QAAQ,eAAc;AACtC,SAASC,UAAU,QAAQ,kBAAiB;AAQ5C,OAAO,SAASC,gBAAgB,EAC9BC,MAAM,EACNC,aAAa,EACbC,uBAAuB,KAAK,EACR;IACpB,OAAO,CAAC,EAAEC,UAAU,EAAEC,SAAS,EAAE,EAAE,GAAwB,CAAA;YACzDC,MAAM;YACNJ;YACAK,6BAA6B;YAE7BC,cAAc,CAAC,EAAEC,eAAe,EAAE,GAChCZ,WAAW;oBACTI;oBACAQ;gBACF;YAEFC,cAAc,CAAC,EAAEC,IAAI,EAAEF,eAAe,EAAE,GACtCV,WAAW;oBACTE;oBACAW,QAAQD,KAAKC,MAAM;oBACnBC,UAAUF,KAAKE,QAAQ;oBACvBJ;gBACF;YAEFK,eAAe,CAACC,KAAK,EAAEC,GAAG,EAAEC,OAAO,EAAEC,QAAQ,EAAEC,mBAAmB,EAAEC,QAAQ,EAAE,EAAE,GAC9EtB,QAAQ;oBACNG;oBACAkB;oBACAf;oBACAY;oBACAI;oBACAC,iBAAiBJ;oBACjBZ;oBACAU;oBACAZ;gBACF;QACJ,CAAA;AACF"}
1
+ {"version":3,"sources":["../src/adapter.ts"],"sourcesContent":["import type {\n Adapter,\n ClientUploadsConfig,\n DeleteFile,\n GeneratedAdapter,\n} from '@payloadcms/plugin-cloud-storage/types'\n\nimport type { R2StorageOptions } from './index.js'\nimport type { R2Bucket } from './types.js'\n\nimport { deleteFile } from './deleteFile.js'\nimport { getFile } from './getFile.js'\nimport { getHandleMultiPartUpload } from './handleMultiPartUpload.js'\nimport { uploadFile } from './uploadFile.js'\n\ninterface CreateR2AdapterArgs {\n bucket: R2Bucket\n clientUploads?: ClientUploadsConfig\n collections: R2StorageOptions['collections']\n copyCredentials?: R2StorageOptions['copyCredentials']\n useCompositePrefixes?: boolean\n}\n\nexport function createR2Adapter({\n bucket,\n clientUploads,\n collections,\n copyCredentials,\n useCompositePrefixes = false,\n}: CreateR2AdapterArgs): Adapter {\n const access = typeof clientUploads === 'object' ? clientUploads.access : undefined\n const uploadInstructions: GeneratedAdapter['uploadInstructions'] = {\n adminHandler: {\n path: '@payloadcms/storage-r2/client#R2ClientUploadHandler',\n },\n enabled: Boolean(clientUploads),\n endpoint: {\n handler: getHandleMultiPartUpload({\n access,\n bucket,\n collections,\n useCompositePrefixes,\n }),\n path: '/storage-r2-multi-part-upload',\n },\n generate: ({ filename, filesize, mimeType }) => ({\n name: 'uploadToR2',\n type: 'dispatch',\n file: {\n filename,\n mimeType,\n size: filesize,\n uploadReference: {},\n },\n }),\n requiresUploadReceipt: true,\n useInAdmin: true,\n }\n\n const deleteStoredFile: DeleteFile = ({ storageFilePath }) =>\n deleteFile({ bucket, storageFilePath })\n\n return ({ collection, prefix = '' }): GeneratedAdapter => ({\n name: 'r2',\n uploadInstructions,\n\n copyFile: async ({ from, req, to }) => {\n if (!copyCredentials) {\n throw new Error('R2 file copy requires S3 API credentials')\n }\n\n const [{ S3 }, { copyS3File }] = await Promise.all([\n import('@aws-sdk/client-s3'),\n import('@payloadcms/storage-s3/copy-file'),\n ])\n const client = new S3({\n credentials: {\n accessKeyId: copyCredentials.accessKeyId,\n secretAccessKey: copyCredentials.secretAccessKey,\n },\n endpoint: `https://${copyCredentials.accountId}.r2.cloudflarestorage.com`,\n region: 'auto',\n })\n\n try {\n await copyS3File({ bucket: copyCredentials.bucket, client, from, req, to })\n } finally {\n client.destroy()\n }\n },\n\n deleteFile: deleteStoredFile,\n handleDelete: deleteStoredFile,\n\n handleUpload: ({ file, storageFilePath }) =>\n uploadFile({\n bucket,\n buffer: file.buffer,\n mimeType: file.mimeType,\n storageFilePath,\n }),\n\n staticHandler: (req, { doc, headers, params: { filename, operation, uploadReference } }) =>\n getFile({\n bucket,\n collection,\n doc,\n filename,\n incomingHeaders: headers,\n operation,\n prefix,\n req,\n uploadReference,\n useCompositePrefixes,\n }),\n })\n}\n"],"names":["deleteFile","getFile","getHandleMultiPartUpload","uploadFile","createR2Adapter","bucket","clientUploads","collections","copyCredentials","useCompositePrefixes","access","undefined","uploadInstructions","adminHandler","path","enabled","Boolean","endpoint","handler","generate","filename","filesize","mimeType","name","type","file","size","uploadReference","requiresUploadReceipt","useInAdmin","deleteStoredFile","storageFilePath","collection","prefix","copyFile","from","req","to","Error","S3","copyS3File","Promise","all","client","credentials","accessKeyId","secretAccessKey","accountId","region","destroy","handleDelete","handleUpload","buffer","staticHandler","doc","headers","params","operation","incomingHeaders"],"mappings":"AAUA,SAASA,UAAU,QAAQ,kBAAiB;AAC5C,SAASC,OAAO,QAAQ,eAAc;AACtC,SAASC,wBAAwB,QAAQ,6BAA4B;AACrE,SAASC,UAAU,QAAQ,kBAAiB;AAU5C,OAAO,SAASC,gBAAgB,EAC9BC,MAAM,EACNC,aAAa,EACbC,WAAW,EACXC,eAAe,EACfC,uBAAuB,KAAK,EACR;IACpB,MAAMC,SAAS,OAAOJ,kBAAkB,WAAWA,cAAcI,MAAM,GAAGC;IAC1E,MAAMC,qBAA6D;QACjEC,cAAc;YACZC,MAAM;QACR;QACAC,SAASC,QAAQV;QACjBW,UAAU;YACRC,SAAShB,yBAAyB;gBAChCQ;gBACAL;gBACAE;gBACAE;YACF;YACAK,MAAM;QACR;QACAK,UAAU,CAAC,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,QAAQ,EAAE,GAAM,CAAA;gBAC/CC,MAAM;gBACNC,MAAM;gBACNC,MAAM;oBACJL;oBACAE;oBACAI,MAAML;oBACNM,iBAAiB,CAAC;gBACpB;YACF,CAAA;QACAC,uBAAuB;QACvBC,YAAY;IACd;IAEA,MAAMC,mBAA+B,CAAC,EAAEC,eAAe,EAAE,GACvD/B,WAAW;YAAEK;YAAQ0B;QAAgB;IAEvC,OAAO,CAAC,EAAEC,UAAU,EAAEC,SAAS,EAAE,EAAE,GAAwB,CAAA;YACzDV,MAAM;YACNX;YAEAsB,UAAU,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAEC,EAAE,EAAE;gBAChC,IAAI,CAAC7B,iBAAiB;oBACpB,MAAM,IAAI8B,MAAM;gBAClB;gBAEA,MAAM,CAAC,EAAEC,EAAE,EAAE,EAAE,EAAEC,UAAU,EAAE,CAAC,GAAG,MAAMC,QAAQC,GAAG,CAAC;oBACjD,MAAM,CAAC;oBACP,MAAM,CAAC;iBACR;gBACD,MAAMC,SAAS,IAAIJ,GAAG;oBACpBK,aAAa;wBACXC,aAAarC,gBAAgBqC,WAAW;wBACxCC,iBAAiBtC,gBAAgBsC,eAAe;oBAClD;oBACA7B,UAAU,CAAC,QAAQ,EAAET,gBAAgBuC,SAAS,CAAC,yBAAyB,CAAC;oBACzEC,QAAQ;gBACV;gBAEA,IAAI;oBACF,MAAMR,WAAW;wBAAEnC,QAAQG,gBAAgBH,MAAM;wBAAEsC;wBAAQR;wBAAMC;wBAAKC;oBAAG;gBAC3E,SAAU;oBACRM,OAAOM,OAAO;gBAChB;YACF;YAEAjD,YAAY8B;YACZoB,cAAcpB;YAEdqB,cAAc,CAAC,EAAE1B,IAAI,EAAEM,eAAe,EAAE,GACtC5B,WAAW;oBACTE;oBACA+C,QAAQ3B,KAAK2B,MAAM;oBACnB9B,UAAUG,KAAKH,QAAQ;oBACvBS;gBACF;YAEFsB,eAAe,CAACjB,KAAK,EAAEkB,GAAG,EAAEC,OAAO,EAAEC,QAAQ,EAAEpC,QAAQ,EAAEqC,SAAS,EAAE9B,eAAe,EAAE,EAAE,GACrF1B,QAAQ;oBACNI;oBACA2B;oBACAsB;oBACAlC;oBACAsC,iBAAiBH;oBACjBE;oBACAxB;oBACAG;oBACAT;oBACAlB;gBACF;QACJ,CAAA;AACF"}
@@ -1,10 +1,9 @@
1
1
  import type { R2StorageClientUploadHandlerParams } from '../types.js';
2
- export declare const R2ClientUploadHandler: ({ children, collectionSlug, enabled, extra, prefix, serverHandlerPath, }: {
2
+ export declare const R2ClientUploadHandler: ({ children, collectionSlug, endpointPath, prefix, props, }: {
3
3
  children: import("react").ReactNode;
4
- collectionSlug: import("packages/payload/src/index.js").UploadCollectionSlug;
5
- enabled?: boolean;
6
- extra: R2StorageClientUploadHandlerParams;
4
+ collectionSlug: import("payload").UploadCollectionSlug;
5
+ endpointPath?: `/${string}`;
7
6
  prefix?: string;
8
- serverHandlerPath: `/${string}`;
7
+ props: R2StorageClientUploadHandlerParams;
9
8
  }) => import("react").JSX.Element;
10
9
  //# sourceMappingURL=R2ClientUploadHandler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"R2ClientUploadHandler.d.ts","sourceRoot":"","sources":["../../src/client/R2ClientUploadHandler.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAGV,kCAAkC,EAGnC,MAAM,aAAa,CAAA;AAEpB,eAAO,MAAM,qBAAqB;;;;;;;aAYS,OAAO,aAqFhD,CAAA"}
1
+ {"version":3,"file":"R2ClientUploadHandler.d.ts","sourceRoot":"","sources":["../../src/client/R2ClientUploadHandler.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,kCAAkC,EAInC,MAAM,aAAa,CAAA;AAEpB,eAAO,MAAM,qBAAqB;;;;;;iCAqFhC,CAAA"}
@@ -1,41 +1,36 @@
1
1
  'use client';
2
- import { buildUploadStoragePathData, createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client';
2
+ import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client';
3
3
  import { formatAdminURL } from 'payload/shared';
4
4
  export const R2ClientUploadHandler = createClientUploadHandler({
5
- handler: async ({ apiRoute, collectionSlug, docPrefix, extra: { chunkSize = 5 * 1024 * 1024, useCompositePrefixes = false }, file, prefix, serverHandlerPath, serverURL, updateFilename })=>{
6
- const { sanitizedDocPrefix } = buildUploadStoragePathData({
7
- collectionPrefix: prefix,
8
- docPrefix,
9
- filename: file.name,
10
- useCompositePrefixes
11
- });
5
+ name: 'uploadToR2',
6
+ handler: async ({ apiRoute, collectionSlug, docPrefix, endpointPath, file, props: { chunkSize = 5 * 1024 * 1024 }, serverURL, updateFilename })=>{
12
7
  const params = {
13
8
  collection: collectionSlug,
14
- docPrefix: sanitizedDocPrefix,
9
+ docPrefix,
15
10
  fileName: file.name,
16
11
  fileType: file.type
17
12
  };
18
13
  const baseURL = formatAdminURL({
19
14
  apiRoute,
20
- path: serverHandlerPath,
15
+ path: endpointPath,
21
16
  serverURL
22
17
  });
23
18
  const getEndpoint = ()=>`${baseURL}?${String(new URLSearchParams(params))}`;
24
- // upload the file directly to R2 using the signed URL
19
+ // Initialize the multipart upload.
25
20
  const multipart = await fetch(getEndpoint(), {
26
21
  method: 'POST'
27
22
  });
28
23
  if (!multipart.ok) {
29
24
  throw new Error('Failed to initialize multipart upload');
30
25
  }
31
- const { clientUploadContext, filename: sanitizedFilename, ...multipartUpload } = await multipart.json();
26
+ const { filename: sanitizedFilename, uploadReference, ...multipartUpload } = await multipart.json();
32
27
  if (sanitizedFilename && sanitizedFilename !== file.name) {
33
28
  updateFilename(sanitizedFilename);
34
29
  }
35
30
  const multipartUploadedParts = [];
36
31
  params.multipartId = multipartUpload.uploadId;
37
32
  params.multipartKey = multipartUpload.key;
38
- params.signedReceipt = clientUploadContext.signedReceipt;
33
+ params.signedReceipt = uploadReference.signedReceipt;
39
34
  const partTotal = Math.ceil(file.size / chunkSize);
40
35
  for(let part = 1; part <= partTotal; part++){
41
36
  const bytesEnd = Math.min(part * chunkSize, file.size);
@@ -69,11 +64,9 @@ export const R2ClientUploadHandler = createClientUploadHandler({
69
64
  if (!complete.ok) {
70
65
  throw new Error(`Failed to complete multipart upload`);
71
66
  }
72
- const key = await complete.text();
73
67
  return {
74
- key,
75
- prefix: clientUploadContext.prefix,
76
- signedReceipt: clientUploadContext.signedReceipt
68
+ prefix: uploadReference.prefix,
69
+ signedReceipt: uploadReference.signedReceipt
77
70
  };
78
71
  }
79
72
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/R2ClientUploadHandler.ts"],"sourcesContent":["'use client'\n\nimport type { ClientUploadContext } from '@payloadcms/plugin-cloud-storage/types'\n\nimport {\n buildUploadStoragePathData,\n createClientUploadHandler,\n} from '@payloadcms/plugin-cloud-storage/client'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type {\n R2MultipartUpload,\n R2StorageClientUploadContext,\n R2StorageClientUploadHandlerParams,\n R2StorageMultipartUploadHandlerParams,\n R2UploadedPart,\n} from '../types.js'\n\nexport const R2ClientUploadHandler = createClientUploadHandler<R2StorageClientUploadHandlerParams>({\n handler: async ({\n apiRoute,\n collectionSlug,\n docPrefix,\n extra: { chunkSize = 5 * 1024 * 1024, useCompositePrefixes = false },\n file,\n prefix,\n serverHandlerPath,\n serverURL,\n updateFilename,\n }): Promise<R2StorageClientUploadContext | undefined> => {\n const { sanitizedDocPrefix } = buildUploadStoragePathData({\n collectionPrefix: prefix,\n docPrefix,\n filename: file.name,\n useCompositePrefixes,\n })\n\n const params: R2StorageMultipartUploadHandlerParams = {\n collection: collectionSlug,\n docPrefix: sanitizedDocPrefix,\n fileName: file.name,\n fileType: file.type,\n }\n const baseURL = formatAdminURL({\n apiRoute,\n path: serverHandlerPath,\n serverURL,\n })\n\n const getEndpoint = () => `${baseURL}?${String(new URLSearchParams(params))}`\n\n // upload the file directly to R2 using the signed URL\n const multipart = await fetch(getEndpoint(), { method: 'POST' })\n if (!multipart.ok) {\n throw new Error('Failed to initialize multipart upload')\n }\n\n const {\n clientUploadContext,\n filename: sanitizedFilename,\n ...multipartUpload\n } = (await multipart.json()) as {\n clientUploadContext: ClientUploadContext\n filename?: string\n } & Pick<R2MultipartUpload, 'key' | 'uploadId'>\n\n if (sanitizedFilename && sanitizedFilename !== file.name) {\n updateFilename(sanitizedFilename)\n }\n\n const multipartUploadedParts: R2UploadedPart[] = []\n\n params.multipartId = multipartUpload.uploadId\n params.multipartKey = multipartUpload.key\n params.signedReceipt = clientUploadContext.signedReceipt\n\n const partTotal = Math.ceil(file.size / chunkSize)\n\n for (let part = 1; part <= partTotal; part++) {\n const bytesEnd = Math.min(part * chunkSize, file.size)\n const bytesStart = (part - 1) * chunkSize\n\n params.multipartNumber = String(part)\n\n const body = file.slice(bytesStart, bytesEnd)\n const headers = {\n 'Content-Length': String(body.size),\n 'Content-Type': 'application/octet-stream',\n }\n const uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n if (!uploaded.ok) {\n throw new Error(`Failed to upload part ${part} / ${partTotal}`)\n }\n\n multipartUploadedParts.push((await uploaded.json()) as R2UploadedPart)\n\n if (part === partTotal) {\n delete params.multipartNumber\n\n const body = JSON.stringify(multipartUploadedParts)\n const headers = { 'Content-Type': 'application/json' }\n const complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n if (!complete.ok) {\n throw new Error(`Failed to complete multipart upload`)\n }\n\n const key = await complete.text()\n return {\n key,\n prefix: clientUploadContext.prefix,\n signedReceipt: clientUploadContext.signedReceipt,\n }\n }\n }\n },\n})\n"],"names":["buildUploadStoragePathData","createClientUploadHandler","formatAdminURL","R2ClientUploadHandler","handler","apiRoute","collectionSlug","docPrefix","extra","chunkSize","useCompositePrefixes","file","prefix","serverHandlerPath","serverURL","updateFilename","sanitizedDocPrefix","collectionPrefix","filename","name","params","collection","fileName","fileType","type","baseURL","path","getEndpoint","String","URLSearchParams","multipart","fetch","method","ok","Error","clientUploadContext","sanitizedFilename","multipartUpload","json","multipartUploadedParts","multipartId","uploadId","multipartKey","key","signedReceipt","partTotal","Math","ceil","size","part","bytesEnd","min","bytesStart","multipartNumber","body","slice","headers","uploaded","push","JSON","stringify","complete","text"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/client/R2ClientUploadHandler.ts"],"sourcesContent":["'use client'\n\nimport { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type {\n R2MultipartUpload,\n R2StorageClientUploadHandlerParams,\n R2StorageMultipartUploadHandlerParams,\n R2StorageUploadReference,\n R2UploadedPart,\n} from '../types.js'\n\nexport const R2ClientUploadHandler = createClientUploadHandler<R2StorageClientUploadHandlerParams>({\n name: 'uploadToR2',\n handler: async ({\n apiRoute,\n collectionSlug,\n docPrefix,\n endpointPath,\n file,\n props: { chunkSize = 5 * 1024 * 1024 },\n serverURL,\n updateFilename,\n }): Promise<R2StorageUploadReference | undefined> => {\n const params: R2StorageMultipartUploadHandlerParams = {\n collection: collectionSlug,\n docPrefix,\n fileName: file.name,\n fileType: file.type,\n }\n const baseURL = formatAdminURL({\n apiRoute,\n path: endpointPath,\n serverURL,\n })\n\n const getEndpoint = () => `${baseURL}?${String(new URLSearchParams(params))}`\n\n // Initialize the multipart upload.\n const multipart = await fetch(getEndpoint(), { method: 'POST' })\n if (!multipart.ok) {\n throw new Error('Failed to initialize multipart upload')\n }\n\n const {\n filename: sanitizedFilename,\n uploadReference,\n ...multipartUpload\n } = (await multipart.json()) as {\n filename?: string\n uploadReference: R2StorageUploadReference\n } & Pick<R2MultipartUpload, 'key' | 'uploadId'>\n\n if (sanitizedFilename && sanitizedFilename !== file.name) {\n updateFilename(sanitizedFilename)\n }\n\n const multipartUploadedParts: R2UploadedPart[] = []\n\n params.multipartId = multipartUpload.uploadId\n params.multipartKey = multipartUpload.key\n params.signedReceipt = uploadReference.signedReceipt\n\n const partTotal = Math.ceil(file.size / chunkSize)\n\n for (let part = 1; part <= partTotal; part++) {\n const bytesEnd = Math.min(part * chunkSize, file.size)\n const bytesStart = (part - 1) * chunkSize\n\n params.multipartNumber = String(part)\n\n const body = file.slice(bytesStart, bytesEnd)\n const headers = {\n 'Content-Length': String(body.size),\n 'Content-Type': 'application/octet-stream',\n }\n const uploaded = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n if (!uploaded.ok) {\n throw new Error(`Failed to upload part ${part} / ${partTotal}`)\n }\n\n multipartUploadedParts.push((await uploaded.json()) as R2UploadedPart)\n\n if (part === partTotal) {\n delete params.multipartNumber\n\n const body = JSON.stringify(multipartUploadedParts)\n const headers = { 'Content-Type': 'application/json' }\n const complete = await fetch(getEndpoint(), { body, headers, method: 'POST' })\n if (!complete.ok) {\n throw new Error(`Failed to complete multipart upload`)\n }\n\n return { prefix: uploadReference.prefix, signedReceipt: uploadReference.signedReceipt }\n }\n }\n },\n})\n"],"names":["createClientUploadHandler","formatAdminURL","R2ClientUploadHandler","name","handler","apiRoute","collectionSlug","docPrefix","endpointPath","file","props","chunkSize","serverURL","updateFilename","params","collection","fileName","fileType","type","baseURL","path","getEndpoint","String","URLSearchParams","multipart","fetch","method","ok","Error","filename","sanitizedFilename","uploadReference","multipartUpload","json","multipartUploadedParts","multipartId","uploadId","multipartKey","key","signedReceipt","partTotal","Math","ceil","size","part","bytesEnd","min","bytesStart","multipartNumber","body","slice","headers","uploaded","push","JSON","stringify","complete","prefix"],"mappings":"AAAA;AAEA,SAASA,yBAAyB,QAAQ,0CAAyC;AACnF,SAASC,cAAc,QAAQ,iBAAgB;AAU/C,OAAO,MAAMC,wBAAwBF,0BAA8D;IACjGG,MAAM;IACNC,SAAS,OAAO,EACdC,QAAQ,EACRC,cAAc,EACdC,SAAS,EACTC,YAAY,EACZC,IAAI,EACJC,OAAO,EAAEC,YAAY,IAAI,OAAO,IAAI,EAAE,EACtCC,SAAS,EACTC,cAAc,EACf;QACC,MAAMC,SAAgD;YACpDC,YAAYT;YACZC;YACAS,UAAUP,KAAKN,IAAI;YACnBc,UAAUR,KAAKS,IAAI;QACrB;QACA,MAAMC,UAAUlB,eAAe;YAC7BI;YACAe,MAAMZ;YACNI;QACF;QAEA,MAAMS,cAAc,IAAM,GAAGF,QAAQ,CAAC,EAAEG,OAAO,IAAIC,gBAAgBT,UAAU;QAE7E,mCAAmC;QACnC,MAAMU,YAAY,MAAMC,MAAMJ,eAAe;YAAEK,QAAQ;QAAO;QAC9D,IAAI,CAACF,UAAUG,EAAE,EAAE;YACjB,MAAM,IAAIC,MAAM;QAClB;QAEA,MAAM,EACJC,UAAUC,iBAAiB,EAC3BC,eAAe,EACf,GAAGC,iBACJ,GAAI,MAAMR,UAAUS,IAAI;QAKzB,IAAIH,qBAAqBA,sBAAsBrB,KAAKN,IAAI,EAAE;YACxDU,eAAeiB;QACjB;QAEA,MAAMI,yBAA2C,EAAE;QAEnDpB,OAAOqB,WAAW,GAAGH,gBAAgBI,QAAQ;QAC7CtB,OAAOuB,YAAY,GAAGL,gBAAgBM,GAAG;QACzCxB,OAAOyB,aAAa,GAAGR,gBAAgBQ,aAAa;QAEpD,MAAMC,YAAYC,KAAKC,IAAI,CAACjC,KAAKkC,IAAI,GAAGhC;QAExC,IAAK,IAAIiC,OAAO,GAAGA,QAAQJ,WAAWI,OAAQ;YAC5C,MAAMC,WAAWJ,KAAKK,GAAG,CAACF,OAAOjC,WAAWF,KAAKkC,IAAI;YACrD,MAAMI,aAAa,AAACH,CAAAA,OAAO,CAAA,IAAKjC;YAEhCG,OAAOkC,eAAe,GAAG1B,OAAOsB;YAEhC,MAAMK,OAAOxC,KAAKyC,KAAK,CAACH,YAAYF;YACpC,MAAMM,UAAU;gBACd,kBAAkB7B,OAAO2B,KAAKN,IAAI;gBAClC,gBAAgB;YAClB;YACA,MAAMS,WAAW,MAAM3B,MAAMJ,eAAe;gBAAE4B;gBAAME;gBAASzB,QAAQ;YAAO;YAC5E,IAAI,CAAC0B,SAASzB,EAAE,EAAE;gBAChB,MAAM,IAAIC,MAAM,CAAC,sBAAsB,EAAEgB,KAAK,GAAG,EAAEJ,WAAW;YAChE;YAEAN,uBAAuBmB,IAAI,CAAE,MAAMD,SAASnB,IAAI;YAEhD,IAAIW,SAASJ,WAAW;gBACtB,OAAO1B,OAAOkC,eAAe;gBAE7B,MAAMC,OAAOK,KAAKC,SAAS,CAACrB;gBAC5B,MAAMiB,UAAU;oBAAE,gBAAgB;gBAAmB;gBACrD,MAAMK,WAAW,MAAM/B,MAAMJ,eAAe;oBAAE4B;oBAAME;oBAASzB,QAAQ;gBAAO;gBAC5E,IAAI,CAAC8B,SAAS7B,EAAE,EAAE;oBAChB,MAAM,IAAIC,MAAM,CAAC,mCAAmC,CAAC;gBACvD;gBAEA,OAAO;oBAAE6B,QAAQ1B,gBAAgB0B,MAAM;oBAAElB,eAAeR,gBAAgBQ,aAAa;gBAAC;YACxF;QACF;IACF;AACF,GAAE"}
package/dist/getFile.d.ts CHANGED
@@ -1,16 +1,17 @@
1
- import type { CollectionConfig, PayloadRequest, TypeWithID } from 'payload';
1
+ import type { CollectionConfig, FileHandlerOperation, PayloadRequest, TypeWithID } from 'payload';
2
2
  import type { R2Bucket } from './types.js';
3
3
  interface GetFileArgs {
4
4
  bucket: R2Bucket;
5
- clientUploadContext?: unknown;
6
5
  collection: CollectionConfig;
7
6
  doc?: TypeWithID;
8
7
  filename: string;
9
8
  incomingHeaders?: Headers;
9
+ operation?: FileHandlerOperation;
10
10
  prefix: string;
11
11
  req: PayloadRequest;
12
+ uploadReference?: unknown;
12
13
  useCompositePrefixes?: boolean;
13
14
  }
14
- export declare function getFile({ bucket, clientUploadContext, collection, doc, filename, incomingHeaders, prefix, req, useCompositePrefixes, }: GetFileArgs): Promise<Response>;
15
+ export declare function getFile({ bucket, collection, doc, filename, incomingHeaders, operation, prefix, req, uploadReference, useCompositePrefixes, }: GetFileArgs): Promise<Response>;
15
16
  export {};
16
17
  //# sourceMappingURL=getFile.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../src/getFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAS3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,UAAU,WAAW;IACnB,MAAM,EAAE,QAAQ,CAAA;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,GAAG,CAAC,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAID,wBAAsB,OAAO,CAAC,EAC5B,MAAM,EACN,mBAAmB,EACnB,UAAU,EACV,GAAG,EACH,QAAQ,EACR,eAAe,EACf,MAAW,EACX,GAAG,EACH,oBAA4B,GAC7B,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiHjC"}
1
+ {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../src/getFile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAQjG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,UAAU,WAAW;IACnB,MAAM,EAAE,QAAQ,CAAA;IAChB,UAAU,EAAE,gBAAgB,CAAA;IAC5B,GAAG,CAAC,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,oBAAoB,CAAA;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,cAAc,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAID,wBAAsB,OAAO,CAAC,EAC5B,MAAM,EACN,UAAU,EACV,GAAG,EACH,QAAQ,EACR,eAAe,EACf,SAAkB,EAClB,MAAW,EACX,GAAG,EACH,eAAe,EACf,oBAA4B,GAC7B,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwHjC"}
package/dist/getFile.js CHANGED
@@ -1,19 +1,26 @@
1
- import { getStorageFilePath } from '@payloadcms/plugin-cloud-storage/utilities';
2
- import { getRangeRequestInfo, isXmlMimeType, UPLOAD_CONTENT_SECURITY_POLICY } from 'payload/internal';
1
+ import { buildStoragePathData, getFilePrefix as getDocPrefix } from '@payloadcms/plugin-cloud-storage/utilities';
2
+ import { getRangeRequestInfo, isXmlMimeType, uploadContentSecurityPolicy } from 'payload/internal';
3
3
  const isMiniflare = process.env.NODE_ENV === 'development';
4
- export async function getFile({ bucket, clientUploadContext, collection, doc, filename, incomingHeaders, prefix = '', req, useCompositePrefixes = false }) {
4
+ export async function getFile({ bucket, collection, doc, filename, incomingHeaders, operation = 'read', prefix = '', req, uploadReference, useCompositePrefixes = false }) {
5
+ const isTransformSource = operation === 'transform';
5
6
  try {
6
- const filePath = await getStorageFilePath({
7
- clientUploadContext,
7
+ const docPrefix = await getDocPrefix({
8
8
  collection,
9
9
  collectionPrefix: prefix,
10
10
  doc,
11
11
  filename,
12
12
  req,
13
+ uploadReference,
14
+ useCompositePrefixes
15
+ });
16
+ const { storageFilePath } = buildStoragePathData({
17
+ collectionPrefix: prefix,
18
+ docPrefix,
19
+ filename,
13
20
  useCompositePrefixes
14
21
  });
15
22
  // Get file size for range validation
16
- const headObj = await bucket?.head(filePath);
23
+ const headObj = await bucket?.head(storageFilePath);
17
24
  if (!headObj) {
18
25
  return new Response(null, {
19
26
  status: 404,
@@ -21,14 +28,14 @@ export async function getFile({ bucket, clientUploadContext, collection, doc, fi
21
28
  });
22
29
  }
23
30
  const fileSize = headObj.size;
24
- // Don't return large file uploads back to the client, or the Worker will run out of memory
25
- if (fileSize > 50 * 1024 * 1024 && clientUploadContext) {
31
+ // Don't return large file uploads back to the client, or the Worker will run out of memory.
32
+ // Skipped for `transform`, which needs the real bytes and would otherwise silently corrupt.
33
+ if (fileSize > 50 * 1024 * 1024 && uploadReference && !isTransformSource) {
26
34
  return new Response(null, {
27
35
  status: 200
28
36
  });
29
37
  }
30
- // Handle range request
31
- const rangeHeader = req.headers.get('range');
38
+ const rangeHeader = isTransformSource ? null : req.headers.get('range');
32
39
  const rangeResult = getRangeRequestInfo({
33
40
  fileSize,
34
41
  rangeHeader
@@ -39,15 +46,14 @@ export async function getFile({ bucket, clientUploadContext, collection, doc, fi
39
46
  status: rangeResult.status
40
47
  });
41
48
  }
42
- // Get object with range if needed
43
49
  // Due to https://github.com/cloudflare/workers-sdk/issues/6047
44
50
  // We cannot send a Headers instance to Miniflare
45
- const obj = rangeResult.type === 'partial' && !isMiniflare ? await bucket?.get(filePath, {
51
+ const obj = rangeResult.type === 'partial' && !isMiniflare ? await bucket?.get(storageFilePath, {
46
52
  range: {
47
53
  length: rangeResult.rangeEnd - rangeResult.rangeStart + 1,
48
54
  offset: rangeResult.rangeStart
49
55
  }
50
- }) : await bucket?.get(filePath);
56
+ }) : await bucket?.get(storageFilePath);
51
57
  if (!obj || obj.body == undefined) {
52
58
  return new Response(null, {
53
59
  status: 404,
@@ -55,13 +61,10 @@ export async function getFile({ bucket, clientUploadContext, collection, doc, fi
55
61
  });
56
62
  }
57
63
  let headers = new Headers(incomingHeaders);
58
- // Add range-related headers from the result
59
64
  for (const [headerKey, value] of Object.entries(rangeResult.headers)){
60
65
  headers.append(headerKey, value);
61
66
  }
62
- // Add R2-specific headers
63
67
  if (isMiniflare) {
64
- // In development with Miniflare, manually set headers from httpMetadata
65
68
  const metadata = obj.httpMetadata;
66
69
  if (metadata?.cacheControl) {
67
70
  headers.set('Cache-Control', metadata.cacheControl);
@@ -81,18 +84,18 @@ export async function getFile({ bucket, clientUploadContext, collection, doc, fi
81
84
  } else {
82
85
  obj.writeHttpMetadata(headers);
83
86
  }
84
- // Add Content-Security-Policy header for XML-family files
85
87
  const contentType = headers.get('Content-Type');
88
+ // Apply a restrictive policy to XML-family responses served through Payload.
86
89
  if (isXmlMimeType(contentType)) {
87
- headers.set('Content-Security-Policy', UPLOAD_CONTENT_SECURITY_POLICY);
90
+ headers.set('Content-Security-Policy', uploadContentSecurityPolicy);
88
91
  }
89
92
  const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match');
90
- if (collection.upload && typeof collection.upload === 'object' && typeof collection.upload.modifyResponseHeaders === 'function') {
93
+ if (!isTransformSource && collection.upload && typeof collection.upload === 'object' && typeof collection.upload.modifyResponseHeaders === 'function') {
91
94
  headers = collection.upload.modifyResponseHeaders({
92
95
  headers
93
96
  }) || headers;
94
97
  }
95
- if (etagFromHeaders && etagFromHeaders === obj.etag) {
98
+ if (!isTransformSource && etagFromHeaders && etagFromHeaders === obj.etag) {
96
99
  return new Response(null, {
97
100
  headers,
98
101
  status: 304
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/getFile.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest, TypeWithID } from 'payload'\n\nimport { getStorageFilePath } from '@payloadcms/plugin-cloud-storage/utilities'\nimport {\n getRangeRequestInfo,\n isXmlMimeType,\n UPLOAD_CONTENT_SECURITY_POLICY,\n} from 'payload/internal'\n\nimport type { R2Bucket } from './types.js'\n\ninterface GetFileArgs {\n bucket: R2Bucket\n clientUploadContext?: unknown\n collection: CollectionConfig\n doc?: TypeWithID\n filename: string\n incomingHeaders?: Headers\n prefix: string\n req: PayloadRequest\n useCompositePrefixes?: boolean\n}\n\nconst isMiniflare = process.env.NODE_ENV === 'development'\n\nexport async function getFile({\n bucket,\n clientUploadContext,\n collection,\n doc,\n filename,\n incomingHeaders,\n prefix = '',\n req,\n useCompositePrefixes = false,\n}: GetFileArgs): Promise<Response> {\n try {\n const filePath = await getStorageFilePath({\n clientUploadContext,\n collection,\n collectionPrefix: prefix,\n doc,\n filename,\n req,\n useCompositePrefixes,\n })\n\n // Get file size for range validation\n const headObj = await bucket?.head(filePath)\n if (!headObj) {\n return new Response(null, { status: 404, statusText: 'Not Found' })\n }\n\n const fileSize = headObj.size\n\n // Don't return large file uploads back to the client, or the Worker will run out of memory\n if (fileSize > 50 * 1024 * 1024 && clientUploadContext) {\n return new Response(null, { status: 200 })\n }\n\n // Handle range request\n const rangeHeader = req.headers.get('range')\n const rangeResult = getRangeRequestInfo({ fileSize, rangeHeader })\n\n if (rangeResult.type === 'invalid') {\n return new Response(null, {\n headers: new Headers(rangeResult.headers),\n status: rangeResult.status,\n })\n }\n\n // Get object with range if needed\n // Due to https://github.com/cloudflare/workers-sdk/issues/6047\n // We cannot send a Headers instance to Miniflare\n const obj =\n rangeResult.type === 'partial' && !isMiniflare\n ? await bucket?.get(filePath, {\n range: {\n length: rangeResult.rangeEnd - rangeResult.rangeStart + 1,\n offset: rangeResult.rangeStart,\n },\n })\n : await bucket?.get(filePath)\n\n if (!obj || obj.body == undefined) {\n return new Response(null, { status: 404, statusText: 'Not Found' })\n }\n\n let headers = new Headers(incomingHeaders)\n\n // Add range-related headers from the result\n for (const [headerKey, value] of Object.entries(rangeResult.headers)) {\n headers.append(headerKey, value)\n }\n\n // Add R2-specific headers\n if (isMiniflare) {\n // In development with Miniflare, manually set headers from httpMetadata\n const metadata = obj.httpMetadata\n if (metadata?.cacheControl) {\n headers.set('Cache-Control', metadata.cacheControl)\n }\n if (metadata?.contentDisposition) {\n headers.set('Content-Disposition', metadata.contentDisposition)\n }\n if (metadata?.contentEncoding) {\n headers.set('Content-Encoding', metadata.contentEncoding)\n }\n if (metadata?.contentLanguage) {\n headers.set('Content-Language', metadata.contentLanguage)\n }\n if (metadata?.contentType) {\n headers.set('Content-Type', metadata.contentType)\n }\n } else {\n obj.writeHttpMetadata(headers)\n }\n\n // Add Content-Security-Policy header for XML-family files\n const contentType = headers.get('Content-Type')\n if (isXmlMimeType(contentType)) {\n headers.set('Content-Security-Policy', UPLOAD_CONTENT_SECURITY_POLICY)\n }\n\n const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')\n\n if (\n collection.upload &&\n typeof collection.upload === 'object' &&\n typeof collection.upload.modifyResponseHeaders === 'function'\n ) {\n headers = collection.upload.modifyResponseHeaders({ headers }) || headers\n }\n\n if (etagFromHeaders && etagFromHeaders === obj.etag) {\n return new Response(null, {\n headers,\n status: 304,\n })\n }\n\n return new Response(obj.body, {\n headers,\n status: rangeResult.status,\n })\n } catch (_err: unknown) {\n return new Response('Internal Server Error', { status: 500 })\n }\n}\n"],"names":["getStorageFilePath","getRangeRequestInfo","isXmlMimeType","UPLOAD_CONTENT_SECURITY_POLICY","isMiniflare","process","env","NODE_ENV","getFile","bucket","clientUploadContext","collection","doc","filename","incomingHeaders","prefix","req","useCompositePrefixes","filePath","collectionPrefix","headObj","head","Response","status","statusText","fileSize","size","rangeHeader","headers","get","rangeResult","type","Headers","obj","range","length","rangeEnd","rangeStart","offset","body","undefined","headerKey","value","Object","entries","append","metadata","httpMetadata","cacheControl","set","contentDisposition","contentEncoding","contentLanguage","contentType","writeHttpMetadata","etagFromHeaders","upload","modifyResponseHeaders","etag","_err"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/getFile.ts"],"sourcesContent":["import type { CollectionConfig, FileHandlerOperation, PayloadRequest, TypeWithID } from 'payload'\n\nimport {\n buildStoragePathData,\n getFilePrefix as getDocPrefix,\n} from '@payloadcms/plugin-cloud-storage/utilities'\nimport { getRangeRequestInfo, isXmlMimeType, uploadContentSecurityPolicy } from 'payload/internal'\n\nimport type { R2Bucket } from './types.js'\n\ninterface GetFileArgs {\n bucket: R2Bucket\n collection: CollectionConfig\n doc?: TypeWithID\n filename: string\n incomingHeaders?: Headers\n operation?: FileHandlerOperation\n prefix: string\n req: PayloadRequest\n uploadReference?: unknown\n useCompositePrefixes?: boolean\n}\n\nconst isMiniflare = process.env.NODE_ENV === 'development'\n\nexport async function getFile({\n bucket,\n collection,\n doc,\n filename,\n incomingHeaders,\n operation = 'read',\n prefix = '',\n req,\n uploadReference,\n useCompositePrefixes = false,\n}: GetFileArgs): Promise<Response> {\n const isTransformSource = operation === 'transform'\n\n try {\n const docPrefix = await getDocPrefix({\n collection,\n collectionPrefix: prefix,\n doc,\n filename,\n req,\n uploadReference,\n useCompositePrefixes,\n })\n\n const { storageFilePath } = buildStoragePathData({\n collectionPrefix: prefix,\n docPrefix,\n filename,\n useCompositePrefixes,\n })\n\n // Get file size for range validation\n const headObj = await bucket?.head(storageFilePath)\n if (!headObj) {\n return new Response(null, { status: 404, statusText: 'Not Found' })\n }\n\n const fileSize = headObj.size\n\n // Don't return large file uploads back to the client, or the Worker will run out of memory.\n // Skipped for `transform`, which needs the real bytes and would otherwise silently corrupt.\n if (fileSize > 50 * 1024 * 1024 && uploadReference && !isTransformSource) {\n return new Response(null, { status: 200 })\n }\n\n const rangeHeader = isTransformSource ? null : req.headers.get('range')\n const rangeResult = getRangeRequestInfo({ fileSize, rangeHeader })\n\n if (rangeResult.type === 'invalid') {\n return new Response(null, {\n headers: new Headers(rangeResult.headers),\n status: rangeResult.status,\n })\n }\n\n // Due to https://github.com/cloudflare/workers-sdk/issues/6047\n // We cannot send a Headers instance to Miniflare\n const obj =\n rangeResult.type === 'partial' && !isMiniflare\n ? await bucket?.get(storageFilePath, {\n range: {\n length: rangeResult.rangeEnd - rangeResult.rangeStart + 1,\n offset: rangeResult.rangeStart,\n },\n })\n : await bucket?.get(storageFilePath)\n\n if (!obj || obj.body == undefined) {\n return new Response(null, { status: 404, statusText: 'Not Found' })\n }\n\n let headers = new Headers(incomingHeaders)\n\n for (const [headerKey, value] of Object.entries(rangeResult.headers)) {\n headers.append(headerKey, value)\n }\n\n if (isMiniflare) {\n const metadata = obj.httpMetadata\n if (metadata?.cacheControl) {\n headers.set('Cache-Control', metadata.cacheControl)\n }\n if (metadata?.contentDisposition) {\n headers.set('Content-Disposition', metadata.contentDisposition)\n }\n if (metadata?.contentEncoding) {\n headers.set('Content-Encoding', metadata.contentEncoding)\n }\n if (metadata?.contentLanguage) {\n headers.set('Content-Language', metadata.contentLanguage)\n }\n if (metadata?.contentType) {\n headers.set('Content-Type', metadata.contentType)\n }\n } else {\n obj.writeHttpMetadata(headers)\n }\n\n const contentType = headers.get('Content-Type')\n\n // Apply a restrictive policy to XML-family responses served through Payload.\n if (isXmlMimeType(contentType)) {\n headers.set('Content-Security-Policy', uploadContentSecurityPolicy)\n }\n\n const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')\n\n if (\n !isTransformSource &&\n collection.upload &&\n typeof collection.upload === 'object' &&\n typeof collection.upload.modifyResponseHeaders === 'function'\n ) {\n headers = collection.upload.modifyResponseHeaders({ headers }) || headers\n }\n\n if (!isTransformSource && etagFromHeaders && etagFromHeaders === obj.etag) {\n return new Response(null, {\n headers,\n status: 304,\n })\n }\n\n return new Response(obj.body, {\n headers,\n status: rangeResult.status,\n })\n } catch (_err: unknown) {\n return new Response('Internal Server Error', { status: 500 })\n }\n}\n"],"names":["buildStoragePathData","getFilePrefix","getDocPrefix","getRangeRequestInfo","isXmlMimeType","uploadContentSecurityPolicy","isMiniflare","process","env","NODE_ENV","getFile","bucket","collection","doc","filename","incomingHeaders","operation","prefix","req","uploadReference","useCompositePrefixes","isTransformSource","docPrefix","collectionPrefix","storageFilePath","headObj","head","Response","status","statusText","fileSize","size","rangeHeader","headers","get","rangeResult","type","Headers","obj","range","length","rangeEnd","rangeStart","offset","body","undefined","headerKey","value","Object","entries","append","metadata","httpMetadata","cacheControl","set","contentDisposition","contentEncoding","contentLanguage","contentType","writeHttpMetadata","etagFromHeaders","upload","modifyResponseHeaders","etag","_err"],"mappings":"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"}
@@ -1,9 +1,8 @@
1
- import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types';
2
- import type { PayloadHandler } from 'payload';
1
+ import type { PayloadHandler, UploadInstructionsAccess } from 'payload';
3
2
  import type { R2StorageOptions } from './index.js';
4
3
  import type { R2Bucket } from './types.js';
5
4
  type Args = {
6
- access?: ClientUploadsAccess;
5
+ access?: UploadInstructionsAccess;
7
6
  bucket: R2Bucket;
8
7
  collections: R2StorageOptions['collections'];
9
8
  useCompositePrefixes?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"handleMultiPartUpload.d.ts","sourceRoot":"","sources":["../src/handleMultiPartUpload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAS7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAyD,MAAM,YAAY,CAAA;AAEjG,KAAK,IAAI,GAAG;IACV,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,MAAM,EAAE,QAAQ,CAAA;IAChB,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IAC5C,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAGD,eAAO,MAAM,wBAAwB,0DAC6B,IAAI,KAAG,cAmFtE,CAAA"}
1
+ {"version":3,"file":"handleMultiPartUpload.d.ts","sourceRoot":"","sources":["../src/handleMultiPartUpload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAUvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAyD,MAAM,YAAY,CAAA;AAEjG,KAAK,IAAI,GAAG;IACV,MAAM,CAAC,EAAE,wBAAwB,CAAA;IACjC,MAAM,EAAE,QAAQ,CAAA;IAChB,WAAW,EAAE,gBAAgB,CAAC,aAAa,CAAC,CAAA;IAC5C,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAGD,eAAO,MAAM,wBAAwB,0DAC6B,IAAI,KAAG,cAqFtE,CAAA"}
@@ -1,6 +1,6 @@
1
- import { resolveSignedURLKey, verifyClientUploadReceiptForFileKey } from '@payloadcms/plugin-cloud-storage/utilities';
1
+ import { resolveSignedURLKey } from '@payloadcms/plugin-cloud-storage/utilities';
2
2
  import { APIError, Forbidden } from 'payload';
3
- import { assertClientUploadAccess, assertClientUploadAllowed } from 'payload/internal';
3
+ import { assertClientUploadAccess, assertClientUploadAllowed, verifyClientUploadReceipt } from 'payload/internal';
4
4
  // Adapted from https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/
5
5
  export const getHandleMultiPartUpload = ({ access, bucket, collections, useCompositePrefixes = false })=>async (req)=>{
6
6
  const params = Object.fromEntries(req.searchParams);
@@ -30,22 +30,24 @@ export const getHandleMultiPartUpload = ({ access, bucket, collections, useCompo
30
30
  const multipartNumber = parseInt(params.multipartNumber || '');
31
31
  const collectionPrefix = typeof collectionConfig === 'object' && collectionConfig.prefix || '';
32
32
  if (multipartId && multipartKey) {
33
- if (!params.signedReceipt) {
34
- throw new APIError('A verified client upload reference is required.', 400);
35
- }
36
- verifyClientUploadReceiptForFileKey({
37
- collectionPrefix,
33
+ const receipt = verifyClientUploadReceipt({
38
34
  collectionSlug,
39
- expectedFileKey: multipartKey,
40
35
  req,
41
- signedReceipt: params.signedReceipt,
42
- useCompositePrefixes
36
+ signedReceipt: params.signedReceipt
43
37
  });
38
+ // The receipt binds the full storage key (including the per-upload _objectKey segment), so
39
+ // compare against it directly rather than recomputing from prefix + filename.
40
+ if (receipt.storageFilePath !== multipartKey) {
41
+ throw new APIError('Invalid upload reference.', 400);
42
+ }
44
43
  const multipartUpload = bucket.resumeMultipartUpload(multipartKey, multipartId);
45
44
  const request = req;
46
45
  if (isNaN(multipartNumber)) {
47
46
  // Upload complete
48
47
  const object = await multipartUpload.complete(await request.json());
48
+ if (object.key !== multipartKey) {
49
+ throw new APIError('Uploaded source was stored at an unexpected location.', 400);
50
+ }
49
51
  return new Response(object.key, {
50
52
  status: 200
51
53
  });
@@ -55,7 +57,7 @@ export const getHandleMultiPartUpload = ({ access, bucket, collections, useCompo
55
57
  return Response.json(uploadedPart);
56
58
  }
57
59
  } else {
58
- const { clientUploadContext, fileKey, sanitizedFilename } = await resolveSignedURLKey({
60
+ const { sanitizedFilename, storageFilePath, uploadReference } = await resolveSignedURLKey({
59
61
  collectionPrefix,
60
62
  collectionSlug,
61
63
  docPrefix: params.docPrefix ?? undefined,
@@ -63,23 +65,23 @@ export const getHandleMultiPartUpload = ({ access, bucket, collections, useCompo
63
65
  req,
64
66
  useCompositePrefixes
65
67
  });
66
- // best-effort create-only check: R2 has no conditional write on complete()
67
- const existing = await bucket.head(fileKey);
68
+ const existing = await bucket.head(storageFilePath);
68
69
  if (existing) {
69
70
  return new Response('Object already exists', {
70
71
  status: 412
71
72
  });
72
73
  }
73
- const multipartUpload = await bucket.createMultipartUpload(fileKey, {
74
+ // Create multipart upload
75
+ const multipartUpload = await bucket.createMultipartUpload(storageFilePath, {
74
76
  httpMetadata: {
75
77
  contentType: filetype
76
78
  }
77
79
  });
78
80
  return Response.json({
79
- clientUploadContext,
80
81
  filename: sanitizedFilename,
81
82
  key: multipartUpload.key,
82
- uploadId: multipartUpload.uploadId
83
+ uploadId: multipartUpload.uploadId,
84
+ uploadReference
83
85
  });
84
86
  }
85
87
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/handleMultiPartUpload.ts"],"sourcesContent":["import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types'\nimport type { PayloadHandler } from 'payload'\n\nimport {\n resolveSignedURLKey,\n verifyClientUploadReceiptForFileKey,\n} from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError, Forbidden } from 'payload'\nimport { assertClientUploadAccess, assertClientUploadAllowed } from 'payload/internal'\n\nimport type { R2StorageOptions } from './index.js'\nimport type { R2Bucket, R2StorageMultipartUploadHandlerParams, R2UploadedPart } from './types.js'\n\ntype Args = {\n access?: ClientUploadsAccess\n bucket: R2Bucket\n collections: R2StorageOptions['collections']\n useCompositePrefixes?: boolean\n}\n\n// Adapted from https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/\nexport const getHandleMultiPartUpload =\n ({ access, bucket, collections, useCompositePrefixes = false }: Args): PayloadHandler =>\n async (req) => {\n const params = Object.fromEntries(req.searchParams) as R2StorageMultipartUploadHandlerParams\n const collectionSlug = params.collection\n const filetype = params.fileType\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const collectionConfig = collections[collectionSlug]\n if (!collectionConfig) {\n throw new APIError(`Collection ${collectionSlug} was not found in R2 Storage options`)\n }\n\n if (access && !(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename: params.fileName,\n mimeType: filetype,\n })\n\n const multipartId = params.multipartId\n const multipartKey = params.multipartKey\n const multipartNumber = parseInt(params.multipartNumber || '')\n const collectionPrefix = (typeof collectionConfig === 'object' && collectionConfig.prefix) || ''\n\n if (multipartId && multipartKey) {\n if (!params.signedReceipt) {\n throw new APIError('A verified client upload reference is required.', 400)\n }\n verifyClientUploadReceiptForFileKey({\n collectionPrefix,\n collectionSlug,\n expectedFileKey: multipartKey,\n req,\n signedReceipt: params.signedReceipt,\n useCompositePrefixes,\n })\n const multipartUpload = bucket.resumeMultipartUpload(multipartKey, multipartId)\n const request = req as Request\n\n if (isNaN(multipartNumber)) {\n // Upload complete\n const object = await multipartUpload.complete((await request.json()) as R2UploadedPart[])\n return new Response(object.key, { status: 200 })\n } else {\n // Upload part\n const uploadedPart = await multipartUpload.uploadPart(\n multipartNumber,\n await request.arrayBuffer(),\n )\n return Response.json(uploadedPart)\n }\n } else {\n const { clientUploadContext, fileKey, sanitizedFilename } = await resolveSignedURLKey({\n collectionPrefix,\n collectionSlug,\n docPrefix: params.docPrefix ?? undefined,\n filename: params.fileName,\n req,\n useCompositePrefixes,\n })\n // best-effort create-only check: R2 has no conditional write on complete()\n const existing = await bucket.head(fileKey)\n if (existing) {\n return new Response('Object already exists', { status: 412 })\n }\n\n const multipartUpload = await bucket.createMultipartUpload(fileKey, {\n httpMetadata: {\n contentType: filetype,\n },\n })\n\n return Response.json({\n clientUploadContext,\n filename: sanitizedFilename,\n key: multipartUpload.key,\n uploadId: multipartUpload.uploadId,\n })\n }\n }\n"],"names":["resolveSignedURLKey","verifyClientUploadReceiptForFileKey","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","getHandleMultiPartUpload","access","bucket","collections","useCompositePrefixes","req","params","Object","fromEntries","searchParams","collectionSlug","collection","filetype","fileType","collectionConfig","t","payload","config","filename","fileName","mimeType","multipartId","multipartKey","multipartNumber","parseInt","collectionPrefix","prefix","signedReceipt","expectedFileKey","multipartUpload","resumeMultipartUpload","request","isNaN","object","complete","json","Response","key","status","uploadedPart","uploadPart","arrayBuffer","clientUploadContext","fileKey","sanitizedFilename","docPrefix","undefined","existing","head","createMultipartUpload","httpMetadata","contentType","uploadId"],"mappings":"AAGA,SACEA,mBAAmB,EACnBC,mCAAmC,QAC9B,6CAA4C;AACnD,SAASC,QAAQ,EAAEC,SAAS,QAAQ,UAAS;AAC7C,SAASC,wBAAwB,EAAEC,yBAAyB,QAAQ,mBAAkB;AAYtF,yFAAyF;AACzF,OAAO,MAAMC,2BACX,CAAC,EAAEC,MAAM,EAAEC,MAAM,EAAEC,WAAW,EAAEC,uBAAuB,KAAK,EAAQ,GACpE,OAAOC;QACL,MAAMC,SAASC,OAAOC,WAAW,CAACH,IAAII,YAAY;QAClD,MAAMC,iBAAiBJ,OAAOK,UAAU;QACxC,MAAMC,WAAWN,OAAOO,QAAQ;QAEhC,MAAMf,yBAAyB;YAAEY;YAAgBL;QAAI;QAErD,MAAMS,mBAAmBX,WAAW,CAACO,eAAe;QACpD,IAAI,CAACI,kBAAkB;YACrB,MAAM,IAAIlB,SAAS,CAAC,WAAW,EAAEc,eAAe,oCAAoC,CAAC;QACvF;QAEA,IAAIT,UAAU,CAAE,MAAMA,OAAO;YAAES;YAAgBL;QAAI,IAAK;YACtD,MAAM,IAAIR,UAAUQ,IAAIU,CAAC;QAC3B;QAEAhB,0BAA0B;YACxBY,YAAYN,IAAIW,OAAO,CAACb,WAAW,CAACO,eAAe,EAAEO;YACrDC,UAAUZ,OAAOa,QAAQ;YACzBC,UAAUR;QACZ;QAEA,MAAMS,cAAcf,OAAOe,WAAW;QACtC,MAAMC,eAAehB,OAAOgB,YAAY;QACxC,MAAMC,kBAAkBC,SAASlB,OAAOiB,eAAe,IAAI;QAC3D,MAAME,mBAAmB,AAAC,OAAOX,qBAAqB,YAAYA,iBAAiBY,MAAM,IAAK;QAE9F,IAAIL,eAAeC,cAAc;YAC/B,IAAI,CAAChB,OAAOqB,aAAa,EAAE;gBACzB,MAAM,IAAI/B,SAAS,mDAAmD;YACxE;YACAD,oCAAoC;gBAClC8B;gBACAf;gBACAkB,iBAAiBN;gBACjBjB;gBACAsB,eAAerB,OAAOqB,aAAa;gBACnCvB;YACF;YACA,MAAMyB,kBAAkB3B,OAAO4B,qBAAqB,CAACR,cAAcD;YACnE,MAAMU,UAAU1B;YAEhB,IAAI2B,MAAMT,kBAAkB;gBAC1B,kBAAkB;gBAClB,MAAMU,SAAS,MAAMJ,gBAAgBK,QAAQ,CAAE,MAAMH,QAAQI,IAAI;gBACjE,OAAO,IAAIC,SAASH,OAAOI,GAAG,EAAE;oBAAEC,QAAQ;gBAAI;YAChD,OAAO;gBACL,cAAc;gBACd,MAAMC,eAAe,MAAMV,gBAAgBW,UAAU,CACnDjB,iBACA,MAAMQ,QAAQU,WAAW;gBAE3B,OAAOL,SAASD,IAAI,CAACI;YACvB;QACF,OAAO;YACL,MAAM,EAAEG,mBAAmB,EAAEC,OAAO,EAAEC,iBAAiB,EAAE,GAAG,MAAMlD,oBAAoB;gBACpF+B;gBACAf;gBACAmC,WAAWvC,OAAOuC,SAAS,IAAIC;gBAC/B5B,UAAUZ,OAAOa,QAAQ;gBACzBd;gBACAD;YACF;YACA,2EAA2E;YAC3E,MAAM2C,WAAW,MAAM7C,OAAO8C,IAAI,CAACL;YACnC,IAAII,UAAU;gBACZ,OAAO,IAAIX,SAAS,yBAAyB;oBAAEE,QAAQ;gBAAI;YAC7D;YAEA,MAAMT,kBAAkB,MAAM3B,OAAO+C,qBAAqB,CAACN,SAAS;gBAClEO,cAAc;oBACZC,aAAavC;gBACf;YACF;YAEA,OAAOwB,SAASD,IAAI,CAAC;gBACnBO;gBACAxB,UAAU0B;gBACVP,KAAKR,gBAAgBQ,GAAG;gBACxBe,UAAUvB,gBAAgBuB,QAAQ;YACpC;QACF;IACF,EAAC"}
1
+ {"version":3,"sources":["../src/handleMultiPartUpload.ts"],"sourcesContent":["import type { PayloadHandler, UploadInstructionsAccess } from 'payload'\n\nimport { resolveSignedURLKey } from '@payloadcms/plugin-cloud-storage/utilities'\nimport { APIError, Forbidden } from 'payload'\nimport {\n assertClientUploadAccess,\n assertClientUploadAllowed,\n verifyClientUploadReceipt,\n} from 'payload/internal'\n\nimport type { R2StorageOptions } from './index.js'\nimport type { R2Bucket, R2StorageMultipartUploadHandlerParams, R2UploadedPart } from './types.js'\n\ntype Args = {\n access?: UploadInstructionsAccess\n bucket: R2Bucket\n collections: R2StorageOptions['collections']\n useCompositePrefixes?: boolean\n}\n\n// Adapted from https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/\nexport const getHandleMultiPartUpload =\n ({ access, bucket, collections, useCompositePrefixes = false }: Args): PayloadHandler =>\n async (req) => {\n const params = Object.fromEntries(req.searchParams) as R2StorageMultipartUploadHandlerParams\n const collectionSlug = params.collection\n const filetype = params.fileType\n\n await assertClientUploadAccess({ collectionSlug, req })\n\n const collectionConfig = collections[collectionSlug]\n if (!collectionConfig) {\n throw new APIError(`Collection ${collectionSlug} was not found in R2 Storage options`)\n }\n\n if (access && !(await access({ collectionSlug, req }))) {\n throw new Forbidden(req.t)\n }\n\n assertClientUploadAllowed({\n collection: req.payload.collections[collectionSlug]?.config,\n filename: params.fileName,\n mimeType: filetype,\n })\n\n const multipartId = params.multipartId\n const multipartKey = params.multipartKey\n const multipartNumber = parseInt(params.multipartNumber || '')\n const collectionPrefix = (typeof collectionConfig === 'object' && collectionConfig.prefix) || ''\n\n if (multipartId && multipartKey) {\n const receipt = verifyClientUploadReceipt({\n collectionSlug,\n req,\n signedReceipt: params.signedReceipt,\n })\n // The receipt binds the full storage key (including the per-upload _objectKey segment), so\n // compare against it directly rather than recomputing from prefix + filename.\n if (receipt.storageFilePath !== multipartKey) {\n throw new APIError('Invalid upload reference.', 400)\n }\n const multipartUpload = bucket.resumeMultipartUpload(multipartKey, multipartId)\n const request = req as Request\n\n if (isNaN(multipartNumber)) {\n // Upload complete\n const object = await multipartUpload.complete((await request.json()) as R2UploadedPart[])\n if (object.key !== multipartKey) {\n throw new APIError('Uploaded source was stored at an unexpected location.', 400)\n }\n return new Response(object.key, { status: 200 })\n } else {\n // Upload part\n const uploadedPart = await multipartUpload.uploadPart(\n multipartNumber,\n await request.arrayBuffer(),\n )\n return Response.json(uploadedPart)\n }\n } else {\n const { sanitizedFilename, storageFilePath, uploadReference } = await resolveSignedURLKey({\n collectionPrefix,\n collectionSlug,\n docPrefix: params.docPrefix ?? undefined,\n filename: params.fileName,\n req,\n useCompositePrefixes,\n })\n const existing = await bucket.head(storageFilePath)\n if (existing) {\n return new Response('Object already exists', { status: 412 })\n }\n\n // Create multipart upload\n const multipartUpload = await bucket.createMultipartUpload(storageFilePath, {\n httpMetadata: {\n contentType: filetype,\n },\n })\n\n return Response.json({\n filename: sanitizedFilename,\n key: multipartUpload.key,\n uploadId: multipartUpload.uploadId,\n uploadReference,\n })\n }\n }\n"],"names":["resolveSignedURLKey","APIError","Forbidden","assertClientUploadAccess","assertClientUploadAllowed","verifyClientUploadReceipt","getHandleMultiPartUpload","access","bucket","collections","useCompositePrefixes","req","params","Object","fromEntries","searchParams","collectionSlug","collection","filetype","fileType","collectionConfig","t","payload","config","filename","fileName","mimeType","multipartId","multipartKey","multipartNumber","parseInt","collectionPrefix","prefix","receipt","signedReceipt","storageFilePath","multipartUpload","resumeMultipartUpload","request","isNaN","object","complete","json","key","Response","status","uploadedPart","uploadPart","arrayBuffer","sanitizedFilename","uploadReference","docPrefix","undefined","existing","head","createMultipartUpload","httpMetadata","contentType","uploadId"],"mappings":"AAEA,SAASA,mBAAmB,QAAQ,6CAA4C;AAChF,SAASC,QAAQ,EAAEC,SAAS,QAAQ,UAAS;AAC7C,SACEC,wBAAwB,EACxBC,yBAAyB,EACzBC,yBAAyB,QACpB,mBAAkB;AAYzB,yFAAyF;AACzF,OAAO,MAAMC,2BACX,CAAC,EAAEC,MAAM,EAAEC,MAAM,EAAEC,WAAW,EAAEC,uBAAuB,KAAK,EAAQ,GACpE,OAAOC;QACL,MAAMC,SAASC,OAAOC,WAAW,CAACH,IAAII,YAAY;QAClD,MAAMC,iBAAiBJ,OAAOK,UAAU;QACxC,MAAMC,WAAWN,OAAOO,QAAQ;QAEhC,MAAMhB,yBAAyB;YAAEa;YAAgBL;QAAI;QAErD,MAAMS,mBAAmBX,WAAW,CAACO,eAAe;QACpD,IAAI,CAACI,kBAAkB;YACrB,MAAM,IAAInB,SAAS,CAAC,WAAW,EAAEe,eAAe,oCAAoC,CAAC;QACvF;QAEA,IAAIT,UAAU,CAAE,MAAMA,OAAO;YAAES;YAAgBL;QAAI,IAAK;YACtD,MAAM,IAAIT,UAAUS,IAAIU,CAAC;QAC3B;QAEAjB,0BAA0B;YACxBa,YAAYN,IAAIW,OAAO,CAACb,WAAW,CAACO,eAAe,EAAEO;YACrDC,UAAUZ,OAAOa,QAAQ;YACzBC,UAAUR;QACZ;QAEA,MAAMS,cAAcf,OAAOe,WAAW;QACtC,MAAMC,eAAehB,OAAOgB,YAAY;QACxC,MAAMC,kBAAkBC,SAASlB,OAAOiB,eAAe,IAAI;QAC3D,MAAME,mBAAmB,AAAC,OAAOX,qBAAqB,YAAYA,iBAAiBY,MAAM,IAAK;QAE9F,IAAIL,eAAeC,cAAc;YAC/B,MAAMK,UAAU5B,0BAA0B;gBACxCW;gBACAL;gBACAuB,eAAetB,OAAOsB,aAAa;YACrC;YACA,2FAA2F;YAC3F,8EAA8E;YAC9E,IAAID,QAAQE,eAAe,KAAKP,cAAc;gBAC5C,MAAM,IAAI3B,SAAS,6BAA6B;YAClD;YACA,MAAMmC,kBAAkB5B,OAAO6B,qBAAqB,CAACT,cAAcD;YACnE,MAAMW,UAAU3B;YAEhB,IAAI4B,MAAMV,kBAAkB;gBAC1B,kBAAkB;gBAClB,MAAMW,SAAS,MAAMJ,gBAAgBK,QAAQ,CAAE,MAAMH,QAAQI,IAAI;gBACjE,IAAIF,OAAOG,GAAG,KAAKf,cAAc;oBAC/B,MAAM,IAAI3B,SAAS,yDAAyD;gBAC9E;gBACA,OAAO,IAAI2C,SAASJ,OAAOG,GAAG,EAAE;oBAAEE,QAAQ;gBAAI;YAChD,OAAO;gBACL,cAAc;gBACd,MAAMC,eAAe,MAAMV,gBAAgBW,UAAU,CACnDlB,iBACA,MAAMS,QAAQU,WAAW;gBAE3B,OAAOJ,SAASF,IAAI,CAACI;YACvB;QACF,OAAO;YACL,MAAM,EAAEG,iBAAiB,EAAEd,eAAe,EAAEe,eAAe,EAAE,GAAG,MAAMlD,oBAAoB;gBACxF+B;gBACAf;gBACAmC,WAAWvC,OAAOuC,SAAS,IAAIC;gBAC/B5B,UAAUZ,OAAOa,QAAQ;gBACzBd;gBACAD;YACF;YACA,MAAM2C,WAAW,MAAM7C,OAAO8C,IAAI,CAACnB;YACnC,IAAIkB,UAAU;gBACZ,OAAO,IAAIT,SAAS,yBAAyB;oBAAEC,QAAQ;gBAAI;YAC7D;YAEA,0BAA0B;YAC1B,MAAMT,kBAAkB,MAAM5B,OAAO+C,qBAAqB,CAACpB,iBAAiB;gBAC1EqB,cAAc;oBACZC,aAAavC;gBACf;YACF;YAEA,OAAO0B,SAASF,IAAI,CAAC;gBACnBlB,UAAUyB;gBACVN,KAAKP,gBAAgBO,GAAG;gBACxBe,UAAUtB,gBAAgBsB,QAAQ;gBAClCR;YACF;QACF;IACF,EAAC"}