@payloadcms/plugin-cloud-storage 4.0.0-internal.688c4d0 → 4.0.0-internal.77191e5

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 (85) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +2 -0
  3. package/dist/client/createClientUploadHandler.d.ts +8 -7
  4. package/dist/client/createClientUploadHandler.d.ts.map +1 -1
  5. package/dist/client/createClientUploadHandler.js +26 -22
  6. package/dist/client/createClientUploadHandler.js.map +1 -1
  7. package/dist/exports/client.d.ts +2 -0
  8. package/dist/exports/client.d.ts.map +1 -1
  9. package/dist/exports/client.js +2 -0
  10. package/dist/exports/client.js.map +1 -1
  11. package/dist/exports/utilities.d.ts +1 -2
  12. package/dist/exports/utilities.d.ts.map +1 -1
  13. package/dist/exports/utilities.js +1 -2
  14. package/dist/exports/utilities.js.map +1 -1
  15. package/dist/fields/getFields.d.ts +1 -5
  16. package/dist/fields/getFields.d.ts.map +1 -1
  17. package/dist/fields/getFields.js +41 -18
  18. package/dist/fields/getFields.js.map +1 -1
  19. package/dist/hooks/afterChange.d.ts +3 -1
  20. package/dist/hooks/afterChange.d.ts.map +1 -1
  21. package/dist/hooks/afterChange.js +79 -22
  22. package/dist/hooks/afterChange.js.map +1 -1
  23. package/dist/hooks/afterDelete.d.ts +3 -1
  24. package/dist/hooks/afterDelete.d.ts.map +1 -1
  25. package/dist/hooks/afterDelete.js +16 -2
  26. package/dist/hooks/afterDelete.js.map +1 -1
  27. package/dist/hooks/afterRead.d.ts.map +1 -1
  28. package/dist/hooks/afterRead.js +15 -2
  29. package/dist/hooks/afterRead.js.map +1 -1
  30. package/dist/hooks/beforeChange.d.ts.map +1 -1
  31. package/dist/hooks/beforeChange.js +12 -1
  32. package/dist/hooks/beforeChange.js.map +1 -1
  33. package/dist/hooks/normalizeUploadPrefix.d.ts +11 -0
  34. package/dist/hooks/normalizeUploadPrefix.d.ts.map +1 -0
  35. package/dist/hooks/normalizeUploadPrefix.js +42 -0
  36. package/dist/hooks/normalizeUploadPrefix.js.map +1 -0
  37. package/dist/plugin.d.ts.map +1 -1
  38. package/dist/plugin.js +178 -128
  39. package/dist/plugin.js.map +1 -1
  40. package/dist/types.d.ts +35 -21
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/utilities/buildPrefixWithObjectKey.d.ts +8 -0
  44. package/dist/utilities/buildPrefixWithObjectKey.d.ts.map +1 -0
  45. package/dist/utilities/buildPrefixWithObjectKey.js +13 -0
  46. package/dist/utilities/buildPrefixWithObjectKey.js.map +1 -0
  47. package/dist/utilities/buildStoragePathData.d.ts +49 -0
  48. package/dist/utilities/buildStoragePathData.d.ts.map +1 -0
  49. package/dist/utilities/buildStoragePathData.js +67 -0
  50. package/dist/utilities/buildStoragePathData.js.map +1 -0
  51. package/dist/utilities/getFilePrefix.d.ts +19 -11
  52. package/dist/utilities/getFilePrefix.d.ts.map +1 -1
  53. package/dist/utilities/getFilePrefix.js +70 -30
  54. package/dist/utilities/getFilePrefix.js.map +1 -1
  55. package/dist/utilities/getIncomingFiles.js +2 -2
  56. package/dist/utilities/getIncomingFiles.js.map +1 -1
  57. package/dist/utilities/resolveSignedURLKey.d.ts +13 -2
  58. package/dist/utilities/resolveSignedURLKey.d.ts.map +1 -1
  59. package/dist/utilities/resolveSignedURLKey.js +43 -9
  60. package/dist/utilities/resolveSignedURLKey.js.map +1 -1
  61. package/dist/utilities/sanitizePrefix.d.ts +1 -9
  62. package/dist/utilities/sanitizePrefix.d.ts.map +1 -1
  63. package/dist/utilities/sanitizePrefix.js +1 -21
  64. package/dist/utilities/sanitizePrefix.js.map +1 -1
  65. package/package.json +4 -4
  66. package/dist/admin/fields/getFields.d.ts +0 -17
  67. package/dist/admin/fields/getFields.d.ts.map +0 -1
  68. package/dist/admin/fields/getFields.js +0 -98
  69. package/dist/admin/fields/getFields.js.map +0 -1
  70. package/dist/admin/index.d.ts +0 -4
  71. package/dist/admin/index.d.ts.map +0 -1
  72. package/dist/admin/index.js +0 -34
  73. package/dist/admin/index.js.map +0 -1
  74. package/dist/utilities/getFileKey.d.ts +0 -22
  75. package/dist/utilities/getFileKey.d.ts.map +0 -1
  76. package/dist/utilities/getFileKey.js +0 -23
  77. package/dist/utilities/getFileKey.js.map +0 -1
  78. package/dist/utilities/getFileKey.spec.js +0 -174
  79. package/dist/utilities/getFileKey.spec.js.map +0 -1
  80. package/dist/utilities/getFilePrefix.spec.js +0 -105
  81. package/dist/utilities/getFilePrefix.spec.js.map +0 -1
  82. package/dist/utilities/initClientUploads.d.ts +0 -13
  83. package/dist/utilities/initClientUploads.d.ts.map +0 -1
  84. package/dist/utilities/initClientUploads.js +0 -63
  85. package/dist/utilities/initClientUploads.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { PayloadRequest } from 'payload';
