@humaan/payload-storage-imagekit 2.9.1 → 3.0.0-canary.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 +54 -7
- package/dist/clientUploads.d.ts +4 -41
- package/dist/clientUploads.js +1 -8
- package/dist/clientUploads.js.map +1 -1
- package/dist/components/BeforeDashboardServer.d.ts +1 -1
- package/dist/components/BeforeDashboardServer.js +6 -1
- package/dist/components/BeforeDashboardServer.js.map +1 -1
- package/dist/components/ImageKitUploadHandler.d.ts +3 -4
- package/dist/components/ImageKitUploadHandler.js +42 -80
- package/dist/components/ImageKitUploadHandler.js.map +1 -1
- package/dist/endpoints/createImageKitUploadInstructions.d.ts +14 -0
- package/dist/endpoints/createImageKitUploadInstructions.js +116 -0
- package/dist/endpoints/createImageKitUploadInstructions.js.map +1 -0
- package/dist/index.d.ts +6 -28
- package/dist/index.js +456 -638
- package/dist/index.js.map +1 -1
- package/dist/migrate.js +4 -1
- package/dist/migrate.js.map +1 -1
- package/package.json +13 -13
- package/dist/clientUploadVerification.d.ts +0 -30
- package/dist/clientUploadVerification.js +0 -106
- package/dist/clientUploadVerification.js.map +0 -1
- package/dist/endpoints/createImageKitUploadAuthHandler.d.ts +0 -26
- package/dist/endpoints/createImageKitUploadAuthHandler.js +0 -215
- package/dist/endpoints/createImageKitUploadAuthHandler.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,691 +1,509 @@
|
|
|
1
1
|
import ImageKit, { toFile } from '@imagekit/nodejs';
|
|
2
2
|
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage';
|
|
3
|
-
import { buildStoragePathData
|
|
3
|
+
import { buildStoragePathData } from '@payloadcms/plugin-cloud-storage/utilities';
|
|
4
4
|
import { openAsBlob } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { APIError } from 'payload';
|
|
7
|
-
import { getRangeRequestInfo, isXmlMimeType,
|
|
8
|
-
import {
|
|
9
|
-
import { imageKitUploadAuthPath, isImageKitReceiptContext } from './clientUploads.js';
|
|
10
|
-
import { createImageKitClientUploadVerifier, getClientUploadStorageFilePath } from './clientUploadVerification.js';
|
|
11
|
-
import { createImageKitUploadAuthHandler } from './endpoints/createImageKitUploadAuthHandler.js';
|
|
7
|
+
import { assertClientUploadAllowed, getRangeRequestInfo, isXmlMimeType, uploadContentSecurityPolicy, verifyClientUploadReceipt } from 'payload/internal';
|
|
8
|
+
import { createImageKitUploadInstructions } from './endpoints/createImageKitUploadInstructions.js';
|
|
12
9
|
import { getAdminThumbnailURL, getPrivateAdminThumbnailURL, isPdfMimeType, isVideoMimeType } from './getAdminThumbnailURL.js';
|
|
13
10
|
import { getImageKitThumbnailSrc, getVideoPosterSrc } from './imageTransforms.js';
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
const upload = collection.upload;
|
|
32
|
-
if (!upload || upload === true) {
|
|
33
|
-
return Boolean(upload);
|
|
34
|
-
}
|
|
35
|
-
// Payload skips MIME and content restrictions when this escape hatch is enabled.
|
|
36
|
-
if (upload.allowRestrictedFileTypes) {
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
const allowedMimeTypes = upload.mimeTypes;
|
|
40
|
-
return !allowedMimeTypes?.length || allowedMimeTypes.some((mimeType)=>mimeType.includes('*') || isXmlMimeType(mimeType)) || XML_FAMILY_MIME_TYPES.some((mimeType)=>validateMimeType(mimeType, allowedMimeTypes));
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Whether a specific document is private. The flag stored on the document at upload time
|
|
44
|
-
* (`imagekit.isPrivate`) is the source of truth — a file's privacy on ImageKit is fixed
|
|
45
|
-
* when it is uploaded and does not change if the collection's `private` setting changes
|
|
46
|
-
* later. Falls back to the collection setting for documents uploaded before this flag existed.
|
|
47
|
-
*/ const isDocumentPrivate = (doc, collectionIsPrivate)=>{
|
|
48
|
-
const stored = doc?.imagekit?.isPrivate;
|
|
49
|
-
return typeof stored === 'boolean' ? stored : collectionIsPrivate;
|
|
50
|
-
};
|
|
51
|
-
const normalizeFolder = (folder)=>{
|
|
52
|
-
const normalized = folder.trim().replace(/^\/+|\/+$/g, '');
|
|
53
|
-
if (!normalized) {
|
|
54
|
-
throw new Error('imagekitStorage: `folder` is required.');
|
|
55
|
-
}
|
|
56
|
-
return `/${normalized}`;
|
|
57
|
-
};
|
|
58
|
-
const getCollectionFolder = (rootFolder, collectionSlug)=>path.posix.join(rootFolder, collectionSlug);
|
|
59
|
-
const findImageKitFileId = async (imagekit, filePath)=>{
|
|
60
|
-
const parentPath = path.posix.dirname(filePath);
|
|
61
|
-
const name = path.posix.basename(filePath);
|
|
62
|
-
const limit = 100;
|
|
63
|
-
for(let skip = 0;; skip += limit){
|
|
64
|
-
const assets = await imagekit.assets.list({
|
|
65
|
-
type: 'file',
|
|
66
|
-
limit,
|
|
67
|
-
path: parentPath === '/' ? '/' : `${parentPath}/`,
|
|
68
|
-
skip
|
|
69
|
-
});
|
|
70
|
-
const found = assets.find((asset)=>'fileId' in asset && asset.filePath === filePath && asset.name === name);
|
|
71
|
-
if (found && 'fileId' in found) {
|
|
72
|
-
return found.fileId;
|
|
73
|
-
}
|
|
74
|
-
if (assets.length < limit) {
|
|
75
|
-
break;
|
|
11
|
+
/** Payload 4 storage capability. Register under config.storage, not config.plugins. */ export const imagekitStorage = (options)=>({
|
|
12
|
+
name: 'imagekit',
|
|
13
|
+
collections: Object.keys(options.collections),
|
|
14
|
+
init: (config)=>initializeStorage(config, options)
|
|
15
|
+
});
|
|
16
|
+
export { migrateFolder } from './migrate.js';
|
|
17
|
+
export const payloadStorageImagekit = imagekitStorage;
|
|
18
|
+
export const payloadStorageImageKit = imagekitStorage;
|
|
19
|
+
function initializeStorage(config, options) {
|
|
20
|
+
for (const key of [
|
|
21
|
+
'privateKey',
|
|
22
|
+
'publicKey',
|
|
23
|
+
'urlEndpoint',
|
|
24
|
+
'folder'
|
|
25
|
+
]){
|
|
26
|
+
if (!options[key]?.trim()) {
|
|
27
|
+
throw new Error(`imagekitStorage: \`${key}\` is required.`);
|
|
76
28
|
}
|
|
77
29
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const getStoredImageKitURL = (doc)=>{
|
|
82
|
-
const storedURL = doc?.imagekit?.url;
|
|
83
|
-
return typeof storedURL === 'string' && storedURL.trim().length > 0 ? storedURL : undefined;
|
|
84
|
-
};
|
|
85
|
-
const hasStoredImageKitURL = (doc)=>!!getStoredImageKitURL(doc);
|
|
86
|
-
const findUploadDocumentByFilename = async ({ collection, filename, prefix, req })=>{
|
|
87
|
-
const constraints = [
|
|
88
|
-
{
|
|
89
|
-
filename: {
|
|
90
|
-
equals: filename
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
];
|
|
94
|
-
if (typeof prefix === 'string') {
|
|
95
|
-
constraints.push({
|
|
96
|
-
prefix: {
|
|
97
|
-
equals: prefix
|
|
98
|
-
}
|
|
99
|
-
});
|
|
30
|
+
const folder = options.folder.trim().replace(/^\/+|\/+$/g, '');
|
|
31
|
+
if (folder.split('/').some((segment)=>!segment || segment === '.' || segment === '..')) {
|
|
32
|
+
throw new Error('imagekitStorage: `folder` must not contain empty or dot segments.');
|
|
100
33
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
req,
|
|
104
|
-
where: {
|
|
105
|
-
and: constraints
|
|
106
|
-
}
|
|
34
|
+
const client = new ImageKit({
|
|
35
|
+
privateKey: options.privateKey
|
|
107
36
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
37
|
+
const collections = options.collections;
|
|
38
|
+
if (!Object.keys(collections).length) {
|
|
39
|
+
throw new Error('imagekitStorage: `collections` must not be empty.');
|
|
40
|
+
}
|
|
41
|
+
const configured = new Set(Object.keys(collections));
|
|
42
|
+
const apiRoute = config.routes?.api ?? '/api';
|
|
43
|
+
const expiresIn = options.signedUrlExpiresIn ?? 900;
|
|
44
|
+
const prefixFor = (slug)=>path.posix.join(folder, slug);
|
|
45
|
+
const collectionIsPrivate = (slug)=>{
|
|
46
|
+
const value = collections[slug];
|
|
47
|
+
return value !== true && value?.private === true;
|
|
115
48
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
49
|
+
const sourceConfig = {
|
|
50
|
+
...config,
|
|
51
|
+
collections: (config.collections ?? []).map((collection)=>{
|
|
52
|
+
if (!configured.has(collection.slug)) {
|
|
53
|
+
return collection;
|
|
54
|
+
}
|
|
55
|
+
const isPrivate = collectionIsPrivate(collection.slug);
|
|
56
|
+
if (options.disablePayloadAccessControl && (isPrivate || canStoreXml(collection))) {
|
|
57
|
+
throw new Error('imagekitStorage: private and XML-capable collections require Payload access control.');
|
|
58
|
+
}
|
|
59
|
+
const setting = collections[collection.slug];
|
|
60
|
+
const focalPoint = setting !== true ? setting?.focalPoint : undefined;
|
|
61
|
+
const description = collection.admin?.description;
|
|
62
|
+
return {
|
|
63
|
+
...collection,
|
|
64
|
+
admin: isPrivate ? {
|
|
65
|
+
...collection.admin,
|
|
66
|
+
description: typeof description === 'string' ? `${description} Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.` : description ?? 'Files in this collection are uploaded to ImageKit as private files and delivered through short-lived signed URLs.'
|
|
67
|
+
} : collection.admin,
|
|
68
|
+
hooks: {
|
|
69
|
+
...collection.hooks,
|
|
70
|
+
beforeChange: [
|
|
71
|
+
...collection.hooks?.beforeChange ?? [],
|
|
72
|
+
async ({ data, operation, originalDoc, req })=>{
|
|
73
|
+
if (operation === 'validate') {
|
|
74
|
+
return data;
|
|
75
|
+
}
|
|
76
|
+
const verified = req.context.imagekitVerifiedUpload;
|
|
77
|
+
if (!verified) {
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
const existing = await req.payload.db.findOne({
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
|
|
82
|
+
collection: collection.slug,
|
|
83
|
+
req,
|
|
84
|
+
where: {
|
|
85
|
+
'imagekit.fileId': {
|
|
86
|
+
equals: verified.fileId
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (existing && existing.id !== originalDoc?.id) {
|
|
91
|
+
throw new APIError('This ImageKit upload reference has already been used.', 400);
|
|
92
|
+
}
|
|
93
|
+
return applyMetadata(data, verified, isPrivate, collection.slug, apiRoute);
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
upload: {
|
|
98
|
+
focalPoint: focalPoint ?? options.focalPoint ?? false,
|
|
99
|
+
...typeof collection.upload === 'object' ? collection.upload : {},
|
|
100
|
+
adminThumbnail: ({ doc })=>isPrivateDocument(doc, isPrivate) ? getPrivateAdminThumbnailURL(doc, collection.slug, apiRoute) : getAdminThumbnailURL(doc),
|
|
101
|
+
crop: false
|
|
142
102
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
});
|
|
146
|
-
if (!response.ok || response.status !== rangeResult.status) {
|
|
147
|
-
void response.body?.cancel().catch(()=>undefined);
|
|
148
|
-
throw new Error(`imagekitStorage: ImageKit returned an unexpected file response (${response.status}).`);
|
|
149
|
-
}
|
|
150
|
-
const upstreamLength = response.headers.get('Content-Length');
|
|
151
|
-
const expectedLength = rangeResult.headers['Content-Length'];
|
|
152
|
-
const upstreamRange = response.headers.get('Content-Range');
|
|
153
|
-
const expectedRange = rangeResult.type === 'partial' ? rangeResult.headers['Content-Range'] : undefined;
|
|
154
|
-
if (upstreamLength !== null && upstreamLength !== expectedLength || expectedRange && upstreamRange !== expectedRange) {
|
|
155
|
-
void response.body?.cancel().catch(()=>undefined);
|
|
156
|
-
throw new Error('imagekitStorage: ImageKit returned mismatched file content metadata.');
|
|
157
|
-
}
|
|
158
|
-
const headers = new Headers(rangeResult.headers);
|
|
159
|
-
headers.set('Content-Type', mimeType);
|
|
160
|
-
if (isXmlMimeType(mimeType)) {
|
|
161
|
-
headers.set('Content-Security-Policy', UPLOAD_CONTENT_SECURITY_POLICY);
|
|
162
|
-
}
|
|
163
|
-
return new Response(response.body, {
|
|
164
|
-
headers,
|
|
165
|
-
status: response.status
|
|
166
|
-
});
|
|
103
|
+
};
|
|
104
|
+
})
|
|
167
105
|
};
|
|
168
|
-
|
|
169
|
-
const isPrivate =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
signed: true,
|
|
176
|
-
src,
|
|
177
|
-
transformation: tr ? [
|
|
106
|
+
const adapter = ({ collection, prefix = prefixFor(collection.slug) })=>{
|
|
107
|
+
const isPrivate = collectionIsPrivate(collection.slug);
|
|
108
|
+
const buildURL = (key, signed, transformation)=>client.helper.buildSrc({
|
|
109
|
+
expiresIn: signed ? expiresIn : undefined,
|
|
110
|
+
signed,
|
|
111
|
+
src: `/${key.replace(/^\/+/, '')}`,
|
|
112
|
+
transformation: transformation ? [
|
|
178
113
|
{
|
|
179
|
-
raw:
|
|
114
|
+
raw: transformation
|
|
180
115
|
}
|
|
181
116
|
] : undefined,
|
|
182
|
-
urlEndpoint
|
|
117
|
+
urlEndpoint: options.urlEndpoint
|
|
183
118
|
});
|
|
184
|
-
const
|
|
119
|
+
const deleteFile = async ({ storageFilePath })=>{
|
|
120
|
+
const file = await findFile(client, storageFilePath);
|
|
121
|
+
if (file) {
|
|
122
|
+
await client.files.delete(file.fileId);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
185
125
|
return {
|
|
186
126
|
name: 'imagekit',
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Presentational wrapper: tucks the ImageKit details into a closed panel without
|
|
191
|
-
// changing the `imagekit.*` data paths.
|
|
192
|
-
type: 'collapsible',
|
|
193
|
-
admin: {
|
|
194
|
-
condition: (data)=>hasStoredImageKitURL(data),
|
|
195
|
-
initCollapsed: true
|
|
196
|
-
},
|
|
197
|
-
fields: [
|
|
198
|
-
{
|
|
199
|
-
name: 'imagekit',
|
|
200
|
-
type: 'group',
|
|
201
|
-
admin: {
|
|
202
|
-
disableListColumn: true,
|
|
203
|
-
disableListFilter: true
|
|
204
|
-
},
|
|
205
|
-
fields: [
|
|
206
|
-
{
|
|
207
|
-
name: 'fileId',
|
|
208
|
-
type: 'text',
|
|
209
|
-
admin: {
|
|
210
|
-
hidden: true
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: 'url',
|
|
215
|
-
type: 'text',
|
|
216
|
-
admin: {
|
|
217
|
-
readOnly: true
|
|
218
|
-
},
|
|
219
|
-
label: 'URL'
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
name: 'isPrivate',
|
|
223
|
-
type: 'checkbox',
|
|
224
|
-
admin: {
|
|
225
|
-
condition: (_data, siblingData)=>isDocumentPrivate({
|
|
226
|
-
imagekit: siblingData
|
|
227
|
-
}, isPrivate),
|
|
228
|
-
description: "Indicates whether this file was uploaded to ImageKit as private. Private files are delivered through signed URLs. Changes to this collection's private option in payload.config only affect future uploads.",
|
|
229
|
-
readOnly: true
|
|
230
|
-
},
|
|
231
|
-
label: 'Private file'
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: 'signedUrl',
|
|
235
|
-
type: 'text',
|
|
236
|
-
admin: {
|
|
237
|
-
// Only relevant for private files; the plain `url` works directly for public ones.
|
|
238
|
-
condition: (_data, siblingData)=>isDocumentPrivate({
|
|
239
|
-
imagekit: siblingData
|
|
240
|
-
}, isPrivate),
|
|
241
|
-
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.`,
|
|
242
|
-
readOnly: true
|
|
243
|
-
},
|
|
244
|
-
hooks: {
|
|
245
|
-
// Computed server-side on read so it is always freshly signed and never persisted.
|
|
246
|
-
afterRead: [
|
|
247
|
-
({ data, siblingData })=>{
|
|
248
|
-
if (!isDocumentPrivate({
|
|
249
|
-
imagekit: siblingData
|
|
250
|
-
}, isPrivate)) {
|
|
251
|
-
return undefined;
|
|
252
|
-
}
|
|
253
|
-
const filename = data?.filename;
|
|
254
|
-
if (!filename) {
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
const source = data || {};
|
|
258
|
-
const documentPrefix = path.posix.join(source.prefix || prefix, source._objectKey || '');
|
|
259
|
-
const storageFilePath = buildStoragePathData({
|
|
260
|
-
collectionPrefix: prefix,
|
|
261
|
-
docPrefix: documentPrefix,
|
|
262
|
-
filename
|
|
263
|
-
}).storageFilePath;
|
|
264
|
-
return buildSignedURL(`/${storageFilePath}`);
|
|
265
|
-
}
|
|
266
|
-
]
|
|
267
|
-
},
|
|
268
|
-
label: 'Signed URL',
|
|
269
|
-
virtual: true
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
name: 'thumbnailUrl',
|
|
273
|
-
type: 'text',
|
|
274
|
-
admin: {
|
|
275
|
-
hidden: true
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
],
|
|
279
|
-
interfaceName: 'imagekitFields',
|
|
280
|
-
label: false
|
|
281
|
-
}
|
|
282
|
-
],
|
|
283
|
-
label: 'Storage: ImageKit'
|
|
127
|
+
copyFile: async ({ from, to })=>{
|
|
128
|
+
if (await findFile(client, to)) {
|
|
129
|
+
throw new Error(`ImageKit destination already exists: ${to}`);
|
|
284
130
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
collectionPrefix: prefix,
|
|
289
|
-
docPrefix: documentPrefix,
|
|
290
|
-
filename
|
|
291
|
-
}).storageFilePath}`,
|
|
292
|
-
urlEndpoint
|
|
293
|
-
}),
|
|
294
|
-
handleDelete: async ({ doc, storageFilePath })=>{
|
|
295
|
-
const storedFileId = doc.imagekit?.fileId;
|
|
296
|
-
const fileId = storedFileId || await findImageKitFileId(client, `/${storageFilePath.replace(/^\/+/, '')}`);
|
|
297
|
-
if (!fileId) {
|
|
298
|
-
return;
|
|
131
|
+
const source = await findFile(client, from);
|
|
132
|
+
if (!source) {
|
|
133
|
+
throw new Error(`ImageKit source not found: ${from}`);
|
|
299
134
|
}
|
|
300
|
-
|
|
135
|
+
// Upload by URL copies the exact original without buffering it in the app process.
|
|
136
|
+
// overwriteFile:false protects a destination created concurrently with the lookup.
|
|
137
|
+
await client.files.upload({
|
|
138
|
+
customMetadata: source.customMetadata ?? undefined,
|
|
139
|
+
file: buildURL(from, Boolean(source.isPrivateFile), 'orig-true'),
|
|
140
|
+
fileName: path.posix.basename(to),
|
|
141
|
+
folder: `/${path.posix.dirname(to)}`,
|
|
142
|
+
isPrivateFile: source.isPrivateFile ?? false,
|
|
143
|
+
overwriteFile: false,
|
|
144
|
+
tags: source.tags ?? undefined,
|
|
145
|
+
useUniqueFileName: false
|
|
146
|
+
});
|
|
301
147
|
},
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
148
|
+
deleteFile,
|
|
149
|
+
fields: storageFields({
|
|
150
|
+
buildURL,
|
|
151
|
+
expiresIn,
|
|
152
|
+
isPrivate,
|
|
153
|
+
prefix
|
|
154
|
+
}),
|
|
155
|
+
generateURL: ({ filename, prefix: docPrefix })=>buildURL(buildStoragePathData({
|
|
156
|
+
collectionPrefix: prefix,
|
|
157
|
+
docPrefix,
|
|
158
|
+
filename
|
|
159
|
+
}).storageFilePath, false),
|
|
160
|
+
handleDelete: deleteFile,
|
|
161
|
+
handleUpload: async ({ data, file, storageFilePath })=>{
|
|
312
162
|
const uploadSource = file.tempFilePath ? await openAsBlob(file.tempFilePath, {
|
|
313
163
|
type: file.mimeType
|
|
314
164
|
}) : file.buffer;
|
|
315
|
-
const uploadFile = await toFile(uploadSource, file.filename, {
|
|
316
|
-
type: file.mimeType
|
|
317
|
-
});
|
|
318
|
-
const uploadFolder = path.posix.dirname(`/${storageFilePath.replace(/^\/+/, '')}`);
|
|
319
|
-
const uploadFilename = path.posix.basename(storageFilePath);
|
|
320
165
|
const uploaded = await client.files.upload({
|
|
321
|
-
file:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
166
|
+
file: await toFile(uploadSource, file.filename, {
|
|
167
|
+
type: file.mimeType
|
|
168
|
+
}),
|
|
169
|
+
fileName: path.posix.basename(storageFilePath),
|
|
170
|
+
folder: `/${path.posix.dirname(storageFilePath)}`,
|
|
171
|
+
isPrivateFile: isPrivate,
|
|
172
|
+
overwriteFile: false,
|
|
173
|
+
useUniqueFileName: false
|
|
328
174
|
});
|
|
329
|
-
|
|
175
|
+
if (!uploaded.fileId || !uploaded.url) {
|
|
176
|
+
throw new Error('ImageKit returned incomplete upload metadata.');
|
|
177
|
+
}
|
|
178
|
+
return applyMetadata(data, {
|
|
179
|
+
name: path.posix.basename(storageFilePath),
|
|
330
180
|
fileId: uploaded.fileId,
|
|
331
|
-
isPrivate,
|
|
332
|
-
|
|
181
|
+
isPrivateFile: isPrivate,
|
|
182
|
+
mime: file.mimeType,
|
|
183
|
+
size: file.filesize,
|
|
184
|
+
thumbnail: uploaded.thumbnailUrl,
|
|
333
185
|
url: uploaded.url
|
|
334
|
-
};
|
|
335
|
-
// Set the root-level thumbnailURL that Payload's admin interface expects.
|
|
336
|
-
// `buildThumbnailURL` needs the filename, which Payload may not have copied onto
|
|
337
|
-
// `data` yet at this point, so fall back to the uploaded file's name.
|
|
338
|
-
data.thumbnailURL = buildThumbnailURL({
|
|
339
|
-
...data,
|
|
340
|
-
filename: data.filename || uploadFilename
|
|
341
|
-
});
|
|
342
|
-
return data;
|
|
186
|
+
}, isPrivate, collection.slug, apiRoute);
|
|
343
187
|
},
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
188
|
+
staticHandler: async (req, { doc, params })=>{
|
|
189
|
+
if (params.uploadReference) {
|
|
190
|
+
try {
|
|
191
|
+
const reference = params.uploadReference;
|
|
192
|
+
const receipt = verifyClientUploadReceipt({
|
|
193
|
+
collectionSlug: collection.slug,
|
|
194
|
+
req,
|
|
195
|
+
signedReceipt: reference.signedReceipt
|
|
196
|
+
});
|
|
197
|
+
const expectedPath = buildStoragePathData({
|
|
198
|
+
collectionPrefix: prefix,
|
|
199
|
+
docPrefix: path.posix.join(receipt.filePrefix, receipt._objectKey ?? ''),
|
|
200
|
+
filename: params.filename
|
|
201
|
+
}).storageFilePath;
|
|
202
|
+
if (!receipt._objectKey || expectedPath !== receipt.storageFilePath || !expectedPath.startsWith(`${prefix.replace(/^\/+/, '')}/`)) {
|
|
203
|
+
throw new APIError('Invalid ImageKit upload reference.', 400);
|
|
204
|
+
}
|
|
205
|
+
const uploaded = await findUploadedFile(client, expectedPath);
|
|
206
|
+
if (!uploaded || !uploaded.mime || Boolean(uploaded.isPrivateFile) !== isPrivate) {
|
|
207
|
+
throw new APIError('ImageKit upload did not match its authorization.', 400);
|
|
208
|
+
}
|
|
209
|
+
assertClientUploadAllowed({
|
|
210
|
+
collection,
|
|
211
|
+
filename: uploaded.name,
|
|
212
|
+
mimeType: uploaded.mime
|
|
213
|
+
});
|
|
214
|
+
req.context.imagekitVerifiedUpload = uploaded;
|
|
215
|
+
return await fetchOriginal({
|
|
216
|
+
file: uploaded,
|
|
217
|
+
req,
|
|
218
|
+
url: buildURL(expectedPath, isPrivate, 'orig-true')
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (error instanceof APIError) {
|
|
222
|
+
return Response.json({
|
|
223
|
+
message: error.message
|
|
224
|
+
}, {
|
|
225
|
+
status: error.status
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
353
229
|
}
|
|
354
|
-
const uploadedFile = await verifyClientUpload({
|
|
355
|
-
collectionSlug: collection.slug,
|
|
356
|
-
context: clientUploadContext
|
|
357
|
-
});
|
|
358
|
-
const expectedFilePath = getClientUploadStorageFilePath({
|
|
359
|
-
collectionPrefix: prefix,
|
|
360
|
-
context: clientUploadContext
|
|
361
|
-
});
|
|
362
|
-
const verifiedFileURL = isPrivate ? buildSignedURL(`/${expectedFilePath}`, ORIGINAL_FILE_TRANSFORMATION) : client.helper.buildSrc({
|
|
363
|
-
src: `/${expectedFilePath}`,
|
|
364
|
-
transformation: [
|
|
365
|
-
{
|
|
366
|
-
raw: ORIGINAL_FILE_TRANSFORMATION
|
|
367
|
-
}
|
|
368
|
-
],
|
|
369
|
-
urlEndpoint
|
|
370
|
-
});
|
|
371
|
-
return fetchUploadedFile({
|
|
372
|
-
fileSize: uploadedFile.size,
|
|
373
|
-
fileURL: verifiedFileURL,
|
|
374
|
-
mimeType: uploadedFile.mimeType,
|
|
375
|
-
range: req.headers.get('Range')
|
|
376
|
-
});
|
|
377
230
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
231
|
+
const document = doc ?? await req.payload.db.findOne({
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Generated consumer configs narrow CollectionSlug, unlike the standalone package build.
|
|
233
|
+
collection: collection.slug,
|
|
234
|
+
req,
|
|
235
|
+
where: {
|
|
236
|
+
filename: {
|
|
237
|
+
equals: params.filename
|
|
238
|
+
}
|
|
239
|
+
}
|
|
383
240
|
});
|
|
384
|
-
const
|
|
385
|
-
|
|
241
|
+
const source = getFileLocation(document, params.filename);
|
|
242
|
+
const key = buildStoragePathData({
|
|
386
243
|
collectionPrefix: prefix,
|
|
387
|
-
|
|
388
|
-
filename:
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// present even when ImageKit uses a custom, same-origin delivery domain.
|
|
396
|
-
if (typeof mimeType === 'string' && isXmlMimeType(mimeType)) {
|
|
397
|
-
const filesize = doc?.filesize;
|
|
398
|
-
if (typeof filesize !== 'number' || !Number.isSafeInteger(filesize) || filesize < 0) {
|
|
399
|
-
throw new APIError('Stored ImageKit file metadata is incomplete.', 500);
|
|
244
|
+
docPrefix: path.posix.join(params.prefix ?? source.prefix ?? prefix, source._objectKey ?? ''),
|
|
245
|
+
filename: params.filename
|
|
246
|
+
}).storageFilePath;
|
|
247
|
+
const privateFile = isPrivateDocument(document, isPrivate);
|
|
248
|
+
if (params.operation === 'transform' || isXmlMimeType(source.mimeType ?? '')) {
|
|
249
|
+
const file = await findFile(client, key);
|
|
250
|
+
if (!file?.mime) {
|
|
251
|
+
throw new APIError('ImageKit source not found.', 404);
|
|
400
252
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
raw: ORIGINAL_FILE_TRANSFORMATION
|
|
406
|
-
}
|
|
407
|
-
],
|
|
408
|
-
urlEndpoint
|
|
409
|
-
});
|
|
410
|
-
return fetchUploadedFile({
|
|
411
|
-
fileSize: filesize,
|
|
412
|
-
fileURL,
|
|
413
|
-
mimeType,
|
|
414
|
-
range: req.headers.get('Range')
|
|
253
|
+
return fetchOriginal({
|
|
254
|
+
file,
|
|
255
|
+
req,
|
|
256
|
+
url: buildURL(key, privateFile, 'orig-true')
|
|
415
257
|
});
|
|
416
258
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (tr && isVideoMimeType(mimeType)) {
|
|
424
|
-
signSrc = getVideoPosterSrc(signSrc);
|
|
425
|
-
} else if (tr && isPdfMimeType(mimeType)) {
|
|
426
|
-
signSrc = getImageKitThumbnailSrc(signSrc);
|
|
427
|
-
}
|
|
428
|
-
return Response.redirect(buildSignedURL(signSrc, tr), 302);
|
|
259
|
+
const tr = new URL(req.url ?? 'http://localhost').searchParams.get('tr');
|
|
260
|
+
let src = key;
|
|
261
|
+
if (tr && isVideoMimeType(source.mimeType)) {
|
|
262
|
+
src = getVideoPosterSrc(src);
|
|
263
|
+
} else if (tr && isPdfMimeType(source.mimeType)) {
|
|
264
|
+
src = getImageKitThumbnailSrc(src);
|
|
429
265
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
266
|
+
// Existing v3 files may live under an earlier folder or delivery endpoint.
|
|
267
|
+
const storedURL = document?.imagekit?.url;
|
|
268
|
+
if (!privateFile && !tr && !(document && 'original' in document && document.original) && storedURL) {
|
|
269
|
+
return Response.redirect(storedURL, 302);
|
|
270
|
+
}
|
|
271
|
+
return Response.redirect(buildURL(src, privateFile, tr), 302);
|
|
272
|
+
},
|
|
273
|
+
supportsTempFiles: true,
|
|
274
|
+
uploadInstructions: {
|
|
275
|
+
adminHandler: {
|
|
276
|
+
path: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler'
|
|
277
|
+
},
|
|
278
|
+
enabled: options.enabled !== false && Boolean(options.clientUploads),
|
|
279
|
+
generate: createImageKitUploadInstructions({
|
|
280
|
+
access: typeof options.clientUploads === 'object' ? options.clientUploads.access : undefined,
|
|
281
|
+
isPrivate,
|
|
282
|
+
prefix,
|
|
283
|
+
privateKey: options.privateKey,
|
|
284
|
+
publicKey: options.publicKey
|
|
285
|
+
}),
|
|
286
|
+
requiresUploadReceipt: true,
|
|
287
|
+
useInAdmin: true
|
|
440
288
|
}
|
|
441
289
|
};
|
|
442
290
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
291
|
+
const output = cloudStoragePlugin({
|
|
292
|
+
collections: Object.fromEntries(Object.keys(collections).map((slug)=>[
|
|
293
|
+
slug,
|
|
294
|
+
{
|
|
295
|
+
adapter,
|
|
296
|
+
disableLocalStorage: true,
|
|
297
|
+
disablePayloadAccessControl: options.disablePayloadAccessControl,
|
|
298
|
+
prefix: prefixFor(slug)
|
|
299
|
+
}
|
|
300
|
+
])),
|
|
301
|
+
enabled: options.enabled
|
|
302
|
+
})(sourceConfig);
|
|
303
|
+
// Do not invent a prefix on reads of legacy documents. A stored prefix is retained;
|
|
304
|
+
// new uploads get their normalized destination from the file-operation pipeline.
|
|
305
|
+
output.collections = output.collections?.map((collection)=>!configured.has(collection.slug) ? collection : {
|
|
306
|
+
...collection,
|
|
307
|
+
fields: collection.fields.map((field)=>field.type === 'text' && field.name === 'prefix' ? {
|
|
308
|
+
...field,
|
|
309
|
+
defaultValue: ({ req })=>req.file && typeof field.defaultValue === 'string' ? field.defaultValue : ''
|
|
310
|
+
} : field)
|
|
311
|
+
});
|
|
312
|
+
return output;
|
|
313
|
+
}
|
|
314
|
+
async function findFile(client, storageFilePath) {
|
|
315
|
+
const filePath = `/${storageFilePath.replace(/^\/+/, '')}`;
|
|
316
|
+
const name = path.posix.basename(filePath);
|
|
317
|
+
for(let skip = 0;; skip += 100){
|
|
318
|
+
const assets = await client.assets.list({
|
|
319
|
+
type: 'file',
|
|
320
|
+
limit: 100,
|
|
321
|
+
path: `${path.posix.dirname(filePath)}/`,
|
|
322
|
+
skip
|
|
323
|
+
});
|
|
324
|
+
const found = assets.find((asset)=>'fileId' in asset && asset.filePath === filePath && asset.name === name);
|
|
325
|
+
if (found && 'fileId' in found && found.fileId) {
|
|
326
|
+
const file = await client.files.get(found.fileId);
|
|
327
|
+
if (file.fileId !== found.fileId || file.name !== name || file.filePath !== filePath || typeof file.size !== 'number' || !Number.isSafeInteger(file.size) || file.size < 0 || !file.url) {
|
|
328
|
+
throw new Error('ImageKit returned incomplete file metadata.');
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
...file,
|
|
332
|
+
name: file.name,
|
|
333
|
+
fileId: file.fileId,
|
|
334
|
+
size: file.size,
|
|
335
|
+
url: file.url
|
|
336
|
+
};
|
|
470
337
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (disablePayloadAccessControl && privateSlugs.size > 0) {
|
|
474
|
-
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.');
|
|
338
|
+
if (assets.length < 100) {
|
|
339
|
+
return undefined;
|
|
475
340
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function findUploadedFile(client, key) {
|
|
344
|
+
// ImageKit asset listings can lag a successful upload. Only admission retries;
|
|
345
|
+
// ordinary missing reads/deletes and collision lookups must stay immediate.
|
|
346
|
+
let file = await findFile(client, key);
|
|
347
|
+
for (const delay of [
|
|
348
|
+
250,
|
|
349
|
+
500,
|
|
350
|
+
1000,
|
|
351
|
+
2000
|
|
352
|
+
]){
|
|
353
|
+
if (file) {
|
|
354
|
+
break;
|
|
481
355
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
356
|
+
await new Promise((resolve)=>setTimeout(resolve, delay));
|
|
357
|
+
file = await findFile(client, key);
|
|
358
|
+
}
|
|
359
|
+
return file;
|
|
360
|
+
}
|
|
361
|
+
function isPrivateDocument(doc, fallback) {
|
|
362
|
+
const value = doc?.imagekit?.isPrivate;
|
|
363
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
364
|
+
}
|
|
365
|
+
function getFileLocation(doc, filename) {
|
|
366
|
+
const value = doc ?? {};
|
|
367
|
+
return [
|
|
368
|
+
value,
|
|
369
|
+
value.original,
|
|
370
|
+
...Object.values(value.variants ?? {})
|
|
371
|
+
].find((file)=>file?.filename === filename) ?? value;
|
|
372
|
+
}
|
|
373
|
+
function applyMetadata(data, file, isPrivate, slug, apiRoute) {
|
|
374
|
+
data.imagekit = {
|
|
375
|
+
fileId: file.fileId,
|
|
376
|
+
isPrivate,
|
|
377
|
+
thumbnailUrl: file.thumbnail,
|
|
378
|
+
url: file.url
|
|
379
|
+
};
|
|
380
|
+
data.thumbnailURL = isPrivate ? getPrivateAdminThumbnailURL(data, slug, apiRoute) : getAdminThumbnailURL(data);
|
|
381
|
+
return data;
|
|
382
|
+
}
|
|
383
|
+
async function fetchOriginal({ file, req, url }) {
|
|
384
|
+
const range = getRangeRequestInfo({
|
|
385
|
+
fileSize: file.size,
|
|
386
|
+
rangeHeader: req.headers.get('Range')
|
|
387
|
+
});
|
|
388
|
+
if (range.type === 'invalid') {
|
|
389
|
+
return new Response(null, {
|
|
390
|
+
headers: range.headers,
|
|
391
|
+
status: range.status
|
|
487
392
|
});
|
|
488
|
-
|
|
489
|
-
|
|
393
|
+
}
|
|
394
|
+
const response = await fetch(url, {
|
|
395
|
+
cache: 'no-store',
|
|
396
|
+
headers: range.type === 'partial' ? {
|
|
397
|
+
Range: `bytes=${range.rangeStart}-${range.rangeEnd}`
|
|
398
|
+
} : undefined,
|
|
399
|
+
signal: new AbortController().signal
|
|
400
|
+
});
|
|
401
|
+
if (response.status !== range.status || response.headers.get('Content-Length') !== range.headers['Content-Length'] || range.type === 'partial' && response.headers.get('Content-Range') !== range.headers['Content-Range']) {
|
|
402
|
+
void response.body?.cancel().catch(()=>undefined);
|
|
403
|
+
throw new APIError('ImageKit returned mismatched file content metadata.', 502);
|
|
404
|
+
}
|
|
405
|
+
const headers = new Headers(range.headers);
|
|
406
|
+
headers.set('Content-Type', file.mime ?? 'application/octet-stream');
|
|
407
|
+
if (isXmlMimeType(file.mime ?? '')) {
|
|
408
|
+
headers.set('Content-Security-Policy', uploadContentSecurityPolicy);
|
|
409
|
+
}
|
|
410
|
+
return new Response(response.body, {
|
|
411
|
+
headers,
|
|
412
|
+
status: range.status
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function canStoreXml(collection) {
|
|
416
|
+
const upload = typeof collection.upload === 'object' ? collection.upload : undefined;
|
|
417
|
+
return Boolean(upload?.allowRestrictedFileTypes) || !upload?.mimeTypes?.length || upload.mimeTypes.some((mime)=>mime.includes('*') || isXmlMimeType(mime));
|
|
418
|
+
}
|
|
419
|
+
function storageFields({ buildURL, expiresIn, isPrivate, prefix }) {
|
|
420
|
+
return [
|
|
421
|
+
{
|
|
422
|
+
type: 'collapsible',
|
|
423
|
+
admin: {
|
|
424
|
+
condition: (data)=>Boolean(data?.imagekit?.url),
|
|
425
|
+
initCollapsed: true
|
|
426
|
+
},
|
|
427
|
+
fields: [
|
|
490
428
|
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (!isImageKitReceiptContext(context) || context.filename !== data.filename || context.filename !== req.file.name || // Payload can clear req.file.size when preparing an in-memory image.
|
|
542
|
-
// generateFileData preserves the original size on the document.
|
|
543
|
-
context.expectedSize !== data.filesize) {
|
|
544
|
-
throw new APIError('ImageKit client upload reference does not match this request.', 400);
|
|
545
|
-
}
|
|
546
|
-
const uploaded = await verifyClientUpload({
|
|
547
|
-
collectionSlug: collection.slug,
|
|
548
|
-
context
|
|
549
|
-
});
|
|
550
|
-
// Payload keeps the browser's MIME unless it fetched the file content, in which
|
|
551
|
-
// case it uses the verified ImageKit-detected type the static handler served.
|
|
552
|
-
if (requestMimeType !== context.expectedMimeType && requestMimeType !== uploaded.mimeType) {
|
|
553
|
-
throw new APIError('ImageKit client upload reference does not match this request.', 400);
|
|
554
|
-
}
|
|
555
|
-
// A provider file can only be claimed once, including through legacy receipts.
|
|
556
|
-
// The unique file ID index also rejects concurrent claims.
|
|
557
|
-
const existing = await req.payload.db.findOne({
|
|
558
|
-
collection: collection.slug,
|
|
559
|
-
req,
|
|
560
|
-
where: {
|
|
561
|
-
'imagekit.fileId': {
|
|
562
|
-
equals: uploaded.fileId
|
|
429
|
+
name: 'imagekit',
|
|
430
|
+
type: 'group',
|
|
431
|
+
admin: {
|
|
432
|
+
disabled: {
|
|
433
|
+
column: true,
|
|
434
|
+
filter: true
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
fields: [
|
|
438
|
+
{
|
|
439
|
+
name: 'fileId',
|
|
440
|
+
type: 'text',
|
|
441
|
+
admin: {
|
|
442
|
+
hidden: true
|
|
443
|
+
},
|
|
444
|
+
unique: true
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'url',
|
|
448
|
+
type: 'text',
|
|
449
|
+
admin: {
|
|
450
|
+
readOnly: true
|
|
451
|
+
},
|
|
452
|
+
label: 'URL'
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: 'isPrivate',
|
|
456
|
+
type: 'checkbox',
|
|
457
|
+
admin: {
|
|
458
|
+
readOnly: true
|
|
459
|
+
},
|
|
460
|
+
label: 'Private file'
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'signedUrl',
|
|
464
|
+
type: 'text',
|
|
465
|
+
admin: {
|
|
466
|
+
condition: (_data, siblingData)=>isPrivateDocument({
|
|
467
|
+
imagekit: siblingData
|
|
468
|
+
}, isPrivate),
|
|
469
|
+
description: `Temporary signed URL. Expires ${expiresIn} seconds after load. Anyone with this URL can access the file until it expires.`,
|
|
470
|
+
readOnly: true
|
|
471
|
+
},
|
|
472
|
+
hooks: {
|
|
473
|
+
afterRead: [
|
|
474
|
+
({ data, siblingData })=>{
|
|
475
|
+
if (!data?.filename || !isPrivateDocument({
|
|
476
|
+
imagekit: siblingData
|
|
477
|
+
}, isPrivate)) {
|
|
478
|
+
return undefined;
|
|
563
479
|
}
|
|
480
|
+
const key = buildStoragePathData({
|
|
481
|
+
collectionPrefix: prefix,
|
|
482
|
+
docPrefix: path.posix.join(data.prefix || prefix, data._objectKey || ''),
|
|
483
|
+
filename: data.filename
|
|
484
|
+
}).storageFilePath;
|
|
485
|
+
return buildURL(key, true);
|
|
564
486
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
beforeOperation: [
|
|
576
|
-
...collection.hooks?.beforeOperation || [],
|
|
577
|
-
({ args, operation, req })=>{
|
|
578
|
-
if (operation !== 'create' && operation !== 'update' || req.context?.skipCloudStorage || !req.file || req.file.clientUploadContext || !('data' in args) || !isRecord(args.data) || typeof args.data.prefix !== 'string') {
|
|
579
|
-
return args;
|
|
580
|
-
}
|
|
581
|
-
// Payload checks filename uniqueness before cloud storage's beforeChange
|
|
582
|
-
// normalizes the prefix. Use the persisted form during that lookup too.
|
|
583
|
-
args.data.prefix = buildUploadStoragePathData({
|
|
584
|
-
collectionPrefix: getCollectionFolder(rootFolder, collection.slug),
|
|
585
|
-
docPrefix: args.data.prefix,
|
|
586
|
-
filename: req.file.name
|
|
587
|
-
}).sanitizedDocPrefix;
|
|
588
|
-
return args;
|
|
487
|
+
]
|
|
488
|
+
},
|
|
489
|
+
label: 'Signed URL',
|
|
490
|
+
virtual: true
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'thumbnailUrl',
|
|
494
|
+
type: 'text',
|
|
495
|
+
admin: {
|
|
496
|
+
hidden: true
|
|
589
497
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
// and collection-level options can override this default; a
|
|
595
|
-
// collection's own `upload.focalPoint` still wins because it is
|
|
596
|
-
// spread in afterwards.
|
|
597
|
-
focalPoint: false,
|
|
598
|
-
...typeof focalPoint === 'boolean' ? {
|
|
599
|
-
focalPoint
|
|
600
|
-
} : {},
|
|
601
|
-
...typeof collectionFocalPoint === 'boolean' ? {
|
|
602
|
-
focalPoint: collectionFocalPoint
|
|
603
|
-
} : {},
|
|
604
|
-
...typeof collection.upload === 'object' ? collection.upload : {},
|
|
605
|
-
adminThumbnail: ({ doc })=>buildThumbnailURL(doc),
|
|
606
|
-
crop: false
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
})
|
|
610
|
-
};
|
|
611
|
-
const uploadCollections = Object.fromEntries(Object.keys(collections).map((collectionSlug)=>[
|
|
612
|
-
collectionSlug,
|
|
613
|
-
{
|
|
614
|
-
isPrivate: privateSlugs.has(collectionSlug),
|
|
615
|
-
prefix: getCollectionFolder(rootFolder, collectionSlug)
|
|
616
|
-
}
|
|
617
|
-
]));
|
|
618
|
-
initClientUploads({
|
|
619
|
-
clientHandler: '@humaan/payload-storage-imagekit/client#ImageKitUploadHandler',
|
|
620
|
-
collections: uploadCollections,
|
|
621
|
-
config: configWithCropDisabled,
|
|
622
|
-
enabled: enabled !== false && Boolean(clientUploads),
|
|
623
|
-
serverHandler: createImageKitUploadAuthHandler({
|
|
624
|
-
access: typeof clientUploads === 'object' ? clientUploads.access : undefined,
|
|
625
|
-
collections: uploadCollections,
|
|
626
|
-
privateKey,
|
|
627
|
-
publicKey,
|
|
628
|
-
verifyClientUpload
|
|
629
|
-
}),
|
|
630
|
-
serverHandlerPath: imageKitUploadAuthPath
|
|
631
|
-
});
|
|
632
|
-
const output = cloudStoragePlugin({
|
|
633
|
-
alwaysInsertFields,
|
|
634
|
-
collections: toCloudStorageCollections(collections, adapter, privateSlugs, rootFolder, disablePayloadAccessControl),
|
|
635
|
-
enabled
|
|
636
|
-
})(configWithCropDisabled);
|
|
637
|
-
// Make the provider identity unique at persistence time to reject concurrent receipt replays.
|
|
638
|
-
// Keep the legacy object key index for existing uploads.
|
|
639
|
-
output.collections = (output.collections || []).map((collection)=>{
|
|
640
|
-
if (!configuredSlugs.has(collection.slug)) {
|
|
641
|
-
return collection;
|
|
642
|
-
}
|
|
643
|
-
const originalPrefixField = (config.collections || []).find((original)=>original.slug === collection.slug)?.fields.find((field)=>'name' in field && field.name === 'prefix');
|
|
644
|
-
const withUniqueIdentity = (field)=>{
|
|
645
|
-
if (field.type === 'text' && 'name' in field && field.name === 'prefix' && (!originalPrefixField || !('defaultValue' in originalPrefixField) || originalPrefixField.defaultValue == null)) {
|
|
646
|
-
// Cloud storage's collection-prefix default is applied on reads of old
|
|
647
|
-
// documents that have no stored prefix. Its URL hook then appends that
|
|
648
|
-
// prefix, but Payload's file access check cannot find it in the DB.
|
|
649
|
-
// Upload hooks still persist the collection prefix for new files.
|
|
650
|
-
return {
|
|
651
|
-
...field,
|
|
652
|
-
defaultValue: ({ req })=>req.file && typeof field.defaultValue === 'string' ? field.defaultValue : ''
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
if ('name' in field && field.name === '_objectKey') {
|
|
656
|
-
return {
|
|
657
|
-
...field,
|
|
658
|
-
unique: true
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
if (field.type === 'group' && 'name' in field && field.name === 'imagekit') {
|
|
662
|
-
return {
|
|
663
|
-
...field,
|
|
664
|
-
fields: field.fields.map((metadataField)=>'name' in metadataField && metadataField.name === 'fileId' ? {
|
|
665
|
-
...metadataField,
|
|
666
|
-
unique: true
|
|
667
|
-
} : metadataField)
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
// The imagekit group sits inside the adapter's presentational "Storage: ImageKit" panel.
|
|
671
|
-
if (field.type === 'collapsible') {
|
|
672
|
-
return {
|
|
673
|
-
...field,
|
|
674
|
-
fields: field.fields.map(withUniqueIdentity)
|
|
675
|
-
};
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
interfaceName: 'imagekitFields',
|
|
501
|
+
label: false
|
|
676
502
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
};
|
|
683
|
-
});
|
|
684
|
-
return output;
|
|
685
|
-
};
|
|
686
|
-
export { migrateFolder } from './migrate.js';
|
|
687
|
-
// Backward-compatible alias
|
|
688
|
-
export const payloadStorageImagekit = imagekitStorage;
|
|
689
|
-
export const payloadStorageImageKit = imagekitStorage;
|
|
503
|
+
],
|
|
504
|
+
label: 'Storage: ImageKit'
|
|
505
|
+
}
|
|
506
|
+
];
|
|
507
|
+
}
|
|
690
508
|
|
|
691
509
|
//# sourceMappingURL=index.js.map
|