@maas/payload-plugin-media-cloud 0.0.2 → 0.0.4

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.
@@ -101,6 +101,8 @@ async function tusUpload(args) {
101
101
  const filename = file.name;
102
102
  const filetype = file.type;
103
103
  const filesize = file.size.toString();
104
+ console.debug(`Starting TUS upload for file: ${filename}`);
105
+ console.debug(`Server URL: ${serverURL}${apiRoute}/uploads`);
104
106
  return new Promise((resolve) => {
105
107
  const upload = new tus.Upload(file, {
106
108
  endpoint: `${serverURL}${apiRoute}/uploads`,
@@ -154,7 +156,7 @@ const UploadHandler = createClientUploadHandler({ handler: async (args) => {
154
156
  const mimeType = await getFileType(file);
155
157
  if (!mimeType) {
156
158
  throwError(MediaCloudError.FILE_TYPE_UNKNOWN);
157
- throw new Error();
159
+ return null;
158
160
  }
159
161
  const isVideoFile = await isVideo(file);
160
162
  const uploadArgs = {
@@ -1 +1 @@
1
- {"version":3,"file":"upload-handler.js","names":["uploadUrl?: string | null","args: UploadArgs","uploadArgs: UploadArgs"],"sources":["../../../src/components/upload-handler/upload-handler.tsx"],"sourcesContent":["'use client'\n\nimport * as upchunk from '@mux/upchunk'\nimport * as tus from 'tus-js-client'\n\nimport { toast } from '@payloadcms/ui'\nimport { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'\n\nimport { MediaCloudError } from '../../types/errors'\nimport { emitter } from '../../hooks/useEmitter'\nimport { useErrorHandler } from '../../hooks/useErrorHandler'\nimport { isVideo, getFileType, sanitizeFilename } from '../../utils/file'\n\ninterface UploadArgs {\n serverURL: string\n apiRoute: string\n file: File\n mimeType: string\n updateFilename: (filename: string) => void\n}\n\ninterface UploadResult {\n uploadId: string\n mimeType: string\n storage: 'mux' | 's3'\n}\n\ninterface MuxCreateUploadResponse {\n url: string\n uploadId: string\n}\n\nconst { logError, throwError } = useErrorHandler()\n\nconst MUX_CHUNK_SIZE = 30720\nconst TUS_CHUNK_SIZE = 1024 * 1024\nconst TUS_RETRY_DELAYS = [0, 1000, 2000, 5000]\n\n/**\n * Utility function to parse upload ID from URL\n * @param uploadUrl - The upload URL to parse\n * @returns The extracted upload ID or empty string if parsing fails\n */\nfunction parseUploadId(uploadUrl?: string | null): string {\n if (!uploadUrl) {\n logError(MediaCloudError.UPLOAD_NO_URL)\n return ''\n }\n const url = new URL(uploadUrl)\n return url.pathname.split('/').pop() || ''\n}\n\n/**\n * Handles Mux video upload with progress tracking\n * @param args - The upload arguments including file, server URL, and callbacks\n * @returns Promise that resolves to upload result or null if upload fails\n */\nasync function muxUpload(args: UploadArgs): Promise<UploadResult | null> {\n const { file, serverURL, apiRoute, mimeType, updateFilename } = args\n\n const filename = sanitizeFilename(file.name)\n const getUploadUrlEndpoint = `${serverURL}${apiRoute}/mux/upload`\n const getAssetEndpoint = `${serverURL}${apiRoute}/mux/asset`\n\n updateFilename(filename)\n\n try {\n // Request upload URL from Mux\n const response = await fetch(getUploadUrlEndpoint, {\n body: JSON.stringify({ filename, mimeType }),\n credentials: 'include',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n\n const { url, uploadId } = (await response.json()) as MuxCreateUploadResponse\n\n // Create upchunk uploader\n const uploader = await upchunk.createUpload({\n endpoint: url,\n file,\n chunkSize: MUX_CHUNK_SIZE,\n })\n\n // Add upload to tracker\n emitter.emit('add-upload', {\n id: uploadId,\n filename,\n polling: false,\n pollingUrl: getAssetEndpoint,\n })\n\n // Set up event handlers\n uploader.on('error', () => {\n logError(MediaCloudError.MUX_UPLOAD_ERROR)\n toast.error('Video upload failed')\n emitter.emit('remove-upload', { id: uploadId })\n })\n\n uploader.on('progress', (progress) => {\n emitter.emit('update-upload', {\n id: uploadId,\n progress: progress.detail,\n })\n })\n\n uploader.on('success', () => {\n emitter.emit('upload-completed', { id: uploadId })\n })\n\n return {\n uploadId,\n mimeType,\n storage: 'mux',\n }\n } catch (_error) {\n logError(MediaCloudError.MUX_DIRECT_UPLOAD_ERROR)\n toast.error('Video upload failed')\n return null\n }\n}\n\n/**\n * Handles TUS file upload with resumable capabilities\n * @param args - The upload arguments including file, server URL, and callbacks\n * @returns Promise that resolves to upload result or null if upload fails\n */\nasync function tusUpload(args: UploadArgs): Promise<UploadResult | null> {\n const { apiRoute, serverURL, file, mimeType, updateFilename } = args\n\n const filename = file.name\n const filetype = file.type\n const filesize = file.size.toString()\n\n return new Promise((resolve) => {\n const upload = new tus.Upload(file, {\n endpoint: `${serverURL}${apiRoute}/uploads`,\n retryDelays: TUS_RETRY_DELAYS,\n chunkSize: TUS_CHUNK_SIZE,\n metadata: {\n filename,\n filetype,\n filesize,\n contentType: filetype,\n contentDisposition: 'inline',\n contentLength: filesize,\n },\n onError: () => {\n logError(MediaCloudError.TUS_UPLOAD_ERROR)\n toast.error('File upload failed')\n resolve(null)\n },\n onProgress: (bytesUploaded, bytesTotal) => {\n const percentage = Math.round((bytesUploaded / bytesTotal) * 100)\n const uploadId = parseUploadId(upload?.url)\n emitter.emit('update-upload', {\n id: uploadId,\n progress: percentage,\n })\n },\n onSuccess: () => {\n const uploadId = parseUploadId(upload?.url)\n emitter.emit('upload-completed', { id: uploadId })\n },\n onUploadUrlAvailable: () => {\n const uploadId = parseUploadId(upload?.url)\n updateFilename(uploadId)\n emitter.emit('add-upload', { id: uploadId, filename })\n resolve({\n uploadId,\n mimeType,\n storage: 's3',\n })\n },\n })\n\n upload.start()\n })\n}\n\nexport const UploadHandler = createClientUploadHandler({\n handler: async (args) => {\n const { serverURL, apiRoute, file, updateFilename } = args\n\n try {\n const mimeType = await getFileType(file)\n\n if (!mimeType) {\n throwError(MediaCloudError.FILE_TYPE_UNKNOWN)\n throw new Error() // This will never execute but satisfies TypeScript\n }\n\n const isVideoFile = await isVideo(file)\n\n const uploadArgs: UploadArgs = {\n file,\n serverURL,\n apiRoute,\n mimeType,\n updateFilename,\n }\n\n if (isVideoFile) {\n return await muxUpload(uploadArgs)\n } else {\n return await tusUpload(uploadArgs)\n }\n } catch (_error) {\n logError(MediaCloudError.UPLOAD_HANDLER_ERROR)\n toast.error('Upload failed')\n return null\n }\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"upload-handler.js","names":["uploadUrl?: string | null","args: UploadArgs","uploadArgs: UploadArgs"],"sources":["../../../src/components/upload-handler/upload-handler.tsx"],"sourcesContent":["'use client'\n\nimport * as upchunk from '@mux/upchunk'\nimport * as tus from 'tus-js-client'\n\nimport { toast } from '@payloadcms/ui'\nimport { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client'\n\nimport { MediaCloudError } from '../../types/errors'\nimport { emitter } from '../../hooks/useEmitter'\nimport { useErrorHandler } from '../../hooks/useErrorHandler'\nimport { isVideo, getFileType, sanitizeFilename } from '../../utils/file'\n\ninterface UploadArgs {\n serverURL: string\n apiRoute: string\n file: File\n mimeType: string\n updateFilename: (filename: string) => void\n}\n\ninterface UploadResult {\n uploadId: string\n mimeType: string\n storage: 'mux' | 's3'\n}\n\ninterface MuxCreateUploadResponse {\n url: string\n uploadId: string\n}\n\nconst { logError, throwError } = useErrorHandler()\n\nconst MUX_CHUNK_SIZE = 30720\nconst TUS_CHUNK_SIZE = 1024 * 1024\nconst TUS_RETRY_DELAYS = [0, 1000, 2000, 5000]\n\n/**\n * Utility function to parse upload ID from URL\n * @param uploadUrl - The upload URL to parse\n * @returns The extracted upload ID or empty string if parsing fails\n */\nfunction parseUploadId(uploadUrl?: string | null): string {\n if (!uploadUrl) {\n logError(MediaCloudError.UPLOAD_NO_URL)\n return ''\n }\n const url = new URL(uploadUrl)\n return url.pathname.split('/').pop() || ''\n}\n\n/**\n * Handles Mux video upload with progress tracking\n * @param args - The upload arguments including file, server URL, and callbacks\n * @returns Promise that resolves to upload result or null if upload fails\n */\nasync function muxUpload(args: UploadArgs): Promise<UploadResult | null> {\n const { file, serverURL, apiRoute, mimeType, updateFilename } = args\n\n const filename = sanitizeFilename(file.name)\n const getUploadUrlEndpoint = `${serverURL}${apiRoute}/mux/upload`\n const getAssetEndpoint = `${serverURL}${apiRoute}/mux/asset`\n\n updateFilename(filename)\n\n try {\n // Request upload URL from Mux\n const response = await fetch(getUploadUrlEndpoint, {\n body: JSON.stringify({ filename, mimeType }),\n credentials: 'include',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n\n const { url, uploadId } = (await response.json()) as MuxCreateUploadResponse\n\n // Create upchunk uploader\n const uploader = await upchunk.createUpload({\n endpoint: url,\n file,\n chunkSize: MUX_CHUNK_SIZE,\n })\n\n // Add upload to tracker\n emitter.emit('add-upload', {\n id: uploadId,\n filename,\n polling: false,\n pollingUrl: getAssetEndpoint,\n })\n\n // Set up event handlers\n uploader.on('error', () => {\n logError(MediaCloudError.MUX_UPLOAD_ERROR)\n toast.error('Video upload failed')\n emitter.emit('remove-upload', { id: uploadId })\n })\n\n uploader.on('progress', (progress) => {\n emitter.emit('update-upload', {\n id: uploadId,\n progress: progress.detail,\n })\n })\n\n uploader.on('success', () => {\n emitter.emit('upload-completed', { id: uploadId })\n })\n\n return {\n uploadId,\n mimeType,\n storage: 'mux',\n }\n } catch (_error) {\n logError(MediaCloudError.MUX_DIRECT_UPLOAD_ERROR)\n toast.error('Video upload failed')\n return null\n }\n}\n\n/**\n * Handles TUS file upload with resumable capabilities\n * @param args - The upload arguments including file, server URL, and callbacks\n * @returns Promise that resolves to upload result or null if upload fails\n */\nasync function tusUpload(args: UploadArgs): Promise<UploadResult | null> {\n const { apiRoute, serverURL, file, mimeType, updateFilename } = args\n\n const filename = file.name\n const filetype = file.type\n const filesize = file.size.toString()\n\n console.debug(`Starting TUS upload for file: ${filename}`)\n console.debug(`Server URL: ${serverURL}${apiRoute}/uploads`)\n\n return new Promise((resolve) => {\n const upload = new tus.Upload(file, {\n endpoint: `${serverURL}${apiRoute}/uploads`,\n retryDelays: TUS_RETRY_DELAYS,\n chunkSize: TUS_CHUNK_SIZE,\n metadata: {\n filename,\n filetype,\n filesize,\n contentType: filetype,\n contentDisposition: 'inline',\n contentLength: filesize,\n },\n onError: () => {\n logError(MediaCloudError.TUS_UPLOAD_ERROR)\n toast.error('File upload failed')\n resolve(null)\n },\n onProgress: (bytesUploaded, bytesTotal) => {\n const percentage = Math.round((bytesUploaded / bytesTotal) * 100)\n const uploadId = parseUploadId(upload?.url)\n emitter.emit('update-upload', {\n id: uploadId,\n progress: percentage,\n })\n },\n onSuccess: () => {\n const uploadId = parseUploadId(upload?.url)\n emitter.emit('upload-completed', { id: uploadId })\n },\n onUploadUrlAvailable: () => {\n const uploadId = parseUploadId(upload?.url)\n updateFilename(uploadId)\n emitter.emit('add-upload', { id: uploadId, filename })\n resolve({\n uploadId,\n mimeType,\n storage: 's3',\n })\n },\n })\n\n upload.start()\n })\n}\n\nexport const UploadHandler = createClientUploadHandler({\n handler: async (args) => {\n const { serverURL, apiRoute, file, updateFilename } = args\n\n try {\n const mimeType = await getFileType(file)\n\n if (!mimeType) {\n throwError(MediaCloudError.FILE_TYPE_UNKNOWN)\n return null\n }\n\n const isVideoFile = await isVideo(file)\n const uploadArgs: UploadArgs = {\n file,\n serverURL,\n apiRoute,\n mimeType,\n updateFilename,\n }\n\n if (isVideoFile) {\n return await muxUpload(uploadArgs)\n } else {\n return await tusUpload(uploadArgs)\n }\n } catch (_error) {\n logError(MediaCloudError.UPLOAD_HANDLER_ERROR)\n toast.error('Upload failed')\n return null\n }\n },\n})\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import "./upload-manager.css";
2
3
 
3
4
  //#region src/components/upload-manager/upload-manager.d.ts
4
5
  interface Upload {
@@ -4,10 +4,10 @@
4
4
  import { MediaCloudError } from "../../types/errors.js";
5
5
  import { useErrorHandler } from "../../hooks/useErrorHandler.js";
6
6
  import { useEmitter } from "../../hooks/useEmitter.js";
7
- import "./upload-manager2.js";
8
7
  import { createContext, use, useEffect, useRef, useState } from "react";
9
8
  import { jsx, jsxs } from "react/jsx-runtime";
10
9
  import { Button } from "@payloadcms/ui";
10
+ import "./upload-manager.css";
11
11
 
12
12
  //#region src/components/upload-manager/upload-manager.tsx
13
13
  const { logError } = useErrorHandler();
@@ -20,7 +20,7 @@ function createErrorHandler(config) {
20
20
  }
21
21
  function logError(errorKey, overrideLevel) {
22
22
  const formattedMessage = formatMessage(errorKey);
23
- const logLevel = overrideLevel || level;
23
+ const logLevel = overrideLevel ?? level;
24
24
  switch (logLevel) {
25
25
  case "LOG":
26
26
  console.log(formattedMessage);
@@ -34,6 +34,10 @@ function createErrorHandler(config) {
34
34
  default: console.error(formattedMessage);
35
35
  }
36
36
  }
37
+ function logConsole(errorKey) {
38
+ const formattedMessage = formatMessage(errorKey);
39
+ console.log(formattedMessage);
40
+ }
37
41
  function throwError(errorKey) {
38
42
  const formattedMessage = formatMessage(errorKey);
39
43
  throw new BaseError({
@@ -43,7 +47,8 @@ function createErrorHandler(config) {
43
47
  }
44
48
  return {
45
49
  logError,
46
- throwError
50
+ throwError,
51
+ logConsole
47
52
  };
48
53
  }
49
54
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../error-handler/dist/index.js"],"sourcesContent":["//#region src/createErrorHandler.ts\n/**\n* Creates an error handler with configurable prefix and error sets\n* @param config - Configuration object containing prefix, level, and error set\n* @returns Object with logError and throwError functions\n*/\nfunction createErrorHandler(config) {\n\tconst { prefix, level, errors } = config;\n\tclass BaseError extends Error {\n\t\tkey;\n\t\tconstructor({ message, key }) {\n\t\t\tsuper(`[${prefix}] ${message}`);\n\t\t\tthis.name = \"BaseError\";\n\t\t\tthis.key = key;\n\t\t}\n\t}\n\tfunction formatMessage(errorKey) {\n\t\tconst message = errors[errorKey] || `Unknown error: ${String(errorKey)}`;\n\t\treturn `[${prefix}] ${message}`;\n\t}\n\tfunction logError(errorKey, overrideLevel) {\n\t\tconst formattedMessage = formatMessage(errorKey);\n\t\tconst logLevel = overrideLevel || level;\n\t\tswitch (logLevel) {\n\t\t\tcase \"LOG\":\n\t\t\t\tconsole.log(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tcase \"WARNING\":\n\t\t\t\tconsole.warn(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tcase \"ERROR\":\n\t\t\t\tconsole.error(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tdefault: console.error(formattedMessage);\n\t\t}\n\t}\n\tfunction throwError(errorKey) {\n\t\tconst formattedMessage = formatMessage(errorKey);\n\t\tthrow new BaseError({\n\t\t\tkey: String(errorKey),\n\t\t\tmessage: formattedMessage\n\t\t});\n\t}\n\treturn {\n\t\tlogError,\n\t\tthrowError\n\t};\n}\n\n//#endregion\n//#region src/types.ts\n/**\n* Error level enum for categorizing error severity\n*/\nlet ErrorLevel = /* @__PURE__ */ function(ErrorLevel$1) {\n\tErrorLevel$1[\"LOG\"] = \"LOG\";\n\tErrorLevel$1[\"WARNING\"] = \"WARNING\";\n\tErrorLevel$1[\"ERROR\"] = \"ERROR\";\n\treturn ErrorLevel$1;\n}({});\n\n//#endregion\nexport { ErrorLevel, createErrorHandler };\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;AAMA,SAAS,mBAAmB,QAAQ;CACnC,MAAM,EAAE,QAAQ,OAAO,QAAQ,GAAG;CAClC,MAAM,kBAAkB,MAAM;EAC7B;EACA,YAAY,EAAE,SAAS,KAAK,EAAE;GAC7B,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS,CAAC;GAC/B,KAAK,OAAO;GACZ,KAAK,MAAM;EACX;CACD;CACD,SAAS,cAAc,UAAU;EAChC,MAAM,UAAU,OAAO,aAAa,CAAC,eAAe,EAAE,OAAO,SAAS,EAAE;AACxE,SAAO,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS;CAC/B;CACD,SAAS,SAAS,UAAU,eAAe;EAC1C,MAAM,mBAAmB,cAAc,SAAS;EAChD,MAAM,WAAW,iBAAiB;AAClC,UAAQ,UAAR;GACC,KAAK;IACJ,QAAQ,IAAI,iBAAiB;AAC7B;GACD,KAAK;IACJ,QAAQ,KAAK,iBAAiB;AAC9B;GACD,KAAK;IACJ,QAAQ,MAAM,iBAAiB;AAC/B;GACD,SAAS,QAAQ,MAAM,iBAAiB;EACxC;CACD;CACD,SAAS,WAAW,UAAU;EAC7B,MAAM,mBAAmB,cAAc,SAAS;AAChD,QAAM,IAAI,UAAU;GACnB,KAAK,OAAO,SAAS;GACrB,SAAS;EACT;CACD;AACD,QAAO;EACN;EACA;CACA;AACD;;;;AAOD,IAAI,6BAA6B,SAAS,cAAc;CACvD,aAAa,SAAS;CACtB,aAAa,aAAa;CAC1B,aAAa,WAAW;AACxB,QAAO;AACP,EAAC,CAAE,EAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../error-handler/dist/index.js"],"sourcesContent":["//#region src/createErrorHandler.ts\n/**\n* Creates an error handler with configurable prefix and error sets\n* @param config - Configuration object containing prefix, level, and error set\n* @returns Object with logError and throwError functions\n*/\nfunction createErrorHandler(config) {\n\tconst { prefix, level, errors } = config;\n\tclass BaseError extends Error {\n\t\tkey;\n\t\tconstructor({ message, key }) {\n\t\t\tsuper(`[${prefix}] ${message}`);\n\t\t\tthis.name = \"BaseError\";\n\t\t\tthis.key = key;\n\t\t}\n\t}\n\tfunction formatMessage(errorKey) {\n\t\tconst message = errors[errorKey] || `Unknown error: ${String(errorKey)}`;\n\t\treturn `[${prefix}] ${message}`;\n\t}\n\tfunction logError(errorKey, overrideLevel) {\n\t\tconst formattedMessage = formatMessage(errorKey);\n\t\tconst logLevel = overrideLevel ?? level;\n\t\tswitch (logLevel) {\n\t\t\tcase \"LOG\":\n\t\t\t\tconsole.log(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tcase \"WARNING\":\n\t\t\t\tconsole.warn(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tcase \"ERROR\":\n\t\t\t\tconsole.error(formattedMessage);\n\t\t\t\tbreak;\n\t\t\tdefault: console.error(formattedMessage);\n\t\t}\n\t}\n\tfunction logConsole(errorKey) {\n\t\tconst formattedMessage = formatMessage(errorKey);\n\t\tconsole.log(formattedMessage);\n\t}\n\tfunction throwError(errorKey) {\n\t\tconst formattedMessage = formatMessage(errorKey);\n\t\tthrow new BaseError({\n\t\t\tkey: String(errorKey),\n\t\t\tmessage: formattedMessage\n\t\t});\n\t}\n\treturn {\n\t\tlogError,\n\t\tthrowError,\n\t\tlogConsole\n\t};\n}\n\n//#endregion\n//#region src/types.ts\n/**\n* Error level enum for categorizing error severity\n*/\nlet ErrorLevel = /* @__PURE__ */ function(ErrorLevel$1) {\n\tErrorLevel$1[\"LOG\"] = \"LOG\";\n\tErrorLevel$1[\"WARNING\"] = \"WARNING\";\n\tErrorLevel$1[\"ERROR\"] = \"ERROR\";\n\treturn ErrorLevel$1;\n}({});\n\n//#endregion\nexport { ErrorLevel, createErrorHandler };\n//# sourceMappingURL=index.js.map"],"mappings":";;;;;;AAMA,SAAS,mBAAmB,QAAQ;CACnC,MAAM,EAAE,QAAQ,OAAO,QAAQ,GAAG;CAClC,MAAM,kBAAkB,MAAM;EAC7B;EACA,YAAY,EAAE,SAAS,KAAK,EAAE;GAC7B,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS,CAAC;GAC/B,KAAK,OAAO;GACZ,KAAK,MAAM;EACX;CACD;CACD,SAAS,cAAc,UAAU;EAChC,MAAM,UAAU,OAAO,aAAa,CAAC,eAAe,EAAE,OAAO,SAAS,EAAE;AACxE,SAAO,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,SAAS;CAC/B;CACD,SAAS,SAAS,UAAU,eAAe;EAC1C,MAAM,mBAAmB,cAAc,SAAS;EAChD,MAAM,WAAW,iBAAiB;AAClC,UAAQ,UAAR;GACC,KAAK;IACJ,QAAQ,IAAI,iBAAiB;AAC7B;GACD,KAAK;IACJ,QAAQ,KAAK,iBAAiB;AAC9B;GACD,KAAK;IACJ,QAAQ,MAAM,iBAAiB;AAC/B;GACD,SAAS,QAAQ,MAAM,iBAAiB;EACxC;CACD;CACD,SAAS,WAAW,UAAU;EAC7B,MAAM,mBAAmB,cAAc,SAAS;EAChD,QAAQ,IAAI,iBAAiB;CAC7B;CACD,SAAS,WAAW,UAAU;EAC7B,MAAM,mBAAmB,cAAc,SAAS;AAChD,QAAM,IAAI,UAAU;GACnB,KAAK,OAAO,SAAS;GACrB,SAAS;EACT;CACD;AACD,QAAO;EACN;EACA;EACA;CACA;AACD;;;;AAOD,IAAI,6BAA6B,SAAS,cAAc;CACvD,aAAa,SAAS;CACtB,aAAa,aAAa;CAC1B,aAAa,WAAW;AACxB,QAAO;AACP,EAAC,CAAE,EAAC"}
@@ -5,6 +5,7 @@ import { ErrorLevel } from "@maas/error-handler";
5
5
  declare function useErrorHandler(): {
6
6
  logError: (errorKey: MediaCloudError, level?: ErrorLevel) => void;
7
7
  throwError: (errorKey: MediaCloudError) => never;
8
+ logConsole: (errorKey: MediaCloudError) => void;
8
9
  };
9
10
  //#endregion
10
11
  export { useErrorHandler };
@@ -3,14 +3,15 @@ import { ErrorLevel, createErrorHandler } from "../error-handler/dist/index.js";
3
3
 
4
4
  //#region src/hooks/useErrorHandler.ts
5
5
  function useErrorHandler() {
6
- const { logError, throwError } = createErrorHandler({
6
+ const { logError, throwError, logConsole } = createErrorHandler({
7
7
  prefix: "PLUGIN-MEDIA-CLOUD",
8
8
  level: ErrorLevel.ERROR,
9
9
  errors: MediaCloudError
10
10
  });
11
11
  return {
12
12
  logError,
13
- throwError
13
+ throwError,
14
+ logConsole
14
15
  };
15
16
  }
16
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"useErrorHandler.js","names":[],"sources":["../../src/hooks/useErrorHandler.ts"],"sourcesContent":["import { createErrorHandler, ErrorLevel } from '@maas/error-handler'\nimport { MediaCloudError } from '../types/errors'\n\nexport function useErrorHandler() {\n const { logError, throwError } = createErrorHandler({\n prefix: 'PLUGIN-MEDIA-CLOUD',\n level: ErrorLevel.ERROR,\n errors: MediaCloudError,\n })\n\n return {\n logError,\n throwError,\n }\n}\n"],"mappings":";;;;AAGA,SAAgB,kBAAkB;CAChC,MAAM,EAAE,UAAU,YAAY,GAAG,mBAAmB;EAClD,QAAQ;EACR,OAAO,WAAW;EAClB,QAAQ;CACT,EAAC;AAEF,QAAO;EACL;EACA;CACD;AACF"}
1
+ {"version":3,"file":"useErrorHandler.js","names":[],"sources":["../../src/hooks/useErrorHandler.ts"],"sourcesContent":["import { createErrorHandler, ErrorLevel } from '@maas/error-handler'\nimport { MediaCloudError } from '../types/errors'\n\nexport function useErrorHandler() {\n const { logError, throwError, logConsole } = createErrorHandler({\n prefix: 'PLUGIN-MEDIA-CLOUD',\n level: ErrorLevel.ERROR,\n errors: MediaCloudError,\n })\n\n return {\n logError,\n throwError,\n logConsole,\n }\n}\n"],"mappings":";;;;AAGA,SAAgB,kBAAkB;CAChC,MAAM,EAAE,UAAU,YAAY,YAAY,GAAG,mBAAmB;EAC9D,QAAQ;EACR,OAAO,WAAW;EAClB,QAAQ;CACT,EAAC;AAEF,QAAO;EACL;EACA;EACA;CACD;AACF"}
@@ -1,7 +1,9 @@
1
- import { log } from "./log.js";
1
+ import { MediaCloudError } from "../../../types/errors.js";
2
+ import { useErrorHandler } from "../../../hooks/useErrorHandler.js";
2
3
  import { TUS_RESUMABLE, Upload } from "@tus/utils";
3
4
 
4
5
  //#region src/tus/stores/s3/metadata-manager.ts
6
+ const { logConsole } = useErrorHandler();
5
7
  var S3MetadataManager = class {
6
8
  constructor(client, bucket, cache, shouldUseExpirationTags, generateCompleteTag) {
7
9
  this.client = client;
@@ -77,7 +79,7 @@ var S3MetadataManager = class {
77
79
  */
78
80
  async saveMetadata(args) {
79
81
  const { upload, uploadId } = args;
80
- log(`[${upload.id}] saving metadata`);
82
+ logConsole(MediaCloudError.S3_STORE_METADATA_SAVING);
81
83
  await this.client.putObject({
82
84
  Body: JSON.stringify(upload),
83
85
  Bucket: this.bucket,
@@ -88,7 +90,7 @@ var S3MetadataManager = class {
88
90
  },
89
91
  Tagging: this.generateCompleteTag("false")
90
92
  });
91
- log(`[${upload.id}] metadata file saved`);
93
+ logConsole(MediaCloudError.S3_STORE_METADATA_SAVED);
92
94
  }
93
95
  /**
94
96
  * Completes metadata by updating it with completion tags
@@ -125,7 +127,7 @@ var S3MetadataManager = class {
125
127
  */
126
128
  async clearCache(args) {
127
129
  const { id } = args;
128
- log(`[${id}] removing cached data`);
130
+ logConsole(MediaCloudError.S3_STORE_METADATA_CACHE_CLEARED);
129
131
  await this.cache.delete(id);
130
132
  }
131
133
  };
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-manager.js","names":["client: S3","bucket: string","cache: KvStore<TusUploadMetadata>","shouldUseExpirationTags: () => boolean","generateCompleteTag: (value: 'false' | 'true') => string | undefined","args: GenerateInfoKeyArgs","args: GeneratePartKeyArgs","args: GetMetadataArgs","metadata: TusUploadMetadata","args: SaveMetadataArgs","args: CompleteMetadataArgs","completedMetadata: TusUploadMetadata","args: ClearCacheArgs"],"sources":["../../../../src/tus/stores/s3/metadata-manager.ts"],"sourcesContent":["import { TUS_RESUMABLE, Upload } from '@tus/utils'\nimport { log } from './log'\n\nimport type { S3 } from '@aws-sdk/client-s3'\nimport type { KvStore } from '@tus/utils'\nimport type { TusUploadMetadata } from '../../../types'\n\ntype GenerateInfoKeyArgs = {\n id: string\n}\n\ntype GeneratePartKeyArgs = {\n id: string\n isIncomplete?: boolean\n}\n\ntype GetMetadataArgs = {\n id: string\n}\n\ntype SaveMetadataArgs = {\n upload: Upload\n uploadId: string\n}\n\ntype CompleteMetadataArgs = {\n upload: Upload\n}\n\ntype ClearCacheArgs = {\n id: string\n}\n\nexport class S3MetadataManager {\n constructor(\n private client: S3,\n private bucket: string,\n private cache: KvStore<TusUploadMetadata>,\n private shouldUseExpirationTags: () => boolean,\n private generateCompleteTag: (value: 'false' | 'true') => string | undefined\n ) {}\n\n /**\n * Generates the S3 key for metadata info files\n * @param args - The function arguments\n * @param args.id - The file ID\n * @returns The S3 key for the metadata file\n */\n generateInfoKey(args: GenerateInfoKeyArgs): string {\n const { id } = args\n return `${id}.info`\n }\n\n /**\n * Generates the S3 key for part files\n * @param args - The function arguments\n * @param args.id - The file ID\n * @param args.isIncomplete - Whether this is an incomplete part\n * @returns The S3 key for the part file\n */\n generatePartKey(args: GeneratePartKeyArgs): string {\n const { id, isIncomplete = false } = args\n let key = id\n if (isIncomplete) {\n key += '.part'\n }\n\n // TODO: introduce ObjectPrefixing for parts and incomplete parts.\n // ObjectPrefix is prepended to the name of each S3 object that is created\n // to store uploaded files. It can be used to create a pseudo-directory\n // structure in the bucket, e.g. \"path/to/my/uploads\".\n return key\n }\n\n /**\n * Retrieves upload metadata previously saved in `${file_id}.info`.\n * There's a small and simple caching mechanism to avoid multiple\n * HTTP calls to S3.\n * @param args - The function arguments\n * @param args.id - The file ID to retrieve metadata for\n * @returns Promise that resolves to the upload metadata\n */\n async getMetadata(args: GetMetadataArgs): Promise<TusUploadMetadata> {\n const { id } = args\n const cached = await this.cache.get(id)\n if (cached) {\n return cached\n }\n\n const { Body, Metadata } = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id }),\n })\n const file = JSON.parse((await Body?.transformToString()) as string)\n const metadata: TusUploadMetadata = {\n file: new Upload({\n id,\n creation_date: file.creation_date,\n metadata: file.metadata,\n offset: Number.parseInt(file.offset, 10),\n size: Number.isFinite(file.size)\n ? Number.parseInt(file.size, 10)\n : undefined,\n storage: file.storage,\n }),\n 'tus-version': Metadata?.['tus-version'] as string,\n 'upload-id': Metadata?.['upload-id'] as string,\n }\n await this.cache.set(id, metadata)\n return metadata\n }\n\n /**\n * Saves upload metadata to a `${file_id}.info` file on S3.\n * Please note that the file is empty and the metadata is saved\n * on the S3 object's `Metadata` field, so that only a `headObject`\n * is necessary to retrieve the data.\n * @param args - The function arguments\n * @param args.upload - The upload object containing metadata\n * @param args.uploadId - The upload ID for the multipart upload\n * @returns Promise that resolves when metadata is saved\n */\n async saveMetadata(args: SaveMetadataArgs): Promise<void> {\n const { upload, uploadId } = args\n log(`[${upload.id}] saving metadata`)\n await this.client.putObject({\n Body: JSON.stringify(upload),\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id: upload.id }),\n Metadata: {\n 'tus-version': TUS_RESUMABLE,\n 'upload-id': uploadId,\n },\n Tagging: this.generateCompleteTag('false'),\n })\n log(`[${upload.id}] metadata file saved`)\n }\n\n /**\n * Completes metadata by updating it with completion tags\n * @param args - The function arguments\n * @param args.upload - The completed upload object\n * @returns Promise that resolves when metadata is updated\n */\n async completeMetadata(args: CompleteMetadataArgs): Promise<void> {\n const { upload } = args\n // Always update the cache with the completed upload\n const metadata = await this.getMetadata({ id: upload.id })\n const completedMetadata: TusUploadMetadata = {\n ...metadata,\n file: upload,\n }\n await this.cache.set(upload.id, completedMetadata)\n\n if (!this.shouldUseExpirationTags()) {\n return\n }\n\n const { 'upload-id': uploadId } = metadata\n await this.client.putObject({\n Body: JSON.stringify(upload),\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id: upload.id }),\n Metadata: {\n 'tus-version': TUS_RESUMABLE,\n 'upload-id': uploadId,\n },\n Tagging: this.generateCompleteTag('true'),\n })\n }\n\n /**\n * Removes cached data for a given file\n * @param args - The function arguments\n * @param args.id - The file ID to remove from cache\n * @returns Promise that resolves when cache is cleared\n */\n async clearCache(args: ClearCacheArgs): Promise<void> {\n const { id } = args\n log(`[${id}] removing cached data`)\n await this.cache.delete(id)\n }\n}\n"],"mappings":";;;;AAiCA,IAAa,oBAAb,MAA+B;CAC7B,YACUA,QACAC,QACAC,OACAC,yBACAC,qBACR;EALQ;EACA;EACA;EACA;EACA;CACN;;;;;;;CAQJ,gBAAgBC,MAAmC;EACjD,MAAM,EAAE,IAAI,GAAG;AACf,SAAO,GAAG,GAAG,KAAK,CAAC;CACpB;;;;;;;;CASD,gBAAgBC,MAAmC;EACjD,MAAM,EAAE,IAAI,eAAe,OAAO,GAAG;EACrC,IAAI,MAAM;AACV,MAAI,cACF,OAAO;AAOT,SAAO;CACR;;;;;;;;;CAUD,MAAM,YAAYC,MAAmD;EACnE,MAAM,EAAE,IAAI,GAAG;EACf,MAAM,SAAS,MAAM,KAAK,MAAM,IAAI,GAAG;AACvC,MAAI,OACF,QAAO;EAGT,MAAM,EAAE,MAAM,UAAU,GAAG,MAAM,KAAK,OAAO,UAAU;GACrD,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,GAAI,EAAC;EAClC,EAAC;EACF,MAAM,OAAO,KAAK,MAAO,MAAM,MAAM,mBAAmB,CAAY;EACpE,MAAMC,WAA8B;GAClC,MAAM,IAAI,OAAO;IACf;IACA,eAAe,KAAK;IACpB,UAAU,KAAK;IACf,QAAQ,OAAO,SAAS,KAAK,QAAQ,GAAG;IACxC,MAAM,OAAO,SAAS,KAAK,KAAK,GAC5B,OAAO,SAAS,KAAK,MAAM,GAAG,GAC9B;IACJ,SAAS,KAAK;GACf;GACD,eAAe,WAAW;GAC1B,aAAa,WAAW;EACzB;EACD,MAAM,KAAK,MAAM,IAAI,IAAI,SAAS;AAClC,SAAO;CACR;;;;;;;;;;;CAYD,MAAM,aAAaC,MAAuC;EACxD,MAAM,EAAE,QAAQ,UAAU,GAAG;EAC7B,IAAI,CAAC,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAC,CAAC;EACrC,MAAM,KAAK,OAAO,UAAU;GAC1B,MAAM,KAAK,UAAU,OAAO;GAC5B,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,IAAI,OAAO,GAAI,EAAC;GAC5C,UAAU;IACR,eAAe;IACf,aAAa;GACd;GACD,SAAS,KAAK,oBAAoB,QAAQ;EAC3C,EAAC;EACF,IAAI,CAAC,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC,CAAC;CAC1C;;;;;;;CAQD,MAAM,iBAAiBC,MAA2C;EAChE,MAAM,EAAE,QAAQ,GAAG;EAEnB,MAAM,WAAW,MAAM,KAAK,YAAY,EAAE,IAAI,OAAO,GAAI,EAAC;EAC1D,MAAMC,oBAAuC;GAC3C,GAAG;GACH,MAAM;EACP;EACD,MAAM,KAAK,MAAM,IAAI,OAAO,IAAI,kBAAkB;AAElD,MAAI,CAAC,KAAK,yBAAyB,CACjC;EAGF,MAAM,EAAE,aAAa,UAAU,GAAG;EAClC,MAAM,KAAK,OAAO,UAAU;GAC1B,MAAM,KAAK,UAAU,OAAO;GAC5B,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,IAAI,OAAO,GAAI,EAAC;GAC5C,UAAU;IACR,eAAe;IACf,aAAa;GACd;GACD,SAAS,KAAK,oBAAoB,OAAO;EAC1C,EAAC;CACH;;;;;;;CAQD,MAAM,WAAWC,MAAqC;EACpD,MAAM,EAAE,IAAI,GAAG;EACf,IAAI,CAAC,CAAC,EAAE,GAAG,sBAAsB,CAAC,CAAC;EACnC,MAAM,KAAK,MAAM,OAAO,GAAG;CAC5B;AACF"}
1
+ {"version":3,"file":"metadata-manager.js","names":["client: S3","bucket: string","cache: KvStore<TusUploadMetadata>","shouldUseExpirationTags: () => boolean","generateCompleteTag: (value: 'false' | 'true') => string | undefined","args: GenerateInfoKeyArgs","args: GeneratePartKeyArgs","args: GetMetadataArgs","metadata: TusUploadMetadata","args: SaveMetadataArgs","args: CompleteMetadataArgs","completedMetadata: TusUploadMetadata","args: ClearCacheArgs"],"sources":["../../../../src/tus/stores/s3/metadata-manager.ts"],"sourcesContent":["import { TUS_RESUMABLE, Upload } from '@tus/utils'\n\nimport { useErrorHandler } from '../../../hooks/useErrorHandler'\nimport { MediaCloudError } from '../../../types/errors'\n\nimport type { S3 } from '@aws-sdk/client-s3'\nimport type { KvStore } from '@tus/utils'\nimport type { TusUploadMetadata } from '../../../types'\n\ntype GenerateInfoKeyArgs = {\n id: string\n}\n\ntype GeneratePartKeyArgs = {\n id: string\n isIncomplete?: boolean\n}\n\ntype GetMetadataArgs = {\n id: string\n}\n\ntype SaveMetadataArgs = {\n upload: Upload\n uploadId: string\n}\n\ntype CompleteMetadataArgs = {\n upload: Upload\n}\n\ntype ClearCacheArgs = {\n id: string\n}\n\nconst { logConsole } = useErrorHandler()\n\nexport class S3MetadataManager {\n constructor(\n private client: S3,\n private bucket: string,\n private cache: KvStore<TusUploadMetadata>,\n private shouldUseExpirationTags: () => boolean,\n private generateCompleteTag: (value: 'false' | 'true') => string | undefined\n ) {}\n /**\n * Generates the S3 key for metadata info files\n * @param args - The function arguments\n * @param args.id - The file ID\n * @returns The S3 key for the metadata file\n */\n generateInfoKey(args: GenerateInfoKeyArgs): string {\n const { id } = args\n return `${id}.info`\n }\n\n /**\n * Generates the S3 key for part files\n * @param args - The function arguments\n * @param args.id - The file ID\n * @param args.isIncomplete - Whether this is an incomplete part\n * @returns The S3 key for the part file\n */\n generatePartKey(args: GeneratePartKeyArgs): string {\n const { id, isIncomplete = false } = args\n let key = id\n if (isIncomplete) {\n key += '.part'\n }\n\n // TODO: introduce ObjectPrefixing for parts and incomplete parts.\n // ObjectPrefix is prepended to the name of each S3 object that is created\n // to store uploaded files. It can be used to create a pseudo-directory\n // structure in the bucket, e.g. \"path/to/my/uploads\".\n return key\n }\n\n /**\n * Retrieves upload metadata previously saved in `${file_id}.info`.\n * There's a small and simple caching mechanism to avoid multiple\n * HTTP calls to S3.\n * @param args - The function arguments\n * @param args.id - The file ID to retrieve metadata for\n * @returns Promise that resolves to the upload metadata\n */\n async getMetadata(args: GetMetadataArgs): Promise<TusUploadMetadata> {\n const { id } = args\n const cached = await this.cache.get(id)\n if (cached) {\n return cached\n }\n\n const { Body, Metadata } = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id }),\n })\n const file = JSON.parse((await Body?.transformToString()) as string)\n const metadata: TusUploadMetadata = {\n file: new Upload({\n id,\n creation_date: file.creation_date,\n metadata: file.metadata,\n offset: Number.parseInt(file.offset, 10),\n size: Number.isFinite(file.size)\n ? Number.parseInt(file.size, 10)\n : undefined,\n storage: file.storage,\n }),\n 'tus-version': Metadata?.['tus-version'] as string,\n 'upload-id': Metadata?.['upload-id'] as string,\n }\n await this.cache.set(id, metadata)\n return metadata\n }\n\n /**\n * Saves upload metadata to a `${file_id}.info` file on S3.\n * Please note that the file is empty and the metadata is saved\n * on the S3 object's `Metadata` field, so that only a `headObject`\n * is necessary to retrieve the data.\n * @param args - The function arguments\n * @param args.upload - The upload object containing metadata\n * @param args.uploadId - The upload ID for the multipart upload\n * @returns Promise that resolves when metadata is saved\n */\n async saveMetadata(args: SaveMetadataArgs): Promise<void> {\n const { upload, uploadId } = args\n logConsole(MediaCloudError.S3_STORE_METADATA_SAVING)\n await this.client.putObject({\n Body: JSON.stringify(upload),\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id: upload.id }),\n Metadata: {\n 'tus-version': TUS_RESUMABLE,\n 'upload-id': uploadId,\n },\n Tagging: this.generateCompleteTag('false'),\n })\n logConsole(MediaCloudError.S3_STORE_METADATA_SAVED)\n }\n\n /**\n * Completes metadata by updating it with completion tags\n * @param args - The function arguments\n * @param args.upload - The completed upload object\n * @returns Promise that resolves when metadata is updated\n */\n async completeMetadata(args: CompleteMetadataArgs): Promise<void> {\n const { upload } = args\n // Always update the cache with the completed upload\n const metadata = await this.getMetadata({ id: upload.id })\n const completedMetadata: TusUploadMetadata = {\n ...metadata,\n file: upload,\n }\n await this.cache.set(upload.id, completedMetadata)\n\n if (!this.shouldUseExpirationTags()) {\n return\n }\n\n const { 'upload-id': uploadId } = metadata\n await this.client.putObject({\n Body: JSON.stringify(upload),\n Bucket: this.bucket,\n Key: this.generateInfoKey({ id: upload.id }),\n Metadata: {\n 'tus-version': TUS_RESUMABLE,\n 'upload-id': uploadId,\n },\n Tagging: this.generateCompleteTag('true'),\n })\n }\n\n /**\n * Removes cached data for a given file\n * @param args - The function arguments\n * @param args.id - The file ID to remove from cache\n * @returns Promise that resolves when cache is cleared\n */\n async clearCache(args: ClearCacheArgs): Promise<void> {\n const { id } = args\n logConsole(MediaCloudError.S3_STORE_METADATA_CACHE_CLEARED)\n await this.cache.delete(id)\n }\n}\n"],"mappings":";;;;;AAmCA,MAAM,EAAE,YAAY,GAAG,iBAAiB;AAExC,IAAa,oBAAb,MAA+B;CAC7B,YACUA,QACAC,QACAC,OACAC,yBACAC,qBACR;EALQ;EACA;EACA;EACA;EACA;CACN;;;;;;;CAOJ,gBAAgBC,MAAmC;EACjD,MAAM,EAAE,IAAI,GAAG;AACf,SAAO,GAAG,GAAG,KAAK,CAAC;CACpB;;;;;;;;CASD,gBAAgBC,MAAmC;EACjD,MAAM,EAAE,IAAI,eAAe,OAAO,GAAG;EACrC,IAAI,MAAM;AACV,MAAI,cACF,OAAO;AAOT,SAAO;CACR;;;;;;;;;CAUD,MAAM,YAAYC,MAAmD;EACnE,MAAM,EAAE,IAAI,GAAG;EACf,MAAM,SAAS,MAAM,KAAK,MAAM,IAAI,GAAG;AACvC,MAAI,OACF,QAAO;EAGT,MAAM,EAAE,MAAM,UAAU,GAAG,MAAM,KAAK,OAAO,UAAU;GACrD,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,GAAI,EAAC;EAClC,EAAC;EACF,MAAM,OAAO,KAAK,MAAO,MAAM,MAAM,mBAAmB,CAAY;EACpE,MAAMC,WAA8B;GAClC,MAAM,IAAI,OAAO;IACf;IACA,eAAe,KAAK;IACpB,UAAU,KAAK;IACf,QAAQ,OAAO,SAAS,KAAK,QAAQ,GAAG;IACxC,MAAM,OAAO,SAAS,KAAK,KAAK,GAC5B,OAAO,SAAS,KAAK,MAAM,GAAG,GAC9B;IACJ,SAAS,KAAK;GACf;GACD,eAAe,WAAW;GAC1B,aAAa,WAAW;EACzB;EACD,MAAM,KAAK,MAAM,IAAI,IAAI,SAAS;AAClC,SAAO;CACR;;;;;;;;;;;CAYD,MAAM,aAAaC,MAAuC;EACxD,MAAM,EAAE,QAAQ,UAAU,GAAG;EAC7B,WAAW,gBAAgB,yBAAyB;EACpD,MAAM,KAAK,OAAO,UAAU;GAC1B,MAAM,KAAK,UAAU,OAAO;GAC5B,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,IAAI,OAAO,GAAI,EAAC;GAC5C,UAAU;IACR,eAAe;IACf,aAAa;GACd;GACD,SAAS,KAAK,oBAAoB,QAAQ;EAC3C,EAAC;EACF,WAAW,gBAAgB,wBAAwB;CACpD;;;;;;;CAQD,MAAM,iBAAiBC,MAA2C;EAChE,MAAM,EAAE,QAAQ,GAAG;EAEnB,MAAM,WAAW,MAAM,KAAK,YAAY,EAAE,IAAI,OAAO,GAAI,EAAC;EAC1D,MAAMC,oBAAuC;GAC3C,GAAG;GACH,MAAM;EACP;EACD,MAAM,KAAK,MAAM,IAAI,OAAO,IAAI,kBAAkB;AAElD,MAAI,CAAC,KAAK,yBAAyB,CACjC;EAGF,MAAM,EAAE,aAAa,UAAU,GAAG;EAClC,MAAM,KAAK,OAAO,UAAU;GAC1B,MAAM,KAAK,UAAU,OAAO;GAC5B,QAAQ,KAAK;GACb,KAAK,KAAK,gBAAgB,EAAE,IAAI,OAAO,GAAI,EAAC;GAC5C,UAAU;IACR,eAAe;IACf,aAAa;GACd;GACD,SAAS,KAAK,oBAAoB,OAAO;EAC1C,EAAC;CACH;;;;;;;CAQD,MAAM,WAAWC,MAAqC;EACpD,MAAM,EAAE,IAAI,GAAG;EACf,WAAW,gBAAgB,gCAAgC;EAC3D,MAAM,KAAK,MAAM,OAAO,GAAG;CAC5B;AACF"}
@@ -1,6 +1,5 @@
1
1
  import { MediaCloudError } from "../../../types/errors.js";
2
2
  import { useErrorHandler } from "../../../hooks/useErrorHandler.js";
3
- import { log } from "./log.js";
4
3
  import stream from "node:stream";
5
4
  import { NoSuchKey, NotFound } from "@aws-sdk/client-s3";
6
5
  import { StreamSplitter } from "@tus/utils";
@@ -8,7 +7,7 @@ import fs from "node:fs";
8
7
  import os from "node:os";
9
8
 
10
9
  //#region src/tus/stores/s3/parts-manager.ts
11
- const { throwError } = useErrorHandler();
10
+ const { throwError, logConsole } = useErrorHandler();
12
11
  var S3PartsManager = class {
13
12
  constructor(client, bucket, minPartSize, partUploadSemaphore, metadataManager, fileOperations, generateCompleteTag) {
14
13
  this.client = client;
@@ -200,7 +199,7 @@ var S3PartsManager = class {
200
199
  }),
201
200
  Tagging: this.generateCompleteTag("false")
202
201
  });
203
- log(`[${id}] finished uploading incomplete part`);
202
+ logConsole(MediaCloudError.S3_STORE_INCOMPLETE_PART_UPLOADED);
204
203
  return data.ETag;
205
204
  }
206
205
  /**
@@ -311,7 +310,7 @@ var S3PartsManager = class {
311
310
  if (pendingChunkFilepath !== null) try {
312
311
  await fs.promises.rm(pendingChunkFilepath);
313
312
  } catch {
314
- log(`[${metadata.file.id}] failed to remove chunk ${String(pendingChunkFilepath)}`);
313
+ logConsole(MediaCloudError.S3_STORE_CHUNK_REMOVAL_FAILED);
315
314
  }
316
315
  const mappedError = error instanceof Error ? error : new Error(String(error));
317
316
  promises.push(Promise.reject(mappedError));
@@ -1 +1 @@
1
- {"version":3,"file":"parts-manager.js","names":["client: S3","bucket: string","minPartSize: number","partUploadSemaphore: Semaphore","metadataManager: S3MetadataManager","fileOperations: S3FileOperations","generateCompleteTag: (value: 'false' | 'true') => string | undefined","args: RetrievePartsArgs","params: AWS.ListPartsCommandInput","args: FinishMultipartUploadArgs","args: GetIncompletePartArgs","args: GetIncompletePartSizeArgs","args: DeleteIncompletePartArgs","args: DownloadIncompletePartArgs","options: { cleanUpOnEnd: boolean }","args: UploadIncompletePartArgs","args: UploadPartArgs","params: AWS.UploadPartCommandInput","args: UploadPartsArgs","promises: Promise<void>[]","pendingChunkFilepath: null | string","permit: SemaphorePermit | undefined"],"sources":["../../../../src/tus/stores/s3/parts-manager.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 { MediaCloudError } from '../../../types/errors'\nimport { log } from './log'\n\nimport type { Readable } from 'node:stream'\nimport type AWS from '@aws-sdk/client-s3'\nimport type { IncompletePartInfo, TusUploadMetadata } from '../../../types'\nimport type { S3FileOperations } from './file-operations'\nimport type { S3MetadataManager } from './metadata-manager'\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 { 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(MediaCloudError.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: id,\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 const params = {\n Bucket: this.bucket,\n Key: metadata.file.id,\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(MediaCloudError.TUS_UPLOAD_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 try {\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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 try {\n const data = await this.client.headObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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 await this.client.deleteObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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', (err) => {\n fileReader.destroy(err)\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 const data = await this.client.putObject({\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\n Tagging: this.generateCompleteTag('false'),\n })\n log(`[${id}] finished uploading incomplete part`)\n return data.ETag as string\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(MediaCloudError.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: metadata.file.id,\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(MediaCloudError.TUS_UPLOAD_ERROR)\n throw new Error() // This will never execute but satisfies TypeScript\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 let permit: SemaphorePermit | undefined = undefined\n\n const splitterStream = new StreamSplitter({\n chunkSize: this.fileOperations.calculateOptimalPartSize({ size }),\n directory: os.tmpdir(),\n })\n .on('beforeChunkStarted', async () => {\n permit = await this.partUploadSemaphore.acquire()\n })\n .on('chunkStarted', (filepath) => {\n pendingChunkFilepath = filepath\n })\n .on('chunkFinished', ({ path, size: partSize }) => {\n pendingChunkFilepath = null\n\n const acquiredPermit = permit\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 throw error\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 acquiredPermit?.()\n }\n }\n\n const deferred = uploadChunk()\n\n promises.push(deferred)\n })\n .on('chunkError', () => {\n permit?.()\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(\n `[${metadata.file.id}] failed to remove chunk ${String(pendingChunkFilepath)}`\n )\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":"parts-manager.js","names":["client: S3","bucket: string","minPartSize: number","partUploadSemaphore: Semaphore","metadataManager: S3MetadataManager","fileOperations: S3FileOperations","generateCompleteTag: (value: 'false' | 'true') => string | undefined","args: RetrievePartsArgs","params: AWS.ListPartsCommandInput","args: FinishMultipartUploadArgs","args: GetIncompletePartArgs","args: GetIncompletePartSizeArgs","args: DeleteIncompletePartArgs","args: DownloadIncompletePartArgs","options: { cleanUpOnEnd: boolean }","args: UploadIncompletePartArgs","args: UploadPartArgs","params: AWS.UploadPartCommandInput","args: UploadPartsArgs","promises: Promise<void>[]","pendingChunkFilepath: null | string","permit: SemaphorePermit | undefined"],"sources":["../../../../src/tus/stores/s3/parts-manager.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 { MediaCloudError } from '../../../types/errors'\n\nimport type { Readable } from 'node:stream'\nimport type AWS from '@aws-sdk/client-s3'\nimport type { IncompletePartInfo, TusUploadMetadata } from '../../../types'\nimport type { S3FileOperations } from './file-operations'\nimport type { S3MetadataManager } from './metadata-manager'\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 { throwError, logConsole } = 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(MediaCloudError.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: id,\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 const params = {\n Bucket: this.bucket,\n Key: metadata.file.id,\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(MediaCloudError.TUS_UPLOAD_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 try {\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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 try {\n const data = await this.client.headObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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 await this.client.deleteObject({\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\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', (err) => {\n fileReader.destroy(err)\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 const data = await this.client.putObject({\n Body: readStream,\n Bucket: this.bucket,\n Key: this.metadataManager.generatePartKey({ id, isIncomplete: true }),\n Tagging: this.generateCompleteTag('false'),\n })\n logConsole(MediaCloudError.S3_STORE_INCOMPLETE_PART_UPLOADED)\n return data.ETag as string\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(MediaCloudError.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: metadata.file.id,\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(MediaCloudError.TUS_UPLOAD_ERROR)\n throw new Error() // This will never execute but satisfies TypeScript\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 let permit: SemaphorePermit | undefined = undefined\n\n const splitterStream = new StreamSplitter({\n chunkSize: this.fileOperations.calculateOptimalPartSize({ size }),\n directory: os.tmpdir(),\n })\n .on('beforeChunkStarted', async () => {\n permit = await this.partUploadSemaphore.acquire()\n })\n .on('chunkStarted', (filepath) => {\n pendingChunkFilepath = filepath\n })\n .on('chunkFinished', ({ path, size: partSize }) => {\n pendingChunkFilepath = null\n\n const acquiredPermit = permit\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 throw error\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 acquiredPermit?.()\n }\n }\n\n const deferred = uploadChunk()\n\n promises.push(deferred)\n })\n .on('chunkError', () => {\n permit?.()\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 logConsole(MediaCloudError.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,4 +1,5 @@
1
- import { log } from "./log.js";
1
+ import { MediaCloudError } from "../../../types/errors.js";
2
+ import { useErrorHandler } from "../../../hooks/useErrorHandler.js";
2
3
  import { Semaphore } from "./semaphore.js";
3
4
  import { S3ExpirationManager } from "./expiration-manager.js";
4
5
  import { S3FileOperations } from "./file-operations.js";
@@ -9,6 +10,7 @@ import { NoSuchKey, NotFound, S3 } from "@aws-sdk/client-s3";
9
10
  import { DataStore, ERRORS, MemoryKvStore, TUS_RESUMABLE, Upload } from "@tus/utils";
10
11
 
11
12
  //#region src/tus/stores/s3/s3-store.ts
13
+ const { logConsole } = useErrorHandler();
12
14
  var S3Store = class extends DataStore {
13
15
  client;
14
16
  bucket;
@@ -95,7 +97,7 @@ var S3Store = class extends DataStore {
95
97
  * @returns Promise that resolves to the created upload
96
98
  */
97
99
  async create(upload) {
98
- log(`[${upload.id}] initializing multipart upload`);
100
+ logConsole(MediaCloudError.S3_STORE_MULTIPART_INIT);
99
101
  const request = {
100
102
  Bucket: this.bucket,
101
103
  Key: upload.id,
@@ -115,7 +117,7 @@ var S3Store = class extends DataStore {
115
117
  upload,
116
118
  uploadId: response.UploadId
117
119
  });
118
- log(`[${upload.id}] multipart upload created (${response.UploadId})`);
120
+ logConsole(MediaCloudError.S3_STORE_MULTIPART_CREATED);
119
121
  return upload;
120
122
  }
121
123
  /**
@@ -180,7 +182,7 @@ var S3Store = class extends DataStore {
180
182
  });
181
183
  await this.metadataManager.completeMetadata({ upload: completedUpload });
182
184
  } catch (error) {
183
- log(`[${id}] failed to finish upload`, error);
185
+ logConsole(MediaCloudError.S3_STORE_UPLOAD_FAILED);
184
186
  throw error;
185
187
  }
186
188
  return newOffset;
@@ -209,7 +211,7 @@ var S3Store = class extends DataStore {
209
211
  size: metadata.file.size,
210
212
  storage: metadata.file.storage
211
213
  });
212
- log("getUpload: Error retrieving parts.", error);
214
+ logConsole(MediaCloudError.S3_STORE_RETRIEVE_PARTS_ERROR);
213
215
  throw error;
214
216
  }
215
217
  const incompletePartSize = await this.partsManager.getIncompletePartSize({ id });
@@ -224,7 +226,7 @@ var S3Store = class extends DataStore {
224
226
  * Reads the file specified by the upload's `id` and returns a readable stream
225
227
  */
226
228
  async read(id) {
227
- log(`[${id}] attempting to read file from S3`);
229
+ logConsole(MediaCloudError.S3_STORE_READ_ATTEMPT);
228
230
  let retries = 3;
229
231
  let lastError = null;
230
232
  while (retries > 0) try {
@@ -232,15 +234,15 @@ var S3Store = class extends DataStore {
232
234
  Bucket: this.bucket,
233
235
  Key: id
234
236
  });
235
- log(`[${id}] successfully read file from S3`);
237
+ logConsole(MediaCloudError.S3_STORE_READ_SUCCESS);
236
238
  return data.Body;
237
239
  } catch (error) {
238
- log(`[${id}] failed to read file, retries left: ${retries - 1}`, error);
240
+ logConsole(MediaCloudError.S3_STORE_READ_RETRY);
239
241
  lastError = error;
240
242
  retries--;
241
243
  if (retries > 0) await new Promise((resolve) => setTimeout(resolve, 100));
242
244
  }
243
- log(`[${id}] failed to read file after all retries`);
245
+ logConsole(MediaCloudError.S3_STORE_READ_FAILED);
244
246
  throw lastError || /* @__PURE__ */ new Error(`Failed to read file ${id} after retries`);
245
247
  }
246
248
  /**
@@ -260,7 +262,7 @@ var S3Store = class extends DataStore {
260
262
  "NoSuchUpload",
261
263
  "NotFound"
262
264
  ].includes(error.Code || "")) {
263
- log("remove: No file found.", error);
265
+ logConsole(MediaCloudError.S3_STORE_FILE_NOT_FOUND);
264
266
  throw ERRORS.FILE_NOT_FOUND;
265
267
  }
266
268
  throw error;
@@ -327,7 +329,7 @@ var S3Store = class extends DataStore {
327
329
  * Returns the number of deleted uploads.
328
330
  */
329
331
  async deleteExpired() {
330
- return this.expirationManager.deleteExpired({});
332
+ return this.expirationManager.deleteExpired();
331
333
  }
332
334
  /**
333
335
  * Returns the expiration period in milliseconds
@@ -1 +1 @@
1
- {"version":3,"file":"s3-store.js","names":["options: S3StoreConfig","id: string","isIncompletePart: boolean","value: 'false' | 'true'","upload: Upload","request: AWS.CreateMultipartUploadCommandInput","file_id: string","upload_length: number","readable: stream.Readable","offset: number","metadata: TusUploadMetadata","lastError: Error | null","region: string"],"sources":["../../../../src/tus/stores/s3/s3-store.ts"],"sourcesContent":["import { NoSuchKey, NotFound, S3 } from '@aws-sdk/client-s3'\nimport {\n DataStore,\n ERRORS,\n MemoryKvStore,\n TUS_RESUMABLE,\n Upload,\n type KvStore,\n} from '@tus/utils'\nimport stream, { type Readable } from 'node:stream'\n\nimport { log } from './log'\nimport { Semaphore } from './semaphore'\nimport { S3ExpirationManager } from './expiration-manager'\nimport { S3FileOperations } from './file-operations'\nimport { S3MetadataManager } from './metadata-manager'\nimport { S3PartsManager } from './parts-manager'\n\nimport type AWS from '@aws-sdk/client-s3'\nimport type { AWSError, S3StoreConfig, TusUploadMetadata } from '../../../types'\n\nexport class S3Store extends DataStore {\n public client: S3\n public bucket: string\n public partSize = 8 * 1024 * 1024 // 8MB preferred part size\n public minPartSize = 5 * 1024 * 1024 // 5MB minimum part size\n public maxMultipartParts = 10_000\n public maxUploadSize = 5_497_558_138_880 as const // 5TiB\n public useTags = false\n public expirationPeriodInMilliseconds = 0\n protected acl?: string\n\n protected cache: KvStore<TusUploadMetadata>\n protected partUploadSemaphore: Semaphore\n protected metadataManager: S3MetadataManager\n protected fileOperations: S3FileOperations\n protected partsManager: S3PartsManager\n protected expirationManager: S3ExpirationManager\n protected customEndpoint: string\n\n constructor(options: S3StoreConfig) {\n super()\n const {\n maxMultipartParts,\n minPartSize,\n partSize,\n s3ClientConfig,\n maxConcurrentPartUploads,\n useTags,\n expirationPeriodInMilliseconds,\n cache,\n } = options\n const { acl, bucket, ...restS3ClientConfig } = s3ClientConfig\n\n this.extensions = [\n 'creation',\n 'creation-with-upload',\n 'creation-defer-length',\n 'termination',\n 'expiration',\n ]\n\n this.bucket = bucket\n this.acl = acl\n this.client = new S3(restS3ClientConfig)\n this.customEndpoint = String(restS3ClientConfig.endpoint)\n\n this.partSize = partSize ?? this.partSize\n this.minPartSize = minPartSize ?? this.minPartSize\n this.maxMultipartParts = maxMultipartParts ?? this.maxMultipartParts\n\n this.useTags = useTags ?? this.useTags\n this.expirationPeriodInMilliseconds =\n expirationPeriodInMilliseconds ?? this.expirationPeriodInMilliseconds\n this.cache = cache ?? new MemoryKvStore<TusUploadMetadata>()\n this.partUploadSemaphore = new Semaphore(maxConcurrentPartUploads ?? 60)\n\n // Initialize component managers\n this.metadataManager = new S3MetadataManager(\n this.client,\n this.bucket,\n this.cache,\n this.shouldUseExpirationTags.bind(this),\n this.generateCompleteTag.bind(this)\n )\n\n this.fileOperations = new S3FileOperations(\n this.maxMultipartParts,\n this.maxUploadSize,\n this.minPartSize,\n this.partSize\n )\n\n this.partsManager = new S3PartsManager(\n this.client,\n this.bucket,\n this.minPartSize,\n this.partUploadSemaphore,\n this.metadataManager,\n this.fileOperations,\n this.generateCompleteTag.bind(this)\n )\n\n this.expirationManager = new S3ExpirationManager(\n this.client,\n this.bucket,\n this.expirationPeriodInMilliseconds,\n this.generateInfoKey.bind(this),\n this.generatePartKey.bind(this)\n )\n\n // Cleanup expired uploads when the store is initialized\n this.deleteExpired()\n }\n\n /**\n * Generate the key name for the info file\n * @param id - The upload ID\n * @returns The info file key\n */\n protected generateInfoKey(id: string): string {\n return `${id}.info`\n }\n\n /**\n * Generate the key name for a part file\n * @param id - The upload ID\n * @param isIncompletePart - Whether this is an incomplete part (default: false)\n * @returns The part file key\n */\n protected generatePartKey(\n id: string,\n isIncompletePart: boolean = false\n ): string {\n return isIncompletePart ? `${id}.part` : id\n }\n\n /**\n * Helper method to check if expiration tags should be used\n * @returns True if expiration tags should be used\n */\n protected shouldUseExpirationTags(): boolean {\n return this.expirationPeriodInMilliseconds !== 0 && this.useTags\n }\n\n /**\n * Generates a tag for marking complete/incomplete uploads\n * @param value - Either 'false' or 'true' to mark completion status\n * @returns The tag string or undefined if tags shouldn't be used\n */\n protected generateCompleteTag(value: 'false' | 'true'): string | undefined {\n if (!this.shouldUseExpirationTags()) {\n return undefined\n }\n return `Tus-Completed=${value}`\n }\n\n /**\n * Creates a multipart upload on S3 attaching any metadata to it.\n * Also, a `${file_id}.info` file is created which holds some information\n * about the upload itself like: `upload-id`, `upload-length`, etc.\n * @param upload - The upload object to create\n * @returns Promise that resolves to the created upload\n */\n public async create(upload: Upload): Promise<Upload> {\n log(`[${upload.id}] initializing multipart upload`)\n const request: AWS.CreateMultipartUploadCommandInput = {\n Bucket: this.bucket,\n Key: upload.id,\n Metadata: { 'tus-version': TUS_RESUMABLE },\n }\n\n if (upload.metadata?.contentType) {\n request.ContentType = upload.metadata.contentType as string\n }\n\n if (upload.metadata?.cacheControl) {\n request.CacheControl = upload.metadata.cacheControl as string\n }\n\n if (this.acl) {\n request.ACL = this.acl as AWS.ObjectCannedACL\n }\n\n upload.creation_date = new Date().toISOString()\n\n const response = await this.client.createMultipartUpload(request)\n upload.storage = {\n type: 's3',\n bucket: this.bucket,\n path: response.Key as string,\n }\n await this.metadataManager.saveMetadata({\n upload,\n uploadId: response.UploadId as string,\n })\n log(`[${upload.id}] multipart upload created (${response.UploadId})`)\n\n return upload\n }\n\n /**\n * Declares the length of the upload\n * @param file_id - The file ID\n * @param upload_length - The length of the upload\n * @returns Promise that resolves when length is declared\n */\n public async declareUploadLength(\n file_id: string,\n upload_length: number\n ): Promise<void> {\n const { file, 'upload-id': uploadId } =\n await this.metadataManager.getMetadata({ id: file_id })\n if (!file) {\n throw ERRORS.FILE_NOT_FOUND\n }\n\n file.size = upload_length\n\n await this.metadataManager.saveMetadata({ upload: file, uploadId })\n }\n\n /**\n * Writes `buffer` to the file specified by the upload's `id` at `offset`\n * @param readable - The readable stream to write\n * @param id - The upload ID\n * @param offset - The byte offset to write at\n * @returns Promise that resolves to the number of bytes written\n */\n public async write(\n readable: stream.Readable,\n id: string,\n offset: number\n ): Promise<number> {\n const metadata = await this.metadataManager.getMetadata({ id })\n\n // TUS sends PATCH requests with an `upload-offset` header.\n // Offset the write by the offset in the PATCH request.\n const calculatedOffset = this.fileOperations.calculateOffsetFromParts({\n parts: await this.partsManager.retrieveParts({ id }),\n })\n const offsetDiff = offset - calculatedOffset\n const requestedOffset = offset\n\n let finalReadable = readable\n\n if (offsetDiff < 0) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n // If the offset given in the PATCH request is higher than\n // the expected offset, we need to prepend an incomplete\n // part to the readable stream, if one exists.\n if (offsetDiff > 0) {\n const incompletePart = await this.partsManager.downloadIncompletePart({\n id,\n })\n\n if (!incompletePart) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n if (incompletePart.size !== offsetDiff) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n // Clear the incomplete part from S3 since it's going to be combined with the current request\n await this.partsManager.deleteIncompletePart({ id })\n\n // Adjust offset to account for the incomplete part\n offset = requestedOffset - incompletePart.size\n\n finalReadable = stream.Readable.from(\n (async function* () {\n yield* incompletePart.createReader({ cleanUpOnEnd: true })\n yield* readable\n })()\n )\n }\n\n const partNumber = this.fileOperations.calculatePartNumber({\n parts: await this.partsManager.retrieveParts({ id }),\n })\n\n const bytesUploaded = await this.partsManager.uploadParts({\n metadata,\n readStream: finalReadable,\n currentPartNumber: partNumber,\n offset,\n })\n\n // The size of the incomplete part should not be counted, because the\n // process of the incomplete part should be fully transparent to the user.\n const newOffset =\n requestedOffset + bytesUploaded - (offsetDiff > 0 ? offsetDiff : 0)\n\n // Check if the upload is complete\n if (metadata.file.size === newOffset) {\n try {\n const parts = await this.partsManager.retrieveParts({ id })\n await this.partsManager.finishMultipartUpload({ metadata, parts })\n\n // Update the metadata with completed state\n const completedUpload = new Upload({\n ...metadata.file,\n offset: newOffset,\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n\n await this.metadataManager.completeMetadata({ upload: completedUpload })\n // Don't clear cache immediately - Payload might still need the metadata\n // await this.metadataManager.clearCache(id)\n } catch (error) {\n log(`[${id}] failed to finish upload`, error)\n throw error\n }\n }\n\n return newOffset\n }\n\n /**\n * Returns the current state of the upload, i.e how much data has been\n * uploaded and if the upload is complete.\n */\n public async getUpload(id: string): Promise<Upload> {\n let metadata: TusUploadMetadata\n\n try {\n metadata = await this.metadataManager.getMetadata({ id })\n } catch (error) {\n if (\n error instanceof NoSuchKey ||\n error instanceof NotFound ||\n (error as AWSError)?.Code === 'NotFound' ||\n (error as AWSError)?.Code === 'NoSuchKey'\n ) {\n throw ERRORS.FILE_NOT_FOUND\n }\n throw error\n }\n\n let offset: number\n\n try {\n const parts = await this.partsManager.retrieveParts({ id })\n offset = this.fileOperations.calculateOffsetFromParts({ parts })\n } catch (error) {\n // Check if the error is caused by the upload not being found. This happens\n // when the multipart upload has already been completed or aborted.\n if (\n (error as AWSError)?.Code === 'NoSuchUpload' ||\n (error as AWSError)?.Code === 'NoSuchKey'\n ) {\n return new Upload({\n ...metadata.file,\n metadata: metadata.file.metadata,\n offset: metadata.file.size as number,\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n }\n\n log('getUpload: Error retrieving parts.', error)\n throw error\n }\n\n const incompletePartSize = await this.partsManager.getIncompletePartSize({\n id,\n })\n\n return new Upload({\n ...metadata.file,\n offset: offset + (incompletePartSize ?? 0),\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n }\n\n /**\n * Reads the file specified by the upload's `id` and returns a readable stream\n */\n async read(id: string): Promise<Readable> {\n log(`[${id}] attempting to read file from S3`)\n let retries = 3\n let lastError: Error | null = null\n\n while (retries > 0) {\n try {\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: id,\n })\n log(`[${id}] successfully read file from S3`)\n return data.Body as Readable\n } catch (error) {\n log(`[${id}] failed to read file, retries left: ${retries - 1}`, error)\n lastError = error as Error\n retries--\n\n if (retries > 0) {\n // Wait a bit before retrying, in case S3 needs time for consistency\n await new Promise((resolve) => setTimeout(resolve, 100))\n }\n }\n }\n\n log(`[${id}] failed to read file after all retries`)\n throw lastError || new Error(`Failed to read file ${id} after retries`)\n }\n\n /**\n * Removes the file specified by the upload's `id`\n */\n public async remove(id: string): Promise<void> {\n try {\n const { 'upload-id': uploadId } = await this.metadataManager.getMetadata({\n id,\n })\n if (uploadId) {\n await this.client.abortMultipartUpload({\n Bucket: this.bucket,\n Key: id,\n UploadId: uploadId,\n })\n }\n } catch (error) {\n if (\n (error as AWSError)?.code &&\n ['NoSuchKey', 'NoSuchUpload', 'NotFound'].includes(\n (error as AWSError).Code || ''\n )\n ) {\n log('remove: No file found.', error)\n throw ERRORS.FILE_NOT_FOUND\n }\n throw error\n }\n\n await this.client.deleteObjects({\n Bucket: this.bucket,\n Delete: {\n Objects: [\n { Key: id },\n { Key: this.metadataManager.generateInfoKey({ id }) },\n {\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n }),\n },\n ],\n },\n })\n\n await this.metadataManager.clearCache({ id })\n }\n\n /**\n * Combine all multipart uploads into a single object\n */\n public async completeMultipartUpload(id: string): Promise<Upload> {\n const metadata = await this.metadataManager.getMetadata({ id })\n const parts = await this.partsManager.retrieveParts({ id })\n\n const incompletePartInfo = await this.partsManager.downloadIncompletePart({\n id,\n })\n\n if (incompletePartInfo) {\n // Upload the incomplete part as a regular part\n await this.partsManager.uploadPart({\n metadata,\n readStream: incompletePartInfo.createReader({ cleanUpOnEnd: true }),\n partNumber: parts.length + 1,\n })\n\n // Remove the incomplete part\n await this.partsManager.deleteIncompletePart({ id })\n\n // Re-fetch parts to include the newly uploaded part\n const updatedParts = await this.partsManager.retrieveParts({ id })\n await this.partsManager.finishMultipartUpload({\n metadata,\n parts: updatedParts,\n })\n } else {\n await this.partsManager.finishMultipartUpload({ metadata, parts })\n }\n\n const completedUpload = new Upload({\n ...metadata.file,\n offset: metadata.file.size ?? 0,\n size: metadata.file.size ?? 0,\n storage: metadata.file.storage,\n })\n\n await this.metadataManager.completeMetadata({ upload: completedUpload })\n\n return completedUpload\n }\n\n /**\n * Get the full S3 URL for an uploaded file\n */\n public getUrl(id: string): string {\n // Use the custom endpoint if available\n if (this.customEndpoint) {\n return `${this.customEndpoint}/${this.bucket}/${id}`\n }\n\n // Fallback to standard AWS S3 URL format\n const regionConfig = this.client.config.region\n let region: string\n\n // If region is a function, we can't resolve it synchronously, so use a fallback\n if (typeof regionConfig === 'function') {\n region = 'us-east-1' // fallback for sync calls\n } else {\n region = regionConfig || 'us-east-1'\n }\n\n // Standard AWS S3 URL format\n if (region === 'us-east-1') {\n return `https://${this.bucket}.s3.amazonaws.com/${id}`\n } else {\n return `https://${this.bucket}.s3.${region}.amazonaws.com/${id}`\n }\n }\n\n /**\n * Deletes expired incomplete uploads.\n * Returns the number of deleted uploads.\n */\n async deleteExpired(): Promise<number> {\n return this.expirationManager.deleteExpired({})\n }\n\n /**\n * Returns the expiration period in milliseconds\n */\n getExpiration(): number {\n return this.expirationManager.getExpiration()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"s3-store.js","names":["options: S3StoreConfig","id: string","isIncompletePart: boolean","value: 'false' | 'true'","upload: Upload","request: AWS.CreateMultipartUploadCommandInput","file_id: string","upload_length: number","readable: stream.Readable","offset: number","metadata: TusUploadMetadata","lastError: Error | null","region: string"],"sources":["../../../../src/tus/stores/s3/s3-store.ts"],"sourcesContent":["import { NoSuchKey, NotFound, S3 } from '@aws-sdk/client-s3'\nimport {\n DataStore,\n ERRORS,\n MemoryKvStore,\n TUS_RESUMABLE,\n Upload,\n type KvStore,\n} from '@tus/utils'\nimport stream, { type Readable } from 'node:stream'\n\nimport { useErrorHandler } from '../../../hooks/useErrorHandler'\nimport { Semaphore } from './semaphore'\nimport { S3ExpirationManager } from './expiration-manager'\nimport { S3FileOperations } from './file-operations'\nimport { S3MetadataManager } from './metadata-manager'\nimport { S3PartsManager } from './parts-manager'\n\nimport type AWS from '@aws-sdk/client-s3'\nimport type { AWSError, S3StoreConfig, TusUploadMetadata } from '../../../types'\nimport { MediaCloudError } from '../../../types/errors'\n\nconst { logConsole } = useErrorHandler()\n\nexport class S3Store extends DataStore {\n public client: S3\n public bucket: string\n public partSize = 8 * 1024 * 1024 // 8MB preferred part size\n public minPartSize = 5 * 1024 * 1024 // 5MB minimum part size\n public maxMultipartParts = 10_000\n public maxUploadSize = 5_497_558_138_880 as const // 5TiB\n public useTags = false\n public expirationPeriodInMilliseconds = 0\n protected acl?: string\n\n protected cache: KvStore<TusUploadMetadata>\n protected partUploadSemaphore: Semaphore\n protected metadataManager: S3MetadataManager\n protected fileOperations: S3FileOperations\n protected partsManager: S3PartsManager\n protected expirationManager: S3ExpirationManager\n protected customEndpoint: string\n\n constructor(options: S3StoreConfig) {\n super()\n const {\n maxMultipartParts,\n minPartSize,\n partSize,\n s3ClientConfig,\n maxConcurrentPartUploads,\n useTags,\n expirationPeriodInMilliseconds,\n cache,\n } = options\n const { acl, bucket, ...restS3ClientConfig } = s3ClientConfig\n\n this.extensions = [\n 'creation',\n 'creation-with-upload',\n 'creation-defer-length',\n 'termination',\n 'expiration',\n ]\n\n this.bucket = bucket\n this.acl = acl\n this.client = new S3(restS3ClientConfig)\n this.customEndpoint = String(restS3ClientConfig.endpoint)\n\n this.partSize = partSize ?? this.partSize\n this.minPartSize = minPartSize ?? this.minPartSize\n this.maxMultipartParts = maxMultipartParts ?? this.maxMultipartParts\n\n this.useTags = useTags ?? this.useTags\n this.expirationPeriodInMilliseconds =\n expirationPeriodInMilliseconds ?? this.expirationPeriodInMilliseconds\n this.cache = cache ?? new MemoryKvStore<TusUploadMetadata>()\n this.partUploadSemaphore = new Semaphore(maxConcurrentPartUploads ?? 60)\n\n // Initialize component managers\n this.metadataManager = new S3MetadataManager(\n this.client,\n this.bucket,\n this.cache,\n this.shouldUseExpirationTags.bind(this),\n this.generateCompleteTag.bind(this)\n )\n\n this.fileOperations = new S3FileOperations(\n this.maxMultipartParts,\n this.maxUploadSize,\n this.minPartSize,\n this.partSize\n )\n\n this.partsManager = new S3PartsManager(\n this.client,\n this.bucket,\n this.minPartSize,\n this.partUploadSemaphore,\n this.metadataManager,\n this.fileOperations,\n this.generateCompleteTag.bind(this)\n )\n\n this.expirationManager = new S3ExpirationManager(\n this.client,\n this.bucket,\n this.expirationPeriodInMilliseconds,\n this.generateInfoKey.bind(this),\n this.generatePartKey.bind(this)\n )\n\n // Cleanup expired uploads when the store is initialized\n this.deleteExpired()\n }\n\n /**\n * Generate the key name for the info file\n * @param id - The upload ID\n * @returns The info file key\n */\n protected generateInfoKey(id: string): string {\n return `${id}.info`\n }\n\n /**\n * Generate the key name for a part file\n * @param id - The upload ID\n * @param isIncompletePart - Whether this is an incomplete part (default: false)\n * @returns The part file key\n */\n protected generatePartKey(\n id: string,\n isIncompletePart: boolean = false\n ): string {\n return isIncompletePart ? `${id}.part` : id\n }\n\n /**\n * Helper method to check if expiration tags should be used\n * @returns True if expiration tags should be used\n */\n protected shouldUseExpirationTags(): boolean {\n return this.expirationPeriodInMilliseconds !== 0 && this.useTags\n }\n\n /**\n * Generates a tag for marking complete/incomplete uploads\n * @param value - Either 'false' or 'true' to mark completion status\n * @returns The tag string or undefined if tags shouldn't be used\n */\n protected generateCompleteTag(value: 'false' | 'true'): string | undefined {\n if (!this.shouldUseExpirationTags()) {\n return undefined\n }\n return `Tus-Completed=${value}`\n }\n\n /**\n * Creates a multipart upload on S3 attaching any metadata to it.\n * Also, a `${file_id}.info` file is created which holds some information\n * about the upload itself like: `upload-id`, `upload-length`, etc.\n * @param upload - The upload object to create\n * @returns Promise that resolves to the created upload\n */\n public async create(upload: Upload): Promise<Upload> {\n logConsole(MediaCloudError.S3_STORE_MULTIPART_INIT)\n const request: AWS.CreateMultipartUploadCommandInput = {\n Bucket: this.bucket,\n Key: upload.id,\n Metadata: { 'tus-version': TUS_RESUMABLE },\n }\n\n if (upload.metadata?.contentType) {\n request.ContentType = upload.metadata.contentType as string\n }\n\n if (upload.metadata?.cacheControl) {\n request.CacheControl = upload.metadata.cacheControl as string\n }\n\n if (this.acl) {\n request.ACL = this.acl as AWS.ObjectCannedACL\n }\n\n upload.creation_date = new Date().toISOString()\n\n const response = await this.client.createMultipartUpload(request)\n upload.storage = {\n type: 's3',\n bucket: this.bucket,\n path: response.Key as string,\n }\n await this.metadataManager.saveMetadata({\n upload,\n uploadId: response.UploadId as string,\n })\n logConsole(MediaCloudError.S3_STORE_MULTIPART_CREATED)\n\n return upload\n }\n\n /**\n * Declares the length of the upload\n * @param file_id - The file ID\n * @param upload_length - The length of the upload\n * @returns Promise that resolves when length is declared\n */\n public async declareUploadLength(\n file_id: string,\n upload_length: number\n ): Promise<void> {\n const { file, 'upload-id': uploadId } =\n await this.metadataManager.getMetadata({ id: file_id })\n if (!file) {\n throw ERRORS.FILE_NOT_FOUND\n }\n\n file.size = upload_length\n\n await this.metadataManager.saveMetadata({ upload: file, uploadId })\n }\n\n /**\n * Writes `buffer` to the file specified by the upload's `id` at `offset`\n * @param readable - The readable stream to write\n * @param id - The upload ID\n * @param offset - The byte offset to write at\n * @returns Promise that resolves to the number of bytes written\n */\n public async write(\n readable: stream.Readable,\n id: string,\n offset: number\n ): Promise<number> {\n const metadata = await this.metadataManager.getMetadata({ id })\n\n // TUS sends PATCH requests with an `upload-offset` header.\n // Offset the write by the offset in the PATCH request.\n const calculatedOffset = this.fileOperations.calculateOffsetFromParts({\n parts: await this.partsManager.retrieveParts({ id }),\n })\n const offsetDiff = offset - calculatedOffset\n const requestedOffset = offset\n\n let finalReadable = readable\n\n if (offsetDiff < 0) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n // If the offset given in the PATCH request is higher than\n // the expected offset, we need to prepend an incomplete\n // part to the readable stream, if one exists.\n if (offsetDiff > 0) {\n const incompletePart = await this.partsManager.downloadIncompletePart({\n id,\n })\n\n if (!incompletePart) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n if (incompletePart.size !== offsetDiff) {\n throw ERRORS.FILE_WRITE_ERROR\n }\n\n // Clear the incomplete part from S3 since it's going to be combined with the current request\n await this.partsManager.deleteIncompletePart({ id })\n\n // Adjust offset to account for the incomplete part\n offset = requestedOffset - incompletePart.size\n\n finalReadable = stream.Readable.from(\n (async function* () {\n yield* incompletePart.createReader({ cleanUpOnEnd: true })\n yield* readable\n })()\n )\n }\n\n const partNumber = this.fileOperations.calculatePartNumber({\n parts: await this.partsManager.retrieveParts({ id }),\n })\n\n const bytesUploaded = await this.partsManager.uploadParts({\n metadata,\n readStream: finalReadable,\n currentPartNumber: partNumber,\n offset,\n })\n\n // The size of the incomplete part should not be counted, because the\n // process of the incomplete part should be fully transparent to the user.\n const newOffset =\n requestedOffset + bytesUploaded - (offsetDiff > 0 ? offsetDiff : 0)\n\n // Check if the upload is complete\n if (metadata.file.size === newOffset) {\n try {\n const parts = await this.partsManager.retrieveParts({ id })\n await this.partsManager.finishMultipartUpload({ metadata, parts })\n\n // Update the metadata with completed state\n const completedUpload = new Upload({\n ...metadata.file,\n offset: newOffset,\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n\n await this.metadataManager.completeMetadata({ upload: completedUpload })\n // Don't clear cache immediately - Payload might still need the metadata\n // await this.metadataManager.clearCache(id)\n } catch (error) {\n logConsole(MediaCloudError.S3_STORE_UPLOAD_FAILED)\n throw error\n }\n }\n\n return newOffset\n }\n\n /**\n * Returns the current state of the upload, i.e how much data has been\n * uploaded and if the upload is complete.\n */\n public async getUpload(id: string): Promise<Upload> {\n let metadata: TusUploadMetadata\n\n try {\n metadata = await this.metadataManager.getMetadata({ id })\n } catch (error) {\n if (\n error instanceof NoSuchKey ||\n error instanceof NotFound ||\n (error as AWSError)?.Code === 'NotFound' ||\n (error as AWSError)?.Code === 'NoSuchKey'\n ) {\n throw ERRORS.FILE_NOT_FOUND\n }\n throw error\n }\n\n let offset: number\n\n try {\n const parts = await this.partsManager.retrieveParts({ id })\n offset = this.fileOperations.calculateOffsetFromParts({ parts })\n } catch (error) {\n // Check if the error is caused by the upload not being found. This happens\n // when the multipart upload has already been completed or aborted.\n if (\n (error as AWSError)?.Code === 'NoSuchUpload' ||\n (error as AWSError)?.Code === 'NoSuchKey'\n ) {\n return new Upload({\n ...metadata.file,\n metadata: metadata.file.metadata,\n offset: metadata.file.size as number,\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n }\n\n logConsole(MediaCloudError.S3_STORE_RETRIEVE_PARTS_ERROR)\n throw error\n }\n\n const incompletePartSize = await this.partsManager.getIncompletePartSize({\n id,\n })\n\n return new Upload({\n ...metadata.file,\n offset: offset + (incompletePartSize ?? 0),\n size: metadata.file.size,\n storage: metadata.file.storage,\n })\n }\n\n /**\n * Reads the file specified by the upload's `id` and returns a readable stream\n */\n async read(id: string): Promise<Readable> {\n logConsole(MediaCloudError.S3_STORE_READ_ATTEMPT)\n let retries = 3\n let lastError: Error | null = null\n\n while (retries > 0) {\n try {\n const data = await this.client.getObject({\n Bucket: this.bucket,\n Key: id,\n })\n logConsole(MediaCloudError.S3_STORE_READ_SUCCESS)\n return data.Body as Readable\n } catch (error) {\n logConsole(MediaCloudError.S3_STORE_READ_RETRY)\n lastError = error as Error\n retries--\n\n if (retries > 0) {\n // Wait a bit before retrying, in case S3 needs time for consistency\n await new Promise((resolve) => setTimeout(resolve, 100))\n }\n }\n }\n\n logConsole(MediaCloudError.S3_STORE_READ_FAILED)\n throw lastError || new Error(`Failed to read file ${id} after retries`)\n }\n\n /**\n * Removes the file specified by the upload's `id`\n */\n public async remove(id: string): Promise<void> {\n try {\n const { 'upload-id': uploadId } = await this.metadataManager.getMetadata({\n id,\n })\n if (uploadId) {\n await this.client.abortMultipartUpload({\n Bucket: this.bucket,\n Key: id,\n UploadId: uploadId,\n })\n }\n } catch (error) {\n if (\n (error as AWSError)?.code &&\n ['NoSuchKey', 'NoSuchUpload', 'NotFound'].includes(\n (error as AWSError).Code || ''\n )\n ) {\n logConsole(MediaCloudError.S3_STORE_FILE_NOT_FOUND)\n throw ERRORS.FILE_NOT_FOUND\n }\n throw error\n }\n\n await this.client.deleteObjects({\n Bucket: this.bucket,\n Delete: {\n Objects: [\n { Key: id },\n { Key: this.metadataManager.generateInfoKey({ id }) },\n {\n Key: this.metadataManager.generatePartKey({\n id,\n isIncomplete: true,\n }),\n },\n ],\n },\n })\n\n await this.metadataManager.clearCache({ id })\n }\n\n /**\n * Combine all multipart uploads into a single object\n */\n public async completeMultipartUpload(id: string): Promise<Upload> {\n const metadata = await this.metadataManager.getMetadata({ id })\n const parts = await this.partsManager.retrieveParts({ id })\n\n const incompletePartInfo = await this.partsManager.downloadIncompletePart({\n id,\n })\n\n if (incompletePartInfo) {\n // Upload the incomplete part as a regular part\n await this.partsManager.uploadPart({\n metadata,\n readStream: incompletePartInfo.createReader({ cleanUpOnEnd: true }),\n partNumber: parts.length + 1,\n })\n\n // Remove the incomplete part\n await this.partsManager.deleteIncompletePart({ id })\n\n // Re-fetch parts to include the newly uploaded part\n const updatedParts = await this.partsManager.retrieveParts({ id })\n await this.partsManager.finishMultipartUpload({\n metadata,\n parts: updatedParts,\n })\n } else {\n await this.partsManager.finishMultipartUpload({ metadata, parts })\n }\n\n const completedUpload = new Upload({\n ...metadata.file,\n offset: metadata.file.size ?? 0,\n size: metadata.file.size ?? 0,\n storage: metadata.file.storage,\n })\n\n await this.metadataManager.completeMetadata({ upload: completedUpload })\n\n return completedUpload\n }\n\n /**\n * Get the full S3 URL for an uploaded file\n */\n public getUrl(id: string): string {\n // Use the custom endpoint if available\n if (this.customEndpoint) {\n return `${this.customEndpoint}/${this.bucket}/${id}`\n }\n\n // Fallback to standard AWS S3 URL format\n const regionConfig = this.client.config.region\n let region: string\n\n // If region is a function, we can't resolve it synchronously, so use a fallback\n if (typeof regionConfig === 'function') {\n region = 'us-east-1' // fallback for sync calls\n } else {\n region = regionConfig || 'us-east-1'\n }\n\n // Standard AWS S3 URL format\n if (region === 'us-east-1') {\n return `https://${this.bucket}.s3.amazonaws.com/${id}`\n } else {\n return `https://${this.bucket}.s3.${region}.amazonaws.com/${id}`\n }\n }\n\n /**\n * Deletes expired incomplete uploads.\n * Returns the number of deleted uploads.\n */\n async deleteExpired(): Promise<number> {\n return this.expirationManager.deleteExpired()\n }\n\n /**\n * Returns the expiration period in milliseconds\n */\n getExpiration(): number {\n return this.expirationManager.getExpiration()\n }\n}\n"],"mappings":"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"}
@@ -20,6 +20,20 @@ declare enum MediaCloudError {
20
20
  UPLOAD_POLLING_ERROR = "Polling error for upload",
21
21
  PLUGIN_NOT_CONFIGURED = "Payload Media Cloud plugin is not configured",
22
22
  NAMING_FUNCTION_ERROR = "Error in namingFunction",
23
+ S3_STORE_MULTIPART_INIT = "Initializing multipart upload",
24
+ S3_STORE_MULTIPART_CREATED = "Multipart upload created",
25
+ S3_STORE_UPLOAD_FAILED = "Failed to finish upload",
26
+ S3_STORE_READ_ATTEMPT = "Attempting to read file from S3",
27
+ S3_STORE_READ_SUCCESS = "Successfully read file from S3",
28
+ S3_STORE_READ_RETRY = "Failed to read file, retries left",
29
+ S3_STORE_READ_FAILED = "Failed to read file after all retries",
30
+ S3_STORE_FILE_NOT_FOUND = "No file found",
31
+ S3_STORE_RETRIEVE_PARTS_ERROR = "Error retrieving parts",
32
+ S3_STORE_METADATA_SAVING = "Saving metadata",
33
+ S3_STORE_METADATA_SAVED = "Metadata file saved",
34
+ S3_STORE_METADATA_CACHE_CLEARED = "Removing cached data",
35
+ S3_STORE_INCOMPLETE_PART_UPLOADED = "Finished uploading incomplete part",
36
+ S3_STORE_CHUNK_REMOVAL_FAILED = "Failed to remove chunk",
23
37
  }
24
38
  //#endregion
25
39
  export { MediaCloudError };
@@ -20,6 +20,20 @@ let MediaCloudError = /* @__PURE__ */ function(MediaCloudError$1) {
20
20
  MediaCloudError$1["UPLOAD_POLLING_ERROR"] = "Polling error for upload";
21
21
  MediaCloudError$1["PLUGIN_NOT_CONFIGURED"] = "Payload Media Cloud plugin is not configured";
22
22
  MediaCloudError$1["NAMING_FUNCTION_ERROR"] = "Error in namingFunction";
23
+ MediaCloudError$1["S3_STORE_MULTIPART_INIT"] = "Initializing multipart upload";
24
+ MediaCloudError$1["S3_STORE_MULTIPART_CREATED"] = "Multipart upload created";
25
+ MediaCloudError$1["S3_STORE_UPLOAD_FAILED"] = "Failed to finish upload";
26
+ MediaCloudError$1["S3_STORE_READ_ATTEMPT"] = "Attempting to read file from S3";
27
+ MediaCloudError$1["S3_STORE_READ_SUCCESS"] = "Successfully read file from S3";
28
+ MediaCloudError$1["S3_STORE_READ_RETRY"] = "Failed to read file, retries left";
29
+ MediaCloudError$1["S3_STORE_READ_FAILED"] = "Failed to read file after all retries";
30
+ MediaCloudError$1["S3_STORE_FILE_NOT_FOUND"] = "No file found";
31
+ MediaCloudError$1["S3_STORE_RETRIEVE_PARTS_ERROR"] = "Error retrieving parts";
32
+ MediaCloudError$1["S3_STORE_METADATA_SAVING"] = "Saving metadata";
33
+ MediaCloudError$1["S3_STORE_METADATA_SAVED"] = "Metadata file saved";
34
+ MediaCloudError$1["S3_STORE_METADATA_CACHE_CLEARED"] = "Removing cached data";
35
+ MediaCloudError$1["S3_STORE_INCOMPLETE_PART_UPLOADED"] = "Finished uploading incomplete part";
36
+ MediaCloudError$1["S3_STORE_CHUNK_REMOVAL_FAILED"] = "Failed to remove chunk";
23
37
  return MediaCloudError$1;
24
38
  }({});
25
39
 
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/types/errors.ts"],"sourcesContent":["export enum MediaCloudError {\n // Mux Errors\n MUX_CONFIG_MISSING = 'Mux configuration (tokenId and tokenSecret) must be provided in pluginOptions to use Mux',\n MUX_CONFIG_INCOMPLETE = 'Mux configuration is missing. Mux features will not be available',\n MUX_UPLOAD_ID_MISSING = 'No upload-id found for upload',\n MUX_ASSET_DELETE_ERROR = 'Error deleting Mux asset',\n MUX_UPLOAD_ERROR = 'Mux video upload failed',\n MUX_DIRECT_UPLOAD_ERROR = 'Mux direct upload failed',\n MUX_CREATE_UPLOAD_ERROR = 'Error in Mux create upload handler',\n MUX_REQUEST_NO_JSON = 'Request does not support json() method',\n\n // S3 Errors\n S3_CONFIG_MISSING = 'S3 configuration (bucket, region, accessKeyId, secretAccessKey) must be provided in pluginOptions',\n S3_DELETE_ERROR = 'Error deleting file from S3',\n S3_UNIQUE_NAME_ERROR = 'Could not find a unique file name after maximum tries',\n\n // TUS Errors\n TUS_UPLOAD_ERROR = 'TUS file upload error occurred',\n\n // File Errors\n FILE_TYPE_UNKNOWN = 'Unable to determine file type',\n FILE_TYPE_ERROR = 'Error determining file type',\n FILENAME_SANITIZE_ERROR = 'Error sanitizing filename',\n\n // Upload Errors\n UPLOAD_NO_URL = 'No upload URL provided, cannot parse upload ID',\n UPLOAD_HANDLER_ERROR = 'Upload handler error occurred',\n UPLOAD_POLLING_ERROR = 'Polling error for upload',\n\n // Plugin Errors\n PLUGIN_NOT_CONFIGURED = 'Payload Media Cloud plugin is not configured',\n NAMING_FUNCTION_ERROR = 'Error in namingFunction',\n}\n"],"mappings":";AAAA,IAAY,8DAAL;;;;;;;;;;;;;;;;;;;;;;AAgCN"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/types/errors.ts"],"sourcesContent":["export enum MediaCloudError {\n // Mux Errors\n MUX_CONFIG_MISSING = 'Mux configuration (tokenId and tokenSecret) must be provided in pluginOptions to use Mux',\n MUX_CONFIG_INCOMPLETE = 'Mux configuration is missing. Mux features will not be available',\n MUX_UPLOAD_ID_MISSING = 'No upload-id found for upload',\n MUX_ASSET_DELETE_ERROR = 'Error deleting Mux asset',\n MUX_UPLOAD_ERROR = 'Mux video upload failed',\n MUX_DIRECT_UPLOAD_ERROR = 'Mux direct upload failed',\n MUX_CREATE_UPLOAD_ERROR = 'Error in Mux create upload handler',\n MUX_REQUEST_NO_JSON = 'Request does not support json() method',\n\n // S3 Errors\n S3_CONFIG_MISSING = 'S3 configuration (bucket, region, accessKeyId, secretAccessKey) must be provided in pluginOptions',\n S3_DELETE_ERROR = 'Error deleting file from S3',\n S3_UNIQUE_NAME_ERROR = 'Could not find a unique file name after maximum tries',\n\n // TUS Errors\n TUS_UPLOAD_ERROR = 'TUS file upload error occurred',\n\n // File Errors\n FILE_TYPE_UNKNOWN = 'Unable to determine file type',\n FILE_TYPE_ERROR = 'Error determining file type',\n FILENAME_SANITIZE_ERROR = 'Error sanitizing filename',\n\n // Upload Errors\n UPLOAD_NO_URL = 'No upload URL provided, cannot parse upload ID',\n UPLOAD_HANDLER_ERROR = 'Upload handler error occurred',\n UPLOAD_POLLING_ERROR = 'Polling error for upload',\n\n // Plugin Errors\n PLUGIN_NOT_CONFIGURED = 'Payload Media Cloud plugin is not configured',\n NAMING_FUNCTION_ERROR = 'Error in namingFunction',\n\n // S3 Store Logging\n S3_STORE_MULTIPART_INIT = 'Initializing multipart upload',\n S3_STORE_MULTIPART_CREATED = 'Multipart upload created',\n S3_STORE_UPLOAD_FAILED = 'Failed to finish upload',\n S3_STORE_READ_ATTEMPT = 'Attempting to read file from S3',\n S3_STORE_READ_SUCCESS = 'Successfully read file from S3',\n S3_STORE_READ_RETRY = 'Failed to read file, retries left',\n S3_STORE_READ_FAILED = 'Failed to read file after all retries',\n S3_STORE_FILE_NOT_FOUND = 'No file found',\n S3_STORE_RETRIEVE_PARTS_ERROR = 'Error retrieving parts',\n S3_STORE_METADATA_SAVING = 'Saving metadata',\n S3_STORE_METADATA_SAVED = 'Metadata file saved',\n S3_STORE_METADATA_CACHE_CLEARED = 'Removing cached data',\n S3_STORE_INCOMPLETE_PART_UPLOADED = 'Finished uploading incomplete part',\n S3_STORE_CHUNK_REMOVAL_FAILED = 'Failed to remove chunk',\n}\n"],"mappings":";AAAA,IAAY,8DAAL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maas/payload-plugin-media-cloud",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "contributors": [
6
6
  {
@@ -58,6 +58,9 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
+ "@payloadcms/plugin-cloud-storage": "3.49.1",
62
+ "@payloadcms/ui": "3.49.1",
63
+ "payload": "3.49.1",
61
64
  "@types/multistream": "^4.1.3",
62
65
  "@types/node": "^22.17.0",
63
66
  "@types/react": "^19.1.9",
@@ -72,6 +75,6 @@
72
75
  "dev": "tsdown --watch",
73
76
  "lint": "eslint .",
74
77
  "format": "prettier --write .",
75
- "release": "release-it && tsx ../../scripts/publish.ts"
78
+ "release": "release-it && pnpx tsx ../../scripts/publish.ts"
76
79
  }
77
80
  }
@@ -1,204 +0,0 @@
1
- @keyframes pulse {
2
- 0% {
3
- opacity: 0.6;
4
- }
5
- 50% {
6
- opacity: 1;
7
- }
8
- 100% {
9
- opacity: 0.6;
10
- }
11
- }
12
-
13
- @keyframes shimmer {
14
- 0% {
15
- background-position: -200% 0;
16
- }
17
- 100% {
18
- background-position: 200% 0;
19
- }
20
- }
21
-
22
- .upload-manager {
23
- position: fixed;
24
- overflow: hidden;
25
- bottom: 0;
26
- left: 50%;
27
- transform: translateX(-50%);
28
- width: 400px;
29
- max-height: 500px;
30
- margin: 2.25rem;
31
- background: var(--theme-bg);
32
- border: 1px solid var(--theme-elevation-100);
33
- border-radius: var(--style-radius-m);
34
- box-shadow: var(--box-shadow-lg);
35
- z-index: 1024;
36
- display: flex;
37
- flex-direction: column;
38
- }
39
-
40
- .upload-manager__header {
41
- display: flex;
42
- align-items: center;
43
- justify-content: space-between;
44
- padding: 0.75rem 1rem;
45
- border-bottom: 1px solid var(--theme-elevation-100);
46
- background: var(--theme-elevation-50);
47
-
48
- & h4 {
49
- margin: 0;
50
- font-size: 0.875rem;
51
- font-weight: 600;
52
- color: var(--theme-text);
53
- }
54
- }
55
-
56
- .upload-manager__tabs {
57
- display: flex;
58
- border-bottom: 1px solid var(--theme-elevation-100);
59
- background: var(--theme-elevation-25);
60
-
61
- & .upload-tab {
62
- flex: 1;
63
- padding: 0.5rem 0.75rem;
64
- background: none;
65
- border: none;
66
- color: var(--theme-text-dim);
67
- font-size: 0.75rem;
68
- font-weight: 500;
69
- cursor: pointer;
70
- transition: all 0.15s ease;
71
- border-bottom: 2px solid transparent;
72
-
73
- &:hover {
74
- background: var(--theme-elevation-50);
75
- color: var(--theme-text);
76
- }
77
-
78
- &[data-active='true'] {
79
- color: var(--theme-success-500);
80
- border-bottom-color: var(--theme-success-500);
81
- background: var(--theme-bg);
82
- }
83
- }
84
- }
85
-
86
- .upload-manager__content {
87
- flex: 1;
88
- overflow-y: auto;
89
- max-height: 300px;
90
- padding: 0;
91
-
92
- & ul {
93
- list-style: none;
94
- margin: 0;
95
- padding: 0;
96
-
97
- & li {
98
- padding: 0.75rem 1rem;
99
- border-bottom: 1px solid var(--theme-elevation-50);
100
-
101
- &:last-child {
102
- border-bottom: none;
103
- }
104
-
105
- &[data-status='uploading'] {
106
- background: var(--theme-bg);
107
- }
108
-
109
- &[data-status='processing'] {
110
- background: var(--theme-warning-50);
111
- }
112
-
113
- &[data-status='completed'] {
114
- background: var(--theme-success-50);
115
- }
116
- }
117
- }
118
-
119
- & .upload-empty-state {
120
- padding: 2rem 1rem;
121
- text-align: center;
122
- color: var(--theme-text-dim);
123
- font-size: 0.875rem;
124
- margin: 0;
125
- }
126
- }
127
-
128
- .upload-progress-bar {
129
- height: 3px;
130
- background: var(--theme-elevation-100);
131
- border-radius: 2px;
132
- overflow: hidden;
133
- position: relative;
134
-
135
- & .upload-progress {
136
- height: 100%;
137
- background: var(--theme-success-500);
138
- width: calc(var(--progress) * 100%);
139
- transition: width 0.3s ease;
140
- border-radius: inherit;
141
-
142
- &[data-active='true'] {
143
- background: var(--theme-warning-500);
144
- animation: pulse 1.5s ease-in-out infinite;
145
- width: 100%;
146
- }
147
- }
148
- }
149
-
150
- .upload-info {
151
- display: flex;
152
- align-items: center;
153
- justify-content: space-between;
154
- margin-bottom: 0.5rem;
155
-
156
- & .upload-filename {
157
- font-size: 0.875rem;
158
- font-weight: 500;
159
- color: var(--theme-text);
160
- flex: 1;
161
- margin-right: 0.5rem;
162
- overflow: hidden;
163
- text-overflow: ellipsis;
164
- white-space: nowrap;
165
- }
166
-
167
- & .upload-meta {
168
- font-size: 0.75rem;
169
- color: var(--theme-text-dim);
170
- font-weight: 500;
171
- flex-shrink: 0;
172
- }
173
- }
174
-
175
- .upload-manager__file {
176
- & .upload-manager__file--polling {
177
- animation: pulse 2s ease-in-out infinite;
178
- background: linear-gradient(90deg, #3b82f6, #8b5cf6, #3b82f6);
179
- background-size: 200% 100%;
180
- animation:
181
- pulse 2s ease-in-out infinite,
182
- shimmer 3s ease-in-out infinite;
183
- }
184
- }
185
-
186
- .upload-manager__footer {
187
- padding: 0.75rem 1rem;
188
- border-top: 1px solid var(--theme-elevation-100);
189
- background: var(--theme-elevation-50);
190
- display: flex;
191
- justify-content: center;
192
- }
193
-
194
- // Responsive adjustments
195
- @media (max-width: 768px) {
196
- .upload-manager {
197
- width: calc(100vw - 2rem);
198
- right: 1rem;
199
- left: 1rem;
200
- }
201
- }
202
-
203
-
204
- /*# sourceMappingURL=upload-manager-D9UZkMR8.css.map*/
@@ -1 +0,0 @@
1
- {"version":3,"file":"upload-manager-D9UZkMR8.css","names":[],"sources":["../../../src/components/upload-manager/upload-manager.css"],"sourcesContent":["@keyframes pulse {\n 0% {\n opacity: 0.6;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0.6;\n }\n}\n\n@keyframes shimmer {\n 0% {\n background-position: -200% 0;\n }\n 100% {\n background-position: 200% 0;\n }\n}\n\n.upload-manager {\n position: fixed;\n overflow: hidden;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: 400px;\n max-height: 500px;\n margin: 2.25rem;\n background: var(--theme-bg);\n border: 1px solid var(--theme-elevation-100);\n border-radius: var(--style-radius-m);\n box-shadow: var(--box-shadow-lg);\n z-index: 1024;\n display: flex;\n flex-direction: column;\n}\n\n.upload-manager__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--theme-elevation-100);\n background: var(--theme-elevation-50);\n\n & h4 {\n margin: 0;\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--theme-text);\n }\n}\n\n.upload-manager__tabs {\n display: flex;\n border-bottom: 1px solid var(--theme-elevation-100);\n background: var(--theme-elevation-25);\n\n & .upload-tab {\n flex: 1;\n padding: 0.5rem 0.75rem;\n background: none;\n border: none;\n color: var(--theme-text-dim);\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n border-bottom: 2px solid transparent;\n\n &:hover {\n background: var(--theme-elevation-50);\n color: var(--theme-text);\n }\n\n &[data-active='true'] {\n color: var(--theme-success-500);\n border-bottom-color: var(--theme-success-500);\n background: var(--theme-bg);\n }\n }\n}\n\n.upload-manager__content {\n flex: 1;\n overflow-y: auto;\n max-height: 300px;\n padding: 0;\n\n & ul {\n list-style: none;\n margin: 0;\n padding: 0;\n\n & li {\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--theme-elevation-50);\n\n &:last-child {\n border-bottom: none;\n }\n\n &[data-status='uploading'] {\n background: var(--theme-bg);\n }\n\n &[data-status='processing'] {\n background: var(--theme-warning-50);\n }\n\n &[data-status='completed'] {\n background: var(--theme-success-50);\n }\n }\n }\n\n & .upload-empty-state {\n padding: 2rem 1rem;\n text-align: center;\n color: var(--theme-text-dim);\n font-size: 0.875rem;\n margin: 0;\n }\n}\n\n.upload-progress-bar {\n height: 3px;\n background: var(--theme-elevation-100);\n border-radius: 2px;\n overflow: hidden;\n position: relative;\n\n & .upload-progress {\n height: 100%;\n background: var(--theme-success-500);\n width: calc(var(--progress) * 100%);\n transition: width 0.3s ease;\n border-radius: inherit;\n\n &[data-active='true'] {\n background: var(--theme-warning-500);\n animation: pulse 1.5s ease-in-out infinite;\n width: 100%;\n }\n }\n}\n\n.upload-info {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 0.5rem;\n\n & .upload-filename {\n font-size: 0.875rem;\n font-weight: 500;\n color: var(--theme-text);\n flex: 1;\n margin-right: 0.5rem;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n & .upload-meta {\n font-size: 0.75rem;\n color: var(--theme-text-dim);\n font-weight: 500;\n flex-shrink: 0;\n }\n}\n\n.upload-manager__file {\n & .upload-manager__file--polling {\n animation: pulse 2s ease-in-out infinite;\n background: linear-gradient(90deg, #3b82f6, #8b5cf6, #3b82f6);\n background-size: 200% 100%;\n animation:\n pulse 2s ease-in-out infinite,\n shimmer 3s ease-in-out infinite;\n }\n}\n\n.upload-manager__footer {\n padding: 0.75rem 1rem;\n border-top: 1px solid var(--theme-elevation-100);\n background: var(--theme-elevation-50);\n display: flex;\n justify-content: center;\n}\n\n// Responsive adjustments\n@media (max-width: 768px) {\n .upload-manager {\n width: calc(100vw - 2rem);\n right: 1rem;\n left: 1rem;\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
File without changes
@@ -1,5 +0,0 @@
1
- //#region src/tus/stores/s3/log.d.ts
2
- declare function log(message: string, ...args: unknown[]): void;
3
- //#endregion
4
- export { log };
5
- //# sourceMappingURL=log.d.ts.map
@@ -1,8 +0,0 @@
1
- //#region src/tus/stores/s3/log.ts
2
- function log(message, ...args) {
3
- console.log(`tus:stores:s3store - ${message}`, ...args);
4
- }
5
-
6
- //#endregion
7
- export { log };
8
- //# sourceMappingURL=log.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"log.js","names":["message: string"],"sources":["../../../../src/tus/stores/s3/log.ts"],"sourcesContent":["export function log(message: string, ...args: unknown[]) {\n console.log(`tus:stores:s3store - ${message}`, ...args)\n}\n"],"mappings":";AAAA,SAAgB,IAAIA,SAAiB,GAAG,MAAiB;CACvD,QAAQ,IAAI,CAAC,qBAAqB,EAAE,SAAS,EAAE,GAAG,KAAK;AACxD"}