2
+ import type { UploadReference } from '../types.js';
2
3
  type Args = {
3
4
  collectionPrefix?: string;
4
5
  collectionSlug: string;
@@ -8,17 +9,27 @@ type Args = {
8
9
  useCompositePrefixes?: boolean;
9
10
  };
10
11
  /**
11
- * Resolves the storage key for a clientUploads signed-URL request, deduping
12
+ * Resolves the storage key for an upload-instructions request, deduping
12
13
  * the filename via {@link getSafeFileName} so a duplicate upload does not
13
14
  * overwrite an existing blob.
14
15
  *
16
+ * A unique per-upload segment is included in the storage key so each issued
17
+ * key is distinct. The stored `filename` stays clean and the semantic `prefix`
18
+ * stays exactly what the collection configured — the per-upload entropy is
19
+ * persisted separately in `_objectKey`.
20
+ *
15
21
  * The resolved `sanitizedFilename` is returned so the browser-side handler
16
22
  * can update the form via `updateFilename`.
23
+ *
24
+ * The semantic prefix is resolved with {@link buildUploadPrefix} and the final
25
+ * key with {@link buildUploadStoragePathData}, so the receipt only ever signs a key
26
+ * beneath the configured collection prefix.
17
27
  */
18
28
  export declare function resolveSignedURLKey({ collectionPrefix, collectionSlug, docPrefix, filename, req, useCompositePrefixes, }: Args): Promise<{
19
- fileKey: string;
20
29
  sanitizedDocPrefix: string;
21
30
  sanitizedFilename: string;
31
+ storageFilePath: string;
32
+ uploadReference: UploadReference;
22
33
  }>;
23
34
  export {};
24
35
  //# sourceMappingURL=resolveSignedURLKey.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolveSignedURLKey.d.ts","sourceRoot":"","sources":["../../src/utilities/resolveSignedURLKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM7C,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,gBAAqB,EACrB,cAAc,EACd,SAAS,EACT,QAAQ,EACR,GAAG,EACH,oBAA4B,GAC7B,EAAE,IAAI;;;;GAgBN"}
1
+ {"version":3,"file":"resolveSignedURLKey.d.ts","sourceRoot":"","sources":["../../src/utilities/resolveSignedURLKey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAM7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD,KAAK,IAAI,GAAG;IACV,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,cAAc,CAAA;IACnB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,gBAAqB,EACrB,cAAc,EACd,SAAS,EACT,QAAQ,EACR,GAAG,EACH,oBAA4B,GAC7B,EAAE,IAAI,GAAG,OAAO,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,eAAe,CAAA;CACjC,CAAC,CAyCD"}
@@ -1,29 +1,63 @@
1
- import { getSafeFileName } from 'payload/internal';
2
- import { getFileKey } from './getFileKey.js';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createClientUploadReceipt, getSafeFileName } from 'payload/internal';
3
+ import { getSanitizedUploadFilename } from 'payload/shared';
4
+ import { buildUploadPrefix, buildUploadStoragePathData } from './buildStoragePathData.js';
3
5
  /**
4
- * Resolves the storage key for a clientUploads signed-URL request, deduping
6
+ * Resolves the storage key for an upload-instructions request, deduping
5
7
  * the filename via {@link getSafeFileName} so a duplicate upload does not
6
8
  * overwrite an existing blob.
7
9
  *
10
+ * A unique per-upload segment is included in the storage key so each issued
11
+ * key is distinct. The stored `filename` stays clean and the semantic `prefix`
12
+ * stays exactly what the collection configured — the per-upload entropy is
13
+ * persisted separately in `_objectKey`.
14
+ *
8
15
  * The resolved `sanitizedFilename` is returned so the browser-side handler
9
16
  * can update the form via `updateFilename`.
17
+ *
18
+ * The semantic prefix is resolved with {@link buildUploadPrefix} and the final
19
+ * key with {@link buildUploadStoragePathData}, so the receipt only ever signs a key
20
+ * beneath the configured collection prefix.
10
21
  */ export async function resolveSignedURLKey({ collectionPrefix = '', collectionSlug, docPrefix, filename, req, useCompositePrefixes = false }) {
22
+ // Sanitize with the same helper generateFileData uses for the DB filename so the storage key
23
+ // and doc.filename stay in sync (#16694).
11
24
  const sanitizedFilename = await getSafeFileName({
12
25
  collectionSlug,
13
- desiredFilename: filename,
14
- prefix: docPrefix,
26
+ desiredFilename: getSanitizedUploadFilename(filename),
15
27
  req
16
28
  });
17
- const { fileKey, sanitizedDocPrefix } = getFileKey({
29
+ const rawBaseDocPrefix = useCompositePrefixes ? docPrefix : docPrefix || collectionPrefix;
30
+ const { sanitizedDocPrefix } = buildUploadPrefix({
31
+ collectionPrefix,
32
+ docPrefix: rawBaseDocPrefix,
33
+ useCompositePrefixes
34
+ });
35
+ const _objectKey = randomUUID();
36
+ // Per-upload segment lives in the key; it is persisted in `_objectKey`, keeping `prefix` semantic.
37
+ const keyedDocPrefix = sanitizedDocPrefix ? `${sanitizedDocPrefix}/${_objectKey}` : _objectKey;
38
+ const { storageFilePath } = buildUploadStoragePathData({
18
39
  collectionPrefix,
19
- docPrefix,
40
+ docPrefix: keyedDocPrefix,
20
41
  filename: sanitizedFilename,
21
42
  useCompositePrefixes
22
43
  });
44
+ const uploadReference = {
45
+ _objectKey,
46
+ prefix: sanitizedDocPrefix,
47
+ signedReceipt: createClientUploadReceipt({
48
+ _objectKey,
49
+ collectionSlug,
50
+ filename: sanitizedFilename,
51
+ filePrefix: sanitizedDocPrefix,
52
+ req,
53
+ storageFilePath
54
+ })
55
+ };
23
56
  return {
24
- fileKey,
25
57
  sanitizedDocPrefix,
26
- sanitizedFilename
58
+ sanitizedFilename,
59
+ storageFilePath,
60
+ uploadReference
27
61
  };
28
62
  }
29
63
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/resolveSignedURLKey.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { getSafeFileName } from 'payload/internal'\n\nimport { getFileKey } from './getFileKey.js'\n\ntype Args = {\n collectionPrefix?: string\n collectionSlug: string\n docPrefix?: string\n filename: string\n req: PayloadRequest\n useCompositePrefixes?: boolean\n}\n\n/**\n * Resolves the storage key for a clientUploads signed-URL request, deduping\n * the filename via {@link getSafeFileName} so a duplicate upload does not\n * overwrite an existing blob.\n *\n * The resolved `sanitizedFilename` is returned so the browser-side handler\n * can update the form via `updateFilename`.\n */\nexport async function resolveSignedURLKey({\n collectionPrefix = '',\n collectionSlug,\n docPrefix,\n filename,\n req,\n useCompositePrefixes = false,\n}: Args) {\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: filename,\n prefix: docPrefix,\n req,\n })\n\n const { fileKey, sanitizedDocPrefix } = getFileKey({\n collectionPrefix,\n docPrefix,\n filename: sanitizedFilename,\n useCompositePrefixes,\n })\n\n return { fileKey, sanitizedDocPrefix, sanitizedFilename }\n}\n"],"names":["getSafeFileName","getFileKey","resolveSignedURLKey","collectionPrefix","collectionSlug","docPrefix","filename","req","useCompositePrefixes","sanitizedFilename","desiredFilename","prefix","fileKey","sanitizedDocPrefix"],"mappings":"AAEA,SAASA,eAAe,QAAQ,mBAAkB;AAElD,SAASC,UAAU,QAAQ,kBAAiB;AAW5C;;;;;;;CAOC,GACD,OAAO,eAAeC,oBAAoB,EACxCC,mBAAmB,EAAE,EACrBC,cAAc,EACdC,SAAS,EACTC,QAAQ,EACRC,GAAG,EACHC,uBAAuB,KAAK,EACvB;IACL,MAAMC,oBAAoB,MAAMT,gBAAgB;QAC9CI;QACAM,iBAAiBJ;QACjBK,QAAQN;QACRE;IACF;IAEA,MAAM,EAAEK,OAAO,EAAEC,kBAAkB,EAAE,GAAGZ,WAAW;QACjDE;QACAE;QACAC,UAAUG;QACVD;IACF;IAEA,OAAO;QAAEI;QAASC;QAAoBJ;IAAkB;AAC1D"}
1
+ {"version":3,"sources":["../../src/utilities/resolveSignedURLKey.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { createClientUploadReceipt, getSafeFileName } from 'payload/internal'\nimport { getSanitizedUploadFilename } from 'payload/shared'\n\nimport type { UploadReference } from '../types.js'\n\nimport { buildUploadPrefix, buildUploadStoragePathData } from './buildStoragePathData.js'\n\ntype Args = {\n collectionPrefix?: string\n collectionSlug: string\n docPrefix?: string\n filename: string\n req: PayloadRequest\n useCompositePrefixes?: boolean\n}\n\n/**\n * Resolves the storage key for an upload-instructions request, deduping\n * the filename via {@link getSafeFileName} so a duplicate upload does not\n * overwrite an existing blob.\n *\n * A unique per-upload segment is included in the storage key so each issued\n * key is distinct. The stored `filename` stays clean and the semantic `prefix`\n * stays exactly what the collection configured — the per-upload entropy is\n * persisted separately in `_objectKey`.\n *\n * The resolved `sanitizedFilename` is returned so the browser-side handler\n * can update the form via `updateFilename`.\n *\n * The semantic prefix is resolved with {@link buildUploadPrefix} and the final\n * key with {@link buildUploadStoragePathData}, so the receipt only ever signs a key\n * beneath the configured collection prefix.\n */\nexport async function resolveSignedURLKey({\n collectionPrefix = '',\n collectionSlug,\n docPrefix,\n filename,\n req,\n useCompositePrefixes = false,\n}: Args): Promise<{\n sanitizedDocPrefix: string\n sanitizedFilename: string\n storageFilePath: string\n uploadReference: UploadReference\n}> {\n // Sanitize with the same helper generateFileData uses for the DB filename so the storage key\n // and doc.filename stay in sync (#16694).\n const sanitizedFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: getSanitizedUploadFilename(filename),\n req,\n })\n\n const rawBaseDocPrefix = useCompositePrefixes ? docPrefix : docPrefix || collectionPrefix\n const { sanitizedDocPrefix } = buildUploadPrefix({\n collectionPrefix,\n docPrefix: rawBaseDocPrefix,\n useCompositePrefixes,\n })\n\n const _objectKey = randomUUID()\n // Per-upload segment lives in the key; it is persisted in `_objectKey`, keeping `prefix` semantic.\n const keyedDocPrefix = sanitizedDocPrefix ? `${sanitizedDocPrefix}/${_objectKey}` : _objectKey\n\n const { storageFilePath } = buildUploadStoragePathData({\n collectionPrefix,\n docPrefix: keyedDocPrefix,\n filename: sanitizedFilename,\n useCompositePrefixes,\n })\n\n const uploadReference = {\n _objectKey,\n prefix: sanitizedDocPrefix,\n signedReceipt: createClientUploadReceipt({\n _objectKey,\n collectionSlug,\n filename: sanitizedFilename,\n filePrefix: sanitizedDocPrefix,\n req,\n storageFilePath,\n }),\n }\n\n return { sanitizedDocPrefix, sanitizedFilename, storageFilePath, uploadReference }\n}\n"],"names":["randomUUID","createClientUploadReceipt","getSafeFileName","getSanitizedUploadFilename","buildUploadPrefix","buildUploadStoragePathData","resolveSignedURLKey","collectionPrefix","collectionSlug","docPrefix","filename","req","useCompositePrefixes","sanitizedFilename","desiredFilename","rawBaseDocPrefix","sanitizedDocPrefix","_objectKey","keyedDocPrefix","storageFilePath","uploadReference","prefix","signedReceipt","filePrefix"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,yBAAyB,EAAEC,eAAe,QAAQ,mBAAkB;AAC7E,SAASC,0BAA0B,QAAQ,iBAAgB;AAI3D,SAASC,iBAAiB,EAAEC,0BAA0B,QAAQ,4BAA2B;AAWzF;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,eAAeC,oBAAoB,EACxCC,mBAAmB,EAAE,EACrBC,cAAc,EACdC,SAAS,EACTC,QAAQ,EACRC,GAAG,EACHC,uBAAuB,KAAK,EACvB;IAML,6FAA6F;IAC7F,0CAA0C;IAC1C,MAAMC,oBAAoB,MAAMX,gBAAgB;QAC9CM;QACAM,iBAAiBX,2BAA2BO;QAC5CC;IACF;IAEA,MAAMI,mBAAmBH,uBAAuBH,YAAYA,aAAaF;IACzE,MAAM,EAAES,kBAAkB,EAAE,GAAGZ,kBAAkB;QAC/CG;QACAE,WAAWM;QACXH;IACF;IAEA,MAAMK,aAAajB;IACnB,mGAAmG;IACnG,MAAMkB,iBAAiBF,qBAAqB,GAAGA,mBAAmB,CAAC,EAAEC,YAAY,GAAGA;IAEpF,MAAM,EAAEE,eAAe,EAAE,GAAGd,2BAA2B;QACrDE;QACAE,WAAWS;QACXR,UAAUG;QACVD;IACF;IAEA,MAAMQ,kBAAkB;QACtBH;QACAI,QAAQL;QACRM,eAAerB,0BAA0B;YACvCgB;YACAT;YACAE,UAAUG;YACVU,YAAYP;YACZL;YACAQ;QACF;IACF;IAEA,OAAO;QAAEH;QAAoBH;QAAmBM;QAAiBC;IAAgB;AACnF"}
@@ -1,10 +1,2 @@
1
- /**
2
- * Normalizes a storage prefix for use in object keys and URLs.
3
- *
4
- * Decodes URI components once (so query-style `%2F` becomes `/`), rejects
5
- * values that still contain percent-encodings after decoding (e.g. `%252f`),
6
- * then normalizes slashes, drops `.` / `..` segments, strips leading slashes,
7
- * and removes control characters.
8
- */
9
- export declare function sanitizePrefix(prefix: string): string;
1
+ export { sanitizeUploadPrefix as sanitizePrefix } from 'payload/shared';
10
2
  //# sourceMappingURL=sanitizePrefix.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitizePrefix.d.ts","sourceRoot":"","sources":["../../src/utilities/sanitizePrefix.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwBrD"}
1
+ {"version":3,"file":"sanitizePrefix.d.ts","sourceRoot":"","sources":["../../src/utilities/sanitizePrefix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
@@ -1,23 +1,3 @@
1
- /**
2
- * Normalizes a storage prefix for use in object keys and URLs.
3
- *
4
- * Decodes URI components once (so query-style `%2F` becomes `/`), rejects
5
- * values that still contain percent-encodings after decoding (e.g. `%252f`),
6
- * then normalizes slashes, drops `.` / `..` segments, strips leading slashes,
7
- * and removes control characters.
8
- */ export function sanitizePrefix(prefix) {
9
- let decodedPrefix;
10
- try {
11
- decodedPrefix = decodeURIComponent(prefix);
12
- } catch {
13
- return '';
14
- }
15
- // Reject multi-encoded values (e.g. `%252f`) by allowing only a single decode pass.
16
- if (/%[0-9a-f]{2}/i.test(decodedPrefix)) {
17
- return '';
18
- }
19
- return decodedPrefix.replace(/\\/g, '/').split('/').filter((segment)=>segment !== '..' && segment !== '.').join('/').replace(/^\/+/, '')// eslint-disable-next-line no-control-regex
20
- .replace(/[\x00-\x1f\x80-\x9f]/g, '');
21
- }
1
+ export { sanitizeUploadPrefix as sanitizePrefix } from 'payload/shared';
22
2
 
23
3
  //# sourceMappingURL=sanitizePrefix.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utilities/sanitizePrefix.ts"],"sourcesContent":["/**\n * Normalizes a storage prefix for use in object keys and URLs.\n *\n * Decodes URI components once (so query-style `%2F` becomes `/`), rejects\n * values that still contain percent-encodings after decoding (e.g. `%252f`),\n * then normalizes slashes, drops `.` / `..` segments, strips leading slashes,\n * and removes control characters.\n */\nexport function sanitizePrefix(prefix: string): string {\n let decodedPrefix: string\n\n try {\n decodedPrefix = decodeURIComponent(prefix)\n } catch {\n return ''\n }\n\n // Reject multi-encoded values (e.g. `%252f`) by allowing only a single decode pass.\n if (/%[0-9a-f]{2}/i.test(decodedPrefix)) {\n return ''\n }\n\n return (\n decodedPrefix\n .replace(/\\\\/g, '/')\n .split('/')\n .filter((segment) => segment !== '..' && segment !== '.')\n .join('/')\n .replace(/^\\/+/, '')\n // eslint-disable-next-line no-control-regex\n .replace(/[\\x00-\\x1f\\x80-\\x9f]/g, '')\n )\n}\n"],"names":["sanitizePrefix","prefix","decodedPrefix","decodeURIComponent","test","replace","split","filter","segment","join"],"mappings":"AAAA;;;;;;;CAOC,GACD,OAAO,SAASA,eAAeC,MAAc;IAC3C,IAAIC;IAEJ,IAAI;QACFA,gBAAgBC,mBAAmBF;IACrC,EAAE,OAAM;QACN,OAAO;IACT;IAEA,oFAAoF;IACpF,IAAI,gBAAgBG,IAAI,CAACF,gBAAgB;QACvC,OAAO;IACT;IAEA,OACEA,cACGG,OAAO,CAAC,OAAO,KACfC,KAAK,CAAC,KACNC,MAAM,CAAC,CAACC,UAAYA,YAAY,QAAQA,YAAY,KACpDC,IAAI,CAAC,KACLJ,OAAO,CAAC,QAAQ,GACjB,4CAA4C;KAC3CA,OAAO,CAAC,yBAAyB;AAExC"}
1
+ {"version":3,"sources":["../../src/utilities/sanitizePrefix.ts"],"sourcesContent":["export { sanitizeUploadPrefix as sanitizePrefix } from 'payload/shared'\n"],"names":["sanitizeUploadPrefix","sanitizePrefix"],"mappings":"AAAA,SAASA,wBAAwBC,cAAc,QAAQ,iBAAgB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/plugin-cloud-storage",
3
- "version": "4.0.0-internal.688c4d0",
3
+ "version": "4.0.0-internal.77191e5",
4
4
  "description": "The official cloud storage plugin for Payload CMS",
5
5
  "homepage": "https://payloadcms.com",
6
6
  "repository": {
@@ -51,18 +51,18 @@
51
51
  "dependencies": {
52
52
  "find-node-modules": "^2.1.3",
53
53
  "range-parser": "^1.2.1",
54
- "@payloadcms/ui": "4.0.0-internal.688c4d0"
54
+ "@payloadcms/ui": "4.0.0-internal.77191e5"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/find-node-modules": "^2.1.2",
58
58
  "@types/react": "19.2.14",
59
59
  "@types/react-dom": "19.2.3",
60
- "payload": "4.0.0-internal.688c4d0"
60
+ "payload": "4.0.0-internal.77191e5"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": "^19.0.1 || ^19.1.2 || ^19.2.1",
64
64
  "react-dom": "^19.0.1 || ^19.1.2 || ^19.2.1",
65
- "payload": "4.0.0-internal.688c4d0"
65
+ "payload": "4.0.0-internal.77191e5"
66
66
  },
67
67
  "publishConfig": {
68
68
  "registry": "https://registry.npmjs.org/"
@@ -1,17 +0,0 @@
1
- import type { CollectionConfig, Field } from 'payload';
2
- interface Args {
3
- /**
4
- * When true, always insert the prefix field regardless of whether a prefix is configured.
5
- */
6
- alwaysInsertFields?: boolean;
7
- collection: CollectionConfig;
8
- prefix?: string;
9
- /**
10
- * When true, do not default the `prefix` field to the collection prefix; the
11
- * document field holds only the document-level segment.
12
- */
13
- useCompositePrefixes?: boolean;
14
- }
15
- export declare const getFields: ({ alwaysInsertFields, collection, prefix, useCompositePrefixes, }: Args) => Field[];
16
- export {};
17
- //# sourceMappingURL=getFields.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"getFields.d.ts","sourceRoot":"","sources":["../../../src/admin/fields/getFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAyB,MAAM,SAAS,CAAA;AAI7E,UAAU,IAAI;IACZ;;OAEG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,UAAU,EAAE,gBAAgB,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B;AAED,eAAO,MAAM,SAAS,GAAI,mEAKvB,IAAI,KAAG,KAAK,EAsHd,CAAA"}
@@ -1,98 +0,0 @@
1
- import path from 'path';
2
- export const getFields = ({ alwaysInsertFields, collection, prefix, useCompositePrefixes = false })=>{
3
- const baseURLField = {
4
- name: 'url',
5
- type: 'text',
6
- admin: {
7
- hidden: true,
8
- readOnly: true
9
- },
10
- label: 'URL'
11
- };
12
- const basePrefixField = {
13
- name: 'prefix',
14
- type: 'text',
15
- admin: {
16
- hidden: true,
17
- readOnly: true
18
- }
19
- };
20
- const fields = [
21
- ...collection.fields
22
- ];
23
- // Inject a hook into all URL fields to generate URLs
24
- let existingURLFieldIndex = -1;
25
- const existingURLField = fields.find((existingField, i)=>{
26
- if ('name' in existingField && existingField.name === 'url') {
27
- existingURLFieldIndex = i;
28
- return true;
29
- }
30
- return false;
31
- });
32
- if (existingURLFieldIndex > -1) {
33
- fields.splice(existingURLFieldIndex, 1);
34
- }
35
- fields.push({
36
- ...baseURLField,
37
- ...existingURLField || {}
38
- });
39
- if (typeof collection.upload === 'object' && collection.upload.imageSizes) {
40
- let existingSizesFieldIndex = -1;
41
- const existingSizesField = fields.find((existingField, i)=>{
42
- if ('name' in existingField && existingField.name === 'sizes') {
43
- existingSizesFieldIndex = i;
44
- return true;
45
- }
46
- return false;
47
- });
48
- if (existingSizesFieldIndex > -1) {
49
- fields.splice(existingSizesFieldIndex, 1);
50
- }
51
- const sizesField = {
52
- ...existingSizesField || {},
53
- name: 'sizes',
54
- type: 'group',
55
- admin: {
56
- hidden: true
57
- },
58
- fields: collection.upload.imageSizes.map((size)=>{
59
- const existingSizeField = existingSizesField?.fields.find((existingField)=>'name' in existingField && existingField.name === size.name);
60
- const existingSizeURLField = existingSizeField?.fields.find((existingField)=>'name' in existingField && existingField.name === 'url');
61
- return {
62
- ...existingSizeField,
63
- name: size.name,
64
- type: 'group',
65
- fields: [
66
- {
67
- ...existingSizeURLField || {},
68
- ...baseURLField
69
- }
70
- ]
71
- };
72
- })
73
- };
74
- fields.push(sizesField);
75
- }
76
- // If prefix is enabled or alwaysInsertFields is true, save it to db
77
- if (typeof prefix !== 'undefined' || alwaysInsertFields) {
78
- let existingPrefixFieldIndex = -1;
79
- const existingPrefixField = fields.find((existingField, i)=>{
80
- if ('name' in existingField && existingField.name === 'prefix') {
81
- existingPrefixFieldIndex = i;
82
- return true;
83
- }
84
- return false;
85
- });
86
- if (existingPrefixFieldIndex > -1) {
87
- fields.splice(existingPrefixFieldIndex, 1);
88
- }
89
- fields.push({
90
- ...basePrefixField,
91
- ...existingPrefixField || {},
92
- defaultValue: existingPrefixField?.defaultValue ?? (useCompositePrefixes ? '' : prefix ? path.posix.join(prefix) : '')
93
- });
94
- }
95
- return fields;
96
- };
97
-
98
- //# sourceMappingURL=getFields.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/admin/fields/getFields.ts"],"sourcesContent":["import type { CollectionConfig, Field, GroupField, TextField } from 'payload'\n\nimport path from 'path'\n\ninterface Args {\n /**\n * When true, always insert the prefix field regardless of whether a prefix is configured.\n */\n alwaysInsertFields?: boolean\n collection: CollectionConfig\n prefix?: string\n /**\n * When true, do not default the `prefix` field to the collection prefix; the\n * document field holds only the document-level segment.\n */\n useCompositePrefixes?: boolean\n}\n\nexport const getFields = ({\n alwaysInsertFields,\n collection,\n prefix,\n useCompositePrefixes = false,\n}: Args): Field[] => {\n const baseURLField: TextField = {\n name: 'url',\n type: 'text',\n admin: {\n hidden: true,\n readOnly: true,\n },\n label: 'URL',\n }\n\n const basePrefixField: TextField = {\n name: 'prefix',\n type: 'text',\n admin: {\n hidden: true,\n readOnly: true,\n },\n }\n\n const fields = [...collection.fields]\n\n // Inject a hook into all URL fields to generate URLs\n\n let existingURLFieldIndex = -1\n\n const existingURLField = fields.find((existingField, i) => {\n if ('name' in existingField && existingField.name === 'url') {\n existingURLFieldIndex = i\n return true\n }\n return false\n }) as TextField\n\n if (existingURLFieldIndex > -1) {\n fields.splice(existingURLFieldIndex, 1)\n }\n\n fields.push({\n ...baseURLField,\n ...(existingURLField || {}),\n } as TextField)\n\n if (typeof collection.upload === 'object' && collection.upload.imageSizes) {\n let existingSizesFieldIndex = -1\n\n const existingSizesField = fields.find((existingField, i) => {\n if ('name' in existingField && existingField.name === 'sizes') {\n existingSizesFieldIndex = i\n return true\n }\n\n return false\n }) as GroupField\n\n if (existingSizesFieldIndex > -1) {\n fields.splice(existingSizesFieldIndex, 1)\n }\n\n const sizesField: Field = {\n ...(existingSizesField || {}),\n name: 'sizes',\n type: 'group',\n admin: {\n hidden: true,\n },\n fields: collection.upload.imageSizes.map((size) => {\n const existingSizeField = existingSizesField?.fields.find(\n (existingField) => 'name' in existingField && existingField.name === size.name,\n ) as GroupField\n\n const existingSizeURLField = existingSizeField?.fields.find(\n (existingField) => 'name' in existingField && existingField.name === 'url',\n ) as GroupField\n\n return {\n ...existingSizeField,\n name: size.name,\n type: 'group',\n fields: [\n {\n ...(existingSizeURLField || {}),\n ...baseURLField,\n },\n ],\n } as Field\n }),\n }\n\n fields.push(sizesField)\n }\n\n // If prefix is enabled or alwaysInsertFields is true, save it to db\n if (typeof prefix !== 'undefined' || alwaysInsertFields) {\n let existingPrefixFieldIndex = -1\n\n const existingPrefixField = fields.find((existingField, i) => {\n if ('name' in existingField && existingField.name === 'prefix') {\n existingPrefixFieldIndex = i\n return true\n }\n return false\n }) as TextField\n\n if (existingPrefixFieldIndex > -1) {\n fields.splice(existingPrefixFieldIndex, 1)\n }\n\n fields.push({\n ...basePrefixField,\n ...(existingPrefixField || {}),\n defaultValue:\n existingPrefixField?.defaultValue ??\n (useCompositePrefixes ? '' : prefix ? path.posix.join(prefix) : ''),\n } as TextField)\n }\n\n return fields\n}\n"],"names":["path","getFields","alwaysInsertFields","collection","prefix","useCompositePrefixes","baseURLField","name","type","admin","hidden","readOnly","label","basePrefixField","fields","existingURLFieldIndex","existingURLField","find","existingField","i","splice","push","upload","imageSizes","existingSizesFieldIndex","existingSizesField","sizesField","map","size","existingSizeField","existingSizeURLField","existingPrefixFieldIndex","existingPrefixField","defaultValue","posix","join"],"mappings":"AAEA,OAAOA,UAAU,OAAM;AAgBvB,OAAO,MAAMC,YAAY,CAAC,EACxBC,kBAAkB,EAClBC,UAAU,EACVC,MAAM,EACNC,uBAAuB,KAAK,EACvB;IACL,MAAMC,eAA0B;QAC9BC,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,QAAQ;YACRC,UAAU;QACZ;QACAC,OAAO;IACT;IAEA,MAAMC,kBAA6B;QACjCN,MAAM;QACNC,MAAM;QACNC,OAAO;YACLC,QAAQ;YACRC,UAAU;QACZ;IACF;IAEA,MAAMG,SAAS;WAAIX,WAAWW,MAAM;KAAC;IAErC,qDAAqD;IAErD,IAAIC,wBAAwB,CAAC;IAE7B,MAAMC,mBAAmBF,OAAOG,IAAI,CAAC,CAACC,eAAeC;QACnD,IAAI,UAAUD,iBAAiBA,cAAcX,IAAI,KAAK,OAAO;YAC3DQ,wBAAwBI;YACxB,OAAO;QACT;QACA,OAAO;IACT;IAEA,IAAIJ,wBAAwB,CAAC,GAAG;QAC9BD,OAAOM,MAAM,CAACL,uBAAuB;IACvC;IAEAD,OAAOO,IAAI,CAAC;QACV,GAAGf,YAAY;QACf,GAAIU,oBAAoB,CAAC,CAAC;IAC5B;IAEA,IAAI,OAAOb,WAAWmB,MAAM,KAAK,YAAYnB,WAAWmB,MAAM,CAACC,UAAU,EAAE;QACzE,IAAIC,0BAA0B,CAAC;QAE/B,MAAMC,qBAAqBX,OAAOG,IAAI,CAAC,CAACC,eAAeC;YACrD,IAAI,UAAUD,iBAAiBA,cAAcX,IAAI,KAAK,SAAS;gBAC7DiB,0BAA0BL;gBAC1B,OAAO;YACT;YAEA,OAAO;QACT;QAEA,IAAIK,0BAA0B,CAAC,GAAG;YAChCV,OAAOM,MAAM,CAACI,yBAAyB;QACzC;QAEA,MAAME,aAAoB;YACxB,GAAID,sBAAsB,CAAC,CAAC;YAC5BlB,MAAM;YACNC,MAAM;YACNC,OAAO;gBACLC,QAAQ;YACV;YACAI,QAAQX,WAAWmB,MAAM,CAACC,UAAU,CAACI,GAAG,CAAC,CAACC;gBACxC,MAAMC,oBAAoBJ,oBAAoBX,OAAOG,KACnD,CAACC,gBAAkB,UAAUA,iBAAiBA,cAAcX,IAAI,KAAKqB,KAAKrB,IAAI;gBAGhF,MAAMuB,uBAAuBD,mBAAmBf,OAAOG,KACrD,CAACC,gBAAkB,UAAUA,iBAAiBA,cAAcX,IAAI,KAAK;gBAGvE,OAAO;oBACL,GAAGsB,iBAAiB;oBACpBtB,MAAMqB,KAAKrB,IAAI;oBACfC,MAAM;oBACNM,QAAQ;wBACN;4BACE,GAAIgB,wBAAwB,CAAC,CAAC;4BAC9B,GAAGxB,YAAY;wBACjB;qBACD;gBACH;YACF;QACF;QAEAQ,OAAOO,IAAI,CAACK;IACd;IAEA,oEAAoE;IACpE,IAAI,OAAOtB,WAAW,eAAeF,oBAAoB;QACvD,IAAI6B,2BAA2B,CAAC;QAEhC,MAAMC,sBAAsBlB,OAAOG,IAAI,CAAC,CAACC,eAAeC;YACtD,IAAI,UAAUD,iBAAiBA,cAAcX,IAAI,KAAK,UAAU;gBAC9DwB,2BAA2BZ;gBAC3B,OAAO;YACT;YACA,OAAO;QACT;QAEA,IAAIY,2BAA2B,CAAC,GAAG;YACjCjB,OAAOM,MAAM,CAACW,0BAA0B;QAC1C;QAEAjB,OAAOO,IAAI,CAAC;YACV,GAAGR,eAAe;YAClB,GAAImB,uBAAuB,CAAC,CAAC;YAC7BC,cACED,qBAAqBC,gBACpB5B,CAAAA,uBAAuB,KAAKD,SAASJ,KAAKkC,KAAK,CAACC,IAAI,CAAC/B,UAAU,EAAC;QACrE;IACF;IAEA,OAAOU;AACT,EAAC"}
@@ -1,4 +0,0 @@
1
- import type { Config } from 'payload';
2
- import type { PluginOptions } from '../types.js';
3
- export declare const cloudStorage: (pluginOptions: PluginOptions) => (incomingConfig: Config) => Config;
4
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAOhD,eAAO,MAAM,YAAY,GACtB,eAAe,aAAa,MAC5B,gBAAgB,MAAM,KAAG,MA+BzB,CAAA"}
@@ -1,34 +0,0 @@
1
- import { getFields } from './fields/getFields.js';
2
- // This is the admin plugin cloud-storage stubfile.
3
- // It only extends the config that are required by the admin UI.
4
- export const cloudStorage = (pluginOptions)=>(incomingConfig)=>{
5
- const { alwaysInsertFields, collections: allCollectionOptions, enabled } = pluginOptions;
6
- const config = {
7
- ...incomingConfig
8
- };
9
- // If disabled and alwaysInsertFields is not true, skip processing
10
- if (enabled === false && !alwaysInsertFields) {
11
- return config;
12
- }
13
- return {
14
- ...config,
15
- collections: (config.collections || []).map((existingCollection)=>{
16
- const options = allCollectionOptions[existingCollection.slug];
17
- // Process if adapter exists OR if alwaysInsertFields is true and this collection is configured
18
- if (options?.adapter || alwaysInsertFields && options) {
19
- const fields = getFields({
20
- alwaysInsertFields,
21
- collection: existingCollection,
22
- prefix: options.prefix
23
- });
24
- return {
25
- ...existingCollection,
26
- fields
27
- };
28
- }
29
- return existingCollection;
30
- })
31
- };
32
- };
33
-
34
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/admin/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { PluginOptions } from '../types.js'\n\nimport { getFields } from './fields/getFields.js'\n\n// This is the admin plugin cloud-storage stubfile.\n// It only extends the config that are required by the admin UI.\n\nexport const cloudStorage =\n (pluginOptions: PluginOptions) =>\n (incomingConfig: Config): Config => {\n const { alwaysInsertFields, collections: allCollectionOptions, enabled } = pluginOptions\n const config = { ...incomingConfig }\n\n // If disabled and alwaysInsertFields is not true, skip processing\n if (enabled === false && !alwaysInsertFields) {\n return config\n }\n\n return {\n ...config,\n collections: (config.collections || []).map((existingCollection) => {\n const options = allCollectionOptions[existingCollection.slug]\n\n // Process if adapter exists OR if alwaysInsertFields is true and this collection is configured\n if (options?.adapter || (alwaysInsertFields && options)) {\n const fields = getFields({\n alwaysInsertFields,\n collection: existingCollection,\n prefix: options.prefix,\n })\n\n return {\n ...existingCollection,\n fields,\n }\n }\n\n return existingCollection\n }),\n }\n }\n"],"names":["getFields","cloudStorage","pluginOptions","incomingConfig","alwaysInsertFields","collections","allCollectionOptions","enabled","config","map","existingCollection","options","slug","adapter","fields","collection","prefix"],"mappings":"AAIA,SAASA,SAAS,QAAQ,wBAAuB;AAEjD,mDAAmD;AACnD,gEAAgE;AAEhE,OAAO,MAAMC,eACX,CAACC,gBACD,CAACC;QACC,MAAM,EAAEC,kBAAkB,EAAEC,aAAaC,oBAAoB,EAAEC,OAAO,EAAE,GAAGL;QAC3E,MAAMM,SAAS;YAAE,GAAGL,cAAc;QAAC;QAEnC,kEAAkE;QAClE,IAAII,YAAY,SAAS,CAACH,oBAAoB;YAC5C,OAAOI;QACT;QAEA,OAAO;YACL,GAAGA,MAAM;YACTH,aAAa,AAACG,CAAAA,OAAOH,WAAW,IAAI,EAAE,AAAD,EAAGI,GAAG,CAAC,CAACC;gBAC3C,MAAMC,UAAUL,oBAAoB,CAACI,mBAAmBE,IAAI,CAAC;gBAE7D,+FAA+F;gBAC/F,IAAID,SAASE,WAAYT,sBAAsBO,SAAU;oBACvD,MAAMG,SAASd,UAAU;wBACvBI;wBACAW,YAAYL;wBACZM,QAAQL,QAAQK,MAAM;oBACxB;oBAEA,OAAO;wBACL,GAAGN,kBAAkB;wBACrBI;oBACF;gBACF;gBAEA,OAAOJ;YACT;QACF;IACF,EAAC"}
@@ -1,22 +0,0 @@
1
- type GetFileKeyArgs = {
2
- collectionPrefix?: string;
3
- docPrefix?: string;
4
- filename: string;
5
- useCompositePrefixes?: boolean;
6
- };
7
- type GetFileKeyResult = {
8
- fileKey: string;
9
- sanitizedCollectionPrefix: string;
10
- sanitizedDocPrefix: string;
11
- sanitizedFilename: string;
12
- };
13
- /**
14
- * Computes the file key (path) for storage.
15
- *
16
- * In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.
17
- * In composite mode (useCompositePrefixes: true), both are combined.
18
- * Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
19
- */
20
- export declare function getFileKey({ collectionPrefix, docPrefix, filename, useCompositePrefixes, }: GetFileKeyArgs): GetFileKeyResult;
21
- export {};
22
- //# sourceMappingURL=getFileKey.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"getFileKey.d.ts","sourceRoot":"","sources":["../../src/utilities/getFileKey.ts"],"names":[],"mappings":"AAKA,KAAK,cAAc,GAAG;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAA;AAED,KAAK,gBAAgB,GAAG;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,yBAAyB,EAAE,MAAM,CAAA;IACjC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EACzB,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,oBAA4B,GAC7B,EAAE,cAAc,GAAG,gBAAgB,CAenC"}
@@ -1,23 +0,0 @@
1
- import path from 'path';
2
- import { sanitizeFilename } from 'payload/shared';
3
- import { sanitizePrefix } from './sanitizePrefix.js';
4
- /**
5
- * Computes the file key (path) for storage.
6
- *
7
- * In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.
8
- * In composite mode (useCompositePrefixes: true), both are combined.
9
- * Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.
10
- */ export function getFileKey({ collectionPrefix, docPrefix, filename, useCompositePrefixes = false }) {
11
- const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '');
12
- const safeDocPrefix = sanitizePrefix(docPrefix || '');
13
- const safeFilename = sanitizeFilename(filename);
14
- const fileKey = useCompositePrefixes ? path.posix.join(safeCollectionPrefix, safeDocPrefix, safeFilename) : path.posix.join(safeDocPrefix || safeCollectionPrefix, safeFilename);
15
- return {
16
- fileKey,
17
- sanitizedCollectionPrefix: safeCollectionPrefix,
18
- sanitizedDocPrefix: safeDocPrefix,
19
- sanitizedFilename: safeFilename
20
- };
21
- }
22
-
23
- //# sourceMappingURL=getFileKey.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/utilities/getFileKey.ts"],"sourcesContent":["import path from 'path'\nimport { sanitizeFilename } from 'payload/shared'\n\nimport { sanitizePrefix } from './sanitizePrefix.js'\n\ntype GetFileKeyArgs = {\n collectionPrefix?: string\n docPrefix?: string\n filename: string\n useCompositePrefixes?: boolean\n}\n\ntype GetFileKeyResult = {\n fileKey: string\n sanitizedCollectionPrefix: string\n sanitizedDocPrefix: string\n sanitizedFilename: string\n}\n\n/**\n * Computes the file key (path) for storage.\n *\n * In non-composite mode (useCompositePrefixes: false), docPrefix overrides collectionPrefix.\n * In composite mode (useCompositePrefixes: true), both are combined.\n * Both prefixes are passed through {@link sanitizePrefix} so keys stay normalized.\n */\nexport function getFileKey({\n collectionPrefix,\n docPrefix,\n filename,\n useCompositePrefixes = false,\n}: GetFileKeyArgs): GetFileKeyResult {\n const safeCollectionPrefix = sanitizePrefix(collectionPrefix || '')\n const safeDocPrefix = sanitizePrefix(docPrefix || '')\n const safeFilename = sanitizeFilename(filename)\n\n const fileKey = useCompositePrefixes\n ? path.posix.join(safeCollectionPrefix, safeDocPrefix, safeFilename)\n : path.posix.join(safeDocPrefix || safeCollectionPrefix, safeFilename)\n\n return {\n fileKey,\n sanitizedCollectionPrefix: safeCollectionPrefix,\n sanitizedDocPrefix: safeDocPrefix,\n sanitizedFilename: safeFilename,\n }\n}\n"],"names":["path","sanitizeFilename","sanitizePrefix","getFileKey","collectionPrefix","docPrefix","filename","useCompositePrefixes","safeCollectionPrefix","safeDocPrefix","safeFilename","fileKey","posix","join","sanitizedCollectionPrefix","sanitizedDocPrefix","sanitizedFilename"],"mappings":"AAAA,OAAOA,UAAU,OAAM;AACvB,SAASC,gBAAgB,QAAQ,iBAAgB;AAEjD,SAASC,cAAc,QAAQ,sBAAqB;AAgBpD;;;;;;CAMC,GACD,OAAO,SAASC,WAAW,EACzBC,gBAAgB,EAChBC,SAAS,EACTC,QAAQ,EACRC,uBAAuB,KAAK,EACb;IACf,MAAMC,uBAAuBN,eAAeE,oBAAoB;IAChE,MAAMK,gBAAgBP,eAAeG,aAAa;IAClD,MAAMK,eAAeT,iBAAiBK;IAEtC,MAAMK,UAAUJ,uBACZP,KAAKY,KAAK,CAACC,IAAI,CAACL,sBAAsBC,eAAeC,gBACrDV,KAAKY,KAAK,CAACC,IAAI,CAACJ,iBAAiBD,sBAAsBE;IAE3D,OAAO;QACLC;QACAG,2BAA2BN;QAC3BO,oBAAoBN;QACpBO,mBAAmBN;IACrB;AACF"}
@@ -1,174 +0,0 @@
1
- import { getFileKey } from './getFileKey.js';
2
- import { describe, expect, it } from 'vitest';
3
- describe('getFileKey', ()=>{
4
- describe('non-composite mode (useCompositePrefixes: false)', ()=>{
5
- it('should use docPrefix when provided, ignoring collectionPrefix', ()=>{
6
- const result = getFileKey({
7
- collectionPrefix: 'collection',
8
- docPrefix: 'document',
9
- filename: 'test.png',
10
- useCompositePrefixes: false
11
- });
12
- expect(result).toEqual({
13
- fileKey: 'document/test.png',
14
- sanitizedCollectionPrefix: 'collection',
15
- sanitizedDocPrefix: 'document',
16
- sanitizedFilename: 'test.png'
17
- });
18
- });
19
- it('should fallback to collectionPrefix when docPrefix is empty', ()=>{
20
- const result = getFileKey({
21
- collectionPrefix: 'collection',
22
- docPrefix: '',
23
- filename: 'test.png',
24
- useCompositePrefixes: false
25
- });
26
- expect(result).toEqual({
27
- fileKey: 'collection/test.png',
28
- sanitizedCollectionPrefix: 'collection',
29
- sanitizedDocPrefix: '',
30
- sanitizedFilename: 'test.png'
31
- });
32
- });
33
- it('should fallback to collectionPrefix when docPrefix is undefined', ()=>{
34
- const result = getFileKey({
35
- collectionPrefix: 'collection',
36
- filename: 'test.png',
37
- useCompositePrefixes: false
38
- });
39
- expect(result).toEqual({
40
- fileKey: 'collection/test.png',
41
- sanitizedCollectionPrefix: 'collection',
42
- sanitizedDocPrefix: '',
43
- sanitizedFilename: 'test.png'
44
- });
45
- });
46
- it('should return only filename when both prefixes are empty', ()=>{
47
- const result = getFileKey({
48
- filename: 'test.png',
49
- useCompositePrefixes: false
50
- });
51
- expect(result).toEqual({
52
- fileKey: 'test.png',
53
- sanitizedCollectionPrefix: '',
54
- sanitizedDocPrefix: '',
55
- sanitizedFilename: 'test.png'
56
- });
57
- });
58
- });
59
- describe('composite mode (useCompositePrefixes: true)', ()=>{
60
- it('should combine collectionPrefix and docPrefix', ()=>{
61
- const result = getFileKey({
62
- collectionPrefix: 'collection',
63
- docPrefix: 'document',
64
- filename: 'test.png',
65
- useCompositePrefixes: true
66
- });
67
- expect(result).toEqual({
68
- fileKey: 'collection/document/test.png',
69
- sanitizedCollectionPrefix: 'collection',
70
- sanitizedDocPrefix: 'document',
71
- sanitizedFilename: 'test.png'
72
- });
73
- });
74
- it('should work with only collectionPrefix', ()=>{
75
- const result = getFileKey({
76
- collectionPrefix: 'collection',
77
- filename: 'test.png',
78
- useCompositePrefixes: true
79
- });
80
- expect(result).toEqual({
81
- fileKey: 'collection/test.png',
82
- sanitizedCollectionPrefix: 'collection',
83
- sanitizedDocPrefix: '',
84
- sanitizedFilename: 'test.png'
85
- });
86
- });
87
- it('should work with only docPrefix', ()=>{
88
- const result = getFileKey({
89
- docPrefix: 'document',
90
- filename: 'test.png',
91
- useCompositePrefixes: true
92
- });
93
- expect(result).toEqual({
94
- fileKey: 'document/test.png',
95
- sanitizedCollectionPrefix: '',
96
- sanitizedDocPrefix: 'document',
97
- sanitizedFilename: 'test.png'
98
- });
99
- });
100
- it('should return only filename when both prefixes are empty', ()=>{
101
- const result = getFileKey({
102
- filename: 'test.png',
103
- useCompositePrefixes: true
104
- });
105
- expect(result).toEqual({
106
- fileKey: 'test.png',
107
- sanitizedCollectionPrefix: '',
108
- sanitizedDocPrefix: '',
109
- sanitizedFilename: 'test.png'
110
- });
111
- });
112
- });
113
- describe('sanitization', ()=>{
114
- it('should remove path traversal segments from collectionPrefix', ()=>{
115
- const result = getFileKey({
116
- collectionPrefix: '../../../etc',
117
- filename: 'test.png',
118
- useCompositePrefixes: false
119
- });
120
- expect(result).toEqual({
121
- fileKey: 'etc/test.png',
122
- sanitizedCollectionPrefix: 'etc',
123
- sanitizedDocPrefix: '',
124
- sanitizedFilename: 'test.png'
125
- });
126
- expect(result.fileKey).not.toContain('..');
127
- });
128
- it('should remove path traversal segments from docPrefix', ()=>{
129
- const result = getFileKey({
130
- docPrefix: 'a/../../outside',
131
- filename: 'test.png',
132
- useCompositePrefixes: false
133
- });
134
- expect(result).toEqual({
135
- fileKey: 'a/outside/test.png',
136
- sanitizedCollectionPrefix: '',
137
- sanitizedDocPrefix: 'a/outside',
138
- sanitizedFilename: 'test.png'
139
- });
140
- expect(result.fileKey).not.toContain('..');
141
- });
142
- it('should remove control characters from prefixes', ()=>{
143
- const result = getFileKey({
144
- collectionPrefix: 'test\x00\x1fprefix',
145
- filename: 'test.png',
146
- useCompositePrefixes: false
147
- });
148
- expect(result).toEqual({
149
- fileKey: 'testprefix/test.png',
150
- sanitizedCollectionPrefix: 'testprefix',
151
- sanitizedDocPrefix: '',
152
- sanitizedFilename: 'test.png'
153
- });
154
- expect(result.fileKey).not.toMatch(/[\x00-\x1f]/);
155
- });
156
- it('should sanitize both prefixes in composite mode', ()=>{
157
- const result = getFileKey({
158
- collectionPrefix: '../collection',
159
- docPrefix: '../../doc',
160
- filename: 'test.png',
161
- useCompositePrefixes: true
162
- });
163
- expect(result).toEqual({
164
- fileKey: 'collection/doc/test.png',
165
- sanitizedCollectionPrefix: 'collection',
166
- sanitizedDocPrefix: 'doc',
167
- sanitizedFilename: 'test.png'
168
- });
169
- expect(result.fileKey).not.toContain('..');
170
- });
171
- });
172
- });
173
-
174
- //# sourceMappingURL=getFileKey.spec.js.map