@vivinkv28/strapi-provider-uploadthing 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,25 @@
1
1
  # @vivinkv28/strapi-provider-uploadthing
2
2
 
3
- UploadThing provider for the Strapi Upload plugin.
3
+ UploadThing provider for the Strapi Uploads
4
4
 
5
- This package lets Strapi store Media Library assets in UploadThing while keeping file metadata inside Strapi. It supports regular uploads, stream uploads, private files, signed URLs, remote cleanup on delete, and safer media replacement flows.
5
+ This provider stores Strapi Media Library files in UploadThing while keeping file records and metadata inside Strapi.
6
6
 
7
- ## What is UploadThing?
7
+ ## What This Provider Does
8
8
 
9
- UploadThing is a file upload and storage platform for modern applications. It helps developers handle file uploads, storage delivery, and secure file access with a developer-friendly API.
9
+ - Uploads Strapi media files to UploadThing
10
+ - Stores UploadThing metadata in `provider_metadata.uploadthing`
11
+ - Uses the UploadThing file URL as the Strapi file URL
12
+ - Supports both `upload` and `uploadStream`
13
+ - Supports signed URLs for private files
14
+ - Deletes the remote file when the Strapi file is deleted
15
+ - Uses predictable `customId` values by default
16
+ - Retries transient upload failures automatically
17
+ - Handles replace-media conflicts more safely
10
18
 
