@vivinkv28/strapi-provider-uploadthing 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @vivinkv28/strapi-provider-uploadthing
2
+
3
+ UploadThing provider for the Strapi Upload plugin.
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.
6
+
7
+ ## What is UploadThing?
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.
10
+
11
+ Learn more at [uploadthing.com](https://uploadthing.com/).
12
+
13
+ ## Features
14
+
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
24
+
25
+ ## Installation
26
+
27
+ Install the provider in your Strapi project:
28
+
29
+ ```bash
30
+ npm install @vivinkv28/strapi-provider-uploadthing
31
+ ```
32
+
33
+ ## Requirements
34
+
35
+ - Node.js `>= 20.0.0`
36
+ - Strapi v5
37
+
38
+ ## Environment Variables
39
+
40
+ Add your UploadThing token to your Strapi `.env` file:
41
+
42
+ ```env
43
+ UPLOADTHING_TOKEN=your_uploadthing_token
44
+ ```
45
+
46
+ Example:
47
+
48
+ ```env
49
+ UPLOADTHING_TOKEN=your_uploadthing_token
50
+ UPLOADTHING_ACL=public-read
51
+ UPLOADTHING_PRIVATE_FILES=false
52
+ UPLOADTHING_CONTENT_DISPOSITION=inline
53
+ UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
54
+ UPLOADTHING_UPLOAD_CONCURRENCY=1
55
+ UPLOADTHING_UPLOAD_RETRIES=2
56
+ UPLOADTHING_USE_CUSTOM_ID=true
57
+ UPLOADTHING_LOG_LEVEL=Info
58
+ ```
59
+
60
+ ## Strapi Configuration
61
+
62
+ Create or update `./config/plugins.ts`:
63
+
64
+ ```ts
65
+ export default ({ env }) => ({
66
+ upload: {
67
+ config: {
68
+ provider: '@vivinkv28/strapi-provider-uploadthing',
69
+ providerOptions: {
70
+ token: env('UPLOADTHING_TOKEN'),
71
+ acl: env('UPLOADTHING_ACL', 'public-read'),
72
+ privateFiles: env.bool('UPLOADTHING_PRIVATE_FILES', false),
73
+ contentDisposition: env('UPLOADTHING_CONTENT_DISPOSITION', 'inline'),
74
+ signedUrlExpiresIn: env.int('UPLOADTHING_SIGNED_URL_EXPIRES_IN', 3600),
75
+ uploadConcurrency: env.int('UPLOADTHING_UPLOAD_CONCURRENCY', 1),
76
+ uploadRetries: env.int('UPLOADTHING_UPLOAD_RETRIES', 2),
77
+ useCustomId: env.bool('UPLOADTHING_USE_CUSTOM_ID', true),
78
+ logLevel: env('UPLOADTHING_LOG_LEVEL', 'Info'),
79
+ },
80
+ actionOptions: {
81
+ upload: {},
82
+ uploadStream: {},
83
+ delete: {},
84
+ },
85
+ },
86
+ },
87
+ });
88
+ ```
89
+
90
+ ## Provider Options
91
+
92
+ | Option | Type | Default | Description |
93
+ | --- | --- | --- | --- |
94
+ | `token` | `string` | `process.env.UPLOADTHING_TOKEN` | UploadThing token used to initialize `UTApi`. |
95
+ | `acl` | `string` | `undefined` | ACL passed to UploadThing during upload. |
96
+ | `privateFiles` | `boolean` | `false` | Marks files as private and enables signed URL resolution. |
97
+ | `contentDisposition` | `string` | `'inline'` | Content disposition used during upload. |
98
+ | `signedUrlExpiresIn` | `number` | `3600` | Signed URL expiration time in seconds. |
99
+ | `uploadConcurrency` | `number` | `1` | Maximum concurrent uploads handled by the provider. Values above `25` are capped. |
100
+ | `uploadRetries` | `number` | `2` | Number of retry attempts for transient UploadThing upload failures. |
101
+ | `useCustomId` | `boolean` | `true` | Uses a deterministic UploadThing `customId` based on Strapi file hash and extension. |
102
+ | `apiUrl` | `string` | `undefined` | Optional custom UploadThing API URL. |
103
+ | `ingestUrl` | `string` | `undefined` | Optional custom UploadThing ingest URL. |
104
+ | `logLevel` | `string` | `undefined` | Optional UploadThing log level. |
105
+ | `logFormat` | `string` | `undefined` | Optional UploadThing log format. |
106
+ | `isDev` | `boolean` | `undefined` | Optional UploadThing development mode flag. |
107
+
108
+ ## Private Files
109
+
110
+ If `privateFiles` is enabled, the provider reports files as private and asks UploadThing for a signed URL when Strapi serves them.
111
+
112
+ Example:
113
+
114
+ ```env
115
+ UPLOADTHING_PRIVATE_FILES=true
116
+ UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
117
+ ```
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const crypto = require("crypto");
4
+ const server = require("uploadthing/server");
5
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
+ const crypto__default = /* @__PURE__ */ _interopDefault(crypto);
7
+ const bootstrap = ({ strapi }) => {
8
+ strapi.log.info("[strapi-upload-things] plugin bootstrapped");
9
+ };
10
+ const config = {
11
+ default: {
12
+ enabled: true
13
+ },
14
+ validator() {
15
+ }
16
+ };
17
+ const controllers = {};
18
+ const contentTypes = {};
19
+ const destroy = () => {
20
+ };
21
+ const middlewares = {};
22
+ const policies = {};
23
+ const register = () => {
24
+ };
25
+ const routes = [];
26
+ const DEFAULT_CONTENT_DISPOSITION = "inline";
27
+ const DEFAULT_SIGNED_URL_TTL = 60 * 60;
28
+ const DEFAULT_UPLOAD_CONCURRENCY = 1;
29
+ const DEFAULT_UPLOAD_RETRIES = 2;
30
+ const toPositiveInteger = (value, fallback) => {
31
+ const parsed = Number(value);
32
+ if (!Number.isInteger(parsed) || parsed < 1) {
33
+ return fallback;
34
+ }
35
+ return parsed;
36
+ };
37
+ const buildCustomId = (file) => {
38
+ if (file?.provider_metadata?.uploadthing?.customId) {
39
+ return file.provider_metadata.uploadthing.customId;
40
+ }
41
+ return `${file.hash}${file.ext || ""}`;
42
+ };
43
+ const buildUniqueCustomId = (file) => {
44
+ const extension = file.ext || "";
45
+ const base = file.hash || crypto__default.default.randomUUID();
46
+ const suffix = crypto__default.default.randomBytes(4).toString("hex");
47
+ return `${base}-${suffix}${extension}`;
48
+ };
49
+ const getStoredKey = (file) => file?.provider_metadata?.uploadthing?.fileKey;
50
+ const getStoredCustomId = (file) => file?.provider_metadata?.uploadthing?.customId;
51
+ const normalizeUploadResult = (result) => {
52
+ if (!result) {
53
+ throw new Error("UploadThing returned an empty upload response.");
54
+ }
55
+ if (result.error) {
56
+ throw new Error(`UploadThing upload failed: ${result.error.message}`);
57
+ }
58
+ if (!result.data?.key || !result.data?.ufsUrl) {
59
+ throw new Error("UploadThing upload response is missing the file key or a usable URL.");
60
+ }
61
+ return result.data;
62
+ };
63
+ const streamToBuffer = async (stream) => {
64
+ const chunks = [];
65
+ for await (const chunk of stream) {
66
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
67
+ }
68
+ return Buffer.concat(chunks);
69
+ };
70
+ const isCustomIdConflictError = (error) => {
71
+ const message = `${error?.message || ""}`.toLowerCase();
72
+ if (!message) {
73
+ return false;
74
+ }
75
+ return (message.includes("customid") || message.includes("custom id")) && (message.includes("exist") || message.includes("duplicate") || message.includes("already") || message.includes("conflict") || message.includes("taken"));
76
+ };
77
+ const isMissingFileError = (error) => {
78
+ const message = `${error?.message || ""}`.toLowerCase();
79
+ if (!message) {
80
+ return false;
81
+ }
82
+ return message.includes("not found") || message.includes("no such file") || message.includes("file does not exist") || message.includes("unable to find") || message.includes("unknown file");
83
+ };
84
+ const isRetryableUploadError = (error) => {
85
+ const message = `${error?.message || ""}`.toLowerCase();
86
+ if (!message) {
87
+ return false;
88
+ }
89
+ return message.includes("failed to upload file") || message.includes("transport error") || message.includes("fetch failed") || message.includes("socket") || message.includes("other side closed") || message.includes("econnreset") || message.includes("timeout");
90
+ };
91
+ const provider = (providerOptions = {}) => {
92
+ const {
93
+ token = process.env.UPLOADTHING_TOKEN,
94
+ acl,
95
+ apiUrl,
96
+ ingestUrl,
97
+ logLevel,
98
+ logFormat,
99
+ isDev,
100
+ contentDisposition = DEFAULT_CONTENT_DISPOSITION,
101
+ signedUrlExpiresIn = DEFAULT_SIGNED_URL_TTL,
102
+ uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,
103
+ uploadRetries = DEFAULT_UPLOAD_RETRIES,
104
+ privateFiles = false,
105
+ useCustomId = true
106
+ } = providerOptions;
107
+ if (!token) {
108
+ throw new Error(
109
+ "Missing UploadThing token. Set `providerOptions.token` or the `UPLOADTHING_TOKEN` environment variable."
110
+ );
111
+ }
112
+ const utapi = new server.UTApi({
113
+ token,
114
+ apiUrl,
115
+ ingestUrl,
116
+ logLevel,
117
+ logFormat,
118
+ isDev,
119
+ defaultKeyType: useCustomId ? "customId" : "fileKey"
120
+ });
121
+ const resolvedSignedUrlTtl = signedUrlExpiresIn;
122
+ const resolvedUploadConcurrency = Math.min(
123
+ 25,
124
+ toPositiveInteger(uploadConcurrency, DEFAULT_UPLOAD_CONCURRENCY)
125
+ );
126
+ const resolvedUploadRetries = Math.max(0, toPositiveInteger(uploadRetries, DEFAULT_UPLOAD_RETRIES));
127
+ let activeUploads = 0;
128
+ const queuedUploads = [];
129
+ const runWithUploadSlot = async (task) => {
130
+ if (activeUploads >= resolvedUploadConcurrency) {
131
+ await new Promise((resolve) => {
132
+ queuedUploads.push(resolve);
133
+ });
134
+ }
135
+ activeUploads += 1;
136
+ try {
137
+ return await task();
138
+ } finally {
139
+ activeUploads -= 1;
140
+ const next = queuedUploads.shift();
141
+ if (next) {
142
+ next();
143
+ }
144
+ }
145
+ };
146
+ const assignUploadDataToFile = (file, uploaded, customId) => {
147
+ const publicUrl = uploaded.ufsUrl;
148
+ file.url = publicUrl;
149
+ file.previewUrl = publicUrl;
150
+ file.provider_metadata = {
151
+ ...file.provider_metadata || {},
152
+ uploadthing: {
153
+ fileKey: uploaded.key,
154
+ customId,
155
+ url: publicUrl,
156
+ ufsUrl: uploaded.ufsUrl,
157
+ name: uploaded.name,
158
+ size: uploaded.size
159
+ }
160
+ };
161
+ };
162
+ const performUpload = async (file, buffer, customId) => {
163
+ const uploadFile = new server.UTFile([buffer], file.name || `${file.hash}${file.ext || ""}`, {
164
+ customId,
165
+ type: file.mime
166
+ });
167
+ let lastError;
168
+ for (let attempt = 0; attempt <= resolvedUploadRetries; attempt += 1) {
169
+ try {
170
+ const result = await utapi.uploadFiles(uploadFile, {
171
+ acl,
172
+ contentDisposition,
173
+ concurrency: 1,
174
+ metadata: {
175
+ source: "strapi",
176
+ hash: file.hash,
177
+ ext: file.ext,
178
+ mime: file.mime
179
+ }
180
+ });
181
+ const uploaded = normalizeUploadResult(result);
182
+ assignUploadDataToFile(file, uploaded, customId);
183
+ return;
184
+ } catch (error) {
185
+ lastError = error;
186
+ if (attempt >= resolvedUploadRetries || !isRetryableUploadError(error)) {
187
+ throw error;
188
+ }
189
+ }
190
+ }
191
+ throw lastError;
192
+ };
193
+ const uploadBuffer = async (file, buffer) => {
194
+ const preferredCustomId = useCustomId ? buildCustomId(file) : void 0;
195
+ await runWithUploadSlot(async () => {
196
+ try {
197
+ await performUpload(file, buffer, preferredCustomId);
198
+ } catch (error) {
199
+ if (!useCustomId || !preferredCustomId) {
200
+ throw error;
201
+ }
202
+ const fallbackCustomId = buildUniqueCustomId(file);
203
+ if (!isCustomIdConflictError(error)) {
204
+ try {
205
+ await performUpload(file, buffer, fallbackCustomId);
206
+ return;
207
+ } catch (retryError) {
208
+ throw error;
209
+ }
210
+ }
211
+ await performUpload(file, buffer, fallbackCustomId);
212
+ }
213
+ });
214
+ };
215
+ return {
216
+ async isPrivate() {
217
+ return privateFiles || acl === "private";
218
+ },
219
+ async getSignedUrl(file) {
220
+ const keyType = useCustomId && getStoredCustomId(file) ? "customId" : "fileKey";
221
+ const key = keyType === "customId" ? getStoredCustomId(file) : getStoredKey(file);
222
+ if (!key) {
223
+ return file;
224
+ }
225
+ const signed = await utapi.getSignedURL(key, {
226
+ expiresIn: resolvedSignedUrlTtl,
227
+ keyType
228
+ });
229
+ return {
230
+ url: signed.ufsUrl || signed.url
231
+ };
232
+ },
233
+ async uploadStream(file) {
234
+ if (!file.stream) {
235
+ throw new Error("Missing file stream");
236
+ }
237
+ const buffer = await streamToBuffer(file.stream);
238
+ await uploadBuffer(file, buffer);
239
+ },
240
+ async upload(file) {
241
+ if (!file.buffer) {
242
+ throw new Error("Missing file buffer");
243
+ }
244
+ await uploadBuffer(file, file.buffer);
245
+ },
246
+ async delete(file) {
247
+ const keyType = useCustomId && getStoredCustomId(file) ? "customId" : "fileKey";
248
+ const key = keyType === "customId" ? getStoredCustomId(file) : getStoredKey(file);
249
+ if (!key) {
250
+ return;
251
+ }
252
+ try {
253
+ await utapi.deleteFiles(key, { keyType });
254
+ } catch (error) {
255
+ if (isMissingFileError(error)) {
256
+ return;
257
+ }
258
+ throw error;
259
+ }
260
+ }
261
+ };
262
+ };
263
+ const services = {
264
+ provider
265
+ };
266
+ const index = {
267
+ bootstrap,
268
+ config,
269
+ controllers,
270
+ contentTypes,
271
+ destroy,
272
+ middlewares,
273
+ policies,
274
+ register,
275
+ routes,
276
+ services
277
+ };
278
+ exports.default = index;
279
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,277 @@
1
+ import crypto from "crypto";
2
+ import { UTApi, UTFile } from "uploadthing/server";
3
+ const bootstrap = ({ strapi }) => {
4
+ strapi.log.info("[strapi-upload-things] plugin bootstrapped");
5
+ };
6
+ const config = {
7
+ default: {
8
+ enabled: true
9
+ },
10
+ validator() {
11
+ }
12
+ };
13
+ const controllers = {};
14
+ const contentTypes = {};
15
+ const destroy = () => {
16
+ };
17
+ const middlewares = {};
18
+ const policies = {};
19
+ const register = () => {
20
+ };
21
+ const routes = [];
22
+ const DEFAULT_CONTENT_DISPOSITION = "inline";
23
+ const DEFAULT_SIGNED_URL_TTL = 60 * 60;
24
+ const DEFAULT_UPLOAD_CONCURRENCY = 1;
25
+ const DEFAULT_UPLOAD_RETRIES = 2;
26
+ const toPositiveInteger = (value, fallback) => {
27
+ const parsed = Number(value);
28
+ if (!Number.isInteger(parsed) || parsed < 1) {
29
+ return fallback;
30
+ }
31
+ return parsed;
32
+ };
33
+ const buildCustomId = (file) => {
34
+ if (file?.provider_metadata?.uploadthing?.customId) {
35
+ return file.provider_metadata.uploadthing.customId;
36
+ }
37
+ return `${file.hash}${file.ext || ""}`;
38
+ };
39
+ const buildUniqueCustomId = (file) => {
40
+ const extension = file.ext || "";
41
+ const base = file.hash || crypto.randomUUID();
42
+ const suffix = crypto.randomBytes(4).toString("hex");
43
+ return `${base}-${suffix}${extension}`;
44
+ };
45
+ const getStoredKey = (file) => file?.provider_metadata?.uploadthing?.fileKey;
46
+ const getStoredCustomId = (file) => file?.provider_metadata?.uploadthing?.customId;
47
+ const normalizeUploadResult = (result) => {
48
+ if (!result) {
49
+ throw new Error("UploadThing returned an empty upload response.");
50
+ }
51
+ if (result.error) {
52
+ throw new Error(`UploadThing upload failed: ${result.error.message}`);
53
+ }
54
+ if (!result.data?.key || !result.data?.ufsUrl) {
55
+ throw new Error("UploadThing upload response is missing the file key or a usable URL.");
56
+ }
57
+ return result.data;
58
+ };
59
+ const streamToBuffer = async (stream) => {
60
+ const chunks = [];
61
+ for await (const chunk of stream) {
62
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
63
+ }
64
+ return Buffer.concat(chunks);
65
+ };
66
+ const isCustomIdConflictError = (error) => {
67
+ const message = `${error?.message || ""}`.toLowerCase();
68
+ if (!message) {
69
+ return false;
70
+ }
71
+ return (message.includes("customid") || message.includes("custom id")) && (message.includes("exist") || message.includes("duplicate") || message.includes("already") || message.includes("conflict") || message.includes("taken"));
72
+ };
73
+ const isMissingFileError = (error) => {
74
+ const message = `${error?.message || ""}`.toLowerCase();
75
+ if (!message) {
76
+ return false;
77
+ }
78
+ return message.includes("not found") || message.includes("no such file") || message.includes("file does not exist") || message.includes("unable to find") || message.includes("unknown file");
79
+ };
80
+ const isRetryableUploadError = (error) => {
81
+ const message = `${error?.message || ""}`.toLowerCase();
82
+ if (!message) {
83
+ return false;
84
+ }
85
+ return message.includes("failed to upload file") || message.includes("transport error") || message.includes("fetch failed") || message.includes("socket") || message.includes("other side closed") || message.includes("econnreset") || message.includes("timeout");
86
+ };
87
+ const provider = (providerOptions = {}) => {
88
+ const {
89
+ token = process.env.UPLOADTHING_TOKEN,
90
+ acl,
91
+ apiUrl,
92
+ ingestUrl,
93
+ logLevel,
94
+ logFormat,
95
+ isDev,
96
+ contentDisposition = DEFAULT_CONTENT_DISPOSITION,
97
+ signedUrlExpiresIn = DEFAULT_SIGNED_URL_TTL,
98
+ uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,
99
+ uploadRetries = DEFAULT_UPLOAD_RETRIES,
100
+ privateFiles = false,
101
+ useCustomId = true
102
+ } = providerOptions;
103
+ if (!token) {
104
+ throw new Error(
105
+ "Missing UploadThing token. Set `providerOptions.token` or the `UPLOADTHING_TOKEN` environment variable."
106
+ );
107
+ }
108
+ const utapi = new UTApi({
109
+ token,
110
+ apiUrl,
111
+ ingestUrl,
112
+ logLevel,
113
+ logFormat,
114
+ isDev,
115
+ defaultKeyType: useCustomId ? "customId" : "fileKey"
116
+ });
117
+ const resolvedSignedUrlTtl = signedUrlExpiresIn;
118
+ const resolvedUploadConcurrency = Math.min(
119
+ 25,
120
+ toPositiveInteger(uploadConcurrency, DEFAULT_UPLOAD_CONCURRENCY)
121
+ );
122
+ const resolvedUploadRetries = Math.max(0, toPositiveInteger(uploadRetries, DEFAULT_UPLOAD_RETRIES));
123
+ let activeUploads = 0;
124
+ const queuedUploads = [];
125
+ const runWithUploadSlot = async (task) => {
126
+ if (activeUploads >= resolvedUploadConcurrency) {
127
+ await new Promise((resolve) => {
128
+ queuedUploads.push(resolve);
129
+ });
130
+ }
131
+ activeUploads += 1;
132
+ try {
133
+ return await task();
134
+ } finally {
135
+ activeUploads -= 1;
136
+ const next = queuedUploads.shift();
137
+ if (next) {
138
+ next();
139
+ }
140
+ }
141
+ };
142
+ const assignUploadDataToFile = (file, uploaded, customId) => {
143
+ const publicUrl = uploaded.ufsUrl;
144
+ file.url = publicUrl;
145
+ file.previewUrl = publicUrl;
146
+ file.provider_metadata = {
147
+ ...file.provider_metadata || {},
148
+ uploadthing: {
149
+ fileKey: uploaded.key,
150
+ customId,
151
+ url: publicUrl,
152
+ ufsUrl: uploaded.ufsUrl,
153
+ name: uploaded.name,
154
+ size: uploaded.size
155
+ }
156
+ };
157
+ };
158
+ const performUpload = async (file, buffer, customId) => {
159
+ const uploadFile = new UTFile([buffer], file.name || `${file.hash}${file.ext || ""}`, {
160
+ customId,
161
+ type: file.mime
162
+ });
163
+ let lastError;
164
+ for (let attempt = 0; attempt <= resolvedUploadRetries; attempt += 1) {
165
+ try {
166
+ const result = await utapi.uploadFiles(uploadFile, {
167
+ acl,
168
+ contentDisposition,
169
+ concurrency: 1,
170
+ metadata: {
171
+ source: "strapi",
172
+ hash: file.hash,
173
+ ext: file.ext,
174
+ mime: file.mime
175
+ }
176
+ });
177
+ const uploaded = normalizeUploadResult(result);
178
+ assignUploadDataToFile(file, uploaded, customId);
179
+ return;
180
+ } catch (error) {
181
+ lastError = error;
182
+ if (attempt >= resolvedUploadRetries || !isRetryableUploadError(error)) {
183
+ throw error;
184
+ }
185
+ }
186
+ }
187
+ throw lastError;
188
+ };
189
+ const uploadBuffer = async (file, buffer) => {
190
+ const preferredCustomId = useCustomId ? buildCustomId(file) : void 0;
191
+ await runWithUploadSlot(async () => {
192
+ try {
193
+ await performUpload(file, buffer, preferredCustomId);
194
+ } catch (error) {
195
+ if (!useCustomId || !preferredCustomId) {
196
+ throw error;
197
+ }
198
+ const fallbackCustomId = buildUniqueCustomId(file);
199
+ if (!isCustomIdConflictError(error)) {
200
+ try {
201
+ await performUpload(file, buffer, fallbackCustomId);
202
+ return;
203
+ } catch (retryError) {
204
+ throw error;
205
+ }
206
+ }
207
+ await performUpload(file, buffer, fallbackCustomId);
208
+ }
209
+ });
210
+ };
211
+ return {
212
+ async isPrivate() {
213
+ return privateFiles || acl === "private";
214
+ },
215
+ async getSignedUrl(file) {
216
+ const keyType = useCustomId && getStoredCustomId(file) ? "customId" : "fileKey";
217
+ const key = keyType === "customId" ? getStoredCustomId(file) : getStoredKey(file);
218
+ if (!key) {
219
+ return file;
220
+ }
221
+ const signed = await utapi.getSignedURL(key, {
222
+ expiresIn: resolvedSignedUrlTtl,
223
+ keyType
224
+ });
225
+ return {
226
+ url: signed.ufsUrl || signed.url
227
+ };
228
+ },
229
+ async uploadStream(file) {
230
+ if (!file.stream) {
231
+ throw new Error("Missing file stream");
232
+ }
233
+ const buffer = await streamToBuffer(file.stream);
234
+ await uploadBuffer(file, buffer);
235
+ },
236
+ async upload(file) {
237
+ if (!file.buffer) {
238
+ throw new Error("Missing file buffer");
239
+ }
240
+ await uploadBuffer(file, file.buffer);
241
+ },
242
+ async delete(file) {
243
+ const keyType = useCustomId && getStoredCustomId(file) ? "customId" : "fileKey";
244
+ const key = keyType === "customId" ? getStoredCustomId(file) : getStoredKey(file);
245
+ if (!key) {
246
+ return;
247
+ }
248
+ try {
249
+ await utapi.deleteFiles(key, { keyType });
250
+ } catch (error) {
251
+ if (isMissingFileError(error)) {
252
+ return;
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+ };
258
+ };
259
+ const services = {
260
+ provider
261
+ };
262
+ const index = {
263
+ bootstrap,
264
+ config,
265
+ controllers,
266
+ contentTypes,
267
+ destroy,
268
+ middlewares,
269
+ policies,
270
+ register,
271
+ routes,
272
+ services
273
+ };
274
+ export {
275
+ index as default
276
+ };
277
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
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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"}
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const providerModule = require('./server/src/services/provider');
4
+ const provider = providerModule.default || providerModule;
5
+
6
+ module.exports = {
7
+ init: provider,
8
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@vivinkv28/strapi-provider-uploadthing",
3
+ "version": "0.1.3",
4
+ "description": "UploadThing provider for the Strapi Upload plugin",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./strapi-server": {
9
+ "source": "./server/src/index.js",
10
+ "import": "./dist/server/index.mjs",
11
+ "require": "./dist/server/index.js",
12
+ "default": "./dist/server/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "index.js",
19
+ "server",
20
+ "strapi-server.js",
21
+ "README.md"
22
+ ],
23
+ "keywords": [
24
+ "strapi",
25
+ "strapi-plugin",
26
+ "uploadthing",
27
+ "upload",
28
+ "provider",
29
+ "media-library"
30
+ ],
31
+ "strapi": {
32
+ "kind": "plugin",
33
+ "name": "strapi-upload-things",
34
+ "displayName": "UploadThing Provider",
35
+ "description": "UploadThing integration for the Strapi upload plugin"
36
+ },
37
+ "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "engines": {
42
+ "node": ">=20.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js build",
46
+ "watch": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js watch",
47
+ "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"
49
+ },
50
+ "dependencies": {
51
+ "uploadthing": "^7.7.2"
52
+ },
53
+ "devDependencies": {
54
+ "@babel/runtime": "^7.29.2",
55
+ "@strapi/sdk-plugin": "^6.1.0"
56
+ }
57
+ }
@@ -0,0 +1,3 @@
1
+ export default ({ strapi }) => {
2
+ strapi.log.info('[strapi-upload-things] plugin bootstrapped');
3
+ };
@@ -0,0 +1,6 @@
1
+ export default {
2
+ default: {
3
+ enabled: true,
4
+ },
5
+ validator() {},
6
+ };
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1 @@
1
+ export default () => {};
@@ -0,0 +1,23 @@
1
+ import bootstrap from './bootstrap';
2
+ import config from './config';
3
+ import controllers from './controllers';
4
+ import contentTypes from './content-types';
5
+ import destroy from './destroy';
6
+ import middlewares from './middlewares';
7
+ import policies from './policies';
8
+ import register from './register';
9
+ import routes from './routes';
10
+ import services from './services';
11
+
12
+ export default {
13
+ bootstrap,
14
+ config,
15
+ controllers,
16
+ contentTypes,
17
+ destroy,
18
+ middlewares,
19
+ policies,
20
+ register,
21
+ routes,
22
+ services,
23
+ };
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1 @@
1
+ export default () => {};
@@ -0,0 +1 @@
1
+ export default [];
@@ -0,0 +1,5 @@
1
+ import provider from './provider';
2
+
3
+ export default {
4
+ provider,
5
+ };
@@ -0,0 +1,320 @@
1
+ import crypto from 'crypto';
2
+ import { UTApi, UTFile } from 'uploadthing/server';
3
+
4
+ const DEFAULT_CONTENT_DISPOSITION = 'inline';
5
+ const DEFAULT_SIGNED_URL_TTL = 60 * 60;
6
+ const DEFAULT_UPLOAD_CONCURRENCY = 1;
7
+ const DEFAULT_UPLOAD_RETRIES = 2;
8
+
9
+ const toPositiveInteger = (value, fallback) => {
10
+ const parsed = Number(value);
11
+
12
+ if (!Number.isInteger(parsed) || parsed < 1) {
13
+ return fallback;
14
+ }
15
+
16
+ return parsed;
17
+ };
18
+
19
+ const buildCustomId = (file) => {
20
+ if (file?.provider_metadata?.uploadthing?.customId) {
21
+ return file.provider_metadata.uploadthing.customId;
22
+ }
23
+
24
+ return `${file.hash}${file.ext || ''}`;
25
+ };
26
+
27
+ const buildUniqueCustomId = (file) => {
28
+ const extension = file.ext || '';
29
+ const base = file.hash || crypto.randomUUID();
30
+ const suffix = crypto.randomBytes(4).toString('hex');
31
+
32
+ return `${base}-${suffix}${extension}`;
33
+ };
34
+
35
+ const getStoredKey = (file) => file?.provider_metadata?.uploadthing?.fileKey;
36
+
37
+ const getStoredCustomId = (file) => file?.provider_metadata?.uploadthing?.customId;
38
+
39
+ const normalizeUploadResult = (result) => {
40
+ if (!result) {
41
+ throw new Error('UploadThing returned an empty upload response.');
42
+ }
43
+
44
+ if (result.error) {
45
+ throw new Error(`UploadThing upload failed: ${result.error.message}`);
46
+ }
47
+
48
+ if (!result.data?.key || !result.data?.ufsUrl) {
49
+ throw new Error('UploadThing upload response is missing the file key or a usable URL.');
50
+ }
51
+
52
+ return result.data;
53
+ };
54
+
55
+ const streamToBuffer = async (stream) => {
56
+ const chunks = [];
57
+
58
+ for await (const chunk of stream) {
59
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
60
+ }
61
+
62
+ return Buffer.concat(chunks);
63
+ };
64
+
65
+ const isCustomIdConflictError = (error) => {
66
+ const message = `${error?.message || ''}`.toLowerCase();
67
+
68
+ if (!message) {
69
+ return false;
70
+ }
71
+
72
+ return (
73
+ (message.includes('customid') || message.includes('custom id')) &&
74
+ (message.includes('exist') ||
75
+ message.includes('duplicate') ||
76
+ message.includes('already') ||
77
+ message.includes('conflict') ||
78
+ message.includes('taken'))
79
+ );
80
+ };
81
+
82
+ const isMissingFileError = (error) => {
83
+ const message = `${error?.message || ''}`.toLowerCase();
84
+
85
+ if (!message) {
86
+ return false;
87
+ }
88
+
89
+ return (
90
+ message.includes('not found') ||
91
+ message.includes('no such file') ||
92
+ message.includes('file does not exist') ||
93
+ message.includes('unable to find') ||
94
+ message.includes('unknown file')
95
+ );
96
+ };
97
+
98
+ const isRetryableUploadError = (error) => {
99
+ const message = `${error?.message || ''}`.toLowerCase();
100
+
101
+ if (!message) {
102
+ return false;
103
+ }
104
+
105
+ return (
106
+ message.includes('failed to upload file') ||
107
+ message.includes('transport error') ||
108
+ message.includes('fetch failed') ||
109
+ message.includes('socket') ||
110
+ message.includes('other side closed') ||
111
+ message.includes('econnreset') ||
112
+ message.includes('timeout')
113
+ );
114
+ };
115
+
116
+ export default (providerOptions = {}) => {
117
+ const {
118
+ token = process.env.UPLOADTHING_TOKEN,
119
+ acl,
120
+ apiUrl,
121
+ ingestUrl,
122
+ logLevel,
123
+ logFormat,
124
+ isDev,
125
+ contentDisposition = DEFAULT_CONTENT_DISPOSITION,
126
+ signedUrlExpiresIn = DEFAULT_SIGNED_URL_TTL,
127
+ uploadConcurrency = DEFAULT_UPLOAD_CONCURRENCY,
128
+ uploadRetries = DEFAULT_UPLOAD_RETRIES,
129
+ privateFiles = false,
130
+ useCustomId = true,
131
+ } = providerOptions;
132
+
133
+ if (!token) {
134
+ throw new Error(
135
+ 'Missing UploadThing token. Set `providerOptions.token` or the `UPLOADTHING_TOKEN` environment variable.'
136
+ );
137
+ }
138
+
139
+ const utapi = new UTApi({
140
+ token,
141
+ apiUrl,
142
+ ingestUrl,
143
+ logLevel,
144
+ logFormat,
145
+ isDev,
146
+ defaultKeyType: useCustomId ? 'customId' : 'fileKey',
147
+ });
148
+
149
+ const resolvedSignedUrlTtl = signedUrlExpiresIn;
150
+ const resolvedUploadConcurrency = Math.min(
151
+ 25,
152
+ toPositiveInteger(uploadConcurrency, DEFAULT_UPLOAD_CONCURRENCY)
153
+ );
154
+ const resolvedUploadRetries = Math.max(0, toPositiveInteger(uploadRetries, DEFAULT_UPLOAD_RETRIES));
155
+ let activeUploads = 0;
156
+ const queuedUploads = [];
157
+
158
+ const runWithUploadSlot = async (task) => {
159
+ if (activeUploads >= resolvedUploadConcurrency) {
160
+ await new Promise((resolve) => {
161
+ queuedUploads.push(resolve);
162
+ });
163
+ }
164
+
165
+ activeUploads += 1;
166
+
167
+ try {
168
+ return await task();
169
+ } finally {
170
+ activeUploads -= 1;
171
+ const next = queuedUploads.shift();
172
+
173
+ if (next) {
174
+ next();
175
+ }
176
+ }
177
+ };
178
+
179
+ const assignUploadDataToFile = (file, uploaded, customId) => {
180
+ const publicUrl = uploaded.ufsUrl;
181
+
182
+ file.url = publicUrl;
183
+ file.previewUrl = publicUrl;
184
+ file.provider_metadata = {
185
+ ...(file.provider_metadata || {}),
186
+ uploadthing: {
187
+ fileKey: uploaded.key,
188
+ customId,
189
+ url: publicUrl,
190
+ ufsUrl: uploaded.ufsUrl,
191
+ name: uploaded.name,
192
+ size: uploaded.size,
193
+ },
194
+ };
195
+ };
196
+
197
+ const performUpload = async (file, buffer, customId) => {
198
+ const uploadFile = new UTFile([buffer], file.name || `${file.hash}${file.ext || ''}`, {
199
+ customId,
200
+ type: file.mime,
201
+ });
202
+
203
+ let lastError;
204
+
205
+ for (let attempt = 0; attempt <= resolvedUploadRetries; attempt += 1) {
206
+ try {
207
+ const result = await utapi.uploadFiles(uploadFile, {
208
+ acl,
209
+ contentDisposition,
210
+ concurrency: 1,
211
+ metadata: {
212
+ source: 'strapi',
213
+ hash: file.hash,
214
+ ext: file.ext,
215
+ mime: file.mime,
216
+ },
217
+ });
218
+
219
+ const uploaded = normalizeUploadResult(result);
220
+ assignUploadDataToFile(file, uploaded, customId);
221
+ return;
222
+ } catch (error) {
223
+ lastError = error;
224
+
225
+ if (attempt >= resolvedUploadRetries || !isRetryableUploadError(error)) {
226
+ throw error;
227
+ }
228
+ }
229
+ }
230
+
231
+ throw lastError;
232
+ };
233
+
234
+ const uploadBuffer = async (file, buffer) => {
235
+ const preferredCustomId = useCustomId ? buildCustomId(file) : undefined;
236
+
237
+ await runWithUploadSlot(async () => {
238
+ try {
239
+ await performUpload(file, buffer, preferredCustomId);
240
+ } catch (error) {
241
+ if (!useCustomId || !preferredCustomId) {
242
+ throw error;
243
+ }
244
+
245
+ const fallbackCustomId = buildUniqueCustomId(file);
246
+
247
+ if (!isCustomIdConflictError(error)) {
248
+ try {
249
+ await performUpload(file, buffer, fallbackCustomId);
250
+ return;
251
+ } catch (retryError) {
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ await performUpload(file, buffer, fallbackCustomId);
257
+ }
258
+ });
259
+ };
260
+
261
+ return {
262
+ async isPrivate() {
263
+ return privateFiles || acl === 'private';
264
+ },
265
+
266
+ async getSignedUrl(file) {
267
+ const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';
268
+ const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);
269
+
270
+ if (!key) {
271
+ return file;
272
+ }
273
+
274
+ const signed = await utapi.getSignedURL(key, {
275
+ expiresIn: resolvedSignedUrlTtl,
276
+ keyType,
277
+ });
278
+
279
+ return {
280
+ url: signed.ufsUrl || signed.url,
281
+ };
282
+ },
283
+
284
+ async uploadStream(file) {
285
+ if (!file.stream) {
286
+ throw new Error('Missing file stream');
287
+ }
288
+
289
+ const buffer = await streamToBuffer(file.stream);
290
+ await uploadBuffer(file, buffer);
291
+ },
292
+
293
+ async upload(file) {
294
+ if (!file.buffer) {
295
+ throw new Error('Missing file buffer');
296
+ }
297
+
298
+ await uploadBuffer(file, file.buffer);
299
+ },
300
+
301
+ async delete(file) {
302
+ const keyType = useCustomId && getStoredCustomId(file) ? 'customId' : 'fileKey';
303
+ const key = keyType === 'customId' ? getStoredCustomId(file) : getStoredKey(file);
304
+
305
+ if (!key) {
306
+ return;
307
+ }
308
+
309
+ try {
310
+ await utapi.deleteFiles(key, { keyType });
311
+ } catch (error) {
312
+ if (isMissingFileError(error)) {
313
+ return;
314
+ }
315
+
316
+ throw error;
317
+ }
318
+ },
319
+ };
320
+ };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./dist/server');