@payloadcms/plugin-cloud-storage 4.0.0-canary.35 → 4.0.0-canary.37

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 (34) hide show
  1. package/dist/fields/getFields.d.ts +1 -5
  2. package/dist/fields/getFields.d.ts.map +1 -1
  3. package/dist/fields/getFields.js +27 -28
  4. package/dist/fields/getFields.js.map +1 -1
  5. package/dist/hooks/afterChange.d.ts.map +1 -1
  6. package/dist/hooks/afterChange.js +1 -0
  7. package/dist/hooks/afterChange.js.map +1 -1
  8. package/dist/plugin.d.ts.map +1 -1
  9. package/dist/plugin.js +29 -32
  10. package/dist/plugin.js.map +1 -1
  11. package/dist/types.d.ts +0 -10
  12. package/dist/types.d.ts.map +1 -1
  13. package/dist/types.js.map +1 -1
  14. package/package.json +4 -4
  15. package/dist/admin/fields/getFields.d.ts +0 -17
  16. package/dist/admin/fields/getFields.d.ts.map +0 -1
  17. package/dist/admin/fields/getFields.js +0 -98
  18. package/dist/admin/fields/getFields.js.map +0 -1
  19. package/dist/admin/index.d.ts +0 -4
  20. package/dist/admin/index.d.ts.map +0 -1
  21. package/dist/admin/index.js +0 -34
  22. package/dist/admin/index.js.map +0 -1
  23. package/dist/hooks/afterChange.spec.js +0 -95
  24. package/dist/hooks/afterChange.spec.js.map +0 -1
  25. package/dist/hooks/normalizeUploadPrefix.spec.js +0 -154
  26. package/dist/hooks/normalizeUploadPrefix.spec.js.map +0 -1
  27. package/dist/plugin.spec.js +0 -166
  28. package/dist/plugin.spec.js.map +0 -1
  29. package/dist/utilities/buildStoragePathData.spec.js +0 -241
  30. package/dist/utilities/buildStoragePathData.spec.js.map +0 -1
  31. package/dist/utilities/getFilePrefix.spec.js +0 -248
  32. package/dist/utilities/getFilePrefix.spec.js.map +0 -1
  33. package/dist/utilities/resolveSignedURLKey.spec.js +0 -74
  34. package/dist/utilities/resolveSignedURLKey.spec.js.map +0 -1
