@maas/payload-plugin-media-cloud 0.0.44 → 0.0.45
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/dist/collectionHooks/afterChange.mjs +2 -1
- package/dist/collectionHooks/afterChange.mjs.map +1 -1
- package/dist/collectionHooks/beforeChange.mjs +27 -18
- package/dist/collectionHooks/beforeChange.mjs.map +1 -1
- package/dist/collections/mediaCollection.d.mts +0 -1
- package/dist/collections/mediaCollection.mjs +5 -10
- package/dist/collections/mediaCollection.mjs.map +1 -1
- package/dist/endpoints/fileExistsHandler.mjs +3 -3
- package/dist/endpoints/fileExistsHandler.mjs.map +1 -1
- package/dist/endpoints/tusFolderHandler.d.mts +3 -2
- package/dist/endpoints/tusFolderHandler.mjs +3 -3
- package/dist/endpoints/tusFolderHandler.mjs.map +1 -1
- package/dist/plugin.mjs +14 -4
- package/dist/plugin.mjs.map +1 -1
- package/dist/tus/stores/s3/partsManager.mjs.map +1 -1
- package/dist/tus/stores/s3/semaphore.d.mts +1 -2
- package/dist/tus/stores/s3/semaphore.mjs.map +1 -1
- package/dist/utils/file.d.mts +2 -2
- package/dist/utils/tus.mjs +3 -1
- package/dist/utils/tus.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -6,9 +6,10 @@ import { s3Store } from "../plugin.mjs";
|
|
|
6
6
|
const afterChangeHook = async ({ doc, previousDoc, req }) => {
|
|
7
7
|
const { throwError } = useErrorHandler();
|
|
8
8
|
if (req.context?._mediaCloudPluginInternal) return doc;
|
|
9
|
+
if (!previousDoc?.path) return doc;
|
|
9
10
|
if (doc.path !== previousDoc?.path) {
|
|
10
11
|
if (doc.storage === "s3" && s3Store) try {
|
|
11
|
-
const oldKey = previousDoc?.path
|
|
12
|
+
const oldKey = previousDoc?.path;
|
|
12
13
|
const newKey = doc.path;
|
|
13
14
|
await s3Store.copy(oldKey, newKey);
|
|
14
15
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"afterChange.mjs","names":["afterChangeHook: CollectionAfterChangeHook"],"sources":["../../src/collectionHooks/afterChange.ts"],"sourcesContent":["import { s3Store } from '../plugin'\nimport { MediaCloudErrors } from '../types/errors'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\n\nimport type { CollectionAfterChangeHook } from 'payload'\n\nexport const afterChangeHook: CollectionAfterChangeHook = async ({\n doc,\n previousDoc,\n req,\n}) => {\n const { throwError } = useErrorHandler()\n\n // Skip if this is an internal update to prevent infinite loop\n if (req.context?._mediaCloudPluginInternal) {\n return doc\n }\n\n // Move asset in S3 if path has changed\n if (doc.path !== previousDoc?.path) {\n if (doc.storage === 's3' && s3Store) {\n try {\n const oldKey = previousDoc?.path
|
|
1
|
+
{"version":3,"file":"afterChange.mjs","names":["afterChangeHook: CollectionAfterChangeHook"],"sources":["../../src/collectionHooks/afterChange.ts"],"sourcesContent":["import { s3Store } from '../plugin'\nimport { MediaCloudErrors } from '../types/errors'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\n\nimport type { CollectionAfterChangeHook } from 'payload'\n\nexport const afterChangeHook: CollectionAfterChangeHook = async ({\n doc,\n previousDoc,\n req,\n}) => {\n const { throwError } = useErrorHandler()\n\n // Skip if this is an internal update to prevent infinite loop\n if (req.context?._mediaCloudPluginInternal) {\n return doc\n }\n\n // Skip if this is a new document, only move files on updates\n if (!previousDoc?.path) {\n return doc\n }\n\n // Move asset in S3 if path has changed\n // Path is updated in `beforeChange` if filename or folder changes\n if (doc.path !== previousDoc?.path) {\n if (doc.storage === 's3' && s3Store) {\n try {\n const oldKey = previousDoc?.path\n const newKey = doc.path\n\n await s3Store.copy(oldKey, newKey)\n } catch (error) {\n throwError({ ...MediaCloudErrors.S3_MOVE_ERROR, cause: error })\n }\n }\n }\n\n return doc\n}\n"],"mappings":";;;;;AAMA,MAAaA,kBAA6C,OAAO,EAC/D,KACA,aACA,UACI;CACJ,MAAM,EAAE,eAAe,iBAAiB;AAGxC,KAAI,IAAI,SAAS,0BACf,QAAO;AAIT,KAAI,CAAC,aAAa,KAChB,QAAO;AAKT,KAAI,IAAI,SAAS,aAAa,MAC5B;MAAI,IAAI,YAAY,QAAQ,QAC1B,KAAI;GACF,MAAM,SAAS,aAAa;GAC5B,MAAM,SAAS,IAAI;AAEnB,SAAM,QAAQ,KAAK,QAAQ,OAAO;WAC3B,OAAO;AACd,cAAW;IAAE,GAAG,iBAAiB;IAAe,OAAO;IAAO,CAAC;;;AAKrE,QAAO"}
|
|
@@ -5,25 +5,34 @@ import { buildS3Path } from "../utils/buildS3Path.mjs";
|
|
|
5
5
|
//#region src/collectionHooks/beforeChange.ts
|
|
6
6
|
const beforeChangeHook = async ({ data, originalDoc, req }) => {
|
|
7
7
|
const { throwError } = useErrorHandler();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
8
|
+
const { custom } = req?.payload?.config ?? {};
|
|
9
|
+
const folders = custom?.mediaCloud?.options?.folders;
|
|
10
|
+
if (data.filename && data.filename !== originalDoc?.filename || data.folder && data.folder !== originalDoc?.folder || !data.path) switch (folders) {
|
|
11
|
+
case true: {
|
|
12
|
+
let prefix = "";
|
|
13
|
+
if (data.folder) try {
|
|
14
|
+
const folder = await req.payload.findByID({
|
|
15
|
+
id: data.folder,
|
|
16
|
+
collection: "payload-folders",
|
|
17
|
+
select: {
|
|
18
|
+
name: true,
|
|
19
|
+
folder: true
|
|
20
|
+
},
|
|
21
|
+
depth: 10
|
|
22
|
+
});
|
|
23
|
+
if (folder) prefix = buildS3Path(folder);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throwError({
|
|
26
|
+
...MediaCloudErrors.FOLDER_FETCH_ERROR,
|
|
27
|
+
cause: error
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
data.path = prefix ? `${prefix}/${data.filename}` : data.filename;
|
|
31
|
+
break;
|
|
25
32
|
}
|
|
26
|
-
|
|
33
|
+
case false:
|
|
34
|
+
data.path = data.filename;
|
|
35
|
+
break;
|
|
27
36
|
}
|
|
28
37
|
return data;
|
|
29
38
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"beforeChange.mjs","names":["beforeChangeHook: CollectionBeforeChangeHook"],"sources":["../../src/collectionHooks/beforeChange.ts"],"sourcesContent":["import { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\nimport { buildS3Path } from '../utils/buildS3Path'\nimport type { CollectionBeforeChangeHook } from 'payload'\n\nexport const beforeChangeHook: CollectionBeforeChangeHook = async ({\n data,\n originalDoc,\n req,\n}) => {\n const { throwError } = useErrorHandler()\n // Update path\n if (\n (data.filename && data.filename !== originalDoc?.filename) ||\n (data.folder && data.folder !== originalDoc?.folder) ||\n !data.
|
|
1
|
+
{"version":3,"file":"beforeChange.mjs","names":["beforeChangeHook: CollectionBeforeChangeHook"],"sources":["../../src/collectionHooks/beforeChange.ts"],"sourcesContent":["import { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\nimport { buildS3Path } from '../utils/buildS3Path'\nimport type { CollectionBeforeChangeHook } from 'payload'\n\nexport const beforeChangeHook: CollectionBeforeChangeHook = async ({\n data,\n originalDoc,\n req,\n}) => {\n const { throwError } = useErrorHandler()\n const { custom } = req?.payload?.config ?? {}\n\n // Check if folders are enabled in plugin options\n const folders = custom?.mediaCloud?.options?.folders\n\n // Update path if missing or filename or folder have changed\n if (\n (data.filename && data.filename !== originalDoc?.filename) ||\n (data.folder && data.folder !== originalDoc?.folder) ||\n !data.path\n ) {\n switch (folders) {\n case true: {\n let prefix = ''\n\n if (data.folder) {\n try {\n const folder = await req.payload.findByID({\n id: data.folder,\n collection: 'payload-folders',\n select: { name: true, folder: true },\n depth: 10,\n })\n\n if (folder) {\n prefix = buildS3Path(folder)\n }\n } catch (error) {\n throwError({\n ...MediaCloudErrors.FOLDER_FETCH_ERROR,\n cause: error,\n })\n }\n }\n\n // Save path\n data.path = prefix ? `${prefix}/${data.filename}` : data.filename\n break\n }\n case false: {\n // For flat structures, use filename as path\n data.path = data.filename\n break\n }\n }\n }\n\n return data\n}\n"],"mappings":";;;;;AAKA,MAAaA,mBAA+C,OAAO,EACjE,MACA,aACA,UACI;CACJ,MAAM,EAAE,eAAe,iBAAiB;CACxC,MAAM,EAAE,WAAW,KAAK,SAAS,UAAU,EAAE;CAG7C,MAAM,UAAU,QAAQ,YAAY,SAAS;AAG7C,KACG,KAAK,YAAY,KAAK,aAAa,aAAa,YAChD,KAAK,UAAU,KAAK,WAAW,aAAa,UAC7C,CAAC,KAAK,KAEN,SAAQ,SAAR;EACE,KAAK,MAAM;GACT,IAAI,SAAS;AAEb,OAAI,KAAK,OACP,KAAI;IACF,MAAM,SAAS,MAAM,IAAI,QAAQ,SAAS;KACxC,IAAI,KAAK;KACT,YAAY;KACZ,QAAQ;MAAE,MAAM;MAAM,QAAQ;MAAM;KACpC,OAAO;KACR,CAAC;AAEF,QAAI,OACF,UAAS,YAAY,OAAO;YAEvB,OAAO;AACd,eAAW;KACT,GAAG,iBAAiB;KACpB,OAAO;KACR,CAAC;;AAKN,QAAK,OAAO,SAAS,GAAG,OAAO,GAAG,KAAK,aAAa,KAAK;AACzD;;EAEF,KAAK;AAEH,QAAK,OAAO,KAAK;AACjB;;AAKN,QAAO"}
|
|
@@ -17,15 +17,7 @@ import { thumbnailHook } from "../collectionHooks/thumbnail.mjs";
|
|
|
17
17
|
* @returns A configured Payload collection for media files
|
|
18
18
|
*/
|
|
19
19
|
function getMediaCollection(args) {
|
|
20
|
-
const { baseCollection, view
|
|
21
|
-
const hooks = {
|
|
22
|
-
beforeChange: [],
|
|
23
|
-
afterChange: [thumbnailHook]
|
|
24
|
-
};
|
|
25
|
-
if (folders) {
|
|
26
|
-
hooks.beforeChange = [beforeChangeHook, ...hooks.beforeChange ?? []];
|
|
27
|
-
hooks.afterChange = [afterChangeHook, ...hooks.afterChange ?? []];
|
|
28
|
-
}
|
|
20
|
+
const { baseCollection, view } = args;
|
|
29
21
|
const config = {
|
|
30
22
|
slug: "media",
|
|
31
23
|
access: {
|
|
@@ -56,7 +48,10 @@ function getMediaCollection(args) {
|
|
|
56
48
|
storageField,
|
|
57
49
|
muxField
|
|
58
50
|
],
|
|
59
|
-
hooks
|
|
51
|
+
hooks: {
|
|
52
|
+
beforeChange: [beforeChangeHook],
|
|
53
|
+
afterChange: [afterChangeHook, thumbnailHook]
|
|
54
|
+
}
|
|
60
55
|
};
|
|
61
56
|
if (!baseCollection) return config;
|
|
62
57
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mediaCollection.mjs","names":["
|
|
1
|
+
{"version":3,"file":"mediaCollection.mjs","names":["config: CollectionConfig"],"sources":["../../src/collections/mediaCollection.ts"],"sourcesContent":["import { thumbnailField } from '../fields/thumbnail'\nimport { pathField } from '../fields/path'\nimport { filenameField } from '../fields/filename'\nimport { altField } from '../fields/alt'\nimport { widthField } from '../fields/width'\nimport { heightField } from '../fields/height'\nimport { storageField } from '../fields/storage'\nimport { muxField } from '../fields/mux'\n\nimport { beforeChangeHook } from '../collectionHooks/beforeChange'\nimport { afterChangeHook } from '../collectionHooks/afterChange'\nimport { thumbnailHook } from '../collectionHooks/thumbnail'\n\nimport type { CollectionConfig } from 'payload'\nimport type { Document } from 'payload'\nimport type { MediaCloudPluginOptions } from '../types/index'\n\ninterface GetMediaCollectionArgs {\n view: MediaCloudPluginOptions['view']\n baseCollection?: CollectionConfig\n}\n\n/**\n * Creates a media collection configuration for Payload CMS\n * @param args - Arguments including the S3Store instance\n * @returns A configured Payload collection for media files\n */\nexport function getMediaCollection(\n args: GetMediaCollectionArgs\n): CollectionConfig {\n const { baseCollection, view } = args\n\n const hooks: CollectionConfig['hooks'] = {\n beforeChange: [beforeChangeHook],\n afterChange: [afterChangeHook, thumbnailHook],\n }\n\n // Override list view to use grid view if specified\n const components =\n view === 'grid'\n ? {\n views: {\n list: {\n Component: '@maas/payload-plugin-media-cloud/components#GridView',\n },\n },\n }\n : undefined\n\n const config: CollectionConfig = {\n slug: 'media',\n access: {\n read: () => true,\n delete: () => true,\n },\n admin: {\n components,\n group: 'Media Cloud',\n pagination: {\n defaultLimit: 50,\n },\n },\n upload: {\n crop: false,\n displayPreview: true,\n hideRemoveFile: true,\n adminThumbnail({ doc }: { doc: Document }) {\n return doc.thumbnail ?? null\n },\n },\n fields: [\n thumbnailField,\n pathField,\n altField,\n {\n type: 'row',\n fields: [widthField, heightField],\n },\n storageField,\n muxField,\n ],\n hooks: hooks,\n }\n\n if (!baseCollection) {\n return config\n }\n\n return {\n ...baseCollection,\n slug: baseCollection.slug ?? config.slug,\n admin: {\n ...(config.admin ?? {}),\n ...(baseCollection.admin ?? {}),\n },\n access: {\n ...(config.access ?? {}),\n ...(baseCollection.access ?? {}),\n },\n upload: config.upload,\n fields: [...(baseCollection.fields ?? []), ...config.fields, filenameField],\n hooks: {\n ...baseCollection.hooks,\n beforeChange: [\n ...(baseCollection.hooks?.beforeChange ?? []),\n ...(config.hooks?.beforeChange ?? []),\n ],\n afterChange: [\n ...(baseCollection.hooks?.afterChange ?? []),\n ...(config.hooks?.afterChange ?? []),\n ],\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2BA,SAAgB,mBACd,MACkB;CAClB,MAAM,EAAE,gBAAgB,SAAS;CAmBjC,MAAMA,SAA2B;EAC/B,MAAM;EACN,QAAQ;GACN,YAAY;GACZ,cAAc;GACf;EACD,OAAO;GACL,YAjBF,SAAS,SACL,EACE,OAAO,EACL,MAAM,EACJ,WAAW,wDACZ,EACF,EACF,GACD;GAUF,OAAO;GACP,YAAY,EACV,cAAc,IACf;GACF;EACD,QAAQ;GACN,MAAM;GACN,gBAAgB;GAChB,gBAAgB;GAChB,eAAe,EAAE,OAA0B;AACzC,WAAO,IAAI,aAAa;;GAE3B;EACD,QAAQ;GACN;GACA;GACA;GACA;IACE,MAAM;IACN,QAAQ,CAAC,YAAY,YAAY;IAClC;GACD;GACA;GACD;EACD,OAjDuC;GACvC,cAAc,CAAC,iBAAiB;GAChC,aAAa,CAAC,iBAAiB,cAAc;GAC9C;EA+CA;AAED,KAAI,CAAC,eACH,QAAO;AAGT,QAAO;EACL,GAAG;EACH,MAAM,eAAe,QAAQ,OAAO;EACpC,OAAO;GACL,GAAI,OAAO,SAAS,EAAE;GACtB,GAAI,eAAe,SAAS,EAAE;GAC/B;EACD,QAAQ;GACN,GAAI,OAAO,UAAU,EAAE;GACvB,GAAI,eAAe,UAAU,EAAE;GAChC;EACD,QAAQ,OAAO;EACf,QAAQ;GAAC,GAAI,eAAe,UAAU,EAAE;GAAG,GAAG,OAAO;GAAQ;GAAc;EAC3E,OAAO;GACL,GAAG,eAAe;GAClB,cAAc,CACZ,GAAI,eAAe,OAAO,gBAAgB,EAAE,EAC5C,GAAI,OAAO,OAAO,gBAAgB,EAAE,CACrC;GACD,aAAa,CACX,GAAI,eAAe,OAAO,eAAe,EAAE,EAC3C,GAAI,OAAO,OAAO,eAAe,EAAE,CACpC;GACF;EACF"}
|
|
@@ -20,9 +20,9 @@ function getFileExistsHandler(args) {
|
|
|
20
20
|
const url = getS3Store().getUrl(filename);
|
|
21
21
|
if ((await fetch(url, { method: "HEAD" })).status === 200) return Response.json({ message: "File found [S3]" }, { status: 200 });
|
|
22
22
|
return new Response(null, { status: 204 });
|
|
23
|
-
} catch (
|
|
24
|
-
logError(MediaCloudErrors.
|
|
25
|
-
return Response.json({ message: "
|
|
23
|
+
} catch (_error) {
|
|
24
|
+
logError(MediaCloudErrors.FILE_NOT_FOUND.message);
|
|
25
|
+
return Response.json({ message: "Server error" }, { status: 500 });
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileExistsHandler.mjs","names":[],"sources":["../../src/endpoints/fileExistsHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport { S3Store } from '../tus/stores/s3/s3Store'\n\ninterface GetFileExistsHandlerArgs {\n getS3Store: () => S3Store\n collection: string\n}\n\nexport function getFileExistsHandler(\n args: GetFileExistsHandlerArgs\n): PayloadHandler {\n const { getS3Store, collection } = args\n const { throwError, logError } = useErrorHandler()\n\n return async function (req) {\n try {\n const { routeParams, payload } = req\n const filename = routeParams?.filename as string\n\n if (!filename) {\n throwError(MediaCloudErrors.FILE_MISSING_NAME)\n }\n\n // Check if file exists in Payload database\n const { docs } = await payload.find({\n collection,\n where: {\n filename: {\n equals: filename,\n },\n },\n limit: 1,\n pagination: false,\n })\n\n if (docs.length > 0) {\n return Response.json(\n { message: 'File found [Payload]' },\n { status: 200 }\n )\n }\n\n // Check if completed file exists in S3\n const url = getS3Store().getUrl(filename)\n const s3Response = await fetch(url, { method: 'HEAD' })\n\n if (s3Response.status === 200) {\n return Response.json({ message: 'File found [S3]' }, { status: 200 })\n }\n\n return new Response(null, {\n status: 204,\n })\n } catch (
|
|
1
|
+
{"version":3,"file":"fileExistsHandler.mjs","names":[],"sources":["../../src/endpoints/fileExistsHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport { S3Store } from '../tus/stores/s3/s3Store'\n\ninterface GetFileExistsHandlerArgs {\n getS3Store: () => S3Store\n collection: string\n}\n\nexport function getFileExistsHandler(\n args: GetFileExistsHandlerArgs\n): PayloadHandler {\n const { getS3Store, collection } = args\n const { throwError, logError } = useErrorHandler()\n\n return async function (req) {\n try {\n const { routeParams, payload } = req\n const filename = routeParams?.filename as string\n\n if (!filename) {\n throwError(MediaCloudErrors.FILE_MISSING_NAME)\n }\n\n // Check if file exists in Payload database\n const { docs } = await payload.find({\n collection,\n where: {\n filename: {\n equals: filename,\n },\n },\n limit: 1,\n pagination: false,\n })\n\n if (docs.length > 0) {\n return Response.json(\n { message: 'File found [Payload]' },\n { status: 200 }\n )\n }\n\n // Check if completed file exists in S3\n const url = getS3Store().getUrl(filename)\n const s3Response = await fetch(url, { method: 'HEAD' })\n\n if (s3Response.status === 200) {\n return Response.json({ message: 'File found [S3]' }, { status: 200 })\n }\n\n return new Response(null, {\n status: 204,\n })\n } catch (_error) {\n logError(MediaCloudErrors.FILE_NOT_FOUND.message)\n return Response.json({ message: 'Server error' }, { status: 500 })\n }\n }\n}\n"],"mappings":";;;;AAWA,SAAgB,qBACd,MACgB;CAChB,MAAM,EAAE,YAAY,eAAe;CACnC,MAAM,EAAE,YAAY,aAAa,iBAAiB;AAElD,QAAO,eAAgB,KAAK;AAC1B,MAAI;GACF,MAAM,EAAE,aAAa,YAAY;GACjC,MAAM,WAAW,aAAa;AAE9B,OAAI,CAAC,SACH,YAAW,iBAAiB,kBAAkB;GAIhD,MAAM,EAAE,SAAS,MAAM,QAAQ,KAAK;IAClC;IACA,OAAO,EACL,UAAU,EACR,QAAQ,UACT,EACF;IACD,OAAO;IACP,YAAY;IACb,CAAC;AAEF,OAAI,KAAK,SAAS,EAChB,QAAO,SAAS,KACd,EAAE,SAAS,wBAAwB,EACnC,EAAE,QAAQ,KAAK,CAChB;GAIH,MAAM,MAAM,YAAY,CAAC,OAAO,SAAS;AAGzC,QAFmB,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,CAAC,EAExC,WAAW,IACxB,QAAO,SAAS,KAAK,EAAE,SAAS,mBAAmB,EAAE,EAAE,QAAQ,KAAK,CAAC;AAGvE,UAAO,IAAI,SAAS,MAAM,EACxB,QAAQ,KACT,CAAC;WACK,QAAQ;AACf,YAAS,iBAAiB,eAAe,QAAQ;AACjD,UAAO,SAAS,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAAE,QAAQ,KAAK,CAAC"}
|
|
@@ -2,11 +2,12 @@ import { S3Store } from "../tus/stores/s3/s3Store.mjs";
|
|
|
2
2
|
import { PayloadHandler } from "payload";
|
|
3
3
|
|
|
4
4
|
//#region src/endpoints/tusFolderHandler.d.ts
|
|
5
|
-
interface
|
|
5
|
+
interface GetTusFolderHandlerArgs {
|
|
6
6
|
getS3Store: () => S3Store;
|
|
7
7
|
collection: string;
|
|
8
|
+
folders: boolean;
|
|
8
9
|
}
|
|
9
|
-
declare function getTusFolderHandler(args:
|
|
10
|
+
declare function getTusFolderHandler(args: GetTusFolderHandlerArgs): PayloadHandler;
|
|
10
11
|
//#endregion
|
|
11
12
|
export { getTusFolderHandler };
|
|
12
13
|
//# sourceMappingURL=tusFolderHandler.d.mts.map
|
|
@@ -4,9 +4,10 @@ import { sanitizeFilename } from "../utils/file.mjs";
|
|
|
4
4
|
|
|
5
5
|
//#region src/endpoints/tusFolderHandler.ts
|
|
6
6
|
function getTusFolderHandler(args) {
|
|
7
|
-
const { getS3Store, collection } = args;
|
|
7
|
+
const { getS3Store, collection, folders } = args;
|
|
8
8
|
const { throwError, logError } = useErrorHandler();
|
|
9
9
|
return async function(req) {
|
|
10
|
+
if (!folders) return Response.json({ message: "Folders are disabled, skipping …" }, { status: 200 });
|
|
10
11
|
try {
|
|
11
12
|
const { routeParams, payload } = req;
|
|
12
13
|
const filename = routeParams?.filename;
|
|
@@ -22,8 +23,7 @@ function getTusFolderHandler(args) {
|
|
|
22
23
|
if (!media) throwError(MediaCloudErrors.FILE_NOT_FOUND);
|
|
23
24
|
if (media.storage !== "s3") return Response.json({ message: "Asset not stored on S3, skipping …" }, { status: 200 });
|
|
24
25
|
const s3Store = getS3Store();
|
|
25
|
-
if (
|
|
26
|
-
if (await s3Store?.read(media.path)) return Response.json({ message: "Asset already in correct location, skipping …" }, { status: 200 });
|
|
26
|
+
if (await s3Store?.read(media.path ?? "").catch(() => null)) return Response.json({ message: "Asset already in correct location, skipping …" }, { status: 200 });
|
|
27
27
|
const oldKey = media.filename;
|
|
28
28
|
const newKey = media.path;
|
|
29
29
|
await s3Store?.copy(oldKey, newKey);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tusFolderHandler.mjs","names":[],"sources":["../../src/endpoints/tusFolderHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport { S3Store } from '../tus/stores/s3/s3Store'\nimport { sanitizeFilename } from '../utils/file'\n\ninterface
|
|
1
|
+
{"version":3,"file":"tusFolderHandler.mjs","names":[],"sources":["../../src/endpoints/tusFolderHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport { useErrorHandler } from '../hooks/useErrorHandler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport { S3Store } from '../tus/stores/s3/s3Store'\nimport { sanitizeFilename } from '../utils/file'\n\ninterface GetTusFolderHandlerArgs {\n getS3Store: () => S3Store\n collection: string\n folders: boolean\n}\n\nexport function getTusFolderHandler(\n args: GetTusFolderHandlerArgs\n): PayloadHandler {\n const { getS3Store, collection, folders } = args\n const { throwError, logError } = useErrorHandler()\n\n return async function (req) {\n if (!folders) {\n return Response.json(\n { message: 'Folders are disabled, skipping …' },\n { status: 200 }\n )\n }\n\n try {\n const { routeParams, payload } = req\n const filename = routeParams?.filename as string\n\n if (!filename) {\n throwError(MediaCloudErrors.FILE_MISSING_NAME)\n }\n\n const sanitizedFilename = sanitizeFilename(filename)\n\n const { docs } = await payload.find({\n collection,\n where: {\n filename: {\n equals: sanitizedFilename,\n },\n },\n limit: 1,\n pagination: false,\n })\n\n const media = docs?.[0]\n\n if (!media) {\n throwError(MediaCloudErrors.FILE_NOT_FOUND)\n }\n\n if (media.storage !== 's3') {\n return Response.json(\n { message: 'Asset not stored on S3, skipping …' },\n { status: 200 }\n )\n }\n\n const s3Store = getS3Store()\n const file = await s3Store?.read(media.path ?? '').catch(() => null)\n\n if (file) {\n return Response.json(\n { message: 'Asset already in correct location, skipping …' },\n { status: 200 }\n )\n }\n\n const oldKey = media.filename\n const newKey = media.path\n\n await s3Store?.copy(oldKey, newKey)\n return Response.json({ message: 'Asset moved' }, { status: 200 })\n } catch (_error) {\n logError(MediaCloudErrors.S3_MOVE_ERROR.message)\n return Response.json({ message: 'Failed to move asset' }, { status: 500 })\n }\n }\n}\n"],"mappings":";;;;;AAaA,SAAgB,oBACd,MACgB;CAChB,MAAM,EAAE,YAAY,YAAY,YAAY;CAC5C,MAAM,EAAE,YAAY,aAAa,iBAAiB;AAElD,QAAO,eAAgB,KAAK;AAC1B,MAAI,CAAC,QACH,QAAO,SAAS,KACd,EAAE,SAAS,oCAAoC,EAC/C,EAAE,QAAQ,KAAK,CAChB;AAGH,MAAI;GACF,MAAM,EAAE,aAAa,YAAY;GACjC,MAAM,WAAW,aAAa;AAE9B,OAAI,CAAC,SACH,YAAW,iBAAiB,kBAAkB;GAGhD,MAAM,oBAAoB,iBAAiB,SAAS;GAEpD,MAAM,EAAE,SAAS,MAAM,QAAQ,KAAK;IAClC;IACA,OAAO,EACL,UAAU,EACR,QAAQ,mBACT,EACF;IACD,OAAO;IACP,YAAY;IACb,CAAC;GAEF,MAAM,QAAQ,OAAO;AAErB,OAAI,CAAC,MACH,YAAW,iBAAiB,eAAe;AAG7C,OAAI,MAAM,YAAY,KACpB,QAAO,SAAS,KACd,EAAE,SAAS,sCAAsC,EACjD,EAAE,QAAQ,KAAK,CAChB;GAGH,MAAM,UAAU,YAAY;AAG5B,OAFa,MAAM,SAAS,KAAK,MAAM,QAAQ,GAAG,CAAC,YAAY,KAAK,CAGlE,QAAO,SAAS,KACd,EAAE,SAAS,iDAAiD,EAC5D,EAAE,QAAQ,KAAK,CAChB;GAGH,MAAM,SAAS,MAAM;GACrB,MAAM,SAAS,MAAM;AAErB,SAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,UAAO,SAAS,KAAK,EAAE,SAAS,eAAe,EAAE,EAAE,QAAQ,KAAK,CAAC;WAC1D,QAAQ;AACf,YAAS,iBAAiB,cAAc,QAAQ;AAChD,UAAO,SAAS,KAAK,EAAE,SAAS,wBAAwB,EAAE,EAAE,QAAQ,KAAK,CAAC"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -59,11 +59,10 @@ function mediaCloudPlugin(options) {
|
|
|
59
59
|
tusServer = getTusServer();
|
|
60
60
|
}
|
|
61
61
|
const baseCollection = config.collections?.find(({ slug }) => slug === pluginOptions.collection);
|
|
62
|
-
const { view
|
|
62
|
+
const { view } = pluginOptions;
|
|
63
63
|
const mediaCollection = getMediaCollection({
|
|
64
64
|
baseCollection,
|
|
65
|
-
view
|
|
66
|
-
folders
|
|
65
|
+
view
|
|
67
66
|
});
|
|
68
67
|
if (baseCollection) config = {
|
|
69
68
|
...config,
|
|
@@ -118,7 +117,18 @@ function mediaCloudPlugin(options) {
|
|
|
118
117
|
getS3Store,
|
|
119
118
|
pluginOptions
|
|
120
119
|
})
|
|
121
|
-
]
|
|
120
|
+
],
|
|
121
|
+
custom: {
|
|
122
|
+
...config.custom,
|
|
123
|
+
mediaCloud: { options: {
|
|
124
|
+
enabled: pluginOptions.enabled,
|
|
125
|
+
collection: pluginOptions.collection,
|
|
126
|
+
view: pluginOptions.view,
|
|
127
|
+
storage: pluginOptions.storage,
|
|
128
|
+
folders: pluginOptions.folders,
|
|
129
|
+
limits: pluginOptions.limits
|
|
130
|
+
} }
|
|
131
|
+
}
|
|
122
132
|
};
|
|
123
133
|
return cloudStoragePlugin(cloudStorageConfig)(mergedConfig);
|
|
124
134
|
};
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":["muxClient: Mux | null","s3Store: S3Store | null","tusServer: Server | null","mergedConfig: Config"],"sources":["../src/plugin.ts"],"sourcesContent":["import Mux from '@mux/mux-node'\nimport { defu } from 'defu'\nimport { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'\nimport { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'\n\nimport { MediaCloudErrors } from './types/errors'\nimport { getStorageAdapter } from './adapter/storageAdapter'\nimport { getMediaCollection } from './collections/mediaCollection'\nimport { useErrorHandler } from './hooks/useErrorHandler'\nimport { createS3Store } from './tus/stores/s3'\nimport { createMuxClient, createMuxEndpoints } from './utils/mux'\nimport { createTusEndpoints, createTusServer } from './utils/tus'\nimport { createFileEndpoints } from './utils/file'\nimport { defaultOptions } from './utils/defaultOptions'\n\nimport type { Config } from 'payload'\nimport type { Server } from '@tus/server'\nimport type { MediaCloudPluginOptions } from './types'\nimport type { S3Store } from './tus/stores/s3/s3Store'\n\nconst { logError } = useErrorHandler()\n\nlet muxClient: Mux | null = null\nlet s3Store: S3Store | null = null\nlet tusServer: Server | null = null\n\n/**\n * Media Cloud Plugin for Payload CMS\n * @param options Configuration options\n * @returns Payload config function\n */\nexport function mediaCloudPlugin(options: MediaCloudPluginOptions) {\n return function (config: Config): Config {\n // Check if config is invalid or disabled\n if (!options) {\n logError(MediaCloudErrors.PLUGIN_NOT_CONFIGURED.message)\n return config\n }\n\n // Merge user options with default options\n const pluginOptions = defu(options, defaultOptions)\n\n // Check if the plugin is disabled\n if (pluginOptions.enabled === false) {\n return config\n }\n\n /**\n * Helper function to get or create Mux client instance\n * @returns Mux client instance\n */\n function getMuxClient(): Mux {\n return muxClient ?? createMuxClient(pluginOptions.mux)\n }\n\n /**\n * Helper function to get or create S3 store instance\n * @returns S3 store instance\n */\n function getS3Store(): S3Store {\n return s3Store ?? createS3Store(pluginOptions.s3)\n }\n\n /**\n * Helper function to get or create tus server instance\n * @returns TUS server instance\n */\n function getTusServer(): Server {\n return tusServer ?? createTusServer({ getS3Store, pluginOptions })\n }\n\n // Initialize Mux client if configuration is provided\n if (pluginOptions.mux) {\n muxClient = getMuxClient()\n }\n\n // Initialize S3 store and TUS server if configuration is provided\n if (pluginOptions.s3) {\n s3Store = getS3Store()\n tusServer = getTusServer()\n }\n\n // Check if base collection exists\n const baseCollection = config.collections?.find(\n ({ slug }) => slug === pluginOptions.collection\n )\n\n const { view
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":["muxClient: Mux | null","s3Store: S3Store | null","tusServer: Server | null","mergedConfig: Config"],"sources":["../src/plugin.ts"],"sourcesContent":["import Mux from '@mux/mux-node'\nimport { defu } from 'defu'\nimport { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'\nimport { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'\n\nimport { MediaCloudErrors } from './types/errors'\nimport { getStorageAdapter } from './adapter/storageAdapter'\nimport { getMediaCollection } from './collections/mediaCollection'\nimport { useErrorHandler } from './hooks/useErrorHandler'\nimport { createS3Store } from './tus/stores/s3'\nimport { createMuxClient, createMuxEndpoints } from './utils/mux'\nimport { createTusEndpoints, createTusServer } from './utils/tus'\nimport { createFileEndpoints } from './utils/file'\nimport { defaultOptions } from './utils/defaultOptions'\n\nimport type { Config } from 'payload'\nimport type { Server } from '@tus/server'\nimport type { MediaCloudPluginOptions } from './types'\nimport type { S3Store } from './tus/stores/s3/s3Store'\n\nconst { logError } = useErrorHandler()\n\nlet muxClient: Mux | null = null\nlet s3Store: S3Store | null = null\nlet tusServer: Server | null = null\n\n/**\n * Media Cloud Plugin for Payload CMS\n * @param options Configuration options\n * @returns Payload config function\n */\nexport function mediaCloudPlugin(options: MediaCloudPluginOptions) {\n return function (config: Config): Config {\n // Check if config is invalid or disabled\n if (!options) {\n logError(MediaCloudErrors.PLUGIN_NOT_CONFIGURED.message)\n return config\n }\n\n // Merge user options with default options\n const pluginOptions = defu(options, defaultOptions)\n\n // Check if the plugin is disabled\n if (pluginOptions.enabled === false) {\n return config\n }\n\n /**\n * Helper function to get or create Mux client instance\n * @returns Mux client instance\n */\n function getMuxClient(): Mux {\n return muxClient ?? createMuxClient(pluginOptions.mux)\n }\n\n /**\n * Helper function to get or create S3 store instance\n * @returns S3 store instance\n */\n function getS3Store(): S3Store {\n return s3Store ?? createS3Store(pluginOptions.s3)\n }\n\n /**\n * Helper function to get or create tus server instance\n * @returns TUS server instance\n */\n function getTusServer(): Server {\n return tusServer ?? createTusServer({ getS3Store, pluginOptions })\n }\n\n // Initialize Mux client if configuration is provided\n if (pluginOptions.mux) {\n muxClient = getMuxClient()\n }\n\n // Initialize S3 store and TUS server if configuration is provided\n if (pluginOptions.s3) {\n s3Store = getS3Store()\n tusServer = getTusServer()\n }\n\n // Check if base collection exists\n const baseCollection = config.collections?.find(\n ({ slug }) => slug === pluginOptions.collection\n )\n\n const { view } = pluginOptions\n\n const mediaCollection = getMediaCollection({\n baseCollection,\n view,\n })\n\n // Remove base collection\n // It’ll be replaced with the merged media collection\n if (baseCollection) {\n config = {\n ...config,\n collections:\n config.collections?.filter(\n ({ slug }) => slug !== baseCollection?.slug\n ) ?? [],\n }\n }\n\n initClientUploads({\n config,\n enabled: true,\n clientHandler:\n '@maas/payload-plugin-media-cloud/components#UploadHandler',\n collections: {\n [mediaCollection.slug]: {\n clientUploads: true,\n disableLocalStorage: true,\n prefix: pluginOptions.s3?.prefix ?? '',\n },\n },\n extraClientHandlerProps: () => ({\n pluginOptions,\n }),\n serverHandler: () => {\n return Response.json(\n { message: 'Server handler is not implemented' },\n { status: 501 }\n )\n },\n serverHandlerPath: '/media-cloud/upload',\n })\n\n const cloudStorageConfig = {\n collections: {\n [mediaCollection.slug]: {\n adapter: getStorageAdapter({\n getMuxClient,\n pluginOptions,\n getS3Store,\n }),\n clientUploads: true,\n disableLocalStorage: true,\n },\n },\n }\n\n const mergedConfig: Config = {\n ...config,\n admin: {\n ...config.admin,\n components: {\n ...config.admin?.components,\n providers: [\n ...(config.admin?.components?.providers ?? []),\n '@maas/payload-plugin-media-cloud/components#UploadManagerProvider',\n ],\n },\n },\n collections: [...(config.collections ?? []), mediaCollection],\n endpoints: [\n ...(config.endpoints ?? []),\n ...createTusEndpoints({ getTusServer, getS3Store, pluginOptions }),\n ...createMuxEndpoints({ getMuxClient, pluginOptions }),\n ...createFileEndpoints({ getS3Store, pluginOptions }),\n ],\n custom: {\n ...config.custom,\n mediaCloud: {\n options: {\n enabled: pluginOptions.enabled,\n collection: pluginOptions.collection,\n view: pluginOptions.view,\n storage: pluginOptions.storage,\n folders: pluginOptions.folders,\n limits: pluginOptions.limits,\n },\n },\n },\n }\n\n return cloudStoragePlugin(cloudStorageConfig)(mergedConfig)\n }\n}\n\nexport { s3Store }\n"],"mappings":";;;;;;;;;;;;;;AAoBA,MAAM,EAAE,aAAa,iBAAiB;AAEtC,IAAIA,YAAwB;AAC5B,IAAIC,UAA0B;AAC9B,IAAIC,YAA2B;;;;;;AAO/B,SAAgB,iBAAiB,SAAkC;AACjE,QAAO,SAAU,QAAwB;AAEvC,MAAI,CAAC,SAAS;AACZ,YAAS,iBAAiB,sBAAsB,QAAQ;AACxD,UAAO;;EAIT,MAAM,gBAAgB,KAAK,SAAS,eAAe;AAGnD,MAAI,cAAc,YAAY,MAC5B,QAAO;;;;;EAOT,SAAS,eAAoB;AAC3B,UAAO,aAAa,gBAAgB,cAAc,IAAI;;;;;;EAOxD,SAAS,aAAsB;AAC7B,UAAO,WAAW,cAAc,cAAc,GAAG;;;;;;EAOnD,SAAS,eAAuB;AAC9B,UAAO,aAAa,gBAAgB;IAAE;IAAY;IAAe,CAAC;;AAIpE,MAAI,cAAc,IAChB,aAAY,cAAc;AAI5B,MAAI,cAAc,IAAI;AACpB,aAAU,YAAY;AACtB,eAAY,cAAc;;EAI5B,MAAM,iBAAiB,OAAO,aAAa,MACxC,EAAE,WAAW,SAAS,cAAc,WACtC;EAED,MAAM,EAAE,SAAS;EAEjB,MAAM,kBAAkB,mBAAmB;GACzC;GACA;GACD,CAAC;AAIF,MAAI,eACF,UAAS;GACP,GAAG;GACH,aACE,OAAO,aAAa,QACjB,EAAE,WAAW,SAAS,gBAAgB,KACxC,IAAI,EAAE;GACV;AAGH,oBAAkB;GAChB;GACA,SAAS;GACT,eACE;GACF,aAAa,GACV,gBAAgB,OAAO;IACtB,eAAe;IACf,qBAAqB;IACrB,QAAQ,cAAc,IAAI,UAAU;IACrC,EACF;GACD,gCAAgC,EAC9B,eACD;GACD,qBAAqB;AACnB,WAAO,SAAS,KACd,EAAE,SAAS,qCAAqC,EAChD,EAAE,QAAQ,KAAK,CAChB;;GAEH,mBAAmB;GACpB,CAAC;EAEF,MAAM,qBAAqB,EACzB,aAAa,GACV,gBAAgB,OAAO;GACtB,SAAS,kBAAkB;IACzB;IACA;IACA;IACD,CAAC;GACF,eAAe;GACf,qBAAqB;GACtB,EACF,EACF;EAED,MAAMC,eAAuB;GAC3B,GAAG;GACH,OAAO;IACL,GAAG,OAAO;IACV,YAAY;KACV,GAAG,OAAO,OAAO;KACjB,WAAW,CACT,GAAI,OAAO,OAAO,YAAY,aAAa,EAAE,EAC7C,oEACD;KACF;IACF;GACD,aAAa,CAAC,GAAI,OAAO,eAAe,EAAE,EAAG,gBAAgB;GAC7D,WAAW;IACT,GAAI,OAAO,aAAa,EAAE;IAC1B,GAAG,mBAAmB;KAAE;KAAc;KAAY;KAAe,CAAC;IAClE,GAAG,mBAAmB;KAAE;KAAc;KAAe,CAAC;IACtD,GAAG,oBAAoB;KAAE;KAAY;KAAe,CAAC;IACtD;GACD,QAAQ;IACN,GAAG,OAAO;IACV,YAAY,EACV,SAAS;KACP,SAAS,cAAc;KACvB,YAAY,cAAc;KAC1B,MAAM,cAAc;KACpB,SAAS,cAAc;KACvB,SAAS,cAAc;KACvB,QAAQ,cAAc;KACvB,EACF;IACF;GACF;AAED,SAAO,mBAAmB,mBAAmB,CAAC,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"partsManager.mjs","names":["client: S3","bucket: string","minPartSize: number","partUploadSemaphore: Semaphore","metadataManager: S3MetadataManager","fileOperations: S3FileOperations","generateCompleteTag: (value: 'false' | 'true') => string | undefined","params: AWS.ListPartsCommandInput","params: AWS.UploadPartCommandInput","promises: Promise<void>[]","pendingChunkFilepath: null | string"],"sources":["../../../../src/tus/stores/s3/partsManager.ts"],"sourcesContent":["import fs from 'node:fs'\nimport os from 'node:os'\nimport stream from 'node:stream'\n\nimport { NoSuchKey, NotFound, type S3 } from '@aws-sdk/client-s3'\nimport { StreamSplitter } from '@tus/utils'\n\nimport { useErrorHandler } from '../../../hooks/useErrorHandler'\nimport { MediaCloudErrors, MediaCloudLogs } from '../../../types/errors'\n\nimport type AWS from '@aws-sdk/client-s3'\nimport type { Readable } from 'node:stream'\nimport type { IncompletePartInfo, TusUploadMetadata } from '../../../types'\nimport type { S3FileOperations } from './fileOperations'\nimport type { S3MetadataManager } from './metadataManager'\nimport type { Semaphore, SemaphorePermit } from './semaphore'\n\ntype RetrievePartsArgs = {\n id: string\n partNumberMarker?: string\n}\n\ntype FinishMultipartUploadArgs = {\n metadata: TusUploadMetadata\n parts: Array<AWS.Part>\n}\n\ntype GetIncompletePartArgs = {\n id: string\n}\n\ntype GetIncompletePartSizeArgs = {\n id: string\n}\n\ntype DeleteIncompletePartArgs = {\n id: string\n}\n\ntype DownloadIncompletePartArgs = {\n id: string\n}\n\ntype UploadIncompletePartArgs = {\n id: string\n readStream: fs.ReadStream | Readable\n}\n\ntype UploadPartArgs = {\n metadata: TusUploadMetadata\n readStream: fs.ReadStream | Readable\n partNumber: number\n}\n\ntype UploadPartsArgs = {\n metadata: TusUploadMetadata\n readStream: stream.Readable\n currentPartNumber: number\n offset: number\n}\n\nconst { log, throwError } = useErrorHandler()\n\nexport class S3PartsManager {\n constructor(\n private client: S3,\n private bucket: string,\n private minPartSize: number,\n private partUploadSemaphore: Semaphore,\n private metadataManager: S3MetadataManager,\n private fileOperations: S3FileOperations,\n private generateCompleteTag: (value: 'false' | 'true') => string | undefined\n ) {}\n\n /**\n * Gets the number of complete parts/chunks already uploaded to S3.\n * Retrieves only consecutive parts.\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @param args.partNumberMarker - Marker for pagination (optional)\n * @returns Promise that resolves to array of uploaded parts\n */\n async retrieveParts(args: RetrievePartsArgs): Promise<Array<AWS.Part>> {\n const { id, partNumberMarker } = args\n const metadata = await this.metadataManager.getMetadata({ id })\n\n if (!metadata['upload-id']) {\n throwError(MediaCloudErrors.MUX_UPLOAD_ID_MISSING)\n throw new Error() // This will never execute but satisfies TypeScript\n }\n\n const params: AWS.ListPartsCommandInput = {\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n PartNumberMarker: partNumberMarker,\n UploadId: metadata['upload-id'],\n }\n\n const data = await this.client.listParts(params)\n\n let parts = data.Parts ?? []\n\n if (data.IsTruncated) {\n const rest = await this.retrieveParts({\n id,\n partNumberMarker: data.NextPartNumberMarker,\n })\n parts = [...parts, ...rest]\n }\n\n if (!partNumberMarker) {\n parts.sort((a, b) => (a.PartNumber || 0) - (b.PartNumber || 0))\n }\n\n return parts\n }\n\n /**\n * Completes a multipart upload on S3.\n * This is where S3 concatenates all the uploaded parts.\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.parts - Array of uploaded parts to complete\n * @returns Promise that resolves to the location URL (optional)\n */\n async finishMultipartUpload(\n args: FinishMultipartUploadArgs\n ): Promise<string | undefined> {\n const { metadata, parts } = args\n\n const params = {\n Key: this.metadataManager.generatePartKey({\n id: metadata.file.id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n Bucket: this.bucket,\n MultipartUpload: {\n Parts: parts.map((part) => {\n return {\n ETag: part.ETag,\n PartNumber: part.PartNumber,\n }\n }),\n },\n UploadId: metadata['upload-id'],\n }\n\n try {\n const result = await this.client.completeMultipartUpload(params)\n return result.Location\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw new Error() // This will never execute but satisfies TypeScript\n }\n }\n\n /**\n * Gets incomplete part from S3\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to readable stream or undefined if not found\n */\n async getIncompletePart(\n args: GetIncompletePartArgs\n ): Promise<Readable | undefined> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n prefix: file?.metadata?.prefix ?? undefined,\n isIncomplete: true,\n }),\n })\n return data.Body as Readable\n } catch (error) {\n if (error instanceof NoSuchKey) {\n return undefined\n }\n throw error\n }\n }\n\n /**\n * Gets the size of an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to part size or undefined if not found\n */\n async getIncompletePartSize(\n args: GetIncompletePartSizeArgs\n ): Promise<number | undefined> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.headObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n })\n return data.ContentLength\n } catch (error) {\n if (error instanceof NotFound) {\n return undefined\n }\n throw error\n }\n }\n\n /**\n * Deletes an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves when deletion is complete\n */\n async deleteIncompletePart(args: DeleteIncompletePartArgs): Promise<void> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n await this.client.deleteObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n })\n } catch (error) {\n throwError({\n ...MediaCloudErrors.S3_DELETE_ERROR,\n cause: error,\n })\n }\n }\n\n /**\n * Downloads incomplete part to temporary file\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to incomplete part info or undefined if not found\n */\n async downloadIncompletePart(\n args: DownloadIncompletePartArgs\n ): Promise<IncompletePartInfo | undefined> {\n const { id } = args\n const incompletePart = await this.getIncompletePart({ id })\n\n if (!incompletePart) {\n return\n }\n const filePath = await this.fileOperations.generateUniqueTmpFileName({\n template: 'tus-s3-incomplete-part-',\n })\n\n try {\n let incompletePartSize = 0\n\n const byteCounterTransform = new stream.Transform({\n transform(chunk, _, callback) {\n incompletePartSize += chunk.length\n callback(null, chunk)\n },\n })\n\n // Write to temporary file\n await stream.promises.pipeline(\n incompletePart,\n byteCounterTransform,\n fs.createWriteStream(filePath)\n )\n\n const createReadStream = (options: { cleanUpOnEnd: boolean }) => {\n const fileReader = fs.createReadStream(filePath)\n\n if (options.cleanUpOnEnd) {\n fileReader.on('end', () => {\n fs.unlink(filePath, () => {})\n })\n\n fileReader.on('error', (error) => {\n fileReader.destroy(error)\n fs.unlink(filePath, () => {})\n })\n }\n\n return fileReader\n }\n\n return {\n createReader: createReadStream,\n path: filePath,\n size: incompletePartSize,\n }\n } catch (err) {\n fs.promises.rm(filePath).catch(() => {})\n throw err\n }\n }\n\n /**\n * Uploads an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @param args.readStream - The stream to read data from\n * @returns Promise that resolves to the ETag of the uploaded part\n */\n async uploadIncompletePart(args: UploadIncompletePartArgs): Promise<string> {\n const { id, readStream } = args\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.putObject({\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n Tagging: this.generateCompleteTag('false'),\n })\n log(MediaCloudLogs.S3_STORE_INCOMPLETE_PART_UPLOADED)\n return data.ETag as string\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw error\n }\n }\n\n /**\n * Uploads a single part\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.readStream - The stream to read data from\n * @param args.partNumber - The part number to upload\n * @returns Promise that resolves to the ETag of the uploaded part\n */\n async uploadPart(args: UploadPartArgs): Promise<AWS.Part> {\n const { metadata, readStream, partNumber } = args\n const permit = await this.partUploadSemaphore.acquire()\n\n if (!metadata['upload-id']) {\n throwError(MediaCloudErrors.MUX_UPLOAD_ID_MISSING)\n throw new Error() // This will never execute but satisfies TypeScript\n }\n\n const params: AWS.UploadPartCommandInput = {\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id: metadata.file.id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n PartNumber: partNumber,\n UploadId: metadata['upload-id'],\n }\n\n try {\n const data = await this.client.uploadPart(params)\n return { ETag: data.ETag, PartNumber: partNumber }\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw error\n } finally {\n permit()\n }\n }\n\n /**\n * Uploads a stream to s3 using multiple parts\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.readStream - The stream to read data from\n * @param args.currentPartNumber - The current part number to start from\n * @param args.offset - The byte offset to start from\n * @returns Promise that resolves to the number of bytes uploaded\n */\n async uploadParts(args: UploadPartsArgs): Promise<number> {\n const { metadata, readStream, offset: initialOffset } = args\n let { currentPartNumber } = args\n let offset = initialOffset\n const size = metadata.file.size\n const promises: Promise<void>[] = []\n let pendingChunkFilepath: null | string = null\n let bytesUploaded = 0\n\n const splitterStream = new StreamSplitter({\n chunkSize: this.fileOperations.calculateOptimalPartSize({ size }),\n directory: os.tmpdir(),\n })\n .on('chunkStarted', (filepath) => {\n pendingChunkFilepath = filepath\n })\n .on('chunkFinished', ({ path, size: partSize }) => {\n pendingChunkFilepath = null\n\n const partNumber = currentPartNumber++\n\n offset += partSize\n\n const isFinalPart = size === offset\n\n const uploadChunk = async () => {\n try {\n // Only the first chunk of each PATCH request can prepend\n // an incomplete part (last chunk) from the previous request.\n const readable = fs.createReadStream(path)\n readable.on('error', function (error) {\n readable.destroy(error)\n fs.unlink(path, () => {})\n })\n\n switch (true) {\n case partSize >= this.minPartSize || isFinalPart:\n await this.uploadPart({\n metadata,\n readStream: readable,\n partNumber,\n })\n break\n default:\n await this.uploadIncompletePart({\n id: metadata.file.id,\n readStream: readable,\n })\n break\n }\n\n bytesUploaded += partSize\n } catch (error) {\n // Destroy the splitter to stop processing more chunks\n const mappedError =\n error instanceof Error ? error : new Error(String(error))\n splitterStream.destroy(mappedError)\n throw mappedError\n } finally {\n fs.promises.rm(path).catch(function () {})\n }\n }\n\n const deferred = uploadChunk()\n\n promises.push(deferred)\n })\n\n try {\n await stream.promises.pipeline(readStream, splitterStream)\n } catch (error) {\n if (pendingChunkFilepath !== null) {\n try {\n await fs.promises.rm(pendingChunkFilepath)\n } catch {\n log(MediaCloudLogs.S3_STORE_CHUNK_REMOVAL_FAILED)\n }\n }\n const mappedError =\n error instanceof Error ? error : new Error(String(error))\n promises.push(Promise.reject(mappedError))\n } finally {\n // Wait for all promises\n await Promise.allSettled(promises)\n // Reject the promise if any of the promises reject\n await Promise.all(promises)\n }\n\n return bytesUploaded\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"partsManager.mjs","names":["client: S3","bucket: string","minPartSize: number","partUploadSemaphore: Semaphore","metadataManager: S3MetadataManager","fileOperations: S3FileOperations","generateCompleteTag: (value: 'false' | 'true') => string | undefined","params: AWS.ListPartsCommandInput","params: AWS.UploadPartCommandInput","promises: Promise<void>[]","pendingChunkFilepath: null | string"],"sources":["../../../../src/tus/stores/s3/partsManager.ts"],"sourcesContent":["import fs from 'node:fs'\nimport os from 'node:os'\nimport stream from 'node:stream'\n\nimport { NoSuchKey, NotFound, type S3 } from '@aws-sdk/client-s3'\nimport { StreamSplitter } from '@tus/utils'\n\nimport { useErrorHandler } from '../../../hooks/useErrorHandler'\nimport { MediaCloudErrors, MediaCloudLogs } from '../../../types/errors'\n\nimport type AWS from '@aws-sdk/client-s3'\nimport type { Readable } from 'node:stream'\nimport type { IncompletePartInfo, TusUploadMetadata } from '../../../types'\nimport type { S3FileOperations } from './fileOperations'\nimport type { S3MetadataManager } from './metadataManager'\nimport type { Semaphore } from './semaphore'\n\ntype RetrievePartsArgs = {\n id: string\n partNumberMarker?: string\n}\n\ntype FinishMultipartUploadArgs = {\n metadata: TusUploadMetadata\n parts: Array<AWS.Part>\n}\n\ntype GetIncompletePartArgs = {\n id: string\n}\n\ntype GetIncompletePartSizeArgs = {\n id: string\n}\n\ntype DeleteIncompletePartArgs = {\n id: string\n}\n\ntype DownloadIncompletePartArgs = {\n id: string\n}\n\ntype UploadIncompletePartArgs = {\n id: string\n readStream: fs.ReadStream | Readable\n}\n\ntype UploadPartArgs = {\n metadata: TusUploadMetadata\n readStream: fs.ReadStream | Readable\n partNumber: number\n}\n\ntype UploadPartsArgs = {\n metadata: TusUploadMetadata\n readStream: stream.Readable\n currentPartNumber: number\n offset: number\n}\n\nconst { log, throwError } = useErrorHandler()\n\nexport class S3PartsManager {\n constructor(\n private client: S3,\n private bucket: string,\n private minPartSize: number,\n private partUploadSemaphore: Semaphore,\n private metadataManager: S3MetadataManager,\n private fileOperations: S3FileOperations,\n private generateCompleteTag: (value: 'false' | 'true') => string | undefined\n ) {}\n\n /**\n * Gets the number of complete parts/chunks already uploaded to S3.\n * Retrieves only consecutive parts.\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @param args.partNumberMarker - Marker for pagination (optional)\n * @returns Promise that resolves to array of uploaded parts\n */\n async retrieveParts(args: RetrievePartsArgs): Promise<Array<AWS.Part>> {\n const { id, partNumberMarker } = args\n const metadata = await this.metadataManager.getMetadata({ id })\n\n if (!metadata['upload-id']) {\n throwError(MediaCloudErrors.MUX_UPLOAD_ID_MISSING)\n throw new Error() // This will never execute but satisfies TypeScript\n }\n\n const params: AWS.ListPartsCommandInput = {\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n PartNumberMarker: partNumberMarker,\n UploadId: metadata['upload-id'],\n }\n\n const data = await this.client.listParts(params)\n\n let parts = data.Parts ?? []\n\n if (data.IsTruncated) {\n const rest = await this.retrieveParts({\n id,\n partNumberMarker: data.NextPartNumberMarker,\n })\n parts = [...parts, ...rest]\n }\n\n if (!partNumberMarker) {\n parts.sort((a, b) => (a.PartNumber || 0) - (b.PartNumber || 0))\n }\n\n return parts\n }\n\n /**\n * Completes a multipart upload on S3.\n * This is where S3 concatenates all the uploaded parts.\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.parts - Array of uploaded parts to complete\n * @returns Promise that resolves to the location URL (optional)\n */\n async finishMultipartUpload(\n args: FinishMultipartUploadArgs\n ): Promise<string | undefined> {\n const { metadata, parts } = args\n\n const params = {\n Key: this.metadataManager.generatePartKey({\n id: metadata.file.id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n Bucket: this.bucket,\n MultipartUpload: {\n Parts: parts.map((part) => {\n return {\n ETag: part.ETag,\n PartNumber: part.PartNumber,\n }\n }),\n },\n UploadId: metadata['upload-id'],\n }\n\n try {\n const result = await this.client.completeMultipartUpload(params)\n return result.Location\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw new Error() // This will never execute but satisfies TypeScript\n }\n }\n\n /**\n * Gets incomplete part from S3\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to readable stream or undefined if not found\n */\n async getIncompletePart(\n args: GetIncompletePartArgs\n ): Promise<Readable | undefined> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n prefix: file?.metadata?.prefix ?? undefined,\n isIncomplete: true,\n }),\n })\n return data.Body as Readable\n } catch (error) {\n if (error instanceof NoSuchKey) {\n return undefined\n }\n throw error\n }\n }\n\n /**\n * Gets the size of an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to part size or undefined if not found\n */\n async getIncompletePartSize(\n args: GetIncompletePartSizeArgs\n ): Promise<number | undefined> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.headObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n })\n return data.ContentLength\n } catch (error) {\n if (error instanceof NotFound) {\n return undefined\n }\n throw error\n }\n }\n\n /**\n * Deletes an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves when deletion is complete\n */\n async deleteIncompletePart(args: DeleteIncompletePartArgs): Promise<void> {\n const { id } = args\n\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n await this.client.deleteObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n })\n } catch (error) {\n throwError({\n ...MediaCloudErrors.S3_DELETE_ERROR,\n cause: error,\n })\n }\n }\n\n /**\n * Downloads incomplete part to temporary file\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @returns Promise that resolves to incomplete part info or undefined if not found\n */\n async downloadIncompletePart(\n args: DownloadIncompletePartArgs\n ): Promise<IncompletePartInfo | undefined> {\n const { id } = args\n const incompletePart = await this.getIncompletePart({ id })\n\n if (!incompletePart) {\n return\n }\n const filePath = await this.fileOperations.generateUniqueTmpFileName({\n template: 'tus-s3-incomplete-part-',\n })\n\n try {\n let incompletePartSize = 0\n\n const byteCounterTransform = new stream.Transform({\n transform(chunk, _, callback) {\n incompletePartSize += chunk.length\n callback(null, chunk)\n },\n })\n\n // Write to temporary file\n await stream.promises.pipeline(\n incompletePart,\n byteCounterTransform,\n fs.createWriteStream(filePath)\n )\n\n const createReadStream = (options: { cleanUpOnEnd: boolean }) => {\n const fileReader = fs.createReadStream(filePath)\n\n if (options.cleanUpOnEnd) {\n fileReader.on('end', () => {\n fs.unlink(filePath, () => {})\n })\n\n fileReader.on('error', (error) => {\n fileReader.destroy(error)\n fs.unlink(filePath, () => {})\n })\n }\n\n return fileReader\n }\n\n return {\n createReader: createReadStream,\n path: filePath,\n size: incompletePartSize,\n }\n } catch (err) {\n fs.promises.rm(filePath).catch(() => {})\n throw err\n }\n }\n\n /**\n * Uploads an incomplete part\n * @param args - The function arguments\n * @param args.id - The upload ID\n * @param args.readStream - The stream to read data from\n * @returns Promise that resolves to the ETag of the uploaded part\n */\n async uploadIncompletePart(args: UploadIncompletePartArgs): Promise<string> {\n const { id, readStream } = args\n try {\n const { file } = await this.metadataManager.getMetadata({ id })\n\n const data = await this.client.putObject({\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n prefix: file?.metadata?.prefix ?? undefined,\n }),\n Tagging: this.generateCompleteTag('false'),\n })\n log(MediaCloudLogs.S3_STORE_INCOMPLETE_PART_UPLOADED)\n return data.ETag as string\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw error\n }\n }\n\n /**\n * Uploads a single part\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.readStream - The stream to read data from\n * @param args.partNumber - The part number to upload\n * @returns Promise that resolves to the ETag of the uploaded part\n */\n async uploadPart(args: UploadPartArgs): Promise<AWS.Part> {\n const { metadata, readStream, partNumber } = args\n const permit = await this.partUploadSemaphore.acquire()\n\n if (!metadata['upload-id']) {\n throwError(MediaCloudErrors.MUX_UPLOAD_ID_MISSING)\n throw new Error() // This will never execute but satisfies TypeScript\n }\n\n const params: AWS.UploadPartCommandInput = {\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({\n id: metadata.file.id,\n prefix: metadata.file.metadata?.prefix ?? undefined,\n }),\n PartNumber: partNumber,\n UploadId: metadata['upload-id'],\n }\n\n try {\n const data = await this.client.uploadPart(params)\n return { ETag: data.ETag, PartNumber: partNumber }\n } catch (error) {\n throwError({ ...MediaCloudErrors.TUS_UPLOAD_ERROR, cause: error })\n throw error\n } finally {\n permit()\n }\n }\n\n /**\n * Uploads a stream to s3 using multiple parts\n * @param args - The function arguments\n * @param args.metadata - The upload metadata\n * @param args.readStream - The stream to read data from\n * @param args.currentPartNumber - The current part number to start from\n * @param args.offset - The byte offset to start from\n * @returns Promise that resolves to the number of bytes uploaded\n */\n async uploadParts(args: UploadPartsArgs): Promise<number> {\n const { metadata, readStream, offset: initialOffset } = args\n let { currentPartNumber } = args\n let offset = initialOffset\n const size = metadata.file.size\n const promises: Promise<void>[] = []\n let pendingChunkFilepath: null | string = null\n let bytesUploaded = 0\n\n const splitterStream = new StreamSplitter({\n chunkSize: this.fileOperations.calculateOptimalPartSize({ size }),\n directory: os.tmpdir(),\n })\n .on('chunkStarted', (filepath) => {\n pendingChunkFilepath = filepath\n })\n .on('chunkFinished', ({ path, size: partSize }) => {\n pendingChunkFilepath = null\n\n const partNumber = currentPartNumber++\n\n offset += partSize\n\n const isFinalPart = size === offset\n\n const uploadChunk = async () => {\n try {\n // Only the first chunk of each PATCH request can prepend\n // an incomplete part (last chunk) from the previous request.\n const readable = fs.createReadStream(path)\n readable.on('error', function (error) {\n readable.destroy(error)\n fs.unlink(path, () => {})\n })\n\n switch (true) {\n case partSize >= this.minPartSize || isFinalPart:\n await this.uploadPart({\n metadata,\n readStream: readable,\n partNumber,\n })\n break\n default:\n await this.uploadIncompletePart({\n id: metadata.file.id,\n readStream: readable,\n })\n break\n }\n\n bytesUploaded += partSize\n } catch (error) {\n // Destroy the splitter to stop processing more chunks\n const mappedError =\n error instanceof Error ? error : new Error(String(error))\n splitterStream.destroy(mappedError)\n throw mappedError\n } finally {\n fs.promises.rm(path).catch(function () {})\n }\n }\n\n const deferred = uploadChunk()\n\n promises.push(deferred)\n })\n\n try {\n await stream.promises.pipeline(readStream, splitterStream)\n } catch (error) {\n if (pendingChunkFilepath !== null) {\n try {\n await fs.promises.rm(pendingChunkFilepath)\n } catch {\n log(MediaCloudLogs.S3_STORE_CHUNK_REMOVAL_FAILED)\n }\n }\n const mappedError =\n error instanceof Error ? error : new Error(String(error))\n promises.push(Promise.reject(mappedError))\n } finally {\n // Wait for all promises\n await Promise.allSettled(promises)\n // Reject the promise if any of the promises reject\n await Promise.all(promises)\n }\n\n return bytesUploaded\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semaphore.mjs","names":[],"sources":["../../../../src/tus/stores/s3/semaphore.ts"],"sourcesContent":["/**\n * A semaphore implementation for controlling concurrent operations.\n * Used to limit the number of simultaneous part uploads to S3.\n */\nexport class Semaphore {\n private permits: number\n private queue: (() => void)[] = []\n\n constructor(permits: number) {\n this.permits = permits\n }\n\n private release(): void {\n this.permits++\n const next = this.queue.shift()\n if (next) {\n next()\n }\n }\n\n async acquire(): Promise<() => void> {\n return new Promise((resolve) => {\n if (this.permits > 0) {\n this.permits--\n resolve(() => this.release())\n } else {\n this.queue.push(() => {\n this.permits--\n resolve(() => this.release())\n })\n }\n })\n }\n}\n
|
|
1
|
+
{"version":3,"file":"semaphore.mjs","names":[],"sources":["../../../../src/tus/stores/s3/semaphore.ts"],"sourcesContent":["/**\n * A semaphore implementation for controlling concurrent operations.\n * Used to limit the number of simultaneous part uploads to S3.\n */\nexport class Semaphore {\n private permits: number\n private queue: (() => void)[] = []\n\n constructor(permits: number) {\n this.permits = permits\n }\n\n private release(): void {\n this.permits++\n const next = this.queue.shift()\n if (next) {\n next()\n }\n }\n\n async acquire(): Promise<() => void> {\n return new Promise((resolve) => {\n if (this.permits > 0) {\n this.permits--\n resolve(() => this.release())\n } else {\n this.queue.push(() => {\n this.permits--\n resolve(() => this.release())\n })\n }\n })\n }\n}\n"],"mappings":";;;;;AAIA,IAAa,YAAb,MAAuB;CAIrB,YAAY,SAAiB;eAFG,EAAE;AAGhC,OAAK,UAAU;;CAGjB,AAAQ,UAAgB;AACtB,OAAK;EACL,MAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,MAAI,KACF,OAAM;;CAIV,MAAM,UAA+B;AACnC,SAAO,IAAI,SAAS,YAAY;AAC9B,OAAI,KAAK,UAAU,GAAG;AACpB,SAAK;AACL,kBAAc,KAAK,SAAS,CAAC;SAE7B,MAAK,MAAM,WAAW;AACpB,SAAK;AACL,kBAAc,KAAK,SAAS,CAAC;KAC7B;IAEJ"}
|
package/dist/utils/file.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MediaCloudPluginOptions, MimeType } from "../types/index.mjs";
|
|
2
2
|
import { S3Store } from "../tus/stores/s3/s3Store.mjs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as payload0 from "payload";
|
|
4
4
|
|
|
5
5
|
//#region src/utils/file.d.ts
|
|
6
6
|
|
|
@@ -35,7 +35,7 @@ interface CreateFileEndpointsArgs {
|
|
|
35
35
|
pluginOptions: MediaCloudPluginOptions;
|
|
36
36
|
}
|
|
37
37
|
declare function createFileEndpoints(args: CreateFileEndpointsArgs): {
|
|
38
|
-
handler:
|
|
38
|
+
handler: payload0.PayloadHandler;
|
|
39
39
|
method: "get";
|
|
40
40
|
path: string;
|
|
41
41
|
}[];
|
package/dist/utils/tus.mjs
CHANGED
|
@@ -29,6 +29,7 @@ function createTusEndpoints(args) {
|
|
|
29
29
|
const { getTusServer, getS3Store, pluginOptions } = args;
|
|
30
30
|
const collection = pluginOptions.collection;
|
|
31
31
|
magicError.assert(collection, MediaCloudErrors.COLLECTION_REQUIRED);
|
|
32
|
+
const folders = pluginOptions.folders ?? false;
|
|
32
33
|
/**
|
|
33
34
|
* Handles TUS requests through the server
|
|
34
35
|
* @param req - The payload request object
|
|
@@ -79,7 +80,8 @@ function createTusEndpoints(args) {
|
|
|
79
80
|
{
|
|
80
81
|
handler: getTusFolderHandler({
|
|
81
82
|
getS3Store,
|
|
82
|
-
collection
|
|
83
|
+
collection,
|
|
84
|
+
folders
|
|
83
85
|
}),
|
|
84
86
|
method: "get",
|
|
85
87
|
path: "/uploads/:filename/folder"
|
package/dist/utils/tus.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tus.mjs","names":["magicError: UseMagicErrorReturn","TusServer"],"sources":["../../src/utils/tus.ts"],"sourcesContent":["import { Server as TusServer } from '@tus/server'\n\nimport { getTusPostProcessorHandler } from '../endpoints/tusPostProcessorHandler'\nimport { getTusFolderHandler } from '../endpoints/tusFolderHandler'\nimport { getTusCleanupHandler } from '../endpoints/tusCleanupHandler'\nimport { generateUniqueFilename } from './file'\n\nimport { useMagicError, type UseMagicErrorReturn } from '@maas/error-handler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport type { PayloadRequest } from 'payload'\nimport type { S3Store } from '../tus/stores/s3/s3Store'\nimport type { MediaCloudPluginOptions } from '../types'\n\nconst magicError: UseMagicErrorReturn = useMagicError({\n prefix: 'PLUGIN-MEDIA-CLOUD',\n})\n\n/**\n * Creates a TUS server instance with S3 storage\n * @param args.getS3Store - Function that returns an S3 client instance\n * @param args.pluginOptions - Media cloud plugin options\n * @returns A configured TusServer instance\n */\ninterface CreateTusServerArgs {\n getS3Store: () => S3Store\n pluginOptions: MediaCloudPluginOptions\n}\n\nexport function createTusServer(args: CreateTusServerArgs): TusServer {\n const { getS3Store, pluginOptions } = args\n\n const s3Store = getS3Store()\n\n return new TusServer({\n datastore: s3Store,\n path: '/api/uploads',\n respectForwardedHeaders: pluginOptions.s3?.respectForwardedHeaders ?? true,\n namingFunction: async (_req, metadata) => {\n return metadata?.filename ?? generateUniqueFilename('')\n },\n onUploadFinish: async (_req, upload) => {\n // Clean up .part and .info files\n const filename = upload.metadata?.filename ?? ''\n await s3Store.cleanup(filename)\n\n // Prevent type error\n return Promise.resolve({})\n },\n })\n}\n\n/**\n * Creates TUS upload endpoints for file handling\n * @param args.getTusServer - Function that returns a TUS server instance\n * @param args.getS3Store - Function that returns an S3 client instance\n * @returns An array of endpoint configurations\n */\ninterface CreateTusEndpointsArgs {\n getTusServer: () => TusServer\n getS3Store: () => S3Store\n pluginOptions: MediaCloudPluginOptions\n}\n\nexport function createTusEndpoints(args: CreateTusEndpointsArgs) {\n const { getTusServer, getS3Store, pluginOptions } = args\n\n const collection = pluginOptions.collection\n magicError.assert(collection, MediaCloudErrors.COLLECTION_REQUIRED)\n\n /**\n * Handles TUS requests through the server\n * @param req - The payload request object\n * @returns The server response\n */\n function tusHandler(req: PayloadRequest) {\n return getTusServer().handleWeb(req as Request)\n }\n\n return [\n { handler: tusHandler, method: 'options' as const, path: '/uploads' },\n { handler: tusHandler, method: 'post' as const, path: '/uploads' },\n { handler: tusHandler, method: 'get' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'put' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'patch' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'delete' as const, path: '/uploads/:id' },\n {\n handler: getTusPostProcessorHandler({ getS3Store, collection }),\n method: 'get' as const,\n path: '/uploads/:filename/process',\n },\n {\n handler: getTusFolderHandler({ getS3Store, collection }),\n method: 'get' as const,\n path: '/uploads/:filename/folder',\n },\n {\n handler: getTusCleanupHandler({ getS3Store, collection }),\n method: 'post' as const,\n path: '/uploads/cleanup',\n },\n ]\n}\n"],"mappings":";;;;;;;;;AAcA,MAAMA,aAAkC,cAAc,EACpD,QAAQ,sBACT,CAAC;AAaF,SAAgB,gBAAgB,MAAsC;CACpE,MAAM,EAAE,YAAY,kBAAkB;CAEtC,MAAM,UAAU,YAAY;AAE5B,QAAO,IAAIC,OAAU;EACnB,WAAW;EACX,MAAM;EACN,yBAAyB,cAAc,IAAI,2BAA2B;EACtE,gBAAgB,OAAO,MAAM,aAAa;AACxC,UAAO,UAAU,YAAY,uBAAuB,GAAG;;EAEzD,gBAAgB,OAAO,MAAM,WAAW;GAEtC,MAAM,WAAW,OAAO,UAAU,YAAY;AAC9C,SAAM,QAAQ,QAAQ,SAAS;AAG/B,UAAO,QAAQ,QAAQ,EAAE,CAAC;;EAE7B,CAAC;;AAeJ,SAAgB,mBAAmB,MAA8B;CAC/D,MAAM,EAAE,cAAc,YAAY,kBAAkB;CAEpD,MAAM,aAAa,cAAc;AACjC,YAAW,OAAO,YAAY,iBAAiB,oBAAoB;;;;;;
|
|
1
|
+
{"version":3,"file":"tus.mjs","names":["magicError: UseMagicErrorReturn","TusServer"],"sources":["../../src/utils/tus.ts"],"sourcesContent":["import { Server as TusServer } from '@tus/server'\n\nimport { getTusPostProcessorHandler } from '../endpoints/tusPostProcessorHandler'\nimport { getTusFolderHandler } from '../endpoints/tusFolderHandler'\nimport { getTusCleanupHandler } from '../endpoints/tusCleanupHandler'\nimport { generateUniqueFilename } from './file'\n\nimport { useMagicError, type UseMagicErrorReturn } from '@maas/error-handler'\nimport { MediaCloudErrors } from '../types/errors'\n\nimport type { PayloadRequest } from 'payload'\nimport type { S3Store } from '../tus/stores/s3/s3Store'\nimport type { MediaCloudPluginOptions } from '../types'\n\nconst magicError: UseMagicErrorReturn = useMagicError({\n prefix: 'PLUGIN-MEDIA-CLOUD',\n})\n\n/**\n * Creates a TUS server instance with S3 storage\n * @param args.getS3Store - Function that returns an S3 client instance\n * @param args.pluginOptions - Media cloud plugin options\n * @returns A configured TusServer instance\n */\ninterface CreateTusServerArgs {\n getS3Store: () => S3Store\n pluginOptions: MediaCloudPluginOptions\n}\n\nexport function createTusServer(args: CreateTusServerArgs): TusServer {\n const { getS3Store, pluginOptions } = args\n\n const s3Store = getS3Store()\n\n return new TusServer({\n datastore: s3Store,\n path: '/api/uploads',\n respectForwardedHeaders: pluginOptions.s3?.respectForwardedHeaders ?? true,\n namingFunction: async (_req, metadata) => {\n return metadata?.filename ?? generateUniqueFilename('')\n },\n onUploadFinish: async (_req, upload) => {\n // Clean up .part and .info files\n const filename = upload.metadata?.filename ?? ''\n await s3Store.cleanup(filename)\n\n // Prevent type error\n return Promise.resolve({})\n },\n })\n}\n\n/**\n * Creates TUS upload endpoints for file handling\n * @param args.getTusServer - Function that returns a TUS server instance\n * @param args.getS3Store - Function that returns an S3 client instance\n * @returns An array of endpoint configurations\n */\ninterface CreateTusEndpointsArgs {\n getTusServer: () => TusServer\n getS3Store: () => S3Store\n pluginOptions: MediaCloudPluginOptions\n}\n\nexport function createTusEndpoints(args: CreateTusEndpointsArgs) {\n const { getTusServer, getS3Store, pluginOptions } = args\n\n const collection = pluginOptions.collection\n magicError.assert(collection, MediaCloudErrors.COLLECTION_REQUIRED)\n\n const folders = pluginOptions.folders ?? false\n\n /**\n * Handles TUS requests through the server\n * @param req - The payload request object\n * @returns The server response\n */\n function tusHandler(req: PayloadRequest) {\n return getTusServer().handleWeb(req as Request)\n }\n\n return [\n { handler: tusHandler, method: 'options' as const, path: '/uploads' },\n { handler: tusHandler, method: 'post' as const, path: '/uploads' },\n { handler: tusHandler, method: 'get' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'put' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'patch' as const, path: '/uploads/:id' },\n { handler: tusHandler, method: 'delete' as const, path: '/uploads/:id' },\n {\n handler: getTusPostProcessorHandler({ getS3Store, collection }),\n method: 'get' as const,\n path: '/uploads/:filename/process',\n },\n {\n handler: getTusFolderHandler({ getS3Store, collection, folders }),\n method: 'get' as const,\n path: '/uploads/:filename/folder',\n },\n {\n handler: getTusCleanupHandler({ getS3Store, collection }),\n method: 'post' as const,\n path: '/uploads/cleanup',\n },\n ]\n}\n"],"mappings":";;;;;;;;;AAcA,MAAMA,aAAkC,cAAc,EACpD,QAAQ,sBACT,CAAC;AAaF,SAAgB,gBAAgB,MAAsC;CACpE,MAAM,EAAE,YAAY,kBAAkB;CAEtC,MAAM,UAAU,YAAY;AAE5B,QAAO,IAAIC,OAAU;EACnB,WAAW;EACX,MAAM;EACN,yBAAyB,cAAc,IAAI,2BAA2B;EACtE,gBAAgB,OAAO,MAAM,aAAa;AACxC,UAAO,UAAU,YAAY,uBAAuB,GAAG;;EAEzD,gBAAgB,OAAO,MAAM,WAAW;GAEtC,MAAM,WAAW,OAAO,UAAU,YAAY;AAC9C,SAAM,QAAQ,QAAQ,SAAS;AAG/B,UAAO,QAAQ,QAAQ,EAAE,CAAC;;EAE7B,CAAC;;AAeJ,SAAgB,mBAAmB,MAA8B;CAC/D,MAAM,EAAE,cAAc,YAAY,kBAAkB;CAEpD,MAAM,aAAa,cAAc;AACjC,YAAW,OAAO,YAAY,iBAAiB,oBAAoB;CAEnE,MAAM,UAAU,cAAc,WAAW;;;;;;CAOzC,SAAS,WAAW,KAAqB;AACvC,SAAO,cAAc,CAAC,UAAU,IAAe;;AAGjD,QAAO;EACL;GAAE,SAAS;GAAY,QAAQ;GAAoB,MAAM;GAAY;EACrE;GAAE,SAAS;GAAY,QAAQ;GAAiB,MAAM;GAAY;EAClE;GAAE,SAAS;GAAY,QAAQ;GAAgB,MAAM;GAAgB;EACrE;GAAE,SAAS;GAAY,QAAQ;GAAgB,MAAM;GAAgB;EACrE;GAAE,SAAS;GAAY,QAAQ;GAAkB,MAAM;GAAgB;EACvE;GAAE,SAAS;GAAY,QAAQ;GAAmB,MAAM;GAAgB;EACxE;GACE,SAAS,2BAA2B;IAAE;IAAY;IAAY,CAAC;GAC/D,QAAQ;GACR,MAAM;GACP;EACD;GACE,SAAS,oBAAoB;IAAE;IAAY;IAAY;IAAS,CAAC;GACjE,QAAQ;GACR,MAAM;GACP;EACD;GACE,SAAS,qBAAqB;IAAE;IAAY;IAAY,CAAC;GACzD,QAAQ;GACR,MAAM;GACP;EACF"}
|