@payloadcms/storage-r2 3.91.0-internal.6e3dc85 → 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.
- package/LICENSE.md +1 -1
- package/dist/adapter.d.ts +4 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +68 -9
- package/dist/adapter.js.map +1 -1
- package/dist/client/R2ClientUploadHandler.d.ts +4 -5
- package/dist/client/R2ClientUploadHandler.d.ts.map +1 -1
- package/dist/client/R2ClientUploadHandler.js +10 -17
- package/dist/client/R2ClientUploadHandler.js.map +1 -1
- package/dist/getFile.d.ts +4 -3
- package/dist/getFile.d.ts.map +1 -1
- package/dist/getFile.js +23 -20
- package/dist/getFile.js.map +1 -1
- package/dist/handleMultiPartUpload.d.ts +2 -3
- package/dist/handleMultiPartUpload.d.ts.map +1 -1
- package/dist/handleMultiPartUpload.js +18 -16
- package/dist/handleMultiPartUpload.js.map +1 -1
- package/dist/index.d.ts +11 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -54
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -5
- package/dist/client/R2ClientUploadHandler.spec.d.ts +0 -2
- package/dist/client/R2ClientUploadHandler.spec.d.ts.map +0 -1
- package/dist/client/R2ClientUploadHandler.spec.js +0 -76
- package/dist/client/R2ClientUploadHandler.spec.js.map +0 -1
- package/dist/handleMultiPartUpload.spec.d.ts +0 -2
- package/dist/handleMultiPartUpload.spec.d.ts.map +0 -1
- package/dist/handleMultiPartUpload.spec.js +0 -154
- package/dist/handleMultiPartUpload.spec.js.map +0 -1
- package/dist/staticHandler.spec.d.ts +0 -2
- package/dist/staticHandler.spec.d.ts.map +0 -1
- package/dist/staticHandler.spec.js +0 -60
- 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-
|
|
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
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,mBAAmB,
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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: {
|
|
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
|
});
|
package/dist/adapter.js.map
CHANGED
|
@@ -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
|
|
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,
|
|
2
|
+
export declare const R2ClientUploadHandler: ({ children, collectionSlug, endpointPath, prefix, props, }: {
|
|
3
3
|
children: import("react").ReactNode;
|
|
4
|
-
collectionSlug: import("
|
|
5
|
-
|
|
6
|
-
extra: R2StorageClientUploadHandlerParams;
|
|
4
|
+
collectionSlug: import("payload").UploadCollectionSlug;
|
|
5
|
+
endpointPath?: `/${string}`;
|
|
7
6
|
prefix?: string;
|
|
8
|
-
|
|
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":"
|
|
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 {
|
|
2
|
+
import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client';
|
|
3
3
|
import { formatAdminURL } from 'payload/shared';
|
|
4
4
|
export const R2ClientUploadHandler = createClientUploadHandler({
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
+
docPrefix,
|
|
15
10
|
fileName: file.name,
|
|
16
11
|
fileType: file.type
|
|
17
12
|
};
|
|
18
13
|
const baseURL = formatAdminURL({
|
|
19
14
|
apiRoute,
|
|
20
|
-
path:
|
|
15
|
+
path: endpointPath,
|
|
21
16
|
serverURL
|
|
22
17
|
});
|
|
23
18
|
const getEndpoint = ()=>`${baseURL}?${String(new URLSearchParams(params))}`;
|
|
24
|
-
//
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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,
|
|
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
|
package/dist/getFile.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
2
|
-
import { getRangeRequestInfo, isXmlMimeType,
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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',
|
|
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
|
package/dist/getFile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/getFile.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest, TypeWithID } from 'payload'\n\nimport {
|
|
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 {
|
|
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?:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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":"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"}
|