@humaan/payload-storage-imagekit 2.3.3 → 2.4.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/README.md +36 -3
- package/dist/clientUploads.d.ts +1 -0
- package/dist/clientUploads.js.map +1 -1
- package/dist/components/ImageKitUploadHandler.d.ts +1 -1
- package/dist/components/ImageKitUploadHandler.js +7 -1
- package/dist/components/ImageKitUploadHandler.js.map +1 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.d.ts +1 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.js +1 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.js.map +1 -1
- package/dist/getAdminThumbnailURL.d.ts +11 -0
- package/dist/getAdminThumbnailURL.js +22 -3
- package/dist/getAdminThumbnailURL.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +179 -31
- package/dist/index.js.map +1 -1
- package/dist/migrate.d.ts +12 -2
- package/dist/migrate.js +31 -8
- package/dist/migrate.js.map +1 -1
- package/package.json +31 -30
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ This plugin integrates `@payloadcms/plugin-cloud-storage` with ImageKit and prov
|
|
|
7
7
|
- Uploads to ImageKit per collection
|
|
8
8
|
- Optional client-side uploads for Payload admin to bypass request size limits on platforms like Vercel
|
|
9
9
|
- Required root folder with per-collection subfolders
|
|
10
|
-
- Stored ImageKit references (`imagekit.fileId`, `imagekit.url`, `imagekit.thumbnailUrl`)
|
|
10
|
+
- Stored ImageKit references (`imagekit.fileId`, `imagekit.url`, `imagekit.thumbnailUrl`, `imagekit.isPrivate`)
|
|
11
11
|
- `crop: false` for configured upload collections
|
|
12
12
|
- Admin thumbnails using ImageKit transforms (`tr=w-300,h-300`)
|
|
13
13
|
- Built-in thumbnail path for videos and PDFs in admin previews
|
|
@@ -52,13 +52,14 @@ export default buildConfig({
|
|
|
52
52
|
|
|
53
53
|
`imagekitStorage(options)` options:
|
|
54
54
|
|
|
55
|
-
- `collections` (required): `Partial<Record<UploadCollectionSlug, true
|
|
55
|
+
- `collections` (required): `Partial<Record<UploadCollectionSlug, true | { private?: boolean }>>`. Use `true` for a normal (public) collection, or `{ private: true }` to deliver the collection's files as ImageKit [private files](#private-files).
|
|
56
56
|
- `folder` (required): root upload folder in ImageKit, e.g. `/payload-uploads`
|
|
57
57
|
- `privateKey` (required): ImageKit private API key
|
|
58
58
|
- `publicKey` (required): ImageKit public key
|
|
59
59
|
- `urlEndpoint` (required): ImageKit URL endpoint, e.g. `https://ik.imagekit.io/your_id`
|
|
60
60
|
- `clientUploads` (optional): when `true`, Payload admin uploads go directly from the browser to ImageKit before Payload fetches the file back for persistence and image-size generation. This helps avoid server request body limits such as Vercel's.
|
|
61
61
|
Duplicate filenames still follow Payload's normal safe-name behavior, so a second `pug.jpg` becomes `pug-1.jpg` instead of overwriting the first asset in ImageKit.
|
|
62
|
+
- `signedUrlExpiresIn` (optional): expiry in seconds for the signed URLs generated for [private](#private-files) collections. Defaults to `900` (15 minutes). Signed URLs are generated fresh on every request, so a short window is safe.
|
|
62
63
|
- `enabled` (optional): enable / disable plugin
|
|
63
64
|
- `alwaysInsertFields` (optional): pass-through to cloud storage plugin
|
|
64
65
|
- `disablePayloadAccessControl` (optional): pass-through to cloud storage plugin
|
|
@@ -69,6 +70,36 @@ export default buildConfig({
|
|
|
69
70
|
- `migrateFolder`
|
|
70
71
|
- `payloadStorageImagekit` (alias of `imagekitStorage`)
|
|
71
72
|
|
|
73
|
+
## Private Files
|
|
74
|
+
|
|
75
|
+
ImageKit [private files](https://imagekit.io/docs/media-delivery-basic-security#private-files) are uploaded with `isPrivateFile: true` and **cannot** be accessed through their plain URL — they return `401` unless requested through a time-limited [signed URL](https://imagekit.io/docs/media-delivery-basic-security#how-to-generate-signed-urls).
|
|
76
|
+
|
|
77
|
+
Mark any upload collection private by passing `{ private: true }` instead of `true`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
imagekitStorage({
|
|
81
|
+
folder: '/payload-uploads',
|
|
82
|
+
collections: {
|
|
83
|
+
media: true, // public
|
|
84
|
+
documents: { private: true }, // private
|
|
85
|
+
},
|
|
86
|
+
signedUrlExpiresIn: 900, // optional, seconds (default 900)
|
|
87
|
+
privateKey: process.env.IMAGEKIT_PRIVATE_KEY || '',
|
|
88
|
+
publicKey: process.env.IMAGEKIT_PUBLIC_KEY || '',
|
|
89
|
+
urlEndpoint: process.env.IMAGEKIT_URL_ENDPOINT || '',
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
How it works:
|
|
94
|
+
|
|
95
|
+
- **Upload:** files in a private collection are uploaded with `isPrivateFile: true` (both server-side and client-side uploads).
|
|
96
|
+
- **Per-document status:** each document records its actual ImageKit privacy at upload time in `imagekit.isPrivate` (a read-only "Private file" checkbox in the admin). A file's privacy on ImageKit is fixed when it is uploaded, so this stored flag — not the collection's current setting — is the **source of truth** for delivery. This means existing files keep working if you later flip a collection's `private` setting. (Documents uploaded before this flag existed fall back to the collection setting.)
|
|
97
|
+
- **Delivery:** files are served through Payload's own file route (`/api/<collection>/file/<filename>`), which enforces the collection's `access.read` control and then redirects to a freshly signed, expiring ImageKit URL. Nothing signed is ever persisted to the database.
|
|
98
|
+
- **Admin thumbnails** are routed through the same Payload file route so they sign on demand and respect access control (including ImageKit's `ik-thumbnail.jpg` previews for videos and PDFs).
|
|
99
|
+
- **Admin indicators:** private collections get a note in their admin description (visible in the list and edit views), and each document shows the read-only `imagekit.isPrivate` checkbox.
|
|
100
|
+
- **Signed URL preview:** because the stored `imagekit.url` is the unsigned URL (which 401s for private files), private documents also expose a read-only virtual `imagekit.signedUrl` field — a freshly signed, time-limited URL computed on each read (never persisted). Use it to reach the file directly on ImageKit. The access-control-gated alternative is the document's top-level `url` (the Payload file route).
|
|
101
|
+
- **Access control is required.** Because signing happens at request time behind Payload access control, `disablePayloadAccessControl` cannot be combined with private collections — doing so throws at startup. Set per-collection `access.read` rules to control who can read each file.
|
|
102
|
+
|
|
72
103
|
## Folder Migration
|
|
73
104
|
|
|
74
105
|
Use `migrateFolder` to copy uploads from one ImageKit root folder to another (e.g. staging to production).
|
|
@@ -80,6 +111,7 @@ Behavior:
|
|
|
80
111
|
- Optional database update via Payload local API
|
|
81
112
|
- Dry-run is enabled by default (`dryRun: true`)
|
|
82
113
|
- Copies from the configured source folder path first, with stored `imagekit.url` as a fallback only when the source file cannot be found
|
|
114
|
+
- Handles [private files](#private-files): mark a collection `{ private: true }` and migrated documents are stored with `imagekit.isPrivate: true` and a signed-URL admin thumbnail. The fast server-side copy preserves the source file's ImageKit privacy; when the copy falls back to re-uploading from the source URL, the source URL is signed (so a private source can be read) and the file is re-uploaded with `isPrivateFile: true`. Because ImageKit privacy is fixed at upload, migrating assumes the destination collection's privacy matches the source — copying a public file into a `{ private: true }` collection keeps the file public on ImageKit.
|
|
83
115
|
|
|
84
116
|
Example:
|
|
85
117
|
|
|
@@ -106,13 +138,14 @@ await payload.destroy()
|
|
|
106
138
|
|
|
107
139
|
`migrateFolder(options)` options:
|
|
108
140
|
|
|
109
|
-
- `collections` (required): `Partial<Record<UploadCollectionSlug, true>>`
|
|
141
|
+
- `collections` (required): `Partial<Record<UploadCollectionSlug, true | { private?: boolean }>>`
|
|
110
142
|
- `from` (required): source root folder, e.g. `/payload-uploads-staging`
|
|
111
143
|
- `to` (required): destination root folder, e.g. `/payload-uploads-production`
|
|
112
144
|
- `privateKey` (required): ImageKit private API key
|
|
113
145
|
- `urlEndpoint` (required): ImageKit URL endpoint
|
|
114
146
|
- `dryRun` (optional, default `true`): when `true`, logs what would happen without writes
|
|
115
147
|
- `payload` (optional): when provided and `dryRun` is `false`, updates `url` + `imagekit.*` fields
|
|
148
|
+
- `signedUrlExpiresIn` (optional, default `900`): expiry in seconds for the signed URL used to read a private source file during the re-upload fallback
|
|
116
149
|
|
|
117
150
|
Run a migration script:
|
|
118
151
|
|
package/dist/clientUploads.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type ImageKitUploadHandlerProps = {
|
|
|
11
11
|
authPath: `/${string}`;
|
|
12
12
|
collectionSlug: string;
|
|
13
13
|
folder: string;
|
|
14
|
+
isPrivate?: boolean;
|
|
14
15
|
publicKey: string;
|
|
15
16
|
};
|
|
16
17
|
export declare const isImageKitClientUploadContext: (value: unknown) => value is ImageKitClientUploadContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["export const imageKitUploadAuthBasePath = '/imagekit-upload-auth'\n\nexport const getImageKitUploadAuthPath = (collectionSlug: string): `/${string}` =>\n `${imageKitUploadAuthBasePath}/${collectionSlug}`\n\nexport type ImageKitClientUploadContext = {\n fileId?: string\n filePath?: string\n name?: string\n thumbnailUrl?: string\n url?: string\n}\n\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n publicKey: string\n}\n\nexport const isImageKitClientUploadContext = (\n value: unknown,\n): value is ImageKitClientUploadContext => {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n return 'fileId' in value || 'thumbnailUrl' in value || 'url' in value\n}\n"],"names":["imageKitUploadAuthBasePath","getImageKitUploadAuthPath","collectionSlug","isImageKitClientUploadContext","value"],"mappings":"AAAA,OAAO,MAAMA,6BAA6B,wBAAuB;AAEjE,OAAO,MAAMC,4BAA4B,CAACC,iBACxC,GAAGF,2BAA2B,CAAC,EAAEE,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"sources":["../src/clientUploads.ts"],"sourcesContent":["export const imageKitUploadAuthBasePath = '/imagekit-upload-auth'\n\nexport const getImageKitUploadAuthPath = (collectionSlug: string): `/${string}` =>\n `${imageKitUploadAuthBasePath}/${collectionSlug}`\n\nexport type ImageKitClientUploadContext = {\n fileId?: string\n filePath?: string\n name?: string\n thumbnailUrl?: string\n url?: string\n}\n\nexport type ImageKitUploadHandlerProps = {\n authPath: `/${string}`\n collectionSlug: string\n folder: string\n isPrivate?: boolean\n publicKey: string\n}\n\nexport const isImageKitClientUploadContext = (\n value: unknown,\n): value is ImageKitClientUploadContext => {\n if (!value || typeof value !== 'object') {\n return false\n }\n\n return 'fileId' in value || 'thumbnailUrl' in value || 'url' in value\n}\n"],"names":["imageKitUploadAuthBasePath","getImageKitUploadAuthPath","collectionSlug","isImageKitClientUploadContext","value"],"mappings":"AAAA,OAAO,MAAMA,6BAA6B,wBAAuB;AAEjE,OAAO,MAAMC,4BAA4B,CAACC,iBACxC,GAAGF,2BAA2B,CAAC,EAAEE,gBAAgB,CAAA;AAkBnD,OAAO,MAAMC,gCAAgC,CAC3CC;IAEA,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU;QACvC,OAAO;IACT;IAEA,OAAO,YAAYA,SAAS,kBAAkBA,SAAS,SAASA;AAClE,EAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ImageKitUploadHandlerProps } from '../clientUploads.js';
|
|
2
|
-
export declare const ImageKitUploadHandler: ({ authPath, collectionSlug, folder, publicKey, }: ImageKitUploadHandlerProps) => null;
|
|
2
|
+
export declare const ImageKitUploadHandler: ({ authPath, collectionSlug, folder, isPrivate, publicKey, }: ImageKitUploadHandlerProps) => null;
|
|
@@ -14,7 +14,7 @@ const readErrorMessage = async (response, fallback)=>{
|
|
|
14
14
|
}
|
|
15
15
|
return fallback;
|
|
16
16
|
};
|
|
17
|
-
export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, publicKey })=>{
|
|
17
|
+
export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, isPrivate, publicKey })=>{
|
|
18
18
|
const { config } = useConfig();
|
|
19
19
|
const { setUploadHandler } = useUploadHandlers();
|
|
20
20
|
useEffect(()=>{
|
|
@@ -44,6 +44,11 @@ export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, public
|
|
|
44
44
|
formData.set('signature', auth.signature);
|
|
45
45
|
formData.set('token', auth.token);
|
|
46
46
|
formData.set('useUniqueFileName', 'false');
|
|
47
|
+
if (isPrivate) {
|
|
48
|
+
// ImageKit's upload signature only covers `token` + `expire`, so adding
|
|
49
|
+
// this flag does not invalidate the signed auth parameters.
|
|
50
|
+
formData.set('isPrivateFile', 'true');
|
|
51
|
+
}
|
|
47
52
|
const uploadResponse = await fetch(uploadAPIURL, {
|
|
48
53
|
body: formData,
|
|
49
54
|
method: 'POST'
|
|
@@ -68,6 +73,7 @@ export const ImageKitUploadHandler = ({ authPath, collectionSlug, folder, public
|
|
|
68
73
|
collectionSlug,
|
|
69
74
|
config.routes.api,
|
|
70
75
|
folder,
|
|
76
|
+
isPrivate,
|
|
71
77
|
publicKey,
|
|
72
78
|
setUploadHandler
|
|
73
79
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useUploadHandlers } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect } from 'react'\n\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../src/components/ImageKitUploadHandler.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useUploadHandlers } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect } from 'react'\n\nimport type { ImageKitClientUploadContext, ImageKitUploadHandlerProps } from '../clientUploads.js'\n\ntype ImageKitAuthResponse = {\n expire: number\n fileName?: string\n signature: string\n token: string\n}\n\ntype ImageKitUploadResponse = {\n message?: string\n name?: string\n} & ImageKitClientUploadContext\n\nconst uploadAPIURL = 'https://upload.imagekit.io/api/v1/files/upload'\n\nconst readErrorMessage = async (response: Response, fallback: string): Promise<string> => {\n try {\n const result = (await response.json()) as { message?: string }\n\n if (typeof result.message === 'string' && result.message.trim().length > 0) {\n return result.message\n }\n } catch {\n // Ignore non-JSON responses and fall back to the default message.\n }\n\n return fallback\n}\n\nexport const ImageKitUploadHandler = ({\n authPath,\n collectionSlug,\n folder,\n isPrivate,\n publicKey,\n}: ImageKitUploadHandlerProps) => {\n const { config } = useConfig()\n const { setUploadHandler } = useUploadHandlers()\n\n useEffect(() => {\n const authURL = formatAdminURL({\n apiRoute: config.routes.api,\n path: authPath,\n })\n\n setUploadHandler({\n collectionSlug,\n handler: async ({ file, updateFilename }) => {\n const authRequestURL = new URL(authURL, window.location.origin)\n\n authRequestURL.searchParams.set('filename', file.name)\n\n const authResponse = await fetch(authRequestURL, {\n credentials: 'include',\n })\n\n if (!authResponse.ok) {\n throw new Error(\n await readErrorMessage(authResponse, 'Failed to authenticate direct ImageKit upload.'),\n )\n }\n\n const auth = (await authResponse.json()) as ImageKitAuthResponse\n const safeFilename =\n typeof auth.fileName === 'string' && auth.fileName.length > 0 ? auth.fileName : file.name\n const formData = new FormData()\n\n formData.set('expire', String(auth.expire))\n formData.set('file', file)\n formData.set('fileName', safeFilename)\n formData.set('folder', folder)\n formData.set('publicKey', publicKey)\n formData.set('signature', auth.signature)\n formData.set('token', auth.token)\n formData.set('useUniqueFileName', 'false')\n\n if (isPrivate) {\n // ImageKit's upload signature only covers `token` + `expire`, so adding\n // this flag does not invalidate the signed auth parameters.\n formData.set('isPrivateFile', 'true')\n }\n\n const uploadResponse = await fetch(uploadAPIURL, {\n body: formData,\n method: 'POST',\n })\n\n if (!uploadResponse.ok) {\n throw new Error(\n await readErrorMessage(uploadResponse, 'Failed to upload file to ImageKit.'),\n )\n }\n\n const uploaded = (await uploadResponse.json()) as ImageKitUploadResponse\n const uploadedFilename =\n typeof uploaded.name === 'string' && uploaded.name.length > 0\n ? uploaded.name\n : safeFilename\n\n updateFilename(uploadedFilename)\n\n return {\n name: uploadedFilename,\n fileId: uploaded.fileId,\n filePath: uploaded.filePath,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n },\n })\n }, [authPath, collectionSlug, config.routes.api, folder, isPrivate, publicKey, setUploadHandler])\n\n return null\n}\n"],"names":["useConfig","useUploadHandlers","formatAdminURL","useEffect","uploadAPIURL","readErrorMessage","response","fallback","result","json","message","trim","length","ImageKitUploadHandler","authPath","collectionSlug","folder","isPrivate","publicKey","config","setUploadHandler","authURL","apiRoute","routes","api","path","handler","file","updateFilename","authRequestURL","URL","window","location","origin","searchParams","set","name","authResponse","fetch","credentials","ok","Error","auth","safeFilename","fileName","formData","FormData","String","expire","signature","token","uploadResponse","body","method","uploaded","uploadedFilename","fileId","filePath","thumbnailUrl","url"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import type ImageKit from '@imagekit/nodejs';
|
|
|
2
2
|
import type { PayloadHandler, UploadCollectionSlug } from 'payload';
|
|
3
3
|
type CreateImageKitUploadAuthHandlerArgs = {
|
|
4
4
|
client: ImageKit;
|
|
5
|
-
collections: Partial<Record<UploadCollectionSlug,
|
|
5
|
+
collections: Partial<Record<UploadCollectionSlug, unknown>>;
|
|
6
6
|
};
|
|
7
7
|
export declare const createImageKitUploadAuthHandler: ({ client, collections, }: CreateImageKitUploadAuthHandlerArgs) => PayloadHandler;
|
|
8
8
|
export {};
|
|
@@ -2,7 +2,7 @@ import { getSafeFileName } from 'payload/internal';
|
|
|
2
2
|
export const createImageKitUploadAuthHandler = ({ client, collections })=>{
|
|
3
3
|
return async (req)=>{
|
|
4
4
|
const collectionSlug = req.routeParams?.collection;
|
|
5
|
-
if (typeof collectionSlug !== 'string' || !(collectionSlug in collections)
|
|
5
|
+
if (typeof collectionSlug !== 'string' || !(collectionSlug in collections)) {
|
|
6
6
|
return Response.json({
|
|
7
7
|
message: 'Collection not found.'
|
|
8
8
|
}, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { getSafeFileName } from 'payload/internal'\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n client: ImageKit\n collections: Partial<Record<UploadCollectionSlug,
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/createImageKitUploadAuthHandler.ts"],"sourcesContent":["import type ImageKit from '@imagekit/nodejs'\nimport type { PayloadHandler, UploadCollectionSlug } from 'payload'\n\nimport { getSafeFileName } from 'payload/internal'\n\ntype CreateImageKitUploadAuthHandlerArgs = {\n client: ImageKit\n collections: Partial<Record<UploadCollectionSlug, unknown>>\n}\n\nexport const createImageKitUploadAuthHandler = ({\n client,\n collections,\n}: CreateImageKitUploadAuthHandlerArgs): PayloadHandler => {\n return async (req) => {\n const collectionSlug = req.routeParams?.collection\n\n if (typeof collectionSlug !== 'string' || !(collectionSlug in collections)) {\n return Response.json({ message: 'Collection not found.' }, { status: 404 })\n }\n\n if (!req.user) {\n return Response.json({ message: 'Unauthorized.' }, { status: 401 })\n }\n\n const authenticationParameters = client.helper.getAuthenticationParameters()\n const requestURL = new URL(req.url || 'http://localhost')\n const requestedFilename = requestURL.searchParams.get('filename')?.trim()\n const collection = req.payload.collections[collectionSlug]\n\n if (\n !requestedFilename ||\n typeof collection?.config.upload !== 'object' ||\n typeof collection.config.upload.staticDir !== 'string'\n ) {\n return Response.json(authenticationParameters, {\n headers: {\n 'Cache-Control': 'no-store',\n },\n })\n }\n\n const safeFilename = await getSafeFileName({\n collectionSlug,\n desiredFilename: requestedFilename,\n req,\n staticPath: collection.config.upload.staticDir,\n })\n\n return Response.json(\n { ...authenticationParameters, fileName: safeFilename },\n {\n headers: {\n 'Cache-Control': 'no-store',\n },\n },\n )\n }\n}\n"],"names":["getSafeFileName","createImageKitUploadAuthHandler","client","collections","req","collectionSlug","routeParams","collection","Response","json","message","status","user","authenticationParameters","helper","getAuthenticationParameters","requestURL","URL","url","requestedFilename","searchParams","get","trim","payload","config","upload","staticDir","headers","safeFilename","desiredFilename","staticPath","fileName"],"mappings":"AAGA,SAASA,eAAe,QAAQ,mBAAkB;AAOlD,OAAO,MAAMC,kCAAkC,CAAC,EAC9CC,MAAM,EACNC,WAAW,EACyB;IACpC,OAAO,OAAOC;QACZ,MAAMC,iBAAiBD,IAAIE,WAAW,EAAEC;QAExC,IAAI,OAAOF,mBAAmB,YAAY,CAAEA,CAAAA,kBAAkBF,WAAU,GAAI;YAC1E,OAAOK,SAASC,IAAI,CAAC;gBAAEC,SAAS;YAAwB,GAAG;gBAAEC,QAAQ;YAAI;QAC3E;QAEA,IAAI,CAACP,IAAIQ,IAAI,EAAE;YACb,OAAOJ,SAASC,IAAI,CAAC;gBAAEC,SAAS;YAAgB,GAAG;gBAAEC,QAAQ;YAAI;QACnE;QAEA,MAAME,2BAA2BX,OAAOY,MAAM,CAACC,2BAA2B;QAC1E,MAAMC,aAAa,IAAIC,IAAIb,IAAIc,GAAG,IAAI;QACtC,MAAMC,oBAAoBH,WAAWI,YAAY,CAACC,GAAG,CAAC,aAAaC;QACnE,MAAMf,aAAaH,IAAImB,OAAO,CAACpB,WAAW,CAACE,eAAe;QAE1D,IACE,CAACc,qBACD,OAAOZ,YAAYiB,OAAOC,WAAW,YACrC,OAAOlB,WAAWiB,MAAM,CAACC,MAAM,CAACC,SAAS,KAAK,UAC9C;YACA,OAAOlB,SAASC,IAAI,CAACI,0BAA0B;gBAC7Cc,SAAS;oBACP,iBAAiB;gBACnB;YACF;QACF;QAEA,MAAMC,eAAe,MAAM5B,gBAAgB;YACzCK;YACAwB,iBAAiBV;YACjBf;YACA0B,YAAYvB,WAAWiB,MAAM,CAACC,MAAM,CAACC,SAAS;QAChD;QAEA,OAAOlB,SAASC,IAAI,CAClB;YAAE,GAAGI,wBAAwB;YAAEkB,UAAUH;QAAa,GACtD;YACED,SAAS;gBACP,iBAAiB;YACnB;QACF;IAEJ;AACF,EAAC"}
|
|
@@ -1 +1,12 @@
|
|
|
1
|
+
export declare const isVideoOrPdfMimeType: (mimeType?: string) => boolean;
|
|
1
2
|
export declare const getAdminThumbnailURL: (doc: unknown) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Admin thumbnail URL for a private file.
|
|
5
|
+
*
|
|
6
|
+
* Private files 401 on their plain ImageKit URL, so the preview must be served
|
|
7
|
+
* through Payload's own file route (`<apiRoute>/<collection>/file/<filename>`).
|
|
8
|
+
* That route enforces collection access control and the adapter's `staticHandler`
|
|
9
|
+
* signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for
|
|
10
|
+
* videos and PDFs, derived from the document's mimeType).
|
|
11
|
+
*/
|
|
12
|
+
export declare const getPrivateAdminThumbnailURL: (doc: unknown, collectionSlug: string, apiRoute: string) => string;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
export const isVideoOrPdfMimeType = (mimeType)=>{
|
|
2
|
+
const isVideo = mimeType?.startsWith('video');
|
|
3
|
+
const isPDF = mimeType === 'application/pdf' || mimeType?.endsWith('/pdf');
|
|
4
|
+
return Boolean(isVideo || isPDF);
|
|
5
|
+
};
|
|
1
6
|
export const getAdminThumbnailURL = (doc)=>{
|
|
2
7
|
const file = doc;
|
|
3
8
|
const baseURL = file.imagekit?.url;
|
|
4
9
|
if (!baseURL) {
|
|
5
10
|
return '';
|
|
6
11
|
}
|
|
7
|
-
const isVideo = file.mimeType?.startsWith('video');
|
|
8
|
-
const isPDF = file.mimeType === 'application/pdf' || file.mimeType?.endsWith('/pdf');
|
|
9
12
|
let thumbnailBaseURL = baseURL;
|
|
10
|
-
if (
|
|
13
|
+
if (isVideoOrPdfMimeType(file.mimeType)) {
|
|
11
14
|
try {
|
|
12
15
|
const parsedURL = new URL(baseURL);
|
|
13
16
|
thumbnailBaseURL = `${parsedURL.origin}${parsedURL.pathname}/ik-thumbnail.jpg`;
|
|
@@ -18,5 +21,21 @@ export const getAdminThumbnailURL = (doc)=>{
|
|
|
18
21
|
}
|
|
19
22
|
return `${thumbnailBaseURL}${thumbnailBaseURL.includes('?') ? '&' : '?'}tr=w-300,h-300`;
|
|
20
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Admin thumbnail URL for a private file.
|
|
26
|
+
*
|
|
27
|
+
* Private files 401 on their plain ImageKit URL, so the preview must be served
|
|
28
|
+
* through Payload's own file route (`<apiRoute>/<collection>/file/<filename>`).
|
|
29
|
+
* That route enforces collection access control and the adapter's `staticHandler`
|
|
30
|
+
* signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for
|
|
31
|
+
* videos and PDFs, derived from the document's mimeType).
|
|
32
|
+
*/ export const getPrivateAdminThumbnailURL = (doc, collectionSlug, apiRoute)=>{
|
|
33
|
+
const filename = doc?.filename;
|
|
34
|
+
if (!filename) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const base = `${apiRoute.replace(/\/+$/, '')}/${collectionSlug}/file/${encodeURIComponent(filename)}`;
|
|
38
|
+
return `${base}?tr=w-300,h-300`;
|
|
39
|
+
};
|
|
21
40
|
|
|
22
41
|
//# sourceMappingURL=getAdminThumbnailURL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/getAdminThumbnailURL.ts"],"sourcesContent":["type ImagekitReference = {\n imagekit?: {\n url?: string\n }\n}\n\nexport const getAdminThumbnailURL = (doc: unknown): string => {\n const file = doc as { mimeType?: string } & ImagekitReference\n const baseURL = file.imagekit?.url\n\n if (!baseURL) {\n return ''\n }\n\n
|
|
1
|
+
{"version":3,"sources":["../src/getAdminThumbnailURL.ts"],"sourcesContent":["type ImagekitReference = {\n imagekit?: {\n url?: string\n }\n}\n\nexport const isVideoOrPdfMimeType = (mimeType?: string): boolean => {\n const isVideo = mimeType?.startsWith('video')\n const isPDF = mimeType === 'application/pdf' || mimeType?.endsWith('/pdf')\n\n return Boolean(isVideo || isPDF)\n}\n\nexport const getAdminThumbnailURL = (doc: unknown): string => {\n const file = doc as { mimeType?: string } & ImagekitReference\n const baseURL = file.imagekit?.url\n\n if (!baseURL) {\n return ''\n }\n\n let thumbnailBaseURL = baseURL\n\n if (isVideoOrPdfMimeType(file.mimeType)) {\n try {\n const parsedURL = new URL(baseURL)\n thumbnailBaseURL = `${parsedURL.origin}${parsedURL.pathname}/ik-thumbnail.jpg`\n } catch {\n const normalizedBaseURL = baseURL.split('?')[0].split('#')[0]\n thumbnailBaseURL = `${normalizedBaseURL}/ik-thumbnail.jpg`\n }\n }\n\n return `${thumbnailBaseURL}${thumbnailBaseURL.includes('?') ? '&' : '?'}tr=w-300,h-300`\n}\n\n/**\n * Admin thumbnail URL for a private file.\n *\n * Private files 401 on their plain ImageKit URL, so the preview must be served\n * through Payload's own file route (`<apiRoute>/<collection>/file/<filename>`).\n * That route enforces collection access control and the adapter's `staticHandler`\n * signs the ImageKit URL on demand (including the `/ik-thumbnail.jpg` segment for\n * videos and PDFs, derived from the document's mimeType).\n */\nexport const getPrivateAdminThumbnailURL = (\n doc: unknown,\n collectionSlug: string,\n apiRoute: string,\n): string => {\n const filename = (doc as { filename?: string } | undefined)?.filename\n\n if (!filename) {\n return ''\n }\n\n const base = `${apiRoute.replace(/\\/+$/, '')}/${collectionSlug}/file/${encodeURIComponent(filename)}`\n\n return `${base}?tr=w-300,h-300`\n}\n"],"names":["isVideoOrPdfMimeType","mimeType","isVideo","startsWith","isPDF","endsWith","Boolean","getAdminThumbnailURL","doc","file","baseURL","imagekit","url","thumbnailBaseURL","parsedURL","URL","origin","pathname","normalizedBaseURL","split","includes","getPrivateAdminThumbnailURL","collectionSlug","apiRoute","filename","base","replace","encodeURIComponent"],"mappings":"AAMA,OAAO,MAAMA,uBAAuB,CAACC;IACnC,MAAMC,UAAUD,UAAUE,WAAW;IACrC,MAAMC,QAAQH,aAAa,qBAAqBA,UAAUI,SAAS;IAEnE,OAAOC,QAAQJ,WAAWE;AAC5B,EAAC;AAED,OAAO,MAAMG,uBAAuB,CAACC;IACnC,MAAMC,OAAOD;IACb,MAAME,UAAUD,KAAKE,QAAQ,EAAEC;IAE/B,IAAI,CAACF,SAAS;QACZ,OAAO;IACT;IAEA,IAAIG,mBAAmBH;IAEvB,IAAIV,qBAAqBS,KAAKR,QAAQ,GAAG;QACvC,IAAI;YACF,MAAMa,YAAY,IAAIC,IAAIL;YAC1BG,mBAAmB,GAAGC,UAAUE,MAAM,GAAGF,UAAUG,QAAQ,CAAC,iBAAiB,CAAC;QAChF,EAAE,OAAM;YACN,MAAMC,oBAAoBR,QAAQS,KAAK,CAAC,IAAI,CAAC,EAAE,CAACA,KAAK,CAAC,IAAI,CAAC,EAAE;YAC7DN,mBAAmB,GAAGK,kBAAkB,iBAAiB,CAAC;QAC5D;IACF;IAEA,OAAO,GAAGL,mBAAmBA,iBAAiBO,QAAQ,CAAC,OAAO,MAAM,IAAI,cAAc,CAAC;AACzF,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,MAAMC,8BAA8B,CACzCb,KACAc,gBACAC;IAEA,MAAMC,WAAYhB,KAA2CgB;IAE7D,IAAI,CAACA,UAAU;QACb,OAAO;IACT;IAEA,MAAMC,OAAO,GAAGF,SAASG,OAAO,CAAC,QAAQ,IAAI,CAAC,EAAEJ,eAAe,MAAM,EAAEK,mBAAmBH,WAAW;IAErG,OAAO,GAAGC,KAAK,eAAe,CAAC;AACjC,EAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Config, UploadCollectionSlug } from 'payload';
|
|
2
|
-
type
|
|
2
|
+
export type ImageKitCollectionOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Upload files in this collection as ImageKit private files (`isPrivateFile: true`)
|
|
5
|
+
* and deliver them only through short-lived signed URLs, behind Payload access control.
|
|
6
|
+
*/
|
|
7
|
+
private?: boolean;
|
|
8
|
+
};
|
|
9
|
+
type ImageKitCollectionConfig = ImageKitCollectionOptions | true;
|
|
10
|
+
type ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>;
|
|
3
11
|
export type PayloadStorageImagekitConfig = {
|
|
4
12
|
alwaysInsertFields?: boolean;
|
|
5
13
|
clientUploads?: boolean;
|
|
@@ -9,6 +17,12 @@ export type PayloadStorageImagekitConfig = {
|
|
|
9
17
|
folder: string;
|
|
10
18
|
privateKey: string;
|
|
11
19
|
publicKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Expiry (in seconds) applied to signed URLs generated for private collections.
|
|
22
|
+
* Defaults to 900 (15 minutes). URLs are signed fresh on every request, so a short
|
|
23
|
+
* window is safe.
|
|
24
|
+
*/
|
|
25
|
+
signedUrlExpiresIn?: number;
|
|
12
26
|
urlEndpoint: string;
|
|
13
27
|
};
|
|
14
28
|
export declare const imagekitStorage: (pluginOptions: PayloadStorageImagekitConfig) => (config: Config) => Config;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,19 @@ import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getImageKitUploadAuthPath, isImageKitClientUploadContext } from './clientUploads.js';
|
|
5
5
|
import { createImageKitUploadAuthHandler } from './endpoints/createImageKitUploadAuthHandler.js';
|
|
6
|
-
import { getAdminThumbnailURL } from './getAdminThumbnailURL.js';
|
|
6
|
+
import { getAdminThumbnailURL, getPrivateAdminThumbnailURL, isVideoOrPdfMimeType } from './getAdminThumbnailURL.js';
|
|
7
|
+
const DEFAULT_SIGNED_URL_EXPIRES_IN = 900;
|
|
8
|
+
const PRIVATE_COLLECTION_DESCRIPTION = 'Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.';
|
|
9
|
+
const isPrivateCollectionConfig = (value)=>value !== true && typeof value === 'object' && value?.private === true;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a specific document is private. The flag stored on the document at upload time
|
|
12
|
+
* (`imagekit.isPrivate`) is the source of truth — a file's privacy on ImageKit is fixed
|
|
13
|
+
* when it is uploaded and does not change if the collection's `private` setting changes
|
|
14
|
+
* later. Falls back to the collection setting for documents uploaded before this flag existed.
|
|
15
|
+
*/ const isDocumentPrivate = (doc, collectionIsPrivate)=>{
|
|
16
|
+
const stored = doc?.imagekit?.isPrivate;
|
|
17
|
+
return typeof stored === 'boolean' ? stored : collectionIsPrivate;
|
|
18
|
+
};
|
|
7
19
|
const normalizeFolder = (folder)=>{
|
|
8
20
|
const normalized = folder.trim().replace(/^\/+|\/+$/g, '');
|
|
9
21
|
if (!normalized) {
|
|
@@ -35,20 +47,50 @@ const findImageKitFileId = async (imagekit, filePath)=>{
|
|
|
35
47
|
return undefined;
|
|
36
48
|
};
|
|
37
49
|
const isRecord = (value)=>!!value && typeof value === 'object';
|
|
38
|
-
const
|
|
50
|
+
const getStoredImageKitURL = (doc)=>{
|
|
51
|
+
const storedURL = doc?.imagekit?.url;
|
|
52
|
+
return typeof storedURL === 'string' && storedURL.trim().length > 0 ? storedURL : undefined;
|
|
53
|
+
};
|
|
54
|
+
const findUploadDocumentByFilename = async ({ collection, filename, prefix, req })=>{
|
|
55
|
+
const constraints = [
|
|
56
|
+
{
|
|
57
|
+
filename: {
|
|
58
|
+
equals: filename
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
if (typeof prefix === 'string') {
|
|
63
|
+
constraints.push({
|
|
64
|
+
prefix: {
|
|
65
|
+
equals: prefix
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return req.payload.db.findOne({
|
|
70
|
+
collection,
|
|
71
|
+
req,
|
|
72
|
+
where: {
|
|
73
|
+
and: constraints
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const applyClientUploadContextToData = (data, clientUploadContext, buildThumbnailURL = getAdminThumbnailURL, isPrivate = false)=>{
|
|
39
78
|
if (!isImageKitClientUploadContext(clientUploadContext)) {
|
|
40
79
|
return data;
|
|
41
80
|
}
|
|
42
81
|
data.imagekit = {
|
|
43
82
|
fileId: clientUploadContext.fileId,
|
|
83
|
+
// Record the collection's privacy at upload time; this is the file's permanent
|
|
84
|
+
// ImageKit status and the source of truth for delivery.
|
|
85
|
+
isPrivate,
|
|
44
86
|
thumbnailUrl: clientUploadContext.thumbnailUrl,
|
|
45
87
|
url: clientUploadContext.url
|
|
46
88
|
};
|
|
47
89
|
// Preserve the admin preview when the original file was already uploaded client-side.
|
|
48
|
-
data.thumbnailURL =
|
|
90
|
+
data.thumbnailURL = buildThumbnailURL(data);
|
|
49
91
|
return data;
|
|
50
92
|
};
|
|
51
|
-
const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
93
|
+
const imagekitAdapter = ({ apiRoute, client, clientUploads, privateSlugs, rootFolder, signedUrlExpiresIn, urlEndpoint })=>{
|
|
52
94
|
const fetchUploadedFile = async (fileURL)=>{
|
|
53
95
|
const response = await fetch(fileURL);
|
|
54
96
|
if (!response.ok) {
|
|
@@ -67,7 +109,24 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
67
109
|
statusText: response.statusText
|
|
68
110
|
});
|
|
69
111
|
};
|
|
70
|
-
return ({ collection })=>
|
|
112
|
+
return ({ collection })=>{
|
|
113
|
+
const isPrivate = privateSlugs.has(collection.slug);
|
|
114
|
+
// Sign a relative ImageKit path against the configured urlEndpoint. We always sign
|
|
115
|
+
// a computed path (never a stored absolute URL) because ImageKit's signature is
|
|
116
|
+
// derived from the path after stripping urlEndpoint, so the host must match.
|
|
117
|
+
const buildSignedURL = (src, tr)=>client.helper.buildSrc({
|
|
118
|
+
expiresIn: signedUrlExpiresIn,
|
|
119
|
+
signed: true,
|
|
120
|
+
src,
|
|
121
|
+
transformation: tr ? [
|
|
122
|
+
{
|
|
123
|
+
raw: tr
|
|
124
|
+
}
|
|
125
|
+
] : undefined,
|
|
126
|
+
urlEndpoint
|
|
127
|
+
});
|
|
128
|
+
const buildThumbnailURL = (doc)=>isDocumentPrivate(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc);
|
|
129
|
+
return {
|
|
71
130
|
name: 'imagekit',
|
|
72
131
|
clientUploads,
|
|
73
132
|
fields: [
|
|
@@ -93,6 +152,46 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
93
152
|
readOnly: true
|
|
94
153
|
}
|
|
95
154
|
},
|
|
155
|
+
{
|
|
156
|
+
name: 'isPrivate',
|
|
157
|
+
type: 'checkbox',
|
|
158
|
+
admin: {
|
|
159
|
+
description: 'Whether this file is stored as a private file on ImageKit. Set at upload time from the collection setting and fixed for the life of the file — re-upload to change it.',
|
|
160
|
+
readOnly: true
|
|
161
|
+
},
|
|
162
|
+
label: 'Private file'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'signedUrl',
|
|
166
|
+
type: 'text',
|
|
167
|
+
admin: {
|
|
168
|
+
// Only relevant for private files; the plain `url` works directly for public ones.
|
|
169
|
+
condition: (_data, siblingData)=>isDocumentPrivate({
|
|
170
|
+
imagekit: siblingData
|
|
171
|
+
}, isPrivate),
|
|
172
|
+
description: `Temporary signed URL for reaching this private file directly on ImageKit. Expires ${signedUrlExpiresIn} seconds after load and is regenerated on every read. Note: anyone with this URL can access the file until it expires — it is not gated by Payload access control.`,
|
|
173
|
+
readOnly: true
|
|
174
|
+
},
|
|
175
|
+
hooks: {
|
|
176
|
+
// Computed server-side on read so it is always freshly signed and never persisted.
|
|
177
|
+
afterRead: [
|
|
178
|
+
({ data, siblingData })=>{
|
|
179
|
+
if (!isDocumentPrivate({
|
|
180
|
+
imagekit: siblingData
|
|
181
|
+
}, isPrivate)) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const filename = data?.filename;
|
|
185
|
+
if (!filename) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return buildSignedURL(getFilePath(rootFolder, collection.slug, filename));
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
label: 'Signed URL',
|
|
193
|
+
virtual: true
|
|
194
|
+
},
|
|
96
195
|
{
|
|
97
196
|
name: 'thumbnailUrl',
|
|
98
197
|
type: 'text',
|
|
@@ -118,7 +217,7 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
118
217
|
},
|
|
119
218
|
handleUpload: async ({ clientUploadContext, data, file })=>{
|
|
120
219
|
if (isImageKitClientUploadContext(clientUploadContext)) {
|
|
121
|
-
return applyClientUploadContextToData(data, clientUploadContext);
|
|
220
|
+
return applyClientUploadContextToData(data, clientUploadContext, buildThumbnailURL, isPrivate);
|
|
122
221
|
}
|
|
123
222
|
const uploadFile = await toFile(file.buffer, file.filename, {
|
|
124
223
|
type: file.mimeType
|
|
@@ -127,15 +226,24 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
127
226
|
file: uploadFile,
|
|
128
227
|
fileName: file.filename,
|
|
129
228
|
folder: getCollectionFolder(rootFolder, collection.slug),
|
|
130
|
-
useUniqueFileName: false
|
|
229
|
+
useUniqueFileName: false,
|
|
230
|
+
...isPrivate ? {
|
|
231
|
+
isPrivateFile: true
|
|
232
|
+
} : {}
|
|
131
233
|
});
|
|
132
234
|
data.imagekit = {
|
|
133
235
|
fileId: uploaded.fileId,
|
|
236
|
+
isPrivate,
|
|
134
237
|
thumbnailUrl: uploaded.thumbnailUrl,
|
|
135
238
|
url: uploaded.url
|
|
136
239
|
};
|
|
137
|
-
// Set the root-level thumbnailURL that Payload's admin interface expects
|
|
138
|
-
|
|
240
|
+
// Set the root-level thumbnailURL that Payload's admin interface expects.
|
|
241
|
+
// `buildThumbnailURL` needs the filename, which Payload may not have copied onto
|
|
242
|
+
// `data` yet at this point, so fall back to the uploaded file's name.
|
|
243
|
+
data.thumbnailURL = buildThumbnailURL({
|
|
244
|
+
...data,
|
|
245
|
+
filename: data.filename || file.filename
|
|
246
|
+
});
|
|
139
247
|
return data;
|
|
140
248
|
},
|
|
141
249
|
staticHandler: async (req, args)=>{
|
|
@@ -146,14 +254,32 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
146
254
|
const clientUploadContext = args.params.clientUploadContext;
|
|
147
255
|
const clientUploadURL = isImageKitClientUploadContext(clientUploadContext) && typeof clientUploadContext.url === 'string' && clientUploadContext.url.length > 0 ? clientUploadContext.url : null;
|
|
148
256
|
const filePath = isImageKitClientUploadContext(clientUploadContext) && typeof clientUploadContext.filePath === 'string' && clientUploadContext.filePath.length > 0 ? clientUploadContext.filePath : getFilePath(rootFolder, args.params.collection, args.params.filename);
|
|
149
|
-
const imagekitURL = clientUploadURL || client.helper.buildSrc({
|
|
257
|
+
const imagekitURL = isPrivate ? buildSignedURL(filePath, tr) : clientUploadURL || client.helper.buildSrc({
|
|
150
258
|
src: filePath,
|
|
151
259
|
urlEndpoint
|
|
152
260
|
});
|
|
153
261
|
return fetchUploadedFile(imagekitURL);
|
|
154
262
|
}
|
|
263
|
+
const doc = args.doc || await findUploadDocumentByFilename({
|
|
264
|
+
collection: args.params.collection,
|
|
265
|
+
filename: args.params.filename,
|
|
266
|
+
prefix: args.params.prefix,
|
|
267
|
+
req
|
|
268
|
+
});
|
|
269
|
+
// Sign based on the document's stored status, not the collection's current
|
|
270
|
+
// setting, so files keep working if the collection's privacy is changed later.
|
|
271
|
+
if (isDocumentPrivate(doc, isPrivate)) {
|
|
272
|
+
const mimeType = doc?.mimeType;
|
|
273
|
+
let signSrc = getFilePath(rootFolder, args.params.collection, args.params.filename);
|
|
274
|
+
// A `tr` request for a video/PDF is a thumbnail request (the plain transform
|
|
275
|
+
// can't render a preview); serve ImageKit's generated thumbnail instead.
|
|
276
|
+
if (tr && isVideoOrPdfMimeType(mimeType)) {
|
|
277
|
+
signSrc = `${signSrc}/ik-thumbnail.jpg`;
|
|
278
|
+
}
|
|
279
|
+
return Response.redirect(buildSignedURL(signSrc, tr), 302);
|
|
280
|
+
}
|
|
155
281
|
const imagekitURL = client.helper.buildSrc({
|
|
156
|
-
src: getFilePath(rootFolder, args.params.collection, args.params.filename),
|
|
282
|
+
src: getStoredImageKitURL(doc) || getFilePath(rootFolder, args.params.collection, args.params.filename),
|
|
157
283
|
transformation: tr ? [
|
|
158
284
|
{
|
|
159
285
|
raw: tr
|
|
@@ -163,21 +289,23 @@ const imagekitAdapter = ({ client, clientUploads, rootFolder, urlEndpoint })=>{
|
|
|
163
289
|
});
|
|
164
290
|
return Response.redirect(imagekitURL, 302);
|
|
165
291
|
}
|
|
166
|
-
}
|
|
292
|
+
};
|
|
293
|
+
};
|
|
167
294
|
};
|
|
168
|
-
const toCloudStorageCollections = (collections, adapter, disablePayloadAccessControl)=>{
|
|
295
|
+
const toCloudStorageCollections = (collections, adapter, privateSlugs, disablePayloadAccessControl)=>{
|
|
169
296
|
const mappedCollections = {};
|
|
170
297
|
for (const collectionSlug of Object.keys(collections)){
|
|
171
298
|
mappedCollections[collectionSlug] = {
|
|
172
299
|
adapter,
|
|
173
300
|
disableLocalStorage: true,
|
|
174
|
-
|
|
301
|
+
// Private files require Payload access control so URLs can be signed at serve time.
|
|
302
|
+
disablePayloadAccessControl: privateSlugs.has(collectionSlug) ? undefined : disablePayloadAccessControl
|
|
175
303
|
};
|
|
176
304
|
}
|
|
177
305
|
return mappedCollections;
|
|
178
306
|
};
|
|
179
307
|
export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
180
|
-
const { alwaysInsertFields, clientUploads, collections, disablePayloadAccessControl, enabled, folder, privateKey, publicKey, urlEndpoint } = pluginOptions;
|
|
308
|
+
const { alwaysInsertFields, clientUploads, collections, disablePayloadAccessControl, enabled, folder, privateKey, publicKey, signedUrlExpiresIn: signedUrlExpiresInOption, urlEndpoint } = pluginOptions;
|
|
181
309
|
if (!privateKey?.trim()) {
|
|
182
310
|
throw new Error('imagekitStorage: `privateKey` is required.');
|
|
183
311
|
}
|
|
@@ -190,14 +318,23 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
|
190
318
|
if (!collections || Object.keys(collections).length === 0) {
|
|
191
319
|
throw new Error('imagekitStorage: `collections` must include at least one upload collection.');
|
|
192
320
|
}
|
|
321
|
+
const privateSlugs = new Set(Object.entries(collections).filter(([, value])=>isPrivateCollectionConfig(value)).map(([slug])=>slug));
|
|
322
|
+
if (disablePayloadAccessControl && privateSlugs.size > 0) {
|
|
323
|
+
throw new Error('imagekitStorage: `disablePayloadAccessControl` cannot be used with private collections. Private files require Payload access control to remain enabled so URLs can be signed at serve time.');
|
|
324
|
+
}
|
|
325
|
+
const signedUrlExpiresIn = typeof signedUrlExpiresInOption === 'number' && signedUrlExpiresInOption > 0 ? signedUrlExpiresInOption : DEFAULT_SIGNED_URL_EXPIRES_IN;
|
|
326
|
+
const apiRoute = config.routes?.api ?? '/api';
|
|
193
327
|
const rootFolder = normalizeFolder(folder);
|
|
194
328
|
const imagekitClient = new ImageKit({
|
|
195
329
|
privateKey
|
|
196
330
|
});
|
|
197
331
|
const adapter = imagekitAdapter({
|
|
332
|
+
apiRoute,
|
|
198
333
|
client: imagekitClient,
|
|
199
334
|
clientUploads,
|
|
335
|
+
privateSlugs,
|
|
200
336
|
rootFolder,
|
|
337
|
+
signedUrlExpiresIn,
|
|
201
338
|
urlEndpoint
|
|
202
339
|
});
|
|
203
340
|
const configuredSlugs = new Set(Object.keys(collections));
|
|
@@ -207,23 +344,21 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
|
207
344
|
if (!configuredSlugs.has(collection.slug)) {
|
|
208
345
|
return collection;
|
|
209
346
|
}
|
|
347
|
+
const isCollectionPrivate = privateSlugs.has(collection.slug);
|
|
348
|
+
const buildThumbnailURL = (doc)=>isDocumentPrivate(doc, isCollectionPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc);
|
|
210
349
|
const beforeDocumentControls = collection.admin?.components?.edit?.beforeDocumentControls || [];
|
|
350
|
+
// Surface a "this is a private collection" note in the list and edit views.
|
|
351
|
+
// Preserve an existing string description by appending; leave custom
|
|
352
|
+
// component descriptions untouched.
|
|
353
|
+
const existingDescription = collection.admin?.description;
|
|
354
|
+
const baseAdmin = isCollectionPrivate ? {
|
|
355
|
+
...collection.admin || {},
|
|
356
|
+
description: typeof existingDescription === 'string' && existingDescription.trim().length > 0 ? `${existingDescription} ${PRIVATE_COLLECTION_DESCRIPTION}` : existingDescription ?? PRIVATE_COLLECTION_DESCRIPTION
|
|
357
|
+
} : collection.admin;
|
|
211
358
|
return {
|
|
212
359
|
...collection,
|
|
213
|
-
hooks: {
|
|
214
|
-
...collection.hooks || {},
|
|
215
|
-
beforeChange: [
|
|
216
|
-
...collection.hooks?.beforeChange || [],
|
|
217
|
-
({ data, req })=>{
|
|
218
|
-
if (!isRecord(data) || !req.file?.clientUploadContext) {
|
|
219
|
-
return data;
|
|
220
|
-
}
|
|
221
|
-
return applyClientUploadContextToData(data, req.file.clientUploadContext);
|
|
222
|
-
}
|
|
223
|
-
]
|
|
224
|
-
},
|
|
225
360
|
admin: clientUploads ? {
|
|
226
|
-
...
|
|
361
|
+
...baseAdmin || {},
|
|
227
362
|
components: {
|
|
228
363
|
...collection.admin?.components || {},
|
|
229
364
|
edit: {
|
|
@@ -235,6 +370,7 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
|
235
370
|
authPath: getImageKitUploadAuthPath(collection.slug),
|
|
236
371
|
collectionSlug: collection.slug,
|
|
237
372
|
folder: getCollectionFolder(rootFolder, collection.slug),
|
|
373
|
+
isPrivate: isCollectionPrivate,
|
|
238
374
|
publicKey
|
|
239
375
|
},
|
|
240
376
|
path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler'
|
|
@@ -242,10 +378,22 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
|
242
378
|
]
|
|
243
379
|
}
|
|
244
380
|
}
|
|
245
|
-
} :
|
|
381
|
+
} : baseAdmin,
|
|
382
|
+
hooks: {
|
|
383
|
+
...collection.hooks || {},
|
|
384
|
+
beforeChange: [
|
|
385
|
+
...collection.hooks?.beforeChange || [],
|
|
386
|
+
({ data, req })=>{
|
|
387
|
+
if (!isRecord(data) || !req.file?.clientUploadContext) {
|
|
388
|
+
return data;
|
|
389
|
+
}
|
|
390
|
+
return applyClientUploadContextToData(data, req.file.clientUploadContext, buildThumbnailURL, isCollectionPrivate);
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
},
|
|
246
394
|
upload: {
|
|
247
395
|
...typeof collection.upload === 'object' ? collection.upload : {},
|
|
248
|
-
adminThumbnail: ({ doc })=>
|
|
396
|
+
adminThumbnail: ({ doc })=>buildThumbnailURL(doc),
|
|
249
397
|
crop: false
|
|
250
398
|
}
|
|
251
399
|
};
|
|
@@ -264,7 +412,7 @@ export const imagekitStorage = (pluginOptions)=>(config)=>{
|
|
|
264
412
|
};
|
|
265
413
|
return cloudStoragePlugin({
|
|
266
414
|
alwaysInsertFields,
|
|
267
|
-
collections: toCloudStorageCollections(collections, adapter, disablePayloadAccessControl),
|
|
415
|
+
collections: toCloudStorageCollections(collections, adapter, privateSlugs, disablePayloadAccessControl),
|
|
268
416
|
enabled
|
|
269
417
|
})(configWithCropDisabled);
|
|
270
418
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Adapter, CollectionOptions } from '@payloadcms/plugin-cloud-storage/types'\nimport type { CollectionSlug, Config, UploadCollectionSlug } from 'payload'\n\nimport ImageKit, { toFile } from '@imagekit/nodejs'\nimport { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'\nimport path from 'node:path'\n\nimport { getImageKitUploadAuthPath, isImageKitClientUploadContext } from './clientUploads.js'\nimport { createImageKitUploadAuthHandler } from './endpoints/createImageKitUploadAuthHandler.js'\nimport { getAdminThumbnailURL } from './getAdminThumbnailURL.js'\n\ntype ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, true>>\ntype ImagekitReference = {\n imagekit?: {\n fileId?: string\n thumbnailUrl?: string\n url?: string\n }\n}\n\ntype UploadDocumentData = ImagekitReference & {\n thumbnailURL?: string\n}\n\nexport type PayloadStorageImagekitConfig = {\n alwaysInsertFields?: boolean\n clientUploads?: boolean\n collections: ImageKitCollectionMap\n disablePayloadAccessControl?: true\n enabled?: boolean\n folder: string\n privateKey: string\n publicKey: string\n urlEndpoint: string\n}\n\nconst normalizeFolder = (folder: string): string => {\n const normalized = folder.trim().replace(/^\\/+|\\/+$/g, '')\n\n if (!normalized) {\n throw new Error('imagekitStorage: `folder` is required.')\n }\n\n return `/${normalized}`\n}\n\nconst getCollectionFolder = (rootFolder: string, collectionSlug: string): string =>\n path.posix.join(rootFolder, collectionSlug)\n\nconst getFilePath = (rootFolder: string, collectionSlug: string, filename: string): string =>\n path.posix.join(getCollectionFolder(rootFolder, collectionSlug), filename)\n\nconst findImageKitFileId = async (\n imagekit: ImageKit,\n filePath: string,\n): Promise<string | undefined> => {\n const parentPath = path.posix.dirname(filePath)\n const name = path.posix.basename(filePath)\n\n const limit = 100\n\n for (let skip = 0; ; skip += limit) {\n const assets = await imagekit.assets.list({\n type: 'file',\n limit,\n path: parentPath === '/' ? '/' : `${parentPath}/`,\n skip,\n })\n\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n break\n }\n }\n\n return undefined\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object'\n\nconst applyClientUploadContextToData = <T extends UploadDocumentData>(\n data: T,\n clientUploadContext: unknown,\n): T => {\n if (!isImageKitClientUploadContext(clientUploadContext)) {\n return data\n }\n\n data.imagekit = {\n fileId: clientUploadContext.fileId,\n thumbnailUrl: clientUploadContext.thumbnailUrl,\n url: clientUploadContext.url,\n }\n\n // Preserve the admin preview when the original file was already uploaded client-side.\n data.thumbnailURL = getAdminThumbnailURL(data)\n\n return data\n}\n\nconst imagekitAdapter = ({\n client,\n clientUploads,\n rootFolder,\n urlEndpoint,\n}: {\n client: ImageKit\n clientUploads?: boolean\n rootFolder: string\n urlEndpoint: string\n}): Adapter => {\n const fetchUploadedFile = async (fileURL: string): Promise<Response> => {\n const response = await fetch(fileURL)\n\n if (!response.ok) {\n throw new Error(\n `imagekitStorage: failed to fetch uploaded file from ImageKit (${response.status}).`,\n )\n }\n\n const fileBuffer = await response.arrayBuffer()\n const headers = new Headers()\n const contentType = response.headers.get('Content-Type')\n\n if (contentType) {\n headers.set('Content-Type', contentType)\n }\n\n headers.set('Content-Length', String(fileBuffer.byteLength))\n\n return new Response(fileBuffer, {\n headers,\n status: 200,\n statusText: response.statusText,\n })\n }\n\n return ({ collection }) => ({\n name: 'imagekit',\n clientUploads,\n fields: [\n {\n name: 'imagekit',\n type: 'group',\n admin: {\n disableListColumn: true,\n disableListFilter: true,\n },\n fields: [\n {\n name: 'fileId',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n {\n name: 'url',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'thumbnailUrl',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n ],\n interfaceName: 'imagekitFields',\n },\n ],\n generateURL: ({ filename }) =>\n client.helper.buildSrc({\n src: getFilePath(rootFolder, collection.slug, filename),\n urlEndpoint,\n }),\n handleDelete: async ({ doc, filename }) => {\n const storedFileId = (doc as ImagekitReference & typeof doc).imagekit?.fileId\n const fileId =\n storedFileId ||\n (await findImageKitFileId(client, getFilePath(rootFolder, collection.slug, filename)))\n\n if (!fileId) {\n return\n }\n\n await client.files.delete(fileId)\n },\n handleUpload: async ({ clientUploadContext, data, file }) => {\n if (isImageKitClientUploadContext(clientUploadContext)) {\n return applyClientUploadContextToData(data, clientUploadContext)\n }\n\n const uploadFile = await toFile(file.buffer, file.filename, { type: file.mimeType })\n\n const uploaded = await client.files.upload({\n file: uploadFile,\n fileName: file.filename,\n folder: getCollectionFolder(rootFolder, collection.slug),\n useUniqueFileName: false,\n })\n\n data.imagekit = {\n fileId: uploaded.fileId,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n\n // Set the root-level thumbnailURL that Payload's admin interface expects\n data.thumbnailURL = getAdminThumbnailURL(data)\n\n return data\n },\n staticHandler: async (req, args) => {\n const requestURL = new URL(req.url || 'http://localhost')\n const tr = requestURL.searchParams.get('tr')\n const hasClientUploadContext = 'clientUploadContext' in args.params\n\n if (hasClientUploadContext) {\n const clientUploadContext = args.params.clientUploadContext\n const clientUploadURL =\n isImageKitClientUploadContext(clientUploadContext) &&\n typeof clientUploadContext.url === 'string' &&\n clientUploadContext.url.length > 0\n ? clientUploadContext.url\n : null\n const filePath =\n isImageKitClientUploadContext(clientUploadContext) &&\n typeof clientUploadContext.filePath === 'string' &&\n clientUploadContext.filePath.length > 0\n ? clientUploadContext.filePath\n : getFilePath(rootFolder, args.params.collection, args.params.filename)\n const imagekitURL =\n clientUploadURL ||\n client.helper.buildSrc({\n src: filePath,\n urlEndpoint,\n })\n\n return fetchUploadedFile(imagekitURL)\n }\n\n const imagekitURL = client.helper.buildSrc({\n src: getFilePath(rootFolder, args.params.collection, args.params.filename),\n transformation: tr ? [{ raw: tr }] : undefined,\n urlEndpoint,\n })\n\n return Response.redirect(imagekitURL, 302)\n },\n })\n}\n\nconst toCloudStorageCollections = (\n collections: ImageKitCollectionMap,\n adapter: Adapter,\n disablePayloadAccessControl?: true,\n): Partial<Record<CollectionSlug, CollectionOptions>> => {\n const mappedCollections: Partial<Record<CollectionSlug, CollectionOptions>> = {}\n\n for (const collectionSlug of Object.keys(collections)) {\n mappedCollections[collectionSlug] = {\n adapter,\n disableLocalStorage: true,\n disablePayloadAccessControl,\n }\n }\n\n return mappedCollections\n}\n\nexport const imagekitStorage =\n (pluginOptions: PayloadStorageImagekitConfig) =>\n (config: Config): Config => {\n const {\n alwaysInsertFields,\n clientUploads,\n collections,\n disablePayloadAccessControl,\n enabled,\n folder,\n privateKey,\n publicKey,\n urlEndpoint,\n } = pluginOptions\n\n if (!privateKey?.trim()) {\n throw new Error('imagekitStorage: `privateKey` is required.')\n }\n\n if (!publicKey?.trim()) {\n throw new Error('imagekitStorage: `publicKey` is required.')\n }\n\n if (!urlEndpoint?.trim()) {\n throw new Error('imagekitStorage: `urlEndpoint` is required.')\n }\n\n if (!collections || Object.keys(collections).length === 0) {\n throw new Error('imagekitStorage: `collections` must include at least one upload collection.')\n }\n\n const rootFolder = normalizeFolder(folder)\n\n const imagekitClient = new ImageKit({ privateKey })\n const adapter = imagekitAdapter({\n client: imagekitClient,\n clientUploads,\n rootFolder,\n urlEndpoint,\n })\n\n const configuredSlugs = new Set(Object.keys(collections))\n const configWithCropDisabled: Config = {\n ...config,\n collections: (config.collections || []).map((collection) => {\n if (!configuredSlugs.has(collection.slug)) {\n return collection\n }\n\n const beforeDocumentControls =\n collection.admin?.components?.edit?.beforeDocumentControls || []\n\n return {\n ...collection,\n hooks: {\n ...(collection.hooks || {}),\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n ({ data, req }) => {\n if (!isRecord(data) || !req.file?.clientUploadContext) {\n return data\n }\n\n return applyClientUploadContextToData(data, req.file.clientUploadContext)\n },\n ],\n },\n admin: clientUploads\n ? {\n ...(collection.admin || {}),\n components: {\n ...(collection.admin?.components || {}),\n edit: {\n ...(collection.admin?.components?.edit || {}),\n beforeDocumentControls: [\n ...beforeDocumentControls,\n {\n clientProps: {\n authPath: getImageKitUploadAuthPath(collection.slug),\n collectionSlug: collection.slug,\n folder: getCollectionFolder(rootFolder, collection.slug),\n publicKey,\n },\n path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler',\n },\n ],\n },\n },\n }\n : collection.admin,\n upload: {\n ...(typeof collection.upload === 'object' ? collection.upload : {}),\n adminThumbnail: ({ doc }) => getAdminThumbnailURL(doc),\n crop: false,\n },\n }\n }),\n endpoints: clientUploads\n ? [\n ...(config.endpoints || []),\n {\n handler: createImageKitUploadAuthHandler({\n client: imagekitClient,\n collections,\n }),\n method: 'get',\n path: `${getImageKitUploadAuthPath(':collection')}`,\n },\n ]\n : config.endpoints,\n }\n\n return cloudStoragePlugin({\n alwaysInsertFields,\n collections: toCloudStorageCollections(collections, adapter, disablePayloadAccessControl),\n enabled,\n })(configWithCropDisabled)\n }\n\nexport { migrateFolder } from './migrate.js'\nexport type { MigrateFolderOptions, MigrationJobResult } from './migrate.js'\n\n// Backward-compatible alias\nexport const payloadStorageImagekit = imagekitStorage\n"],"names":["ImageKit","toFile","cloudStoragePlugin","path","getImageKitUploadAuthPath","isImageKitClientUploadContext","createImageKitUploadAuthHandler","getAdminThumbnailURL","normalizeFolder","folder","normalized","trim","replace","Error","getCollectionFolder","rootFolder","collectionSlug","posix","join","getFilePath","filename","findImageKitFileId","imagekit","filePath","parentPath","dirname","name","basename","limit","skip","assets","list","type","found","find","asset","fileId","length","undefined","isRecord","value","applyClientUploadContextToData","data","clientUploadContext","thumbnailUrl","url","thumbnailURL","imagekitAdapter","client","clientUploads","urlEndpoint","fetchUploadedFile","fileURL","response","fetch","ok","status","fileBuffer","arrayBuffer","headers","Headers","contentType","get","set","String","byteLength","Response","statusText","collection","fields","admin","disableListColumn","disableListFilter","hidden","readOnly","interfaceName","generateURL","helper","buildSrc","src","slug","handleDelete","doc","storedFileId","files","delete","handleUpload","file","uploadFile","buffer","mimeType","uploaded","upload","fileName","useUniqueFileName","staticHandler","req","args","requestURL","URL","tr","searchParams","hasClientUploadContext","params","clientUploadURL","imagekitURL","transformation","raw","redirect","toCloudStorageCollections","collections","adapter","disablePayloadAccessControl","mappedCollections","Object","keys","disableLocalStorage","imagekitStorage","pluginOptions","config","alwaysInsertFields","enabled","privateKey","publicKey","imagekitClient","configuredSlugs","Set","configWithCropDisabled","map","has","beforeDocumentControls","components","edit","hooks","beforeChange","clientProps","authPath","adminThumbnail","crop","endpoints","handler","method","migrateFolder","payloadStorageImagekit"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Adapter, CollectionOptions } from '@payloadcms/plugin-cloud-storage/types'\nimport type {\n CollectionSlug,\n Config,\n PayloadRequest,\n TypeWithID,\n UploadCollectionSlug,\n Where,\n} from 'payload'\n\nimport ImageKit, { toFile } from '@imagekit/nodejs'\nimport { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'\nimport path from 'node:path'\n\nimport { getImageKitUploadAuthPath, isImageKitClientUploadContext } from './clientUploads.js'\nimport { createImageKitUploadAuthHandler } from './endpoints/createImageKitUploadAuthHandler.js'\nimport {\n getAdminThumbnailURL,\n getPrivateAdminThumbnailURL,\n isVideoOrPdfMimeType,\n} from './getAdminThumbnailURL.js'\n\nexport type ImageKitCollectionOptions = {\n /**\n * Upload files in this collection as ImageKit private files (`isPrivateFile: true`)\n * and deliver them only through short-lived signed URLs, behind Payload access control.\n */\n private?: boolean\n}\ntype ImageKitCollectionConfig = ImageKitCollectionOptions | true\ntype ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>\n\nconst DEFAULT_SIGNED_URL_EXPIRES_IN = 900\n\nconst PRIVATE_COLLECTION_DESCRIPTION =\n 'Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.'\n\nconst isPrivateCollectionConfig = (value: ImageKitCollectionConfig | undefined): boolean =>\n value !== true && typeof value === 'object' && value?.private === true\n\n/**\n * Whether a specific document is private. The flag stored on the document at upload time\n * (`imagekit.isPrivate`) is the source of truth — a file's privacy on ImageKit is fixed\n * when it is uploaded and does not change if the collection's `private` setting changes\n * later. Falls back to the collection setting for documents uploaded before this flag existed.\n */\nconst isDocumentPrivate = (doc: unknown, collectionIsPrivate: boolean): boolean => {\n const stored = (doc as { imagekit?: { isPrivate?: boolean } } | undefined)?.imagekit?.isPrivate\n\n return typeof stored === 'boolean' ? stored : collectionIsPrivate\n}\ntype ImagekitReference = {\n imagekit?: {\n fileId?: string\n isPrivate?: boolean\n thumbnailUrl?: string\n url?: string\n }\n}\n\ntype UploadDocumentData = {\n thumbnailURL?: string\n} & ImagekitReference\n\nexport type PayloadStorageImagekitConfig = {\n alwaysInsertFields?: boolean\n clientUploads?: boolean\n collections: ImageKitCollectionMap\n disablePayloadAccessControl?: true\n enabled?: boolean\n folder: string\n privateKey: string\n publicKey: string\n /**\n * Expiry (in seconds) applied to signed URLs generated for private collections.\n * Defaults to 900 (15 minutes). URLs are signed fresh on every request, so a short\n * window is safe.\n */\n signedUrlExpiresIn?: number\n urlEndpoint: string\n}\n\nconst normalizeFolder = (folder: string): string => {\n const normalized = folder.trim().replace(/^\\/+|\\/+$/g, '')\n\n if (!normalized) {\n throw new Error('imagekitStorage: `folder` is required.')\n }\n\n return `/${normalized}`\n}\n\nconst getCollectionFolder = (rootFolder: string, collectionSlug: string): string =>\n path.posix.join(rootFolder, collectionSlug)\n\nconst getFilePath = (rootFolder: string, collectionSlug: string, filename: string): string =>\n path.posix.join(getCollectionFolder(rootFolder, collectionSlug), filename)\n\nconst findImageKitFileId = async (\n imagekit: ImageKit,\n filePath: string,\n): Promise<string | undefined> => {\n const parentPath = path.posix.dirname(filePath)\n const name = path.posix.basename(filePath)\n\n const limit = 100\n\n for (let skip = 0; ; skip += limit) {\n const assets = await imagekit.assets.list({\n type: 'file',\n limit,\n path: parentPath === '/' ? '/' : `${parentPath}/`,\n skip,\n })\n\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n break\n }\n }\n\n return undefined\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object'\n\nconst getStoredImageKitURL = (doc: unknown): string | undefined => {\n const storedURL = (doc as ImagekitReference | undefined)?.imagekit?.url\n\n return typeof storedURL === 'string' && storedURL.trim().length > 0 ? storedURL : undefined\n}\n\nconst findUploadDocumentByFilename = async ({\n collection,\n filename,\n prefix,\n req,\n}: {\n collection: string\n filename: string\n prefix?: string\n req: PayloadRequest\n}): Promise<null | TypeWithID> => {\n const constraints: Where[] = [\n {\n filename: {\n equals: filename,\n },\n },\n ]\n\n if (typeof prefix === 'string') {\n constraints.push({\n prefix: {\n equals: prefix,\n },\n })\n }\n\n return req.payload.db.findOne({\n collection,\n req,\n where: {\n and: constraints,\n },\n })\n}\n\nconst applyClientUploadContextToData = <T extends UploadDocumentData>(\n data: T,\n clientUploadContext: unknown,\n buildThumbnailURL: (data: T) => string = getAdminThumbnailURL,\n isPrivate = false,\n): T => {\n if (!isImageKitClientUploadContext(clientUploadContext)) {\n return data\n }\n\n data.imagekit = {\n fileId: clientUploadContext.fileId,\n // Record the collection's privacy at upload time; this is the file's permanent\n // ImageKit status and the source of truth for delivery.\n isPrivate,\n thumbnailUrl: clientUploadContext.thumbnailUrl,\n url: clientUploadContext.url,\n }\n\n // Preserve the admin preview when the original file was already uploaded client-side.\n data.thumbnailURL = buildThumbnailURL(data)\n\n return data\n}\n\nconst imagekitAdapter = ({\n apiRoute,\n client,\n clientUploads,\n privateSlugs,\n rootFolder,\n signedUrlExpiresIn,\n urlEndpoint,\n}: {\n apiRoute: string\n client: ImageKit\n clientUploads?: boolean\n privateSlugs: Set<string>\n rootFolder: string\n signedUrlExpiresIn: number\n urlEndpoint: string\n}): Adapter => {\n const fetchUploadedFile = async (fileURL: string): Promise<Response> => {\n const response = await fetch(fileURL)\n\n if (!response.ok) {\n throw new Error(\n `imagekitStorage: failed to fetch uploaded file from ImageKit (${response.status}).`,\n )\n }\n\n const fileBuffer = await response.arrayBuffer()\n const headers = new Headers()\n const contentType = response.headers.get('Content-Type')\n\n if (contentType) {\n headers.set('Content-Type', contentType)\n }\n\n headers.set('Content-Length', String(fileBuffer.byteLength))\n\n return new Response(fileBuffer, {\n headers,\n status: 200,\n statusText: response.statusText,\n })\n }\n\n return ({ collection }) => {\n const isPrivate = privateSlugs.has(collection.slug)\n\n // Sign a relative ImageKit path against the configured urlEndpoint. We always sign\n // a computed path (never a stored absolute URL) because ImageKit's signature is\n // derived from the path after stripping urlEndpoint, so the host must match.\n const buildSignedURL = (src: string, tr?: null | string): string =>\n client.helper.buildSrc({\n expiresIn: signedUrlExpiresIn,\n signed: true,\n src,\n transformation: tr ? [{ raw: tr }] : undefined,\n urlEndpoint,\n })\n\n const buildThumbnailURL = (doc: UploadDocumentData): string =>\n isDocumentPrivate(doc, isPrivate)\n ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute)\n : getAdminThumbnailURL(doc)\n\n return {\n name: 'imagekit',\n clientUploads,\n fields: [\n {\n name: 'imagekit',\n type: 'group',\n admin: {\n disableListColumn: true,\n disableListFilter: true,\n },\n fields: [\n {\n name: 'fileId',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n {\n name: 'url',\n type: 'text',\n admin: {\n readOnly: true,\n },\n },\n {\n name: 'isPrivate',\n type: 'checkbox',\n admin: {\n description:\n 'Whether this file is stored as a private file on ImageKit. Set at upload time from the collection setting and fixed for the life of the file — re-upload to change it.',\n readOnly: true,\n },\n label: 'Private file',\n },\n {\n name: 'signedUrl',\n type: 'text',\n admin: {\n // Only relevant for private files; the plain `url` works directly for public ones.\n condition: (_data, siblingData) =>\n isDocumentPrivate({ imagekit: siblingData }, isPrivate),\n description: `Temporary signed URL for reaching this private file directly on ImageKit. Expires ${signedUrlExpiresIn} seconds after load and is regenerated on every read. Note: anyone with this URL can access the file until it expires — it is not gated by Payload access control.`,\n readOnly: true,\n },\n hooks: {\n // Computed server-side on read so it is always freshly signed and never persisted.\n afterRead: [\n ({ data, siblingData }) => {\n if (!isDocumentPrivate({ imagekit: siblingData }, isPrivate)) {\n return undefined\n }\n\n const filename = (data as { filename?: string } | undefined)?.filename\n\n if (!filename) {\n return undefined\n }\n\n return buildSignedURL(getFilePath(rootFolder, collection.slug, filename))\n },\n ],\n },\n label: 'Signed URL',\n virtual: true,\n },\n {\n name: 'thumbnailUrl',\n type: 'text',\n admin: {\n hidden: true,\n },\n },\n ],\n interfaceName: 'imagekitFields',\n },\n ],\n generateURL: ({ filename }) =>\n client.helper.buildSrc({\n src: getFilePath(rootFolder, collection.slug, filename),\n urlEndpoint,\n }),\n handleDelete: async ({ doc, filename }) => {\n const storedFileId = (doc as ImagekitReference & typeof doc).imagekit?.fileId\n const fileId =\n storedFileId ||\n (await findImageKitFileId(client, getFilePath(rootFolder, collection.slug, filename)))\n\n if (!fileId) {\n return\n }\n\n await client.files.delete(fileId)\n },\n handleUpload: async ({ clientUploadContext, data, file }) => {\n if (isImageKitClientUploadContext(clientUploadContext)) {\n return applyClientUploadContextToData(\n data,\n clientUploadContext,\n buildThumbnailURL,\n isPrivate,\n )\n }\n\n const uploadFile = await toFile(file.buffer, file.filename, { type: file.mimeType })\n\n const uploaded = await client.files.upload({\n file: uploadFile,\n fileName: file.filename,\n folder: getCollectionFolder(rootFolder, collection.slug),\n useUniqueFileName: false,\n ...(isPrivate ? { isPrivateFile: true } : {}),\n })\n\n data.imagekit = {\n fileId: uploaded.fileId,\n isPrivate,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n\n // Set the root-level thumbnailURL that Payload's admin interface expects.\n // `buildThumbnailURL` needs the filename, which Payload may not have copied onto\n // `data` yet at this point, so fall back to the uploaded file's name.\n data.thumbnailURL = buildThumbnailURL({\n ...data,\n filename: (data as { filename?: string }).filename || file.filename,\n })\n\n return data\n },\n staticHandler: async (req, args) => {\n const requestURL = new URL(req.url || 'http://localhost')\n const tr = requestURL.searchParams.get('tr')\n const hasClientUploadContext = 'clientUploadContext' in args.params\n\n if (hasClientUploadContext) {\n const clientUploadContext = args.params.clientUploadContext\n const clientUploadURL =\n isImageKitClientUploadContext(clientUploadContext) &&\n typeof clientUploadContext.url === 'string' &&\n clientUploadContext.url.length > 0\n ? clientUploadContext.url\n : null\n const filePath =\n isImageKitClientUploadContext(clientUploadContext) &&\n typeof clientUploadContext.filePath === 'string' &&\n clientUploadContext.filePath.length > 0\n ? clientUploadContext.filePath\n : getFilePath(rootFolder, args.params.collection, args.params.filename)\n const imagekitURL = isPrivate\n ? buildSignedURL(filePath, tr)\n : clientUploadURL ||\n client.helper.buildSrc({\n src: filePath,\n urlEndpoint,\n })\n\n return fetchUploadedFile(imagekitURL)\n }\n\n const doc =\n args.doc ||\n (await findUploadDocumentByFilename({\n collection: args.params.collection,\n filename: args.params.filename,\n prefix: args.params.prefix,\n req,\n }))\n\n // Sign based on the document's stored status, not the collection's current\n // setting, so files keep working if the collection's privacy is changed later.\n if (isDocumentPrivate(doc, isPrivate)) {\n const mimeType = (doc as { mimeType?: string } | null)?.mimeType\n let signSrc = getFilePath(rootFolder, args.params.collection, args.params.filename)\n\n // A `tr` request for a video/PDF is a thumbnail request (the plain transform\n // can't render a preview); serve ImageKit's generated thumbnail instead.\n if (tr && isVideoOrPdfMimeType(mimeType)) {\n signSrc = `${signSrc}/ik-thumbnail.jpg`\n }\n\n return Response.redirect(buildSignedURL(signSrc, tr), 302)\n }\n\n const imagekitURL = client.helper.buildSrc({\n src:\n getStoredImageKitURL(doc) ||\n getFilePath(rootFolder, args.params.collection, args.params.filename),\n transformation: tr ? [{ raw: tr }] : undefined,\n urlEndpoint,\n })\n\n return Response.redirect(imagekitURL, 302)\n },\n }\n }\n}\n\nconst toCloudStorageCollections = (\n collections: ImageKitCollectionMap,\n adapter: Adapter,\n privateSlugs: Set<string>,\n disablePayloadAccessControl?: true,\n): Partial<Record<CollectionSlug, CollectionOptions>> => {\n const mappedCollections: Partial<Record<CollectionSlug, CollectionOptions>> = {}\n\n for (const collectionSlug of Object.keys(collections)) {\n mappedCollections[collectionSlug] = {\n adapter,\n disableLocalStorage: true,\n // Private files require Payload access control so URLs can be signed at serve time.\n disablePayloadAccessControl: privateSlugs.has(collectionSlug)\n ? undefined\n : disablePayloadAccessControl,\n }\n }\n\n return mappedCollections\n}\n\nexport const imagekitStorage =\n (pluginOptions: PayloadStorageImagekitConfig) =>\n (config: Config): Config => {\n const {\n alwaysInsertFields,\n clientUploads,\n collections,\n disablePayloadAccessControl,\n enabled,\n folder,\n privateKey,\n publicKey,\n signedUrlExpiresIn: signedUrlExpiresInOption,\n urlEndpoint,\n } = pluginOptions\n\n if (!privateKey?.trim()) {\n throw new Error('imagekitStorage: `privateKey` is required.')\n }\n\n if (!publicKey?.trim()) {\n throw new Error('imagekitStorage: `publicKey` is required.')\n }\n\n if (!urlEndpoint?.trim()) {\n throw new Error('imagekitStorage: `urlEndpoint` is required.')\n }\n\n if (!collections || Object.keys(collections).length === 0) {\n throw new Error('imagekitStorage: `collections` must include at least one upload collection.')\n }\n\n const privateSlugs = new Set(\n Object.entries(collections)\n .filter(([, value]) => isPrivateCollectionConfig(value))\n .map(([slug]) => slug),\n )\n\n if (disablePayloadAccessControl && privateSlugs.size > 0) {\n throw new Error(\n 'imagekitStorage: `disablePayloadAccessControl` cannot be used with private collections. Private files require Payload access control to remain enabled so URLs can be signed at serve time.',\n )\n }\n\n const signedUrlExpiresIn =\n typeof signedUrlExpiresInOption === 'number' && signedUrlExpiresInOption > 0\n ? signedUrlExpiresInOption\n : DEFAULT_SIGNED_URL_EXPIRES_IN\n const apiRoute = config.routes?.api ?? '/api'\n\n const rootFolder = normalizeFolder(folder)\n\n const imagekitClient = new ImageKit({ privateKey })\n const adapter = imagekitAdapter({\n apiRoute,\n client: imagekitClient,\n clientUploads,\n privateSlugs,\n rootFolder,\n signedUrlExpiresIn,\n urlEndpoint,\n })\n\n const configuredSlugs = new Set(Object.keys(collections))\n const configWithCropDisabled: Config = {\n ...config,\n collections: (config.collections || []).map((collection) => {\n if (!configuredSlugs.has(collection.slug)) {\n return collection\n }\n\n const isCollectionPrivate = privateSlugs.has(collection.slug)\n const buildThumbnailURL = (doc: unknown): string =>\n isDocumentPrivate(doc, isCollectionPrivate)\n ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute)\n : getAdminThumbnailURL(doc)\n\n const beforeDocumentControls =\n collection.admin?.components?.edit?.beforeDocumentControls || []\n\n // Surface a \"this is a private collection\" note in the list and edit views.\n // Preserve an existing string description by appending; leave custom\n // component descriptions untouched.\n const existingDescription = collection.admin?.description\n const baseAdmin = isCollectionPrivate\n ? {\n ...(collection.admin || {}),\n description:\n typeof existingDescription === 'string' && existingDescription.trim().length > 0\n ? `${existingDescription} ${PRIVATE_COLLECTION_DESCRIPTION}`\n : (existingDescription ?? PRIVATE_COLLECTION_DESCRIPTION),\n }\n : collection.admin\n\n return {\n ...collection,\n admin: clientUploads\n ? {\n ...(baseAdmin || {}),\n components: {\n ...(collection.admin?.components || {}),\n edit: {\n ...(collection.admin?.components?.edit || {}),\n beforeDocumentControls: [\n ...beforeDocumentControls,\n {\n clientProps: {\n authPath: getImageKitUploadAuthPath(collection.slug),\n collectionSlug: collection.slug,\n folder: getCollectionFolder(rootFolder, collection.slug),\n isPrivate: isCollectionPrivate,\n publicKey,\n },\n path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler',\n },\n ],\n },\n },\n }\n : baseAdmin,\n hooks: {\n ...(collection.hooks || {}),\n beforeChange: [\n ...(collection.hooks?.beforeChange || []),\n ({ data, req }) => {\n if (!isRecord(data) || !req.file?.clientUploadContext) {\n return data\n }\n\n return applyClientUploadContextToData(\n data,\n req.file.clientUploadContext,\n buildThumbnailURL,\n isCollectionPrivate,\n )\n },\n ],\n },\n upload: {\n ...(typeof collection.upload === 'object' ? collection.upload : {}),\n adminThumbnail: ({ doc }) => buildThumbnailURL(doc),\n crop: false,\n },\n }\n }),\n endpoints: clientUploads\n ? [\n ...(config.endpoints || []),\n {\n handler: createImageKitUploadAuthHandler({\n client: imagekitClient,\n collections,\n }),\n method: 'get',\n path: `${getImageKitUploadAuthPath(':collection')}`,\n },\n ]\n : config.endpoints,\n }\n\n return cloudStoragePlugin({\n alwaysInsertFields,\n collections: toCloudStorageCollections(\n collections,\n adapter,\n privateSlugs,\n disablePayloadAccessControl,\n ),\n enabled,\n })(configWithCropDisabled)\n }\n\nexport { migrateFolder } from './migrate.js'\nexport type { MigrateFolderOptions, MigrationJobResult } from './migrate.js'\n\n// Backward-compatible alias\nexport const payloadStorageImagekit = imagekitStorage\n"],"names":["ImageKit","toFile","cloudStoragePlugin","path","getImageKitUploadAuthPath","isImageKitClientUploadContext","createImageKitUploadAuthHandler","getAdminThumbnailURL","getPrivateAdminThumbnailURL","isVideoOrPdfMimeType","DEFAULT_SIGNED_URL_EXPIRES_IN","PRIVATE_COLLECTION_DESCRIPTION","isPrivateCollectionConfig","value","private","isDocumentPrivate","doc","collectionIsPrivate","stored","imagekit","isPrivate","normalizeFolder","folder","normalized","trim","replace","Error","getCollectionFolder","rootFolder","collectionSlug","posix","join","getFilePath","filename","findImageKitFileId","filePath","parentPath","dirname","name","basename","limit","skip","assets","list","type","found","find","asset","fileId","length","undefined","isRecord","getStoredImageKitURL","storedURL","url","findUploadDocumentByFilename","collection","prefix","req","constraints","equals","push","payload","db","findOne","where","and","applyClientUploadContextToData","data","clientUploadContext","buildThumbnailURL","thumbnailUrl","thumbnailURL","imagekitAdapter","apiRoute","client","clientUploads","privateSlugs","signedUrlExpiresIn","urlEndpoint","fetchUploadedFile","fileURL","response","fetch","ok","status","fileBuffer","arrayBuffer","headers","Headers","contentType","get","set","String","byteLength","Response","statusText","has","slug","buildSignedURL","src","tr","helper","buildSrc","expiresIn","signed","transformation","raw","fields","admin","disableListColumn","disableListFilter","hidden","readOnly","description","label","condition","_data","siblingData","hooks","afterRead","virtual","interfaceName","generateURL","handleDelete","storedFileId","files","delete","handleUpload","file","uploadFile","buffer","mimeType","uploaded","upload","fileName","useUniqueFileName","isPrivateFile","staticHandler","args","requestURL","URL","searchParams","hasClientUploadContext","params","clientUploadURL","imagekitURL","signSrc","redirect","toCloudStorageCollections","collections","adapter","disablePayloadAccessControl","mappedCollections","Object","keys","disableLocalStorage","imagekitStorage","pluginOptions","config","alwaysInsertFields","enabled","privateKey","publicKey","signedUrlExpiresInOption","Set","entries","filter","map","size","routes","api","imagekitClient","configuredSlugs","configWithCropDisabled","isCollectionPrivate","beforeDocumentControls","components","edit","existingDescription","baseAdmin","clientProps","authPath","beforeChange","adminThumbnail","crop","endpoints","handler","method","migrateFolder","payloadStorageImagekit"],"mappings":"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"}
|
package/dist/migrate.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Payload, UploadCollectionSlug } from 'payload';
|
|
2
|
-
type
|
|
2
|
+
type ImageKitCollectionConfig = {
|
|
3
|
+
private?: boolean;
|
|
4
|
+
} | true;
|
|
5
|
+
type ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>;
|
|
3
6
|
export type MigrateFolderOptions = {
|
|
4
7
|
/**
|
|
5
|
-
* The Payload upload collections to migrate. Keys are collection slugs
|
|
8
|
+
* The Payload upload collections to migrate. Keys are collection slugs; the value is
|
|
9
|
+
* `true`, or `{ private: true }` to mark migrated files as ImageKit private files.
|
|
6
10
|
*/
|
|
7
11
|
collections: ImageKitCollectionMap;
|
|
8
12
|
/**
|
|
@@ -25,6 +29,12 @@ export type MigrateFolderOptions = {
|
|
|
25
29
|
* ImageKit private API key.
|
|
26
30
|
*/
|
|
27
31
|
privateKey: string;
|
|
32
|
+
/**
|
|
33
|
+
* Expiry (in seconds) for the signed URL used to read a private source file when the
|
|
34
|
+
* fast server-side copy is not possible and the migrator falls back to re-uploading
|
|
35
|
+
* from the source URL. Defaults to 900 (15 minutes).
|
|
36
|
+
*/
|
|
37
|
+
signedUrlExpiresIn?: number;
|
|
28
38
|
/**
|
|
29
39
|
* The destination ImageKit root folder (e.g. `/production`).
|
|
30
40
|
*/
|
package/dist/migrate.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import ImageKit from '@imagekit/nodejs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getAdminThumbnailURL } from './getAdminThumbnailURL.js';
|
|
3
|
+
import { getAdminThumbnailURL, getPrivateAdminThumbnailURL } from './getAdminThumbnailURL.js';
|
|
4
|
+
const DEFAULT_SIGNED_URL_EXPIRES_IN = 900;
|
|
5
|
+
const isPrivateCollectionConfig = (value)=>value !== true && typeof value === 'object' && value?.private === true;
|
|
4
6
|
const normalizeFolder = (folder)=>{
|
|
5
7
|
const normalized = folder.trim().replace(/^\/+|\/+$/g, '');
|
|
6
8
|
if (!normalized) {
|
|
@@ -63,12 +65,15 @@ const isImageKitFileNotFoundError = (error)=>{
|
|
|
63
65
|
const message = error && typeof error === 'object' && 'message' in error ? String(error.message) : '';
|
|
64
66
|
return /does not exist|not found|no such file/i.test(message);
|
|
65
67
|
};
|
|
66
|
-
const uploadFileFromURL = async ({ client, fileName, folder, sourceUrl })=>{
|
|
68
|
+
const uploadFileFromURL = async ({ client, fileName, folder, isPrivate, sourceUrl })=>{
|
|
67
69
|
const uploaded = await client.files.upload({
|
|
68
70
|
file: sourceUrl,
|
|
69
71
|
fileName,
|
|
70
72
|
folder,
|
|
71
|
-
useUniqueFileName: false
|
|
73
|
+
useUniqueFileName: false,
|
|
74
|
+
...isPrivate ? {
|
|
75
|
+
isPrivateFile: true
|
|
76
|
+
} : {}
|
|
72
77
|
});
|
|
73
78
|
if (!uploaded.fileId || !uploaded.url) {
|
|
74
79
|
throw new Error(`migrateFolder: upload from URL did not return file details for "${fileName}".`);
|
|
@@ -105,7 +110,7 @@ const readFileIdFromCopyResponse = async (response)=>{
|
|
|
105
110
|
return undefined;
|
|
106
111
|
};
|
|
107
112
|
export const migrateFolder = async (options)=>{
|
|
108
|
-
const { collections, dryRun = true, from, payload, privateKey, to, urlEndpoint } = options;
|
|
113
|
+
const { collections, dryRun = true, from, payload, privateKey, signedUrlExpiresIn: signedUrlExpiresInOption, to, urlEndpoint } = options;
|
|
109
114
|
if (!privateKey?.trim()) {
|
|
110
115
|
throw new Error('migrateFolder: `privateKey` is required.');
|
|
111
116
|
}
|
|
@@ -115,6 +120,9 @@ export const migrateFolder = async (options)=>{
|
|
|
115
120
|
if (!collections || Object.keys(collections).length === 0) {
|
|
116
121
|
throw new Error('migrateFolder: `collections` must include at least one upload collection.');
|
|
117
122
|
}
|
|
123
|
+
const privateSlugs = new Set(Object.entries(collections).filter(([, value])=>isPrivateCollectionConfig(value)).map(([slug])=>slug));
|
|
124
|
+
const signedUrlExpiresIn = typeof signedUrlExpiresInOption === 'number' && signedUrlExpiresInOption > 0 ? signedUrlExpiresInOption : DEFAULT_SIGNED_URL_EXPIRES_IN;
|
|
125
|
+
const apiRoute = payload?.config.routes?.api ?? '/api';
|
|
118
126
|
const fromFolder = normalizeFolder(from);
|
|
119
127
|
const toFolder = normalizeFolder(to);
|
|
120
128
|
if (fromFolder === toFolder) {
|
|
@@ -145,7 +153,8 @@ export const migrateFolder = async (options)=>{
|
|
|
145
153
|
if (payload) {
|
|
146
154
|
console.log('[migrateFolder] Starting document-by-document migration.');
|
|
147
155
|
for (const slug of collectionSlugs){
|
|
148
|
-
|
|
156
|
+
const isCollectionPrivate = privateSlugs.has(slug);
|
|
157
|
+
console.log(`[migrateFolder] Migrating collection "${slug}"${isCollectionPrivate ? ' (private)' : ''}...`);
|
|
149
158
|
const found = await payload.find({
|
|
150
159
|
collection: slug,
|
|
151
160
|
depth: 0,
|
|
@@ -201,22 +210,35 @@ export const migrateFolder = async (options)=>{
|
|
|
201
210
|
throw error;
|
|
202
211
|
}
|
|
203
212
|
console.warn(`[migrateFolder] Source file not found at ${sourceFilePath}. Falling back to stored ImageKit URL for "${filename}".`);
|
|
213
|
+
// A private source file 401s on its plain URL, so sign it before ImageKit
|
|
214
|
+
// fetches it, and re-upload as private so the copy keeps its privacy.
|
|
215
|
+
const sourceUrl = isCollectionPrivate ? client.helper.buildSrc({
|
|
216
|
+
expiresIn: signedUrlExpiresIn,
|
|
217
|
+
signed: true,
|
|
218
|
+
src: existingImagekit.url,
|
|
219
|
+
urlEndpoint
|
|
220
|
+
}) : existingImagekit.url;
|
|
204
221
|
uploaded = await uploadFileFromURL({
|
|
205
222
|
client,
|
|
206
223
|
fileName: filename,
|
|
207
224
|
folder: destinationFolder,
|
|
208
|
-
|
|
225
|
+
isPrivate: isCollectionPrivate,
|
|
226
|
+
sourceUrl
|
|
209
227
|
});
|
|
210
228
|
}
|
|
211
229
|
const canonicalUrl = client.helper.buildSrc({
|
|
212
230
|
src: destinationFilePath,
|
|
213
231
|
urlEndpoint
|
|
214
232
|
});
|
|
215
|
-
const
|
|
233
|
+
const mimeType = doc.mimeType;
|
|
234
|
+
const adminThumbnailURL = isCollectionPrivate ? getPrivateAdminThumbnailURL({
|
|
235
|
+
filename,
|
|
236
|
+
mimeType
|
|
237
|
+
}, slug, apiRoute) : getAdminThumbnailURL({
|
|
216
238
|
imagekit: {
|
|
217
239
|
url: uploaded.url
|
|
218
240
|
},
|
|
219
|
-
mimeType
|
|
241
|
+
mimeType
|
|
220
242
|
});
|
|
221
243
|
// Update the document with new ImageKit data
|
|
222
244
|
await payload.update({
|
|
@@ -225,6 +247,7 @@ export const migrateFolder = async (options)=>{
|
|
|
225
247
|
data: {
|
|
226
248
|
imagekit: {
|
|
227
249
|
fileId: uploaded.fileId,
|
|
250
|
+
isPrivate: isCollectionPrivate,
|
|
228
251
|
thumbnailUrl: uploaded.thumbnailUrl,
|
|
229
252
|
url: uploaded.url
|
|
230
253
|
},
|
package/dist/migrate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["import type { Payload, UploadCollectionSlug } from 'payload'\n\nimport ImageKit from '@imagekit/nodejs'\nimport path from 'node:path'\n\nimport { getAdminThumbnailURL } from './getAdminThumbnailURL.js'\n\ntype ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, true>>\n\nexport type MigrateFolderOptions = {\n /**\n * The Payload upload collections to migrate. Keys are collection slugs, value is `true`.\n */\n collections: ImageKitCollectionMap\n /**\n * Dry-run mode. When `true`, migration logs what would happen without writing\n * to ImageKit or Payload. Defaults to `true`.\n * @default true\n */\n dryRun?: boolean\n /**\n * The source ImageKit root folder (e.g. `/staging`). Each collection subfolder\n * (`/staging/media`) will be copied into `to`.\n */\n from: string\n /**\n * Optional Payload instance. When provided, the stored `imagekit.*` and `url` fields\n * in the database will be updated to reflect the new file locations after copying.\n */\n payload?: Payload\n /**\n * ImageKit private API key.\n */\n privateKey: string\n /**\n * The destination ImageKit root folder (e.g. `/production`).\n */\n to: string\n /**\n * ImageKit URL endpoint (e.g. `https://ik.imagekit.io/your_id`). Required to\n * reconstruct canonical `url` values when updating the database.\n */\n urlEndpoint: string\n}\n\nexport type MigrationJobResult = {\n collectionSlug: string\n jobId: string\n status: 'Completed' | 'Pending'\n}\n\nconst normalizeFolder = (folder: string): string => {\n const normalized = folder.trim().replace(/^\\/+|\\/+$/g, '')\n\n if (!normalized) {\n throw new Error('migrateFolder: folder path cannot be empty.')\n }\n\n return `/${normalized}`\n}\n\nconst getCollectionFolder = (rootFolder: string, collectionSlug: string): string =>\n path.posix.join(rootFolder, collectionSlug)\n\nconst getFilePath = (rootFolder: string, collectionSlug: string, filename: string): string =>\n path.posix.join(getCollectionFolder(rootFolder, collectionSlug), filename)\n\nconst ensureFolderExists = async (client: ImageKit, folderPath: string): Promise<void> => {\n const normalized = normalizeFolder(folderPath)\n const segments = normalized.split('/').filter(Boolean)\n\n let parentFolderPath = '/'\n\n for (const folderName of segments) {\n try {\n await client.folders.create({\n folderName,\n parentFolderPath,\n })\n } catch (error) {\n const message =\n error && typeof error === 'object' && 'message' in error ? String(error.message) : ''\n\n if (!/already exists/i.test(message)) {\n throw error\n }\n }\n\n parentFolderPath = path.posix.join(parentFolderPath, folderName)\n }\n}\n\nconst findImageKitFileId = async (\n imagekit: ImageKit,\n filePath: string,\n options?: {\n retries?: number\n retryDelayMs?: number\n },\n): Promise<string | undefined> => {\n const parentPath = path.posix.dirname(filePath)\n const name = path.posix.basename(filePath)\n\n const retries = options?.retries ?? 0\n const retryDelayMs = options?.retryDelayMs ?? 250\n const limit = 100\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n for (let skip = 0; ; skip += limit) {\n const assets = await imagekit.assets.list({\n type: 'file',\n limit,\n path: parentPath === '/' ? '/' : `${parentPath}/`,\n skip,\n })\n\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n break\n }\n }\n\n if (attempt < retries) {\n await new Promise((resolve) => {\n setTimeout(resolve, retryDelayMs)\n })\n }\n }\n\n return undefined\n}\n\nconst isImageKitFileNotFoundError = (error: unknown): boolean => {\n const message =\n error && typeof error === 'object' && 'message' in error ? String(error.message) : ''\n\n return /does not exist|not found|no such file/i.test(message)\n}\n\nconst uploadFileFromURL = async ({\n client,\n fileName,\n folder,\n sourceUrl,\n}: {\n client: ImageKit\n fileName: string\n folder: string\n sourceUrl: string\n}) => {\n const uploaded = await client.files.upload({\n file: sourceUrl,\n fileName,\n folder,\n useUniqueFileName: false,\n })\n\n if (!uploaded.fileId || !uploaded.url) {\n throw new Error(`migrateFolder: upload from URL did not return file details for \"${fileName}\".`)\n }\n\n return {\n fileId: uploaded.fileId,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n}\n\nconst getUploadedImagekitFields = async ({\n client,\n fileId,\n}: {\n client: ImageKit\n fileId: string\n}): Promise<{ fileId: string; thumbnailUrl?: string; url: string }> => {\n const file = await client.files.get(fileId)\n\n if (!file.fileId || !file.url) {\n throw new Error(`migrateFolder: missing ImageKit file details for \"${fileId}\".`)\n }\n\n return {\n fileId: file.fileId,\n thumbnailUrl: file.thumbnail,\n url: file.url,\n }\n}\n\nconst readFileIdFromCopyResponse = async (response: Response): Promise<string | undefined> => {\n try {\n const result = (await response.json()) as { fileId?: unknown; file_id?: unknown }\n\n if (typeof result.fileId === 'string' && result.fileId.length > 0) {\n return result.fileId\n }\n\n if (typeof result.file_id === 'string' && result.file_id.length > 0) {\n return result.file_id\n }\n } catch {\n // Ignore empty or non-JSON responses and fall back to asset discovery.\n }\n\n return undefined\n}\n\nexport const migrateFolder = async (\n options: MigrateFolderOptions,\n): Promise<MigrationJobResult[]> => {\n const { collections, dryRun = true, from, payload, privateKey, to, urlEndpoint } = options\n\n if (!privateKey?.trim()) {\n throw new Error('migrateFolder: `privateKey` is required.')\n }\n\n if (!urlEndpoint?.trim()) {\n throw new Error('migrateFolder: `urlEndpoint` is required.')\n }\n\n if (!collections || Object.keys(collections).length === 0) {\n throw new Error('migrateFolder: `collections` must include at least one upload collection.')\n }\n\n const fromFolder = normalizeFolder(from)\n const toFolder = normalizeFolder(to)\n\n if (fromFolder === toFolder) {\n throw new Error('migrateFolder: `from` and `to` cannot be the same folder.')\n }\n\n const client = new ImageKit({ privateKey })\n\n console.log(\n `[migrateFolder] Starting ${dryRun ? 'DRY-RUN' : 'LIVE'} migration from ${fromFolder} to ${toFolder} for ${Object.keys(collections).length} collection(s).`,\n )\n\n if (dryRun) {\n console.log(\n `[migrateFolder] [dry-run] Would ensure destination root folder exists: ${toFolder}`,\n )\n } else {\n console.log(`[migrateFolder] Ensuring destination root folder exists: ${toFolder}`)\n await ensureFolderExists(client, toFolder)\n }\n\n // Always copy file-by-file per collection\n const collectionSlugs = Object.keys(collections)\n const results: MigrationJobResult[] = []\n\n // Ensure destination folder structure exists\n if (!dryRun) {\n for (const slug of collectionSlugs) {\n const destinationFolderPath = path.posix.join(toFolder, slug)\n console.log(`[migrateFolder] Ensuring destination folder: ${destinationFolderPath}`)\n await ensureFolderExists(client, destinationFolderPath)\n }\n }\n\n // Process documents directly like the old migration script\n if (payload) {\n console.log('[migrateFolder] Starting document-by-document migration.')\n\n for (const slug of collectionSlugs) {\n console.log(`[migrateFolder] Migrating collection \"${slug}\"...`)\n\n const found = await payload.find({\n collection: slug,\n depth: 0,\n limit: 0,\n pagination: false,\n })\n\n console.log(\n `[migrateFolder] Processing ${found.docs.length} document(s) in collection \"${slug}\".`,\n )\n\n let updatedCount = 0\n let skippedCount = 0\n let errorCount = 0\n\n // Process each document one by one, like the old migration script\n for (let i = 0; i < found.docs.length; i++) {\n const doc = found.docs[i]\n const filename = (doc as { filename?: string }).filename\n const existingImagekit = (doc as any).imagekit\n\n console.log(`[migrateFolder] [${i + 1}/${found.docs.length}] Processing document ${doc.id}`)\n\n // Skip items that don't have ImageKit data or filename\n if (!filename || !existingImagekit?.fileId || !existingImagekit?.url) {\n skippedCount += 1\n console.log(\n `[migrateFolder] Skipping document ${doc.id} - missing filename or ImageKit data`,\n )\n continue\n }\n\n if (dryRun) {\n console.log(`[migrateFolder] [dry-run] Would upload: ${toFolder}/${slug}/${filename}`)\n console.log(`[migrateFolder] [dry-run] Would update Payload document: ${doc.id}`)\n } else {\n try {\n const sourceFilePath = getFilePath(fromFolder, slug, filename)\n const destinationFolder = getCollectionFolder(toFolder, slug)\n const destinationFilePath = getFilePath(toFolder, slug, filename)\n\n let uploaded: { fileId: string; thumbnailUrl?: string; url: string }\n\n try {\n const copyResponse = await client.files\n .copy({\n destinationPath: destinationFolder,\n sourceFilePath,\n })\n .asResponse()\n\n let copiedFileId = await readFileIdFromCopyResponse(copyResponse)\n\n if (!copiedFileId) {\n copiedFileId = await findImageKitFileId(client, destinationFilePath, {\n retries: 20,\n retryDelayMs: 1000,\n })\n }\n\n if (!copiedFileId) {\n throw new Error(\n `migrateFolder: copy request completed for \"${sourceFilePath}\", but the destination asset at \"${destinationFilePath}\" was not discoverable via ImageKit after retrying.`,\n )\n }\n\n uploaded = await getUploadedImagekitFields({\n client,\n fileId: copiedFileId,\n })\n } catch (error) {\n if (!isImageKitFileNotFoundError(error)) {\n throw error\n }\n\n console.warn(\n `[migrateFolder] Source file not found at ${sourceFilePath}. Falling back to stored ImageKit URL for \"${filename}\".`,\n )\n\n uploaded = await uploadFileFromURL({\n client,\n fileName: filename,\n folder: destinationFolder,\n sourceUrl: existingImagekit.url,\n })\n }\n\n const canonicalUrl = client.helper.buildSrc({\n src: destinationFilePath,\n urlEndpoint,\n })\n\n const adminThumbnailURL = getAdminThumbnailURL({\n imagekit: {\n url: uploaded.url,\n },\n mimeType: (doc as { mimeType?: string }).mimeType,\n })\n\n // Update the document with new ImageKit data\n await payload.update({\n id: doc.id,\n collection: slug,\n data: {\n imagekit: {\n fileId: uploaded.fileId,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n },\n thumbnailURL: adminThumbnailURL || canonicalUrl,\n url: canonicalUrl,\n },\n })\n\n updatedCount += 1\n console.log(`[migrateFolder] Content migrated! ${filename} (${doc.id})`)\n } catch (error) {\n errorCount += 1\n console.error(\n `[migrateFolder] Failed to migrate document \"${doc.id}\" for file \"${filename}\":`,\n error,\n )\n }\n }\n }\n\n results.push({\n collectionSlug: slug,\n jobId: `migrate:${slug}`,\n status: dryRun ? 'Pending' : 'Completed',\n })\n\n console.log(\n `[migrateFolder] Collection \"${slug}\" complete: updated=${updatedCount}, skipped=${skippedCount}, errors=${errorCount}.`,\n )\n }\n\n console.log('[migrateFolder] Document-by-document migration complete.')\n } else {\n console.log('[migrateFolder] No Payload instance provided - skipping migration.')\n }\n\n console.log(`[migrateFolder] ${dryRun ? 'DRY-RUN simulation' : 'Migration'} complete.`)\n\n return results\n}\n"],"names":["ImageKit","path","getAdminThumbnailURL","normalizeFolder","folder","normalized","trim","replace","Error","getCollectionFolder","rootFolder","collectionSlug","posix","join","getFilePath","filename","ensureFolderExists","client","folderPath","segments","split","filter","Boolean","parentFolderPath","folderName","folders","create","error","message","String","test","findImageKitFileId","imagekit","filePath","options","parentPath","dirname","name","basename","retries","retryDelayMs","limit","attempt","skip","assets","list","type","found","find","asset","fileId","length","Promise","resolve","setTimeout","undefined","isImageKitFileNotFoundError","uploadFileFromURL","fileName","sourceUrl","uploaded","files","upload","file","useUniqueFileName","url","thumbnailUrl","getUploadedImagekitFields","get","thumbnail","readFileIdFromCopyResponse","response","result","json","file_id","migrateFolder","collections","dryRun","from","payload","privateKey","to","urlEndpoint","Object","keys","fromFolder","toFolder","console","log","collectionSlugs","results","slug","destinationFolderPath","collection","depth","pagination","docs","updatedCount","skippedCount","errorCount","i","doc","existingImagekit","id","sourceFilePath","destinationFolder","destinationFilePath","copyResponse","copy","destinationPath","asResponse","copiedFileId","warn","canonicalUrl","helper","buildSrc","src","adminThumbnailURL","mimeType","update","data","thumbnailURL","push","jobId","status"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["import type { Payload, UploadCollectionSlug } from 'payload'\n\nimport ImageKit from '@imagekit/nodejs'\nimport path from 'node:path'\n\nimport { getAdminThumbnailURL, getPrivateAdminThumbnailURL } from './getAdminThumbnailURL.js'\n\ntype ImageKitCollectionConfig = { private?: boolean } | true\ntype ImageKitCollectionMap = Partial<Record<UploadCollectionSlug, ImageKitCollectionConfig>>\n\nconst DEFAULT_SIGNED_URL_EXPIRES_IN = 900\n\nconst isPrivateCollectionConfig = (value: ImageKitCollectionConfig | undefined): boolean =>\n value !== true && typeof value === 'object' && value?.private === true\n\nexport type MigrateFolderOptions = {\n /**\n * The Payload upload collections to migrate. Keys are collection slugs; the value is\n * `true`, or `{ private: true }` to mark migrated files as ImageKit private files.\n */\n collections: ImageKitCollectionMap\n /**\n * Dry-run mode. When `true`, migration logs what would happen without writing\n * to ImageKit or Payload. Defaults to `true`.\n * @default true\n */\n dryRun?: boolean\n /**\n * The source ImageKit root folder (e.g. `/staging`). Each collection subfolder\n * (`/staging/media`) will be copied into `to`.\n */\n from: string\n /**\n * Optional Payload instance. When provided, the stored `imagekit.*` and `url` fields\n * in the database will be updated to reflect the new file locations after copying.\n */\n payload?: Payload\n /**\n * ImageKit private API key.\n */\n privateKey: string\n /**\n * Expiry (in seconds) for the signed URL used to read a private source file when the\n * fast server-side copy is not possible and the migrator falls back to re-uploading\n * from the source URL. Defaults to 900 (15 minutes).\n */\n signedUrlExpiresIn?: number\n /**\n * The destination ImageKit root folder (e.g. `/production`).\n */\n to: string\n /**\n * ImageKit URL endpoint (e.g. `https://ik.imagekit.io/your_id`). Required to\n * reconstruct canonical `url` values when updating the database.\n */\n urlEndpoint: string\n}\n\nexport type MigrationJobResult = {\n collectionSlug: string\n jobId: string\n status: 'Completed' | 'Pending'\n}\n\nconst normalizeFolder = (folder: string): string => {\n const normalized = folder.trim().replace(/^\\/+|\\/+$/g, '')\n\n if (!normalized) {\n throw new Error('migrateFolder: folder path cannot be empty.')\n }\n\n return `/${normalized}`\n}\n\nconst getCollectionFolder = (rootFolder: string, collectionSlug: string): string =>\n path.posix.join(rootFolder, collectionSlug)\n\nconst getFilePath = (rootFolder: string, collectionSlug: string, filename: string): string =>\n path.posix.join(getCollectionFolder(rootFolder, collectionSlug), filename)\n\nconst ensureFolderExists = async (client: ImageKit, folderPath: string): Promise<void> => {\n const normalized = normalizeFolder(folderPath)\n const segments = normalized.split('/').filter(Boolean)\n\n let parentFolderPath = '/'\n\n for (const folderName of segments) {\n try {\n await client.folders.create({\n folderName,\n parentFolderPath,\n })\n } catch (error) {\n const message =\n error && typeof error === 'object' && 'message' in error ? String(error.message) : ''\n\n if (!/already exists/i.test(message)) {\n throw error\n }\n }\n\n parentFolderPath = path.posix.join(parentFolderPath, folderName)\n }\n}\n\nconst findImageKitFileId = async (\n imagekit: ImageKit,\n filePath: string,\n options?: {\n retries?: number\n retryDelayMs?: number\n },\n): Promise<string | undefined> => {\n const parentPath = path.posix.dirname(filePath)\n const name = path.posix.basename(filePath)\n\n const retries = options?.retries ?? 0\n const retryDelayMs = options?.retryDelayMs ?? 250\n const limit = 100\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n for (let skip = 0; ; skip += limit) {\n const assets = await imagekit.assets.list({\n type: 'file',\n limit,\n path: parentPath === '/' ? '/' : `${parentPath}/`,\n skip,\n })\n\n const found = assets.find(\n (asset) => 'fileId' in asset && asset.filePath === filePath && asset.name === name,\n )\n\n if (found && 'fileId' in found) {\n return found.fileId\n }\n\n if (assets.length < limit) {\n break\n }\n }\n\n if (attempt < retries) {\n await new Promise((resolve) => {\n setTimeout(resolve, retryDelayMs)\n })\n }\n }\n\n return undefined\n}\n\nconst isImageKitFileNotFoundError = (error: unknown): boolean => {\n const message =\n error && typeof error === 'object' && 'message' in error ? String(error.message) : ''\n\n return /does not exist|not found|no such file/i.test(message)\n}\n\nconst uploadFileFromURL = async ({\n client,\n fileName,\n folder,\n isPrivate,\n sourceUrl,\n}: {\n client: ImageKit\n fileName: string\n folder: string\n isPrivate?: boolean\n sourceUrl: string\n}) => {\n const uploaded = await client.files.upload({\n file: sourceUrl,\n fileName,\n folder,\n useUniqueFileName: false,\n ...(isPrivate ? { isPrivateFile: true } : {}),\n })\n\n if (!uploaded.fileId || !uploaded.url) {\n throw new Error(`migrateFolder: upload from URL did not return file details for \"${fileName}\".`)\n }\n\n return {\n fileId: uploaded.fileId,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n }\n}\n\nconst getUploadedImagekitFields = async ({\n client,\n fileId,\n}: {\n client: ImageKit\n fileId: string\n}): Promise<{ fileId: string; thumbnailUrl?: string; url: string }> => {\n const file = await client.files.get(fileId)\n\n if (!file.fileId || !file.url) {\n throw new Error(`migrateFolder: missing ImageKit file details for \"${fileId}\".`)\n }\n\n return {\n fileId: file.fileId,\n thumbnailUrl: file.thumbnail,\n url: file.url,\n }\n}\n\nconst readFileIdFromCopyResponse = async (response: Response): Promise<string | undefined> => {\n try {\n const result = (await response.json()) as { file_id?: unknown; fileId?: unknown }\n\n if (typeof result.fileId === 'string' && result.fileId.length > 0) {\n return result.fileId\n }\n\n if (typeof result.file_id === 'string' && result.file_id.length > 0) {\n return result.file_id\n }\n } catch {\n // Ignore empty or non-JSON responses and fall back to asset discovery.\n }\n\n return undefined\n}\n\nexport const migrateFolder = async (\n options: MigrateFolderOptions,\n): Promise<MigrationJobResult[]> => {\n const {\n collections,\n dryRun = true,\n from,\n payload,\n privateKey,\n signedUrlExpiresIn: signedUrlExpiresInOption,\n to,\n urlEndpoint,\n } = options\n\n if (!privateKey?.trim()) {\n throw new Error('migrateFolder: `privateKey` is required.')\n }\n\n if (!urlEndpoint?.trim()) {\n throw new Error('migrateFolder: `urlEndpoint` is required.')\n }\n\n if (!collections || Object.keys(collections).length === 0) {\n throw new Error('migrateFolder: `collections` must include at least one upload collection.')\n }\n\n const privateSlugs = new Set(\n Object.entries(collections)\n .filter(([, value]) => isPrivateCollectionConfig(value))\n .map(([slug]) => slug),\n )\n const signedUrlExpiresIn =\n typeof signedUrlExpiresInOption === 'number' && signedUrlExpiresInOption > 0\n ? signedUrlExpiresInOption\n : DEFAULT_SIGNED_URL_EXPIRES_IN\n const apiRoute = payload?.config.routes?.api ?? '/api'\n\n const fromFolder = normalizeFolder(from)\n const toFolder = normalizeFolder(to)\n\n if (fromFolder === toFolder) {\n throw new Error('migrateFolder: `from` and `to` cannot be the same folder.')\n }\n\n const client = new ImageKit({ privateKey })\n\n console.log(\n `[migrateFolder] Starting ${dryRun ? 'DRY-RUN' : 'LIVE'} migration from ${fromFolder} to ${toFolder} for ${Object.keys(collections).length} collection(s).`,\n )\n\n if (dryRun) {\n console.log(\n `[migrateFolder] [dry-run] Would ensure destination root folder exists: ${toFolder}`,\n )\n } else {\n console.log(`[migrateFolder] Ensuring destination root folder exists: ${toFolder}`)\n await ensureFolderExists(client, toFolder)\n }\n\n // Always copy file-by-file per collection\n const collectionSlugs = Object.keys(collections)\n const results: MigrationJobResult[] = []\n\n // Ensure destination folder structure exists\n if (!dryRun) {\n for (const slug of collectionSlugs) {\n const destinationFolderPath = path.posix.join(toFolder, slug)\n console.log(`[migrateFolder] Ensuring destination folder: ${destinationFolderPath}`)\n await ensureFolderExists(client, destinationFolderPath)\n }\n }\n\n // Process documents directly like the old migration script\n if (payload) {\n console.log('[migrateFolder] Starting document-by-document migration.')\n\n for (const slug of collectionSlugs) {\n const isCollectionPrivate = privateSlugs.has(slug)\n\n console.log(\n `[migrateFolder] Migrating collection \"${slug}\"${isCollectionPrivate ? ' (private)' : ''}...`,\n )\n\n const found = await payload.find({\n collection: slug,\n depth: 0,\n limit: 0,\n pagination: false,\n })\n\n console.log(\n `[migrateFolder] Processing ${found.docs.length} document(s) in collection \"${slug}\".`,\n )\n\n let updatedCount = 0\n let skippedCount = 0\n let errorCount = 0\n\n // Process each document one by one, like the old migration script\n for (let i = 0; i < found.docs.length; i++) {\n const doc = found.docs[i]\n const filename = (doc as { filename?: string }).filename\n const existingImagekit = (doc as any).imagekit\n\n console.log(`[migrateFolder] [${i + 1}/${found.docs.length}] Processing document ${doc.id}`)\n\n // Skip items that don't have ImageKit data or filename\n if (!filename || !existingImagekit?.fileId || !existingImagekit?.url) {\n skippedCount += 1\n console.log(\n `[migrateFolder] Skipping document ${doc.id} - missing filename or ImageKit data`,\n )\n continue\n }\n\n if (dryRun) {\n console.log(`[migrateFolder] [dry-run] Would upload: ${toFolder}/${slug}/${filename}`)\n console.log(`[migrateFolder] [dry-run] Would update Payload document: ${doc.id}`)\n } else {\n try {\n const sourceFilePath = getFilePath(fromFolder, slug, filename)\n const destinationFolder = getCollectionFolder(toFolder, slug)\n const destinationFilePath = getFilePath(toFolder, slug, filename)\n\n let uploaded: { fileId: string; thumbnailUrl?: string; url: string }\n\n try {\n const copyResponse = await client.files\n .copy({\n destinationPath: destinationFolder,\n sourceFilePath,\n })\n .asResponse()\n\n let copiedFileId = await readFileIdFromCopyResponse(copyResponse)\n\n if (!copiedFileId) {\n copiedFileId = await findImageKitFileId(client, destinationFilePath, {\n retries: 20,\n retryDelayMs: 1000,\n })\n }\n\n if (!copiedFileId) {\n throw new Error(\n `migrateFolder: copy request completed for \"${sourceFilePath}\", but the destination asset at \"${destinationFilePath}\" was not discoverable via ImageKit after retrying.`,\n )\n }\n\n uploaded = await getUploadedImagekitFields({\n client,\n fileId: copiedFileId,\n })\n } catch (error) {\n if (!isImageKitFileNotFoundError(error)) {\n throw error\n }\n\n console.warn(\n `[migrateFolder] Source file not found at ${sourceFilePath}. Falling back to stored ImageKit URL for \"${filename}\".`,\n )\n\n // A private source file 401s on its plain URL, so sign it before ImageKit\n // fetches it, and re-upload as private so the copy keeps its privacy.\n const sourceUrl = isCollectionPrivate\n ? client.helper.buildSrc({\n expiresIn: signedUrlExpiresIn,\n signed: true,\n src: existingImagekit.url,\n urlEndpoint,\n })\n : existingImagekit.url\n\n uploaded = await uploadFileFromURL({\n client,\n fileName: filename,\n folder: destinationFolder,\n isPrivate: isCollectionPrivate,\n sourceUrl,\n })\n }\n\n const canonicalUrl = client.helper.buildSrc({\n src: destinationFilePath,\n urlEndpoint,\n })\n\n const mimeType = (doc as { mimeType?: string }).mimeType\n const adminThumbnailURL = isCollectionPrivate\n ? getPrivateAdminThumbnailURL({ filename, mimeType }, slug, apiRoute)\n : getAdminThumbnailURL({ imagekit: { url: uploaded.url }, mimeType })\n\n // Update the document with new ImageKit data\n await payload.update({\n id: doc.id,\n collection: slug,\n data: {\n imagekit: {\n fileId: uploaded.fileId,\n isPrivate: isCollectionPrivate,\n thumbnailUrl: uploaded.thumbnailUrl,\n url: uploaded.url,\n },\n thumbnailURL: adminThumbnailURL || canonicalUrl,\n url: canonicalUrl,\n },\n })\n\n updatedCount += 1\n console.log(`[migrateFolder] Content migrated! ${filename} (${doc.id})`)\n } catch (error) {\n errorCount += 1\n console.error(\n `[migrateFolder] Failed to migrate document \"${doc.id}\" for file \"${filename}\":`,\n error,\n )\n }\n }\n }\n\n results.push({\n collectionSlug: slug,\n jobId: `migrate:${slug}`,\n status: dryRun ? 'Pending' : 'Completed',\n })\n\n console.log(\n `[migrateFolder] Collection \"${slug}\" complete: updated=${updatedCount}, skipped=${skippedCount}, errors=${errorCount}.`,\n )\n }\n\n console.log('[migrateFolder] Document-by-document migration complete.')\n } else {\n console.log('[migrateFolder] No Payload instance provided - skipping migration.')\n }\n\n console.log(`[migrateFolder] ${dryRun ? 'DRY-RUN simulation' : 'Migration'} complete.`)\n\n return results\n}\n"],"names":["ImageKit","path","getAdminThumbnailURL","getPrivateAdminThumbnailURL","DEFAULT_SIGNED_URL_EXPIRES_IN","isPrivateCollectionConfig","value","private","normalizeFolder","folder","normalized","trim","replace","Error","getCollectionFolder","rootFolder","collectionSlug","posix","join","getFilePath","filename","ensureFolderExists","client","folderPath","segments","split","filter","Boolean","parentFolderPath","folderName","folders","create","error","message","String","test","findImageKitFileId","imagekit","filePath","options","parentPath","dirname","name","basename","retries","retryDelayMs","limit","attempt","skip","assets","list","type","found","find","asset","fileId","length","Promise","resolve","setTimeout","undefined","isImageKitFileNotFoundError","uploadFileFromURL","fileName","isPrivate","sourceUrl","uploaded","files","upload","file","useUniqueFileName","isPrivateFile","url","thumbnailUrl","getUploadedImagekitFields","get","thumbnail","readFileIdFromCopyResponse","response","result","json","file_id","migrateFolder","collections","dryRun","from","payload","privateKey","signedUrlExpiresIn","signedUrlExpiresInOption","to","urlEndpoint","Object","keys","privateSlugs","Set","entries","map","slug","apiRoute","config","routes","api","fromFolder","toFolder","console","log","collectionSlugs","results","destinationFolderPath","isCollectionPrivate","has","collection","depth","pagination","docs","updatedCount","skippedCount","errorCount","i","doc","existingImagekit","id","sourceFilePath","destinationFolder","destinationFilePath","copyResponse","copy","destinationPath","asResponse","copiedFileId","warn","helper","buildSrc","expiresIn","signed","src","canonicalUrl","mimeType","adminThumbnailURL","update","data","thumbnailURL","push","jobId","status"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humaan/payload-storage-imagekit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "ImageKit storage adapter plugin for Payload CMS uploads",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,46 +32,47 @@
|
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@eslint/eslintrc": "^3.3.5",
|
|
35
|
-
"@payloadcms/db-mongodb": "3.
|
|
36
|
-
"@payloadcms/db-postgres": "3.
|
|
37
|
-
"@payloadcms/db-sqlite": "3.
|
|
35
|
+
"@payloadcms/db-mongodb": "3.85.1",
|
|
36
|
+
"@payloadcms/db-postgres": "3.85.1",
|
|
37
|
+
"@payloadcms/db-sqlite": "3.85.1",
|
|
38
38
|
"@payloadcms/eslint-config": "3.28.0",
|
|
39
|
-
"@payloadcms/next": "3.
|
|
40
|
-
"@payloadcms/
|
|
41
|
-
"@payloadcms/
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@swc/
|
|
45
|
-
"@
|
|
46
|
-
"@types/
|
|
39
|
+
"@payloadcms/next": "3.85.1",
|
|
40
|
+
"@payloadcms/plugin-cloud-storage": "3.85.1",
|
|
41
|
+
"@payloadcms/richtext-lexical": "3.85.1",
|
|
42
|
+
"@payloadcms/ui": "3.85.1",
|
|
43
|
+
"@playwright/test": "1.59.1",
|
|
44
|
+
"@swc-node/register": "1.10.9",
|
|
45
|
+
"@swc/cli": "0.6.0",
|
|
46
|
+
"@types/node": "24.12.3",
|
|
47
|
+
"@types/react": "19.2.17",
|
|
47
48
|
"@types/react-dom": "19.2.3",
|
|
48
49
|
"copyfiles": "2.4.1",
|
|
49
|
-
"cross-env": "
|
|
50
|
+
"cross-env": "10.1.0",
|
|
50
51
|
"eslint": "^9.39.4",
|
|
51
|
-
"eslint-config-next": "16.2.
|
|
52
|
-
"graphql": "^16.
|
|
52
|
+
"eslint-config-next": "16.2.9",
|
|
53
|
+
"graphql": "^16.14.2",
|
|
53
54
|
"mongodb-memory-server": "10.1.4",
|
|
54
|
-
"next": "16.2.
|
|
55
|
+
"next": "16.2.9",
|
|
55
56
|
"open": "^10.2.0",
|
|
56
|
-
"payload": "3.
|
|
57
|
-
"prettier": "^3.
|
|
58
|
-
"qs-esm": "
|
|
59
|
-
"react": "19.2.
|
|
60
|
-
"react-dom": "19.2.
|
|
57
|
+
"payload": "3.85.1",
|
|
58
|
+
"prettier": "^3.9.3",
|
|
59
|
+
"qs-esm": "8.0.1",
|
|
60
|
+
"react": "19.2.7",
|
|
61
|
+
"react-dom": "19.2.7",
|
|
61
62
|
"rimraf": "3.0.2",
|
|
62
|
-
"sharp": "0.34.
|
|
63
|
+
"sharp": "0.34.2",
|
|
63
64
|
"sort-package-json": "^2.15.1",
|
|
64
|
-
"typescript": "
|
|
65
|
-
"vite
|
|
66
|
-
"
|
|
65
|
+
"typescript": "6.0.3",
|
|
66
|
+
"vite": "7.3.6",
|
|
67
|
+
"vite-tsconfig-paths": "6.0.5",
|
|
68
|
+
"vitest": "4.1.9"
|
|
67
69
|
},
|
|
68
70
|
"peerDependencies": {
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
+
"@payloadcms/plugin-cloud-storage": ">=3.85.1 <4.0.0",
|
|
72
|
+
"payload": ">=3.80.0 <4.0.0"
|
|
71
73
|
},
|
|
72
74
|
"engines": {
|
|
73
|
-
"node": "
|
|
74
|
-
"pnpm": "^9 || ^10"
|
|
75
|
+
"node": ">=24.15.0 <25"
|
|
75
76
|
},
|
|
76
77
|
"publishConfig": {
|
|
77
78
|
"access": "public",
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
},
|
|
80
81
|
"registry": "https://registry.npmjs.org/",
|
|
81
82
|
"dependencies": {
|
|
82
|
-
"@imagekit/nodejs": "7.
|
|
83
|
+
"@imagekit/nodejs": "^7.8.1"
|
|
83
84
|
},
|
|
84
85
|
"scripts": {
|
|
85
86
|
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|