@@ -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,kBACP,aAAa,sBACZ,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,95 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { getAfterChangeHook } from './afterChange.js';
3
- describe('upload replacement cleanup', ()=>{
4
- it.each([
5
- [
6
- 'invoices',
7
- 'media/invoices',
8
- true,
9
- false
10
- ],
11
- [
12
- 'invoices',
13
- 'media/invoices',
14
- true,
15
- true
16
- ],
17
- [
18
- '',
19
- 'media',
20
- false,
21
- false
22
- ],
23
- [
24
- 'media/invoices',
25
- 'media/invoices',
26
- false,
27
- false
28
- ]
29
- ])('should compare object locations when replacing %s with %s (delete: %s, selected: %s)', async (oldPrefix, newPrefix, shouldDelete, isSelected)=>{
30
- const handleDelete = vi.fn();
31
- const hook = getAfterChangeHook({
32
- adapter: {
33
- handleUpload: vi.fn(),
34
- handleDelete
35
- },
36
- collection: {
37
- slug: 'media'
38
- },
39
- collectionPrefix: 'media'
40
- });
41
- const uploadData = {
42
- id: 1,
43
- filename: 'file.png',
44
- mimeType: 'image/png',
45
- sizes: {
46
- thumbnail: {
47
- filename: 'thumb.png'
48
- }
49
- },
50
- prefix: newPrefix
51
- };
52
- await hook({
53
- data: uploadData,
54
- doc: isSelected ? {
55
- id: 1
56
- } : uploadData,
57
- select: isSelected ? {} : undefined,
58
- previousDoc: {
59
- id: 1,
60
- filename: 'file.png',
61
- mimeType: 'image/png',
62
- sizes: {
63
- thumbnail: {
64
- filename: 'thumb.png'
65
- }
66
- },
67
- prefix: oldPrefix
68
- },
69
- operation: 'update',
70
- req: {
71
- context: {},
72
- file: {
73
- data: Buffer.from('file'),
74
- size: 4
75
- },
76
- payload: {
77
- logger: {
78
- error: vi.fn()
79
- }
80
- }
81
- }
82
- });
83
- expect(handleDelete).toHaveBeenCalledTimes(shouldDelete ? 2 : 0);
84
- if (shouldDelete) {
85
- expect(handleDelete).toHaveBeenCalledWith(expect.objectContaining({
86
- doc: expect.objectContaining({
87
- prefix: oldPrefix
88
- }),
89
- filename: 'file.png'
90
- }));
91
- }
92
- });
93
- });
94
-
95
- //# sourceMappingURL=afterChange.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/hooks/afterChange.spec.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest'\n\nimport { getAfterChangeHook } from './afterChange.js'\n\ndescribe('upload replacement cleanup', () => {\n it.each([\n ['invoices', 'media/invoices', true, false],\n ['invoices', 'media/invoices', true, true],\n ['', 'media', false, false],\n ['media/invoices', 'media/invoices', false, false],\n ] as const)(\n 'should compare object locations when replacing %s with %s (delete: %s, selected: %s)',\n async (oldPrefix, newPrefix, shouldDelete, isSelected) => {\n const handleDelete = vi.fn()\n const hook = getAfterChangeHook({\n adapter: { handleUpload: vi.fn(), handleDelete } as never,\n collection: { slug: 'media' } as never,\n collectionPrefix: 'media',\n })\n\n const uploadData = {\n id: 1,\n filename: 'file.png',\n mimeType: 'image/png',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n prefix: newPrefix,\n }\n\n await hook({\n data: uploadData,\n doc: isSelected ? { id: 1 } : uploadData,\n select: isSelected ? {} : undefined,\n previousDoc: {\n id: 1,\n filename: 'file.png',\n mimeType: 'image/png',\n sizes: { thumbnail: { filename: 'thumb.png' } },\n prefix: oldPrefix,\n },\n operation: 'update',\n req: {\n context: {},\n file: { data: Buffer.from('file'), size: 4 },\n payload: { logger: { error: vi.fn() } },\n },\n } as never)\n\n expect(handleDelete).toHaveBeenCalledTimes(shouldDelete ? 2 : 0)\n if (shouldDelete) {\n expect(handleDelete).toHaveBeenCalledWith(\n expect.objectContaining({\n doc: expect.objectContaining({ prefix: oldPrefix }),\n filename: 'file.png',\n }),\n )\n }\n },\n )\n})\n"],"names":["describe","expect","it","vi","getAfterChangeHook","each","oldPrefix","newPrefix","shouldDelete","isSelected","handleDelete","fn","hook","adapter","handleUpload","collection","slug","collectionPrefix","uploadData","id","filename","mimeType","sizes","thumbnail","prefix","data","doc","select","undefined","previousDoc","operation","req","context","file","Buffer","from","size","payload","logger","error","toHaveBeenCalledTimes","toHaveBeenCalledWith","objectContaining"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAQ;AAEjD,SAASC,kBAAkB,QAAQ,mBAAkB;AAErDJ,SAAS,8BAA8B;IACrCE,GAAGG,IAAI,CAAC;QACN;YAAC;YAAY;YAAkB;YAAM;SAAM;QAC3C;YAAC;YAAY;YAAkB;YAAM;SAAK;QAC1C;YAAC;YAAI;YAAS;YAAO;SAAM;QAC3B;YAAC;YAAkB;YAAkB;YAAO;SAAM;KACnD,EACC,wFACA,OAAOC,WAAWC,WAAWC,cAAcC;QACzC,MAAMC,eAAeP,GAAGQ,EAAE;QAC1B,MAAMC,OAAOR,mBAAmB;YAC9BS,SAAS;gBAAEC,cAAcX,GAAGQ,EAAE;gBAAID;YAAa;YAC/CK,YAAY;gBAAEC,MAAM;YAAQ;YAC5BC,kBAAkB;QACpB;QAEA,MAAMC,aAAa;YACjBC,IAAI;YACJC,UAAU;YACVC,UAAU;YACVC,OAAO;gBAAEC,WAAW;oBAAEH,UAAU;gBAAY;YAAE;YAC9CI,QAAQjB;QACV;QAEA,MAAMK,KAAK;YACTa,MAAMP;YACNQ,KAAKjB,aAAa;gBAAEU,IAAI;YAAE,IAAID;YAC9BS,QAAQlB,aAAa,CAAC,IAAImB;YAC1BC,aAAa;gBACXV,IAAI;gBACJC,UAAU;gBACVC,UAAU;gBACVC,OAAO;oBAAEC,WAAW;wBAAEH,UAAU;oBAAY;gBAAE;gBAC9CI,QAAQlB;YACV;YACAwB,WAAW;YACXC,KAAK;gBACHC,SAAS,CAAC;gBACVC,MAAM;oBAAER,MAAMS,OAAOC,IAAI,CAAC;oBAASC,MAAM;gBAAE;gBAC3CC,SAAS;oBAAEC,QAAQ;wBAAEC,OAAOpC,GAAGQ,EAAE;oBAAG;gBAAE;YACxC;QACF;QAEAV,OAAOS,cAAc8B,qBAAqB,CAAChC,eAAe,IAAI;QAC9D,IAAIA,cAAc;YAChBP,OAAOS,cAAc+B,oBAAoB,CACvCxC,OAAOyC,gBAAgB,CAAC;gBACtBhB,KAAKzB,OAAOyC,gBAAgB,CAAC;oBAAElB,QAAQlB;gBAAU;gBACjDc,UAAU;YACZ;QAEJ;IACF;AAEJ"}
@@ -1,154 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getFields } from '../fields/getFields.js';
3
- import { getNormalizeUploadPrefixFieldHook, getNormalizeUploadPrefixHook } from './normalizeUploadPrefix.js';
4
- const runHook = ({ data, originalDoc, req = {
5
- context: {},
6
- file: {}
7
- } })=>getNormalizeUploadPrefixHook({
8
- collectionPrefix: 'media'
9
- })({
10
- data,
11
- originalDoc,
12
- req
13
- });
14
- describe('new upload prefixes', ()=>{
15
- it('should normalize after custom prefix field hooks', async ()=>{
16
- const fields = getFields({
17
- collection: {
18
- fields: [
19
- {
20
- name: 'prefix',
21
- type: 'text',
22
- hooks: {
23
- beforeChange: [
24
- ()=>'custom'
25
- ]
26
- }
27
- }
28
- ]
29
- },
30
- prefix: 'media'
31
- });
32
- const field = fields.find((field)=>'name' in field && field.name === 'prefix');
33
- let value = 'original';
34
- for (const hook of field.hooks?.beforeChange || []){
35
- value = await hook({
36
- value,
37
- data: {
38
- filename: 'file.png'
39
- },
40
- req: {
41
- file: {},
42
- context: {}
43
- }
44
- });
45
- }
46
- expect(value).toBe('media/custom');
47
- });
48
- it('should contain a prefix supplied with a file upload', async ()=>{
49
- const result = await runHook({
50
- data: {
51
- filename: 'file.png',
52
- prefix: 'legacy'
53
- }
54
- });
55
- expect(result.prefix).toBe('media/legacy');
56
- });
57
- it('should leave internal cloud storage writes alone', async ()=>{
58
- const result = await runHook({
59
- data: {
60
- filename: 'file.png',
61
- prefix: 'legacy'
62
- },
63
- req: {
64
- context: {
65
- skipCloudStorage: true
66
- },
67
- file: {}
68
- }
69
- });
70
- expect(result.prefix).toBe('legacy');
71
- });
72
- /**
73
- * The `prefix` field is writable through the REST/GraphQL/Local API, so a request
74
- * carrying no file must not be able to point a document at a foreign object.
75
- */ it('should contain a caller-supplied prefix on a metadata-only update', async ()=>{
76
- const result = await runHook({
77
- data: {
78
- prefix: 'tenant-b/private'
79
- },
80
- originalDoc: {
81
- prefix: 'media/legacy'
82
- },
83
- req: {
84
- context: {}
85
- }
86
- });
87
- expect(result.prefix).toBe('media/tenant-b/private');
88
- });
89
- it('should contain a caller-supplied prefix on a create without a file', async ()=>{
90
- const result = await runHook({
91
- data: {
92
- prefix: 'tenant-b/private'
93
- },
94
- req: {
95
- context: {}
96
- }
97
- });
98
- expect(result.prefix).toBe('media/tenant-b/private');
99
- });
100
- it('should leave a stored prefix alone when the request omits it', async ()=>{
101
- const result = await runHook({
102
- data: {
103
- filename: 'legacy.png'
104
- },
105
- originalDoc: {
106
- prefix: 'legacy-invoices'
107
- },
108
- req: {
109
- context: {}
110
- }
111
- });
112
- expect(result.prefix).toBeUndefined();
113
- });
114
- it('should leave a stored prefix alone when the request repeats it unchanged', async ()=>{
115
- const result = await runHook({
116
- data: {
117
- prefix: 'legacy-invoices'
118
- },
119
- originalDoc: {
120
- prefix: 'legacy-invoices'
121
- },
122
- req: {
123
- context: {}
124
- }
125
- });
126
- expect(result.prefix).toBe('legacy-invoices');
127
- });
128
- describe('prefix field hook', ()=>{
129
- const runFieldHook = ({ previousValue, req = {
130
- context: {}
131
- }, value })=>getNormalizeUploadPrefixFieldHook({
132
- collectionPrefix: 'media'
133
- })({
134
- data: {},
135
- previousValue,
136
- req,
137
- value
138
- });
139
- it('should contain a caller-supplied value without a file', async ()=>{
140
- expect(await runFieldHook({
141
- previousValue: 'media/legacy',
142
- value: 'tenant-b/private'
143
- })).toBe('media/tenant-b/private');
144
- });
145
- it('should leave an unchanged value alone', async ()=>{
146
- expect(await runFieldHook({
147
- previousValue: 'legacy-invoices',
148
- value: 'legacy-invoices'
149
- })).toBe('legacy-invoices');
150
- });
151
- });
152
- });
153
-
154
- //# sourceMappingURL=normalizeUploadPrefix.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/hooks/normalizeUploadPrefix.spec.ts"],"sourcesContent":["import type { CollectionConfig, TextField } from 'payload'\n\nimport { describe, expect, it } from 'vitest'\n\nimport { getFields } from '../fields/getFields.js'\nimport {\n getNormalizeUploadPrefixFieldHook,\n getNormalizeUploadPrefixHook,\n} from './normalizeUploadPrefix.js'\n\nconst runHook = ({\n data,\n originalDoc,\n req = { context: {}, file: {} },\n}: {\n data: Record<string, unknown>\n originalDoc?: Record<string, unknown>\n req?: Record<string, unknown>\n}) =>\n getNormalizeUploadPrefixHook({ collectionPrefix: 'media' })({\n data,\n originalDoc,\n req,\n } as never)\n\ndescribe('new upload prefixes', () => {\n it('should normalize after custom prefix field hooks', async () => {\n const fields = getFields({\n collection: {\n fields: [{ name: 'prefix', type: 'text', hooks: { beforeChange: [() => 'custom'] } }],\n } as CollectionConfig,\n prefix: 'media',\n })\n const field = fields.find((field) => 'name' in field && field.name === 'prefix') as TextField\n let value = 'original'\n\n for (const hook of field.hooks?.beforeChange || []) {\n value = await hook({\n value,\n data: { filename: 'file.png' },\n req: { file: {}, context: {} },\n } as never)\n }\n\n expect(value).toBe('media/custom')\n })\n\n it('should contain a prefix supplied with a file upload', async () => {\n const result = await runHook({ data: { filename: 'file.png', prefix: 'legacy' } })\n\n expect(result.prefix).toBe('media/legacy')\n })\n\n it('should leave internal cloud storage writes alone', async () => {\n const result = await runHook({\n data: { filename: 'file.png', prefix: 'legacy' },\n req: { context: { skipCloudStorage: true }, file: {} },\n })\n\n expect(result.prefix).toBe('legacy')\n })\n\n /**\n * The `prefix` field is writable through the REST/GraphQL/Local API, so a request\n * carrying no file must not be able to point a document at a foreign object.\n */\n it('should contain a caller-supplied prefix on a metadata-only update', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n originalDoc: { prefix: 'media/legacy' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should contain a caller-supplied prefix on a create without a file', async () => {\n const result = await runHook({\n data: { prefix: 'tenant-b/private' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('media/tenant-b/private')\n })\n\n it('should leave a stored prefix alone when the request omits it', async () => {\n const result = await runHook({\n data: { filename: 'legacy.png' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBeUndefined()\n })\n\n it('should leave a stored prefix alone when the request repeats it unchanged', async () => {\n const result = await runHook({\n data: { prefix: 'legacy-invoices' },\n originalDoc: { prefix: 'legacy-invoices' },\n req: { context: {} },\n })\n\n expect(result.prefix).toBe('legacy-invoices')\n })\n\n describe('prefix field hook', () => {\n const runFieldHook = ({\n previousValue,\n req = { context: {} },\n value,\n }: {\n previousValue?: string\n req?: Record<string, unknown>\n value: unknown\n }) =>\n getNormalizeUploadPrefixFieldHook({ collectionPrefix: 'media' })({\n data: {},\n previousValue,\n req,\n value,\n } as never)\n\n it('should contain a caller-supplied value without a file', async () => {\n expect(await runFieldHook({ previousValue: 'media/legacy', value: 'tenant-b/private' })).toBe(\n 'media/tenant-b/private',\n )\n })\n\n it('should leave an unchanged value alone', async () => {\n expect(\n await runFieldHook({ previousValue: 'legacy-invoices', value: 'legacy-invoices' }),\n ).toBe('legacy-invoices')\n })\n })\n})\n"],"names":["describe","expect","it","getFields","getNormalizeUploadPrefixFieldHook","getNormalizeUploadPrefixHook","runHook","data","originalDoc","req","context","file","collectionPrefix","fields","collection","name","type","hooks","beforeChange","prefix","field","find","value","hook","filename","toBe","result","skipCloudStorage","toBeUndefined","runFieldHook","previousValue"],"mappings":"AAEA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAE7C,SAASC,SAAS,QAAQ,yBAAwB;AAClD,SACEC,iCAAiC,EACjCC,4BAA4B,QACvB,6BAA4B;AAEnC,MAAMC,UAAU,CAAC,EACfC,IAAI,EACJC,WAAW,EACXC,MAAM;IAAEC,SAAS,CAAC;IAAGC,MAAM,CAAC;AAAE,CAAC,EAKhC,GACCN,6BAA6B;QAAEO,kBAAkB;IAAQ,GAAG;QAC1DL;QACAC;QACAC;IACF;AAEFT,SAAS,uBAAuB;IAC9BE,GAAG,oDAAoD;QACrD,MAAMW,SAASV,UAAU;YACvBW,YAAY;gBACVD,QAAQ;oBAAC;wBAAEE,MAAM;wBAAUC,MAAM;wBAAQC,OAAO;4BAAEC,cAAc;gCAAC,IAAM;6BAAS;wBAAC;oBAAE;iBAAE;YACvF;YACAC,QAAQ;QACV;QACA,MAAMC,QAAQP,OAAOQ,IAAI,CAAC,CAACD,QAAU,UAAUA,SAASA,MAAML,IAAI,KAAK;QACvE,IAAIO,QAAQ;QAEZ,KAAK,MAAMC,QAAQH,MAAMH,KAAK,EAAEC,gBAAgB,EAAE,CAAE;YAClDI,QAAQ,MAAMC,KAAK;gBACjBD;gBACAf,MAAM;oBAAEiB,UAAU;gBAAW;gBAC7Bf,KAAK;oBAAEE,MAAM,CAAC;oBAAGD,SAAS,CAAC;gBAAE;YAC/B;QACF;QAEAT,OAAOqB,OAAOG,IAAI,CAAC;IACrB;IAEAvB,GAAG,uDAAuD;QACxD,MAAMwB,SAAS,MAAMpB,QAAQ;YAAEC,MAAM;gBAAEiB,UAAU;gBAAYL,QAAQ;YAAS;QAAE;QAEhFlB,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,oDAAoD;QACrD,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEiB,UAAU;gBAAYL,QAAQ;YAAS;YAC/CV,KAAK;gBAAEC,SAAS;oBAAEiB,kBAAkB;gBAAK;gBAAGhB,MAAM,CAAC;YAAE;QACvD;QAEAV,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEA;;;GAGC,GACDvB,GAAG,qEAAqE;QACtE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAmB;YACnCX,aAAa;gBAAEW,QAAQ;YAAe;YACtCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,sEAAsE;QACvE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAmB;YACnCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAvB,GAAG,gEAAgE;QACjE,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEiB,UAAU;YAAa;YAC/BhB,aAAa;gBAAEW,QAAQ;YAAkB;YACzCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAES,aAAa;IACrC;IAEA1B,GAAG,4EAA4E;QAC7E,MAAMwB,SAAS,MAAMpB,QAAQ;YAC3BC,MAAM;gBAAEY,QAAQ;YAAkB;YAClCX,aAAa;gBAAEW,QAAQ;YAAkB;YACzCV,KAAK;gBAAEC,SAAS,CAAC;YAAE;QACrB;QAEAT,OAAOyB,OAAOP,MAAM,EAAEM,IAAI,CAAC;IAC7B;IAEAzB,SAAS,qBAAqB;QAC5B,MAAM6B,eAAe,CAAC,EACpBC,aAAa,EACbrB,MAAM;YAAEC,SAAS,CAAC;QAAE,CAAC,EACrBY,KAAK,EAKN,GACClB,kCAAkC;gBAAEQ,kBAAkB;YAAQ,GAAG;gBAC/DL,MAAM,CAAC;gBACPuB;gBACArB;gBACAa;YACF;QAEFpB,GAAG,yDAAyD;YAC1DD,OAAO,MAAM4B,aAAa;gBAAEC,eAAe;gBAAgBR,OAAO;YAAmB,IAAIG,IAAI,CAC3F;QAEJ;QAEAvB,GAAG,yCAAyC;YAC1CD,OACE,MAAM4B,aAAa;gBAAEC,eAAe;gBAAmBR,OAAO;YAAkB,IAChFG,IAAI,CAAC;QACT;IACF;AACF"}
@@ -1,166 +0,0 @@
1
- import { createServer } from 'node:http';
2
- import { downloadFileToBuffer } from 'payload/internal';
3
- import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
4
- import { cloudStoragePlugin } from './plugin.js';
5
- const collectionSlug = 'media';
6
- describe('cloudStoragePlugin external file fetching', ()=>{
7
- const server = createServer((_req, res)=>{
8
- res.writeHead(200, {
9
- 'Content-Length': '12',
10
- 'Content-Type': 'text/plain'
11
- });
12
- res.end('stored file\n');
13
- });
14
- let port;
15
- beforeAll(async ()=>{
16
- await new Promise((resolve)=>server.listen(0, resolve));
17
- port = server.address().port;
18
- });
19
- afterEach(()=>{
20
- vi.unstubAllEnvs();
21
- });
22
- afterAll(async ()=>{
23
- await new Promise((resolve, reject)=>{
24
- server.close((error)=>error ? reject(error) : resolve());
25
- });
26
- });
27
- it('should filter private URLs when Payload file access control is disabled', async ()=>{
28
- vi.stubEnv('NODE_ENV', 'production');
29
- const uploadConfig = buildUploadConfig();
30
- await expect(fetchExternalFile({
31
- uploadConfig,
32
- url: `http://127.0.0.1:${port}/file.txt`
33
- })).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1');
34
- });
35
- it('should allow private URLs when skipSafeFetch is explicitly true', async ()=>{
36
- vi.stubEnv('NODE_ENV', 'production');
37
- const uploadConfig = buildUploadConfig(true);
38
- const file = await fetchExternalFile({
39
- uploadConfig,
40
- url: `http://127.0.0.1:${port}/file.txt`
41
- });
42
- expect(file.data.toString()).toBe('stored file\n');
43
- });
44
- it('should allow private URLs that match an explicit skipSafeFetch allowlist', async ()=>{
45
- vi.stubEnv('NODE_ENV', 'production');
46
- const uploadConfig = buildUploadConfig([
47
- {
48
- hostname: '127.0.0.1',
49
- port: String(port),
50
- protocol: 'http'
51
- }
52
- ]);
53
- const file = await fetchExternalFile({
54
- uploadConfig,
55
- url: `http://127.0.0.1:${port}/file.txt`
56
- });
57
- expect(file.data.toString()).toBe('stored file\n');
58
- });
59
- it('should allow localhost URLs outside production', async ()=>{
60
- vi.stubEnv('NODE_ENV', 'development');
61
- const uploadConfig = buildUploadConfig();
62
- const file = await fetchExternalFile({
63
- uploadConfig,
64
- url: `http://localhost:${port}/file.txt`
65
- });
66
- expect(file.data.toString()).toBe('stored file\n');
67
- });
68
- });
69
- function buildUploadConfig(skipSafeFetch) {
70
- const config = cloudStoragePlugin({
71
- collections: {
72
- [collectionSlug]: {
73
- adapter: ()=>({
74
- handleDelete: async ()=>undefined,
75
- handleUpload: async ()=>undefined,
76
- name: 'test-adapter',
77
- staticHandler: async ()=>new Response(null, {
78
- status: 404
79
- })
80
- }),
81
- disablePayloadAccessControl: true
82
- }
83
- }
84
- })({
85
- collections: [
86
- {
87
- fields: [],
88
- slug: collectionSlug,
89
- upload: typeof skipSafeFetch === 'undefined' ? true : {
90
- skipSafeFetch
91
- }
92
- }
93
- ]
94
- });
95
- const uploadConfig = config.collections?.[0]?.upload;
96
- if (!uploadConfig || typeof uploadConfig !== 'object') {
97
- throw new Error('Expected an upload configuration');
98
- }
99
- return uploadConfig;
100
- }
101
- async function fetchExternalFile({ uploadConfig, url }) {
102
- return downloadFileToBuffer({
103
- data: {
104
- filename: 'file.txt',
105
- url
106
- },
107
- req: {
108
- headers: new Headers(),
109
- payload: {
110
- config: {
111
- cookiePrefix: 'payload'
112
- }
113
- }
114
- },
115
- uploadConfig
116
- });
117
- }
118
- const adapter = ()=>({
119
- handleDelete: ()=>undefined,
120
- handleUpload: ()=>undefined,
121
- name: 'test-adapter',
122
- staticHandler: ()=>new Response()
123
- });
124
- describe('cloudStoragePlugin', ()=>{
125
- it('should normalize a stored prefix during a server-mediated upload', async ()=>{
126
- const config = cloudStoragePlugin({
127
- collections: {
128
- media: {
129
- adapter
130
- }
131
- }
132
- })({
133
- collections: [
134
- {
135
- fields: [],
136
- slug: 'media',
137
- upload: true
138
- }
139
- ]
140
- });
141
- const hooks = config.collections?.[0]?.hooks?.beforeChange || [];
142
- const req = {
143
- context: {},
144
- file: {
145
- mimetype: 'image/png',
146
- name: 'photo.png',
147
- size: 42
148
- }
149
- };
150
- let data = {
151
- filename: 'photo.png',
152
- prefix: '/tenant/../acme'
153
- };
154
- for (const hook of hooks){
155
- const result = await hook({
156
- data,
157
- operation: 'create',
158
- req
159
- });
160
- data = result || data;
161
- }
162
- expect(data.prefix).toBe('tenant/acme');
163
- });
164
- });
165
-
166
- //# sourceMappingURL=plugin.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/plugin.spec.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net'\nimport type { Config, PayloadRequest, UploadConfig } from 'payload'\n\nimport { createServer } from 'node:http'\nimport { downloadFileToBuffer } from 'payload/internal'\nimport { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'\n\nimport { cloudStoragePlugin } from './plugin.js'\n\nconst collectionSlug = 'media'\n\ndescribe('cloudStoragePlugin external file fetching', () => {\n const server = createServer((_req, res) => {\n res.writeHead(200, {\n 'Content-Length': '12',\n 'Content-Type': 'text/plain',\n })\n res.end('stored file\\n')\n })\n\n let port: number\n\n beforeAll(async () => {\n await new Promise<void>((resolve) => server.listen(0, resolve))\n port = (server.address() as AddressInfo).port\n })\n\n afterEach(() => {\n vi.unstubAllEnvs()\n })\n\n afterAll(async () => {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => (error ? reject(error) : resolve()))\n })\n })\n\n it('should filter private URLs when Payload file access control is disabled', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig()\n\n await expect(\n fetchExternalFile({ uploadConfig, url: `http://127.0.0.1:${port}/file.txt` }),\n ).rejects.toThrow('Blocked unsafe attempt to 127.0.0.1')\n })\n\n it('should allow private URLs when skipSafeFetch is explicitly true', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig(true)\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow private URLs that match an explicit skipSafeFetch allowlist', async () => {\n vi.stubEnv('NODE_ENV', 'production')\n const uploadConfig = buildUploadConfig([\n {\n hostname: '127.0.0.1',\n port: String(port),\n protocol: 'http',\n },\n ])\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://127.0.0.1:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n\n it('should allow localhost URLs outside production', async () => {\n vi.stubEnv('NODE_ENV', 'development')\n const uploadConfig = buildUploadConfig()\n\n const file = await fetchExternalFile({\n uploadConfig,\n url: `http://localhost:${port}/file.txt`,\n })\n\n expect(file.data.toString()).toBe('stored file\\n')\n })\n})\n\nfunction buildUploadConfig(skipSafeFetch?: UploadConfig['skipSafeFetch']): UploadConfig {\n const config = cloudStoragePlugin({\n collections: {\n [collectionSlug]: {\n adapter: () => ({\n handleDelete: async () => undefined,\n handleUpload: async () => undefined,\n name: 'test-adapter',\n staticHandler: async () => new Response(null, { status: 404 }),\n }),\n disablePayloadAccessControl: true,\n },\n },\n })({\n collections: [\n {\n fields: [],\n slug: collectionSlug,\n upload: typeof skipSafeFetch === 'undefined' ? true : { skipSafeFetch },\n },\n ],\n } as Config)\n\n const uploadConfig = config.collections?.[0]?.upload\n if (!uploadConfig || typeof uploadConfig !== 'object') {\n throw new Error('Expected an upload configuration')\n }\n\n return uploadConfig\n}\n\nasync function fetchExternalFile({\n uploadConfig,\n url,\n}: {\n uploadConfig: UploadConfig\n url: string\n}) {\n return downloadFileToBuffer({\n data: {\n filename: 'file.txt',\n url,\n },\n req: {\n headers: new Headers(),\n payload: {\n config: {\n cookiePrefix: 'payload',\n },\n },\n } as PayloadRequest,\n uploadConfig,\n })\n}\n\nconst adapter = () => ({\n handleDelete: () => undefined,\n handleUpload: () => undefined,\n name: 'test-adapter',\n staticHandler: () => new Response(),\n})\n\ndescribe('cloudStoragePlugin', () => {\n it('should normalize a stored prefix during a server-mediated upload', async () => {\n const config = cloudStoragePlugin({\n collections: {\n media: { adapter },\n },\n } as any)({\n collections: [{ fields: [], slug: 'media', upload: true }],\n } as any)\n const hooks = config.collections?.[0]?.hooks?.beforeChange || []\n const req = {\n context: {},\n file: { mimetype: 'image/png', name: 'photo.png', size: 42 },\n }\n let data = { filename: 'photo.png', prefix: '/tenant/../acme' }\n\n for (const hook of hooks) {\n const result = await hook({ data, operation: 'create', req } as any)\n data = result || data\n }\n\n expect(data.prefix).toBe('tenant/acme')\n })\n})\n"],"names":["createServer","downloadFileToBuffer","afterAll","afterEach","beforeAll","describe","expect","it","vi","cloudStoragePlugin","collectionSlug","server","_req","res","writeHead","end","port","Promise","resolve","listen","address","unstubAllEnvs","reject","close","error","stubEnv","uploadConfig","buildUploadConfig","fetchExternalFile","url","rejects","toThrow","file","data","toString","toBe","hostname","String","protocol","skipSafeFetch","config","collections","adapter","handleDelete","undefined","handleUpload","name","staticHandler","Response","status","disablePayloadAccessControl","fields","slug","upload","Error","filename","req","headers","Headers","payload","cookiePrefix","media","hooks","beforeChange","context","mimetype","size","prefix","hook","result","operation"],"mappings":"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"}