@vivinkv28/strapi-provider-uploadthing 0.1.5 → 0.1.7
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 +213 -213
- package/dist/server/index.js +2 -3
- package/dist/server/index.mjs +2 -3
- package/index.js +2 -2
- package/package.json +67 -57
- package/server/src/services/provider.js +2 -2
- package/dist/server/index.js.map +0 -1
- package/dist/server/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,213 +1,213 @@
|
|
|
1
|
-
# @vivinkv28/strapi-provider-uploadthing
|
|
2
|
-
|
|
3
|
-
UploadThing provider for the Strapi Uploads
|
|
4
|
-
|
|
5
|
-
This provider stores Strapi Media Library files in UploadThing while keeping file records and metadata inside Strapi.
|
|
6
|
-
|
|
7
|
-
## What This Provider Does
|
|
8
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
## Requirements
|
|
20
|
-
|
|
21
|
-
- Node.js `>= 20.0.0`
|
|
22
|
-
- Strapi v5
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
25
|
-
|
|
26
|
-
Install the provider in your Strapi project:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install @vivinkv28/strapi-provider-uploadthing
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Quick Start
|
|
33
|
-
|
|
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.
|
|
38
|
-
|
|
39
|
-
## Environment Variables
|
|
40
|
-
|
|
41
|
-
Minimum required:
|
|
42
|
-
|
|
43
|
-
```env
|
|
44
|
-
UPLOADTHING_TOKEN=your_uploadthing_token
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Typical public-file setup:
|
|
48
|
-
|
|
49
|
-
```env
|
|
50
|
-
UPLOADTHING_TOKEN=your_uploadthing_token
|
|
51
|
-
UPLOADTHING_ACL=public-read
|
|
52
|
-
UPLOADTHING_PRIVATE_FILES=false
|
|
53
|
-
UPLOADTHING_CONTENT_DISPOSITION=inline
|
|
54
|
-
UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
|
|
55
|
-
UPLOADTHING_UPLOAD_CONCURRENCY=1
|
|
56
|
-
UPLOADTHING_UPLOAD_RETRIES=2
|
|
57
|
-
UPLOADTHING_USE_CUSTOM_ID=true
|
|
58
|
-
UPLOADTHING_LOG_LEVEL=Info
|
|
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
|
-
|
|
70
|
-
## Strapi Configuration
|
|
71
|
-
|
|
72
|
-
Create or update `./config/plugins.ts`:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
export default ({ env }) => ({
|
|
76
|
-
upload: {
|
|
77
|
-
config: {
|
|
78
|
-
provider: '@vivinkv28/strapi-provider-uploadthing',
|
|
79
|
-
providerOptions: {
|
|
80
|
-
token: env('UPLOADTHING_TOKEN'),
|
|
81
|
-
acl: env('UPLOADTHING_ACL', 'public-read'),
|
|
82
|
-
privateFiles: env.bool('UPLOADTHING_PRIVATE_FILES', false),
|
|
83
|
-
contentDisposition: env('UPLOADTHING_CONTENT_DISPOSITION', 'inline'),
|
|
84
|
-
signedUrlExpiresIn: env.int('UPLOADTHING_SIGNED_URL_EXPIRES_IN', 3600),
|
|
85
|
-
uploadConcurrency: env.int('UPLOADTHING_UPLOAD_CONCURRENCY', 1),
|
|
86
|
-
uploadRetries: env.int('UPLOADTHING_UPLOAD_RETRIES', 2),
|
|
87
|
-
useCustomId: env.bool('UPLOADTHING_USE_CUSTOM_ID', true),
|
|
88
|
-
logLevel: env('UPLOADTHING_LOG_LEVEL', 'Info'),
|
|
89
|
-
},
|
|
90
|
-
actionOptions: {
|
|
91
|
-
upload: {},
|
|
92
|
-
uploadStream: {},
|
|
93
|
-
delete: {},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
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:
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
import type { Core } from '@strapi/strapi';
|
|
104
|
-
|
|
105
|
-
const config: Core.Config.Middlewares = [
|
|
106
|
-
'strapi::logger',
|
|
107
|
-
'strapi::errors',
|
|
108
|
-
{
|
|
109
|
-
name: 'strapi::security',
|
|
110
|
-
config: {
|
|
111
|
-
contentSecurityPolicy: {
|
|
112
|
-
useDefaults: true,
|
|
113
|
-
directives: {
|
|
114
|
-
'connect-src': ["'self'", 'https:'],
|
|
115
|
-
'img-src': ["'self'", 'data:', 'blob:', 'https://*.ufs.sh', 'https://utfs.io'],
|
|
116
|
-
'media-src': ["'self'", 'data:', 'blob:', 'https://*.ufs.sh', 'https://utfs.io'],
|
|
117
|
-
upgradeInsecureRequests: null,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
'strapi::cors',
|
|
123
|
-
'strapi::poweredBy',
|
|
124
|
-
'strapi::query',
|
|
125
|
-
'strapi::body',
|
|
126
|
-
'strapi::session',
|
|
127
|
-
'strapi::favicon',
|
|
128
|
-
'strapi::public',
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
export default config;
|
|
132
|
-
```
|
|
133
|
-
|
|
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`.
|
|
169
|
-
|
|
170
|
-
## Provider Options
|
|
171
|
-
|
|
172
|
-
| Option | Type | Default | Description |
|
|
173
|
-
| --- | --- | --- | --- |
|
|
174
|
-
| `token` | `string` | `process.env.UPLOADTHING_TOKEN` | UploadThing token used to initialize `UTApi`. |
|
|
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`. |
|
|
180
|
-
| `uploadRetries` | `number` | `2` | Number of retry attempts for transient UploadThing upload failures. |
|
|
181
|
-
| `useCustomId` | `boolean` | `true` | Uses a deterministic UploadThing `customId` based on the Strapi file hash and extension. |
|
|
182
|
-
| `apiUrl` | `string` | `undefined` | Optional custom UploadThing API URL. |
|
|
183
|
-
| `ingestUrl` | `string` | `undefined` | Optional custom UploadThing ingest URL. |
|
|
184
|
-
| `logLevel` | `string` | `undefined` | Optional UploadThing log level. |
|
|
185
|
-
| `logFormat` | `string` | `undefined` | Optional UploadThing log format. |
|
|
186
|
-
| `isDev` | `boolean` | `undefined` | Optional UploadThing development mode flag. |
|
|
187
|
-
|
|
188
|
-
## Stored Metadata
|
|
189
|
-
|
|
190
|
-
After upload, this provider stores UploadThing-specific metadata in:
|
|
191
|
-
|
|
192
|
-
```txt
|
|
193
|
-
provider_metadata.uploadthing
|
|
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/)
|
|
1
|
+
# @vivinkv28/strapi-provider-uploadthing
|
|
2
|
+
|
|
3
|
+
UploadThing provider for the Strapi Uploads
|
|
4
|
+
|
|
5
|
+
This provider stores Strapi Media Library files in UploadThing while keeping file records and metadata inside Strapi.
|
|
6
|
+
|
|
7
|
+
## What This Provider Does
|
|
8
|
+
|
|
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
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js `>= 20.0.0`
|
|
22
|
+
- Strapi v5
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the provider in your Strapi project:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @vivinkv28/strapi-provider-uploadthing
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
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.
|
|
38
|
+
|
|
39
|
+
## Environment Variables
|
|
40
|
+
|
|
41
|
+
Minimum required:
|
|
42
|
+
|
|
43
|
+
```env
|
|
44
|
+
UPLOADTHING_TOKEN=your_uploadthing_token
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Typical public-file setup:
|
|
48
|
+
|
|
49
|
+
```env
|
|
50
|
+
UPLOADTHING_TOKEN=your_uploadthing_token
|
|
51
|
+
UPLOADTHING_ACL=public-read
|
|
52
|
+
UPLOADTHING_PRIVATE_FILES=false
|
|
53
|
+
UPLOADTHING_CONTENT_DISPOSITION=inline
|
|
54
|
+
UPLOADTHING_SIGNED_URL_EXPIRES_IN=3600
|
|
55
|
+
UPLOADTHING_UPLOAD_CONCURRENCY=1
|
|
56
|
+
UPLOADTHING_UPLOAD_RETRIES=2
|
|
57
|
+
UPLOADTHING_USE_CUSTOM_ID=true
|
|
58
|
+
UPLOADTHING_LOG_LEVEL=Info
|
|
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
|
+
|
|
70
|
+
## Strapi Configuration
|
|
71
|
+
|
|
72
|
+
Create or update `./config/plugins.ts`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export default ({ env }) => ({
|
|
76
|
+
upload: {
|
|
77
|
+
config: {
|
|
78
|
+
provider: '@vivinkv28/strapi-provider-uploadthing',
|
|
79
|
+
providerOptions: {
|
|
80
|
+
token: env('UPLOADTHING_TOKEN'),
|
|
81
|
+
acl: env('UPLOADTHING_ACL', 'public-read'),
|
|
82
|
+
privateFiles: env.bool('UPLOADTHING_PRIVATE_FILES', false),
|
|
83
|
+
contentDisposition: env('UPLOADTHING_CONTENT_DISPOSITION', 'inline'),
|
|
84
|
+
signedUrlExpiresIn: env.int('UPLOADTHING_SIGNED_URL_EXPIRES_IN', 3600),
|
|
85
|
+
uploadConcurrency: env.int('UPLOADTHING_UPLOAD_CONCURRENCY', 1),
|
|
86
|
+
uploadRetries: env.int('UPLOADTHING_UPLOAD_RETRIES', 2),
|
|
87
|
+
useCustomId: env.bool('UPLOADTHING_USE_CUSTOM_ID', true),
|
|
88
|
+
logLevel: env('UPLOADTHING_LOG_LEVEL', 'Info'),
|
|
89
|
+
},
|
|
90
|
+
actionOptions: {
|
|
91
|
+
upload: {},
|
|
92
|
+
uploadStream: {},
|
|
93
|
+
delete: {},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
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:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import type { Core } from '@strapi/strapi';
|
|
104
|
+
|
|
105
|
+
const config: Core.Config.Middlewares = [
|
|
106
|
+
'strapi::logger',
|
|
107
|
+
'strapi::errors',
|
|
108
|
+
{
|
|
109
|
+
name: 'strapi::security',
|
|
110
|
+
config: {
|
|
111
|
+
contentSecurityPolicy: {
|
|
112
|
+
useDefaults: true,
|
|
113
|
+
directives: {
|
|
114
|
+
'connect-src': ["'self'", 'https:'],
|
|
115
|
+
'img-src': ["'self'", 'data:', 'blob:', 'https://*.ufs.sh', 'https://utfs.io'],
|
|
116
|
+
'media-src': ["'self'", 'data:', 'blob:', 'https://*.ufs.sh', 'https://utfs.io'],
|
|
117
|
+
upgradeInsecureRequests: null,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
'strapi::cors',
|
|
123
|
+
'strapi::poweredBy',
|
|
124
|
+
'strapi::query',
|
|
125
|
+
'strapi::body',
|
|
126
|
+
'strapi::session',
|
|
127
|
+
'strapi::favicon',
|
|
128
|
+
'strapi::public',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
export default config;
|
|
132
|
+
```
|
|
133
|
+
|
|
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`.
|
|
169
|
+
|
|
170
|
+
## Provider Options
|
|
171
|
+
|
|
172
|
+
| Option | Type | Default | Description |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| `token` | `string` | `process.env.UPLOADTHING_TOKEN` | UploadThing token used to initialize `UTApi`. |
|
|
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`. |
|
|
180
|
+
| `uploadRetries` | `number` | `2` | Number of retry attempts for transient UploadThing upload failures. |
|
|
181
|
+
| `useCustomId` | `boolean` | `true` | Uses a deterministic UploadThing `customId` based on the Strapi file hash and extension. |
|
|
182
|
+
| `apiUrl` | `string` | `undefined` | Optional custom UploadThing API URL. |
|
|
183
|
+
| `ingestUrl` | `string` | `undefined` | Optional custom UploadThing ingest URL. |
|
|
184
|
+
| `logLevel` | `string` | `undefined` | Optional UploadThing log level. |
|
|
185
|
+
| `logFormat` | `string` | `undefined` | Optional UploadThing log format. |
|
|
186
|
+
| `isDev` | `boolean` | `undefined` | Optional UploadThing development mode flag. |
|
|
187
|
+
|
|
188
|
+
## Stored Metadata
|
|
189
|
+
|
|
190
|
+
After upload, this provider stores UploadThing-specific metadata in:
|
|
191
|
+
|
|
192
|
+
```txt
|
|
193
|
+
provider_metadata.uploadthing
|
|
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/)
|
package/dist/server/index.js
CHANGED
|
@@ -222,12 +222,12 @@ const provider = (providerOptions = {}) => {
|
|
|
222
222
|
if (!key) {
|
|
223
223
|
return file;
|
|
224
224
|
}
|
|
225
|
-
const signed = await utapi.
|
|
225
|
+
const signed = await utapi.generateSignedURL(key, {
|
|
226
226
|
expiresIn: resolvedSignedUrlTtl,
|
|
227
227
|
keyType
|
|
228
228
|
});
|
|
229
229
|
return {
|
|
230
|
-
url: signed.ufsUrl
|
|
230
|
+
url: signed.ufsUrl
|
|
231
231
|
};
|
|
232
232
|
},
|
|
233
233
|
async uploadStream(file) {
|
|
@@ -276,4 +276,3 @@ const index = {
|
|
|
276
276
|
services
|
|
277
277
|
};
|
|
278
278
|
exports.default = index;
|
|
279
|
-
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
|
@@ -218,12 +218,12 @@ const provider = (providerOptions = {}) => {
|
|
|
218
218
|
if (!key) {
|
|
219
219
|
return file;
|
|
220
220
|
}
|
|
221
|
-
const signed = await utapi.
|
|
221
|
+
const signed = await utapi.generateSignedURL(key, {
|
|
222
222
|
expiresIn: resolvedSignedUrlTtl,
|
|
223
223
|
keyType
|
|
224
224
|
});
|
|
225
225
|
return {
|
|
226
|
-
url: signed.ufsUrl
|
|
226
|
+
url: signed.ufsUrl
|
|
227
227
|
};
|
|
228
228
|
},
|
|
229
229
|
async uploadStream(file) {
|
|
@@ -274,4 +274,3 @@ const index = {
|
|
|
274
274
|
export {
|
|
275
275
|
index as default
|
|
276
276
|
};
|
|
277
|
-
//# sourceMappingURL=index.mjs.map
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const provider =
|
|
3
|
+
const server = require('./dist/server');
|
|
4
|
+
const provider = server.default?.services?.provider || server.services?.provider;
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
init: provider,
|
package/package.json
CHANGED
|
@@ -1,57 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vivinkv28/strapi-provider-uploadthing",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "UploadThing provider for the Strapi Upload plugin",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
},
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@vivinkv28/strapi-provider-uploadthing",
|
|
3
|
+
"version": "0.1.7",
|
|
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
|
+
},
|
|
13
|
+
"main": "index.js",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./index.js",
|
|
16
|
+
"./strapi-server": {
|
|
17
|
+
"source": "./server/src/index.js",
|
|
18
|
+
"import": "./dist/server/index.mjs",
|
|
19
|
+
"require": "./dist/server/index.js",
|
|
20
|
+
"default": "./dist/server/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/",
|
|
26
|
+
"index.js",
|
|
27
|
+
"server",
|
|
28
|
+
"strapi-server.js",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"strapi",
|
|
34
|
+
"strapi-plugin",
|
|
35
|
+
"uploadthing",
|
|
36
|
+
"upload",
|
|
37
|
+
"provider",
|
|
38
|
+
"media-library"
|
|
39
|
+
],
|
|
40
|
+
"strapi": {
|
|
41
|
+
"kind": "plugin",
|
|
42
|
+
"name": "strapi-upload-things",
|
|
43
|
+
"displayName": "UploadThing Provider",
|
|
44
|
+
"description": "UploadThing integration for the Strapi upload plugin"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=20.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js build",
|
|
55
|
+
"watch": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js watch",
|
|
56
|
+
"watch:link": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js watch:link",
|
|
57
|
+
"verify": "node ./node_modules/@strapi/sdk-plugin/bin/strapi-plugin.js verify",
|
|
58
|
+
"prepack": "npm run build"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"uploadthing": "^7.7.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@babel/runtime": "^7.29.2",
|
|
65
|
+
"@strapi/sdk-plugin": "^6.1.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -271,13 +271,13 @@ export default (providerOptions = {}) => {
|
|
|
271
271
|
return file;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const signed = await utapi.
|
|
274
|
+
const signed = await utapi.generateSignedURL(key, {
|
|
275
275
|
expiresIn: resolvedSignedUrlTtl,
|
|
276
276
|
keyType,
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
return {
|
|
280
|
-
url: signed.ufsUrl
|
|
280
|
+
url: signed.ufsUrl,
|
|
281
281
|
};
|
|
282
282
|
},
|
|
283
283
|
|
package/dist/server/index.js.map
DELETED
|
@@ -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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"}
|