11
- Learn more at [uploadthing.com](https://uploadthing.com/).
12
-
13
- ## Features
19
+ ## Requirements
14
20
 
15
- - Upload Strapi media files to UploadThing
16
- - Store UploadThing file metadata in `provider_metadata`
17
- - Use UploadThing `ufsUrl` as the Strapi asset URL
18
- - Support `upload` and `uploadStream`
19
- - Support private files with signed URL generation
20
- - Delete remote files when media is removed from Strapi
21
- - Keep predictable custom IDs by default
22
- - Retry transient UploadThing ingest failures automatically
23
- - Improve replace-media reliability with conflict fallback handling
21
+ - Node.js `>= 20.0.0`
22
+ - Strapi v5
24
23
 
25
24
  ## Installation
26
25
 
@@ -30,20 +29,22 @@ Install the provider in your Strapi project:
30
29
  npm install @vivinkv28/strapi-provider-uploadthing
31
30
  ```
32
31
 
33
- ## Requirements
32
+ ## Quick Start
34
33
 
35
- - Node.js `>= 20.0.0`
36
- - Strapi v5
34
+ 1. Add your UploadThing token to `.env`.
35
+ 2. Configure the upload provider in `config/plugins.ts`.
36
+ 3. Update `config/middlewares.ts` so Strapi allows UploadThing media URLs in the admin.
37
+ 4. Restart Strapi.
37
38
 
38
39
  ## Environment Variables
39
40
 
40
- Add your UploadThing token to your Strapi `.env` file:
41
+ Minimum required:
41
42
 
42
43
  ```env
43
44
  UPLOADTHING_TOKEN=your_uploadthing_token
44
45
  ```
45
46
 
46
- Example:
47
+ Typical public-file setup:
47
48
 
48
49
  ```env
49
50
  UPLOADTHING_TOKEN=your_uploadthing_token
@@ -57,6 +58,15 @@ UPLOADTHING_USE_CUSTOM_ID=true
57
58
  UPLOADTHING_LOG_LEVEL=Info
58
59
  ```
59
60
 
61
+ Typical private-file setup:
62
+
63
+ ```env
64
+ UPLOADTHING_TOKEN=your_uploadthing_token
65
+ UPLOADTHING_ACL=private
66
+ UPLOADTHING_PRIVATE_FILES=true
67
+ UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
68
+ ```
69
+
60
70
  ## Strapi Configuration
61
71
 
62
72
  Create or update `./config/plugins.ts`:
@@ -87,7 +97,7 @@ export default ({ env }) => ({
87
97
  });
88
98
  ```
89
99
 
90
- Update `./config/middlewares.ts` as well so Strapi's Content Security Policy allows UploadThing-hosted files to load in the admin and media library:
100
+ Update `./config/middlewares.ts` as well. This step is required so Strapi's Content Security Policy allows UploadThing-hosted images and media to load in the admin panel and Media Library:
91
101
 
92
102
  ```ts
93
103
  import type { Core } from '@strapi/strapi';
@@ -121,33 +131,83 @@ const config: Core.Config.Middlewares = [
121
131
  export default config;
122
132
  ```
123
133
 
124
- If you already have a `strapi::security` middleware entry, merge these UploadThing domains into your existing CSP directives instead of duplicating the middleware.
134
+ If you already have a `strapi::security` middleware entry, merge these UploadThing domains into your existing CSP directives instead of adding a second `strapi::security` entry.
135
+
136
+ ## Public vs Private Files
137
+
138
+ This is the part most people get confused by:
139
+
140
+ - `acl` controls how the file is stored in UploadThing.
141
+ - `privateFiles` controls how Strapi serves the file.
142
+
143
+ Use this combination for public files:
144
+
145
+ ```env
146
+ UPLOADTHING_ACL=public-read
147
+ UPLOADTHING_PRIVATE_FILES=false
148
+ ```
149
+
150
+ Use this combination for private files:
151
+
152
+ ```env
153
+ UPLOADTHING_ACL=private
154
+ UPLOADTHING_PRIVATE_FILES=true
155
+ ```
156
+
157
+ If you set only `privateFiles=true`, Strapi will generate signed URLs, but the uploaded file may still be stored with a public ACL depending on your UploadThing configuration.
158
+
159
+ ## How Private Files Work
160
+
161
+ When `privateFiles` is enabled:
162
+
163
+ 1. The provider tells Strapi that files should be treated as private.
164
+ 2. Strapi asks the provider for a signed URL whenever it needs to serve the file.
165
+ 3. The provider requests a temporary signed URL from UploadThing using the stored `customId` or `fileKey`.
166
+ 4. Strapi returns that temporary URL to the client.
167
+
168
+ The signed URL lifetime is controlled by `signedUrlExpiresIn`.
125
169
 
126
170
  ## Provider Options
127
171
 
128
172
  | Option | Type | Default | Description |
129
173
  | --- | --- | --- | --- |
130
174
  | `token` | `string` | `process.env.UPLOADTHING_TOKEN` | UploadThing token used to initialize `UTApi`. |
131
- | `acl` | `string` | `undefined` | ACL passed to UploadThing during upload. |
132
- | `privateFiles` | `boolean` | `false` | Marks files as private and enables signed URL resolution. |
133
- | `contentDisposition` | `string` | `'inline'` | Content disposition used during upload. |
134
- | `signedUrlExpiresIn` | `number` | `3600` | Signed URL expiration time in seconds. |
135
- | `uploadConcurrency` | `number` | `1` | Maximum concurrent uploads handled by the provider. Values above `25` are capped. |
175
+ | `acl` | `'public-read' \| 'private'` | `undefined` | ACL passed to UploadThing during upload. Use `'public-read'` for public files or `'private'` for storage-level private files. |
176
+ | `privateFiles` | `boolean` | `false` | Tells Strapi to treat files as private and request signed URLs when serving them. |
177
+ | `contentDisposition` | `'inline' \| 'attachment'` | `'inline'` | Content disposition sent to UploadThing during upload. |
178
+ | `signedUrlExpiresIn` | `number` | `3600` | Signed URL lifetime in seconds. Used when Strapi requests a private file URL. |
179
+ | `uploadConcurrency` | `number` | `1` | Maximum number of concurrent uploads handled by the provider. Values above `25` are capped to `25`. |
136
180
  | `uploadRetries` | `number` | `2` | Number of retry attempts for transient UploadThing upload failures. |
137
- | `useCustomId` | `boolean` | `true` | Uses a deterministic UploadThing `customId` based on Strapi file hash and extension. |
181
+ | `useCustomId` | `boolean` | `true` | Uses a deterministic UploadThing `customId` based on the Strapi file hash and extension. |
138
182
  | `apiUrl` | `string` | `undefined` | Optional custom UploadThing API URL. |
139
183
  | `ingestUrl` | `string` | `undefined` | Optional custom UploadThing ingest URL. |
140
184
  | `logLevel` | `string` | `undefined` | Optional UploadThing log level. |
141
185
  | `logFormat` | `string` | `undefined` | Optional UploadThing log format. |
142
186
  | `isDev` | `boolean` | `undefined` | Optional UploadThing development mode flag. |
143
187
 
144
- ## Private Files
188
+ ## Stored Metadata
145
189
 
146
- If `privateFiles` is enabled, the provider reports files as private and asks UploadThing for a signed URL when Strapi serves them.
190
+ After upload, this provider stores UploadThing-specific metadata in:
147
191
 
148
- Example:
149
-
150
- ```env
151
- UPLOADTHING_PRIVATE_FILES=true
152
- UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
192
+ ```txt
193
+ provider_metadata.uploadthing
153
194
  ```
195
+
196
+ That metadata includes values such as:
197
+
198
+ - `fileKey`
199
+ - `customId`
200
+ - `url`
201
+ - `ufsUrl`
202
+ - `name`
203
+ - `size`
204
+
205
+ ## Notes
206
+
207
+ - The provider uses UploadThing `ufsUrl` as the file URL stored in Strapi.
208
+ - If `useCustomId` is enabled, the provider prefers `customId` when generating signed URLs or deleting files.
209
+ - If a deterministic `customId` conflicts during replace-media flows, the provider falls back to a unique ID and retries the upload.
210
+
211
+ ## Learn More
212
+
213
+ - [UploadThing](https://uploadthing.com/)
@@ -276,4 +276,3 @@ const index = {
276
276
  services
277
277
  };
278
278
  exports.default = index;
279
- //# sourceMappingURL=index.js.map
@@ -274,4 +274,3 @@ const index = {
274
274
  export {
275
275
  index as default
276
276
  };
277
- //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@vivinkv28/strapi-provider-uploadthing",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "UploadThing provider for the Strapi Upload plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/vivinkv28/strapi-provider-uploadthing.git"
8
+ },
9
+ "homepage": "https://github.com/vivinkv28/strapi-provider-uploadthing#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/vivinkv28/strapi-provider-uploadthing/issues"
12
+ },
5
13
  "main": "index.js",
6
14
  "exports": {
7
15
  ".": "./index.js",
@@ -18,6 +26,7 @@
18
26
  "index.js",
19
27
  "server",
20
28
  "strapi-server.js",
29
+ "LICENSE",
21
30
  "README.md"
22
31
  ],
23
32
  "keywords": [
@@ -45,7 +54,8 @@
45
54
  "build": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js build",
46
55
  "watch": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js watch",
47
56
  "watch:link": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js watch:link",
48
- "verify": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js verify"
57
+ "verify": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js verify",
58
+ "prepack": "npm run build"
49
59
  },
50
60
  "dependencies": {
51
61
  "uploadthing": "^7.7.2"
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sources":["../../server/src/bootstrap.js","../../server/src/config/index.js","../../server/src/controllers/index.js","../../server/src/content-types/index.js","../../server/src/destroy.js","../../server/src/middlewares/index.js","../../server/src/policies/index.js","../../server/src/register.js","../../server/src/routes/index.js","../../server/src/services/provider.js","../../server/src/services/index.js","../../server/src/index.js"],"sourcesContent":["export default ({ strapi }) => {\n strapi.log.info('[strapi-upload-things] plugin bootstrapped');\n};\n","export default {\n default: {\n enabled: true,\n },\n validator() {},\n};\n","export default {};\n","export default {};\n","export default () => {};\n","export default {};\n","export default {};\n","export default () => {};\n","export default [];\n","import crypto from 'crypto';\nimport { UTApi, UTFile } from 'uploadthing/server';\n\nconst DEFAULT_CONTENT_DISPOSITION = 'inline';\nconst DEFAULT_SIGNED_URL_TTL = 60 * 60;\nconst DEFAULT_UPLOAD_CONCURRENCY = 1;\nconst DEFAULT_UPLOAD_RETRIES = 2;\n\nconst toPositiveInteger = (value, fallback) => {\n const parsed = Number(value);\n\n if (!Number.isInteger(parsed) || parsed < 1) {\n return fallback;\n }\n\n return parsed;\n};\n\nconst buildCustomId = (file) => {\n if (file?.provider_metadata?.uploadthing?.customId) {\n return file.provider_metadata.uploadthing.customId;\n }\n\n return `${file.hash}${file.ext || ''}`;\n};\n\nconst buildUniqueCustomId = (file) => {\n const extension = file.ext || '';\n const base = file.hash || crypto.randomUUID();\n const suffix = crypto.randomBytes(4).toString('hex');\n\n return `${base}-${suffix}${extension}`;\n};\n\nconst getStoredKey = (file) => file?.provider_metadata?.uploadthing?.fileKey;\n\nconst getStoredCustomId = (file) => file?.provider_metadata?.uploadthing?.customId;\n\nconst normalizeUploadResult = (result) => {\n if (!result) {\n throw new Error('UploadThing returned an empty upload response.');\n }\n\n if (result.error) {\n throw new Error(`UploadThing upload failed: ${result.error.message}`);\n }\n\n if (!result.data?.key || !result.data?.ufsUrl) {\n throw new Error('UploadThing upload response is missing the file key or a usable URL.');\n }\n\n return result.data;\n};\n\nconst streamToBuffer = async (stream) => {\n const chunks = [];\n\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n\n return Buffer.concat(chunks);\n};\n\nconst isCustomIdConflictError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n (message.includes('customid') || message.includes('custom id')) &&\n (message.includes('exist') ||\n message.includes('duplicate') ||\n message.includes('already') ||\n message.includes('conflict') ||\n message.includes('taken'))\n );\n};\n\nconst isMissingFileError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n message.includes('not found') ||\n message.includes('no such file') ||\n message.includes('file does not exist') ||\n message.includes('unable to find') ||\n message.includes('unknown file')\n );\n};\n\nconst isRetryableUploadError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n message.includes('failed to upload file') ||\n message.includes('transport error') ||\n message.includes('fetch failed') ||\n message.includes('socket') ||\n message.includes('other side closed') ||\n message.includes('econnreset') ||\n message.includes('timeout')\n );\n};\n\nexport default (providerOptions = {}) => {\n const {\n token = process.env.UPLOADTHING_TOKEN,\n acl,\n apiUrl,\n ingestUrl,\n logLevel,\n logFormat,\n isDev,\n contentDisposition = DEFAULT_CONTENT_DISPOSITION,\n signedUrlExpiresIn = DEFAULT_SIGNED_URL_TTL,\n uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,\n uploadRetries = DEFAULT_UPLOAD_RETRIES,\n privateFiles = false,\n useCustomId = true,\n } = providerOptions;\n\n if (!token) {\n throw new Error(\n 'Missing UploadThing token. Set `providerOptions.token` or the `UPLOADTHING_TOKEN` environment variable.'\n );\n }\n\n const utapi = new UTApi({\n token,\n apiUrl,\n ingestUrl,\n logLevel,\n logFormat,\n isDev,\n defaultKeyType: useCustomId ? 'customId' : 'fileKey',\n });\n\n const resolvedSignedUrlTtl = signedUrlExpiresIn;\n const resolvedUploadConcurrency = Math.min(\n 25,\n toPositiveInteger(uploadConcurrency, DEFAULT_UPLOAD_CONCURRENCY)\n );\n const resolvedUploadRetries = Math.max(0, toPositiveInteger(uploadRetries, DEFAULT_UPLOAD_RETRIES));\n let activeUploads = 0;\n const queuedUploads = [];\n\n const runWithUploadSlot = async (task) => {\n if (activeUploads >= resolvedUploadConcurrency) {\n await new Promise((resolve) => {\n queuedUploads.push(resolve);\n });\n }\n\n activeUploads += 1;\n\n try {\n return await task();\n } finally {\n activeUploads -= 1;\n const next = queuedUploads.shift();\n\n if (next) {\n next();\n }\n }\n };\n\n const assignUploadDataToFile = (file, uploaded, customId) => {\n const publicUrl = uploaded.ufsUrl;\n\n file.url = publicUrl;\n file.previewUrl = publicUrl;\n file.provider_metadata = {\n ...(file.provider_metadata || {}),\n uploadthing: {\n fileKey: uploaded.key,\n customId,\n url: publicUrl,\n ufsUrl: uploaded.ufsUrl,\n name: uploaded.name,\n size: uploaded.size,\n },\n };\n };\n\n const performUpload = async (file, buffer, customId) => {\n const uploadFile = new UTFile([buffer], file.name || `${file.hash}${file.ext || ''}`, {\n customId,\n type: file.mime,\n });\n\n let lastError;\n\n for (let attempt = 0; attempt <= resolvedUploadRetries; attempt += 1) {\n try {\n const result = await utapi.uploadFiles(uploadFile, {\n acl,\n contentDisposition,\n concurrency: 1,\n metadata: {\n source: 'strapi',\n hash: file.hash,\n ext: file.ext,\n mime: file.mime,\n },\n });\n\n const uploaded = normalizeUploadResult(result);\n assignUploadDataToFile(file, uploaded, customId);\n return;\n } catch (error) {\n lastError = error;\n\n if (attempt >= resolvedUploadRetries || !isRetryableUploadError(error)) {\n throw error;\n }\n }\n }\n\n throw lastError;\n };\n\n const uploadBuffer = async (file, buffer) => {\n const preferredCustomId = useCustomId ? buildCustomId(file) : undefined;\n\n await runWithUploadSlot(async () => {\n try {\n await performUpload(file, buffer, preferredCustomId);\n } catch (error) {\n if (!useCustomId || !preferredCustomId) {\n throw error;\n }\n\n const fallbackCustomId = buildUniqueCustomId(file);\n\n if (!isCustomIdConflictError(error)) {\n try {\n await performUpload(file, buffer, fallbackCustomId);\n return;\n } catch (retryError) {\n throw error;\n }\n }\n\n await performUpload(file, buffer, fallbackCustomId);\n }\n });\n };\n\n return {\n async isPrivate() {\n return privateFiles || acl === 'private';\n },\n\n async getSignedUrl(file) {\n const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';\n const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);\n\n if (!key) {\n return file;\n }\n\n const signed = await utapi.getSignedURL(key, {\n expiresIn: resolvedSignedUrlTtl,\n keyType,\n });\n\n return {\n url: signed.ufsUrl || signed.url,\n };\n },\n\n async uploadStream(file) {\n if (!file.stream) {\n throw new Error('Missing file stream');\n }\n\n const buffer = await streamToBuffer(file.stream);\n await uploadBuffer(file, buffer);\n },\n\n async upload(file) {\n if (!file.buffer) {\n throw new Error('Missing file buffer');\n }\n\n await uploadBuffer(file, file.buffer);\n },\n\n async delete(file) {\n const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';\n const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);\n\n if (!key) {\n return;\n }\n\n try {\n await utapi.deleteFiles(key, { keyType });\n } catch (error) {\n if (isMissingFileError(error)) {\n return;\n }\n\n throw error;\n }\n },\n };\n};\n","import provider from './provider';\n\nexport default {\n provider,\n};\n","import bootstrap from './bootstrap';\nimport config from './config';\nimport controllers from './controllers';\nimport contentTypes from './content-types';\nimport destroy from './destroy';\nimport middlewares from './middlewares';\nimport policies from './policies';\nimport register from './register';\nimport routes from './routes';\nimport services from './services';\n\nexport default {\n bootstrap,\n config,\n controllers,\n contentTypes,\n destroy,\n middlewares,\n policies,\n register,\n routes,\n services,\n};\n"],"names":["crypto","UTApi","UTFile"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","sources":["../../server/src/bootstrap.js","../../server/src/config/index.js","../../server/src/controllers/index.js","../../server/src/content-types/index.js","../../server/src/destroy.js","../../server/src/middlewares/index.js","../../server/src/policies/index.js","../../server/src/register.js","../../server/src/routes/index.js","../../server/src/services/provider.js","../../server/src/services/index.js","../../server/src/index.js"],"sourcesContent":["export default ({ strapi }) => {\n strapi.log.info('[strapi-upload-things] plugin bootstrapped');\n};\n","export default {\n default: {\n enabled: true,\n },\n validator() {},\n};\n","export default {};\n","export default {};\n","export default () => {};\n","export default {};\n","export default {};\n","export default () => {};\n","export default [];\n","import crypto from 'crypto';\nimport { UTApi, UTFile } from 'uploadthing/server';\n\nconst DEFAULT_CONTENT_DISPOSITION = 'inline';\nconst DEFAULT_SIGNED_URL_TTL = 60 * 60;\nconst DEFAULT_UPLOAD_CONCURRENCY = 1;\nconst DEFAULT_UPLOAD_RETRIES = 2;\n\nconst toPositiveInteger = (value, fallback) => {\n const parsed = Number(value);\n\n if (!Number.isInteger(parsed) || parsed < 1) {\n return fallback;\n }\n\n return parsed;\n};\n\nconst buildCustomId = (file) => {\n if (file?.provider_metadata?.uploadthing?.customId) {\n return file.provider_metadata.uploadthing.customId;\n }\n\n return `${file.hash}${file.ext || ''}`;\n};\n\nconst buildUniqueCustomId = (file) => {\n const extension = file.ext || '';\n const base = file.hash || crypto.randomUUID();\n const suffix = crypto.randomBytes(4).toString('hex');\n\n return `${base}-${suffix}${extension}`;\n};\n\nconst getStoredKey = (file) => file?.provider_metadata?.uploadthing?.fileKey;\n\nconst getStoredCustomId = (file) => file?.provider_metadata?.uploadthing?.customId;\n\nconst normalizeUploadResult = (result) => {\n if (!result) {\n throw new Error('UploadThing returned an empty upload response.');\n }\n\n if (result.error) {\n throw new Error(`UploadThing upload failed: ${result.error.message}`);\n }\n\n if (!result.data?.key || !result.data?.ufsUrl) {\n throw new Error('UploadThing upload response is missing the file key or a usable URL.');\n }\n\n return result.data;\n};\n\nconst streamToBuffer = async (stream) => {\n const chunks = [];\n\n for await (const chunk of stream) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n\n return Buffer.concat(chunks);\n};\n\nconst isCustomIdConflictError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n (message.includes('customid') || message.includes('custom id')) &&\n (message.includes('exist') ||\n message.includes('duplicate') ||\n message.includes('already') ||\n message.includes('conflict') ||\n message.includes('taken'))\n );\n};\n\nconst isMissingFileError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n message.includes('not found') ||\n message.includes('no such file') ||\n message.includes('file does not exist') ||\n message.includes('unable to find') ||\n message.includes('unknown file')\n );\n};\n\nconst isRetryableUploadError = (error) => {\n const message = `${error?.message || ''}`.toLowerCase();\n\n if (!message) {\n return false;\n }\n\n return (\n message.includes('failed to upload file') ||\n message.includes('transport error') ||\n message.includes('fetch failed') ||\n message.includes('socket') ||\n message.includes('other side closed') ||\n message.includes('econnreset') ||\n message.includes('timeout')\n );\n};\n\nexport default (providerOptions = {}) => {\n const {\n token = process.env.UPLOADTHING_TOKEN,\n acl,\n apiUrl,\n ingestUrl,\n logLevel,\n logFormat,\n isDev,\n contentDisposition = DEFAULT_CONTENT_DISPOSITION,\n signedUrlExpiresIn = DEFAULT_SIGNED_URL_TTL,\n uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,\n uploadRetries = DEFAULT_UPLOAD_RETRIES,\n privateFiles = false,\n useCustomId = true,\n } = providerOptions;\n\n if (!token) {\n throw new Error(\n 'Missing UploadThing token. Set `providerOptions.token` or the `UPLOADTHING_TOKEN` environment variable.'\n );\n }\n\n const utapi = new UTApi({\n token,\n apiUrl,\n ingestUrl,\n logLevel,\n logFormat,\n isDev,\n defaultKeyType: useCustomId ? 'customId' : 'fileKey',\n });\n\n const resolvedSignedUrlTtl = signedUrlExpiresIn;\n const resolvedUploadConcurrency = Math.min(\n 25,\n toPositiveInteger(uploadConcurrency, DEFAULT_UPLOAD_CONCURRENCY)\n );\n const resolvedUploadRetries = Math.max(0, toPositiveInteger(uploadRetries, DEFAULT_UPLOAD_RETRIES));\n let activeUploads = 0;\n const queuedUploads = [];\n\n const runWithUploadSlot = async (task) => {\n if (activeUploads >= resolvedUploadConcurrency) {\n await new Promise((resolve) => {\n queuedUploads.push(resolve);\n });\n }\n\n activeUploads += 1;\n\n try {\n return await task();\n } finally {\n activeUploads -= 1;\n const next = queuedUploads.shift();\n\n if (next) {\n next();\n }\n }\n };\n\n const assignUploadDataToFile = (file, uploaded, customId) => {\n const publicUrl = uploaded.ufsUrl;\n\n file.url = publicUrl;\n file.previewUrl = publicUrl;\n file.provider_metadata = {\n ...(file.provider_metadata || {}),\n uploadthing: {\n fileKey: uploaded.key,\n customId,\n url: publicUrl,\n ufsUrl: uploaded.ufsUrl,\n name: uploaded.name,\n size: uploaded.size,\n },\n };\n };\n\n const performUpload = async (file, buffer, customId) => {\n const uploadFile = new UTFile([buffer], file.name || `${file.hash}${file.ext || ''}`, {\n customId,\n type: file.mime,\n });\n\n let lastError;\n\n for (let attempt = 0; attempt <= resolvedUploadRetries; attempt += 1) {\n try {\n const result = await utapi.uploadFiles(uploadFile, {\n acl,\n contentDisposition,\n concurrency: 1,\n metadata: {\n source: 'strapi',\n hash: file.hash,\n ext: file.ext,\n mime: file.mime,\n },\n });\n\n const uploaded = normalizeUploadResult(result);\n assignUploadDataToFile(file, uploaded, customId);\n return;\n } catch (error) {\n lastError = error;\n\n if (attempt >= resolvedUploadRetries || !isRetryableUploadError(error)) {\n throw error;\n }\n }\n }\n\n throw lastError;\n };\n\n const uploadBuffer = async (file, buffer) => {\n const preferredCustomId = useCustomId ? buildCustomId(file) : undefined;\n\n await runWithUploadSlot(async () => {\n try {\n await performUpload(file, buffer, preferredCustomId);\n } catch (error) {\n if (!useCustomId || !preferredCustomId) {\n throw error;\n }\n\n const fallbackCustomId = buildUniqueCustomId(file);\n\n if (!isCustomIdConflictError(error)) {\n try {\n await performUpload(file, buffer, fallbackCustomId);\n return;\n } catch (retryError) {\n throw error;\n }\n }\n\n await performUpload(file, buffer, fallbackCustomId);\n }\n });\n };\n\n return {\n async isPrivate() {\n return privateFiles || acl === 'private';\n },\n\n async getSignedUrl(file) {\n const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';\n const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);\n\n if (!key) {\n return file;\n }\n\n const signed = await utapi.getSignedURL(key, {\n expiresIn: resolvedSignedUrlTtl,\n keyType,\n });\n\n return {\n url: signed.ufsUrl || signed.url,\n };\n },\n\n async uploadStream(file) {\n if (!file.stream) {\n throw new Error('Missing file stream');\n }\n\n const buffer = await streamToBuffer(file.stream);\n await uploadBuffer(file, buffer);\n },\n\n async upload(file) {\n if (!file.buffer) {\n throw new Error('Missing file buffer');\n }\n\n await uploadBuffer(file, file.buffer);\n },\n\n async delete(file) {\n const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';\n const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);\n\n if (!key) {\n return;\n }\n\n try {\n await utapi.deleteFiles(key, { keyType });\n } catch (error) {\n if (isMissingFileError(error)) {\n return;\n }\n\n throw error;\n }\n },\n };\n};\n","import provider from './provider';\n\nexport default {\n provider,\n};\n","import bootstrap from './bootstrap';\nimport config from './config';\nimport controllers from './controllers';\nimport contentTypes from './content-types';\nimport destroy from './destroy';\nimport middlewares from './middlewares';\nimport policies from './policies';\nimport register from './register';\nimport routes from './routes';\nimport services from './services';\n\nexport default {\n bootstrap,\n config,\n controllers,\n contentTypes,\n destroy,\n middlewares,\n policies,\n register,\n routes,\n services,\n};\n"],"names":[],"mappings":"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"}