@nextlyhq/storage-uploadthing 0.0.1
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 +22 -0
- package/README.md +75 -0
- package/dist/chunk-3GDQP6AS.mjs +14 -0
- package/dist/chunk-3GDQP6AS.mjs.map +1 -0
- package/dist/chunk-CV4XIHXE.mjs +255 -0
- package/dist/chunk-CV4XIHXE.mjs.map +1 -0
- package/dist/index.cjs +242 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +475 -0
- package/dist/index.d.ts +475 -0
- package/dist/index.mjs +142 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib-D34FQA4J.mjs +6285 -0
- package/dist/lib-D34FQA4J.mjs.map +1 -0
- package/dist/local-plugin-PTET4NAT-P6OHYYH6.mjs +4 -0
- package/dist/local-plugin-PTET4NAT-P6OHYYH6.mjs.map +1 -0
- package/dist/main-GMP6CIN5.mjs +400 -0
- package/dist/main-GMP6CIN5.mjs.map +1 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Storage Types
|
|
3
|
+
*
|
|
4
|
+
* Defines interfaces and types for the unified media storage system.
|
|
5
|
+
* Supports cloud storage adapters via plugins.
|
|
6
|
+
*
|
|
7
|
+
* Storage Backends:
|
|
8
|
+
* - AWS S3 / Cloudflare R2 / MinIO (via @nextlyhq/storage-s3)
|
|
9
|
+
* - Vercel Blob (via @nextlyhq/storage-vercel-blob)
|
|
10
|
+
*/
|
|
11
|
+
interface UploadOptions {
|
|
12
|
+
/** Original filename from user */
|
|
13
|
+
filename: string;
|
|
14
|
+
/** MIME type (e.g., 'image/png', 'video/mp4') */
|
|
15
|
+
mimeType: string;
|
|
16
|
+
/** Optional content type override */
|
|
17
|
+
contentType?: string;
|
|
18
|
+
/** Optional folder/prefix for organizing uploads */
|
|
19
|
+
folder?: string;
|
|
20
|
+
/** Collection slug this upload belongs to (for collection-specific storage) */
|
|
21
|
+
collection?: string;
|
|
22
|
+
/** Optional Content-Disposition header value (e.g., 'attachment' for SVG security) */
|
|
23
|
+
contentDisposition?: "inline" | "attachment";
|
|
24
|
+
}
|
|
25
|
+
interface UploadResult {
|
|
26
|
+
/** Public URL to access the file */
|
|
27
|
+
url: string;
|
|
28
|
+
/** Storage path/key (for deletion and metadata retrieval) */
|
|
29
|
+
path: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extended file metadata returned by getMetadata()
|
|
33
|
+
*
|
|
34
|
+
* Contains comprehensive information about an uploaded file,
|
|
35
|
+
* including dimensions for images and creation timestamps.
|
|
36
|
+
*/
|
|
37
|
+
interface FileMetadata {
|
|
38
|
+
/** Unique identifier (typically the storage path/key) */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Storage filename (may differ from original) */
|
|
41
|
+
filename: string;
|
|
42
|
+
/** Original filename as uploaded by user */
|
|
43
|
+
originalFilename: string;
|
|
44
|
+
/** MIME type (e.g., 'image/jpeg', 'application/pdf') */
|
|
45
|
+
mimeType: string;
|
|
46
|
+
/** File size in bytes */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Public URL to access the file */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Thumbnail URL for images (if generated) */
|
|
51
|
+
thumbnailUrl?: string;
|
|
52
|
+
/** Image width in pixels (for images only) */
|
|
53
|
+
width?: number;
|
|
54
|
+
/** Image height in pixels (for images only) */
|
|
55
|
+
height?: number;
|
|
56
|
+
/** ISO timestamp when file was uploaded */
|
|
57
|
+
createdAt: string;
|
|
58
|
+
/** ISO timestamp when file was last modified */
|
|
59
|
+
updatedAt?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Storage type identifier.
|
|
63
|
+
* - "s3": AWS S3 or S3-compatible services (R2, MinIO, DigitalOcean Spaces)
|
|
64
|
+
* - "vercel-blob": Vercel Blob Storage
|
|
65
|
+
* - "local": Local disk storage (default for development)
|
|
66
|
+
* - "uploadthing": Uploadthing cloud storage
|
|
67
|
+
*/
|
|
68
|
+
type StorageType = "s3" | "vercel-blob" | "local" | "uploadthing";
|
|
69
|
+
/**
|
|
70
|
+
* Information about a storage adapter's capabilities.
|
|
71
|
+
* Returned by adapter.getInfo() method.
|
|
72
|
+
*/
|
|
73
|
+
interface StorageAdapterInfo {
|
|
74
|
+
/** Storage type identifier */
|
|
75
|
+
type: StorageType;
|
|
76
|
+
/** Human-readable adapter name */
|
|
77
|
+
name: string;
|
|
78
|
+
/** Whether this adapter supports signed URLs for private access */
|
|
79
|
+
supportsSignedUrls: boolean;
|
|
80
|
+
/** Whether this adapter supports client-side (direct) uploads */
|
|
81
|
+
supportsClientUploads: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Per-collection storage configuration.
|
|
85
|
+
* Allows customizing storage behavior for specific upload collections.
|
|
86
|
+
*/
|
|
87
|
+
interface CollectionStorageConfig {
|
|
88
|
+
/** Prefix/folder for this collection's uploads */
|
|
89
|
+
prefix?: string;
|
|
90
|
+
/** Enable client-side uploads (for serverless platforms with body size limits) */
|
|
91
|
+
clientUploads?: boolean;
|
|
92
|
+
/** Generate signed URLs for downloads (for private buckets) */
|
|
93
|
+
signedDownloads?: boolean;
|
|
94
|
+
/** Signed URL expiry time in seconds (default: 3600) */
|
|
95
|
+
signedUrlExpiresIn?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Collection storage map - maps collection slugs to their config.
|
|
99
|
+
* Used in storage plugin configuration.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* {
|
|
104
|
+
* media: true, // Use default config
|
|
105
|
+
* 'private-docs': {
|
|
106
|
+
* prefix: 'private/',
|
|
107
|
+
* signedDownloads: true,
|
|
108
|
+
* signedUrlExpiresIn: 900
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
type CollectionStorageMap = Record<string, boolean | CollectionStorageConfig>;
|
|
114
|
+
/**
|
|
115
|
+
* Base configuration for storage plugins.
|
|
116
|
+
* Extended by specific adapter configs (S3StorageConfig, etc.)
|
|
117
|
+
*/
|
|
118
|
+
interface StoragePluginConfig {
|
|
119
|
+
/** Enable/disable the plugin (default: true) */
|
|
120
|
+
enabled?: boolean;
|
|
121
|
+
/** Collections to apply this storage adapter to */
|
|
122
|
+
collections: CollectionStorageMap;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Storage plugin returned by adapter plugin functions.
|
|
126
|
+
* These are processed during Nextly initialization.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* // From @nextlyhq/storage-s3
|
|
131
|
+
* const plugin = s3Storage({
|
|
132
|
+
* bucket: 'my-bucket',
|
|
133
|
+
* region: 'us-east-1',
|
|
134
|
+
* collections: { media: true }
|
|
135
|
+
* });
|
|
136
|
+
* // plugin implements StoragePlugin
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
interface StoragePlugin {
|
|
140
|
+
/** Plugin name for identification */
|
|
141
|
+
name: string;
|
|
142
|
+
/** Storage type */
|
|
143
|
+
type: StorageType;
|
|
144
|
+
/** Collections this plugin handles */
|
|
145
|
+
collections: CollectionStorageMap;
|
|
146
|
+
/** The storage adapter instance */
|
|
147
|
+
adapter: IStorageAdapter;
|
|
148
|
+
/**
|
|
149
|
+
* Handler for generating client-side upload URLs.
|
|
150
|
+
* Called when clientUploads is enabled for a collection.
|
|
151
|
+
*/
|
|
152
|
+
getClientUploadUrl?: (filename: string, mimeType: string, collection: string) => Promise<ClientUploadData>;
|
|
153
|
+
/**
|
|
154
|
+
* Handler for generating signed download URLs.
|
|
155
|
+
* Called when signedDownloads is enabled for a collection.
|
|
156
|
+
*/
|
|
157
|
+
getSignedDownloadUrl?: (path: string, expiresIn?: number) => Promise<string>;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Data returned for client-side (direct) uploads.
|
|
161
|
+
* Contains pre-signed URL and headers for direct-to-storage uploads.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* // Usage in frontend
|
|
166
|
+
* const uploadData = await fetch('/api/nextly/storage/upload-url', {
|
|
167
|
+
* method: 'POST',
|
|
168
|
+
* body: JSON.stringify({ filename: 'photo.jpg', mimeType: 'image/jpeg', collection: 'media' })
|
|
169
|
+
* }).then(r => r.json());
|
|
170
|
+
*
|
|
171
|
+
* // Direct upload to storage
|
|
172
|
+
* await fetch(uploadData.uploadUrl, {
|
|
173
|
+
* method: uploadData.method,
|
|
174
|
+
* headers: uploadData.headers,
|
|
175
|
+
* body: file
|
|
176
|
+
* });
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
interface ClientUploadData {
|
|
180
|
+
/** Pre-signed URL for direct upload */
|
|
181
|
+
uploadUrl: string;
|
|
182
|
+
/** Storage path/key that will be used */
|
|
183
|
+
path: string;
|
|
184
|
+
/** HTTP method to use (usually PUT for S3, POST for some services) */
|
|
185
|
+
method: "PUT" | "POST";
|
|
186
|
+
/** Headers to include in upload request */
|
|
187
|
+
headers?: Record<string, string>;
|
|
188
|
+
/** Form fields for multipart uploads (some services require this) */
|
|
189
|
+
fields?: Record<string, string>;
|
|
190
|
+
/** URL expiry timestamp */
|
|
191
|
+
expiresAt: Date;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Base storage adapter interface.
|
|
195
|
+
* All storage adapters must implement this interface.
|
|
196
|
+
*
|
|
197
|
+
* Core methods (required):
|
|
198
|
+
* - upload: Store file buffer
|
|
199
|
+
* - delete: Remove file from storage
|
|
200
|
+
* - exists: Check if file exists
|
|
201
|
+
* - getPublicUrl: Get public URL for file access
|
|
202
|
+
* - getType: Get storage type identifier
|
|
203
|
+
*
|
|
204
|
+
* Optional methods:
|
|
205
|
+
* - getInfo: Get adapter capabilities (recommended)
|
|
206
|
+
* - getMetadata: Retrieve file metadata
|
|
207
|
+
* - getSignedUrl: Generate temporary signed URLs for private access
|
|
208
|
+
* - getPresignedUploadUrl: Generate pre-signed URL for client uploads
|
|
209
|
+
*/
|
|
210
|
+
interface BulkDeleteResult {
|
|
211
|
+
successful: string[];
|
|
212
|
+
failed: Array<{
|
|
213
|
+
filePath: string;
|
|
214
|
+
error: string;
|
|
215
|
+
}>;
|
|
216
|
+
}
|
|
217
|
+
interface IStorageAdapter {
|
|
218
|
+
/** Upload file buffer to storage */
|
|
219
|
+
upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult>;
|
|
220
|
+
/** Delete file from storage */
|
|
221
|
+
delete(filePath: string): Promise<void>;
|
|
222
|
+
/** Bulk delete files from storage. Optional — adapters that support batch operations should implement this. */
|
|
223
|
+
bulkDelete?(filePaths: string[]): Promise<BulkDeleteResult>;
|
|
224
|
+
/** Check if file exists in storage */
|
|
225
|
+
exists(filePath: string): Promise<boolean>;
|
|
226
|
+
/** Get public URL for file */
|
|
227
|
+
getPublicUrl(filePath: string): string;
|
|
228
|
+
/** Get storage type identifier */
|
|
229
|
+
getType(): string;
|
|
230
|
+
/** Read file contents from storage (optional - not all adapters support this) */
|
|
231
|
+
read?(filePath: string): Promise<Buffer | null>;
|
|
232
|
+
/** Get adapter info including capabilities (optional but recommended) */
|
|
233
|
+
getInfo?(): StorageAdapterInfo;
|
|
234
|
+
/** Get file metadata (optional - not all adapters support this) */
|
|
235
|
+
getMetadata?(filePath: string): Promise<FileMetadata | null>;
|
|
236
|
+
/** Generate signed URL for temporary private access (optional) */
|
|
237
|
+
getSignedUrl?(filePath: string, expiresIn?: number): Promise<string>;
|
|
238
|
+
/** Generate pre-signed upload URL for client-side uploads (optional) */
|
|
239
|
+
getPresignedUploadUrl?(key: string, mimeType: string, expiresIn?: number): Promise<ClientUploadData>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Base Storage Adapter
|
|
244
|
+
*
|
|
245
|
+
* BaseStorageAdapter abstract class with common functionality for storage
|
|
246
|
+
* adapters. Concrete adapters extend this class to inherit auto-detected
|
|
247
|
+
* capabilities, sanitizeFilename(), generateKey(), etc.
|
|
248
|
+
*
|
|
249
|
+
* For the IStorageAdapter interface itself, import from ../types directly.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Abstract base class for storage adapters.
|
|
254
|
+
*
|
|
255
|
+
* Provides common functionality and helper methods that all storage adapters
|
|
256
|
+
* can use. Concrete adapters should extend this class to inherit:
|
|
257
|
+
* - Default getInfo() implementation with auto-detected capabilities
|
|
258
|
+
* - sanitizeFilename() helper for secure filename handling
|
|
259
|
+
* - generateKey() helper for unique storage key generation
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* class MyStorageAdapter extends BaseStorageAdapter {
|
|
264
|
+
* async upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult> {
|
|
265
|
+
* const key = this.generateKey(options.filename, options.folder);
|
|
266
|
+
* const sanitized = this.sanitizeFilename(options.filename);
|
|
267
|
+
* // ... upload logic
|
|
268
|
+
* }
|
|
269
|
+
*
|
|
270
|
+
* async delete(filePath: string): Promise<void> { ... }
|
|
271
|
+
* async exists(filePath: string): Promise<boolean> { ... }
|
|
272
|
+
* getPublicUrl(filePath: string): string { ... }
|
|
273
|
+
* getType(): string { return 'my-storage'; }
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
declare abstract class BaseStorageAdapter implements IStorageAdapter {
|
|
278
|
+
/**
|
|
279
|
+
* Upload file buffer to storage.
|
|
280
|
+
* Must be implemented by concrete adapters.
|
|
281
|
+
*/
|
|
282
|
+
abstract upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult>;
|
|
283
|
+
/**
|
|
284
|
+
* Delete file from storage.
|
|
285
|
+
* Must be implemented by concrete adapters.
|
|
286
|
+
*/
|
|
287
|
+
abstract delete(filePath: string): Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* Check if file exists in storage.
|
|
290
|
+
* Must be implemented by concrete adapters.
|
|
291
|
+
*/
|
|
292
|
+
abstract exists(filePath: string): Promise<boolean>;
|
|
293
|
+
/**
|
|
294
|
+
* Get public URL for file.
|
|
295
|
+
* Must be implemented by concrete adapters.
|
|
296
|
+
*/
|
|
297
|
+
abstract getPublicUrl(filePath: string): string;
|
|
298
|
+
/**
|
|
299
|
+
* Get storage type identifier.
|
|
300
|
+
* Must be implemented by concrete adapters.
|
|
301
|
+
*/
|
|
302
|
+
abstract getType(): string;
|
|
303
|
+
/**
|
|
304
|
+
* Get adapter info including capabilities.
|
|
305
|
+
*
|
|
306
|
+
* Default implementation that auto-detects capabilities by checking
|
|
307
|
+
* if getSignedUrl and getPresignedUploadUrl methods are implemented.
|
|
308
|
+
* Override in subclasses for more accurate capability reporting.
|
|
309
|
+
*
|
|
310
|
+
* @returns Adapter info with type, name, and capability flags
|
|
311
|
+
*/
|
|
312
|
+
getInfo(): StorageAdapterInfo;
|
|
313
|
+
/**
|
|
314
|
+
* Sanitize filename to prevent directory traversal and storage issues.
|
|
315
|
+
*
|
|
316
|
+
* Security measures:
|
|
317
|
+
* - Remove path separators (/, \)
|
|
318
|
+
* - Keep only basename (no directories)
|
|
319
|
+
* - Replace problematic characters with hyphens
|
|
320
|
+
* - Preserve alphanumeric, dots, underscores, hyphens
|
|
321
|
+
*
|
|
322
|
+
* @param filename - Original filename to sanitize
|
|
323
|
+
* @returns Sanitized filename safe for storage
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* this.sanitizeFilename('../../../etc/passwd') // 'passwd'
|
|
328
|
+
* this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'
|
|
329
|
+
* this.sanitizeFilename('photo.jpg') // 'photo.jpg'
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
protected sanitizeFilename(filename: string): string;
|
|
333
|
+
/**
|
|
334
|
+
* Generate a unique storage key with date-based prefix.
|
|
335
|
+
*
|
|
336
|
+
* Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}
|
|
337
|
+
* This provides:
|
|
338
|
+
* - Unique keys via UUID to prevent collisions
|
|
339
|
+
* - Date-based organization for easier management
|
|
340
|
+
* - Readable filenames for debugging
|
|
341
|
+
*
|
|
342
|
+
* @param filename - Original filename (will be sanitized)
|
|
343
|
+
* @param folder - Optional folder/prefix for organizing uploads
|
|
344
|
+
* @returns Generated storage key
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* this.generateKey('photo.jpg')
|
|
349
|
+
* // 'uploads/2026/01/abc-123-...-photo.jpg'
|
|
350
|
+
*
|
|
351
|
+
* this.generateKey('doc.pdf', 'documents')
|
|
352
|
+
* // 'documents/2026/01/abc-123-...-doc.pdf'
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
protected generateKey(filename: string, folder?: string): string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Uploadthing Storage Types
|
|
360
|
+
*
|
|
361
|
+
* Configuration for the @nextlyhq/storage-uploadthing package.
|
|
362
|
+
*/
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Uploadthing storage adapter configuration.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* uploadthingStorage({
|
|
370
|
+
* token: process.env.UPLOADTHING_TOKEN,
|
|
371
|
+
* collections: { media: true }
|
|
372
|
+
* })
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
interface UploadthingStorageConfig extends StoragePluginConfig {
|
|
376
|
+
/** Enable/disable this storage plugin (default: true). */
|
|
377
|
+
enabled?: boolean;
|
|
378
|
+
/** Collections this plugin handles. */
|
|
379
|
+
collections: Record<string, boolean | CollectionStorageConfig>;
|
|
380
|
+
/**
|
|
381
|
+
* Uploadthing API token.
|
|
382
|
+
* If not provided, reads from UPLOADTHING_TOKEN env var.
|
|
383
|
+
*/
|
|
384
|
+
token?: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Uploadthing Storage Plugin
|
|
389
|
+
*
|
|
390
|
+
* Factory function that creates a storage plugin for Uploadthing.
|
|
391
|
+
* Returns a StoragePlugin that can be registered with MediaStorage.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```typescript
|
|
395
|
+
* import { uploadthingStorage } from '@nextlyhq/storage-uploadthing'
|
|
396
|
+
* import { defineConfig } from 'nextly/config'
|
|
397
|
+
*
|
|
398
|
+
* export default defineConfig({
|
|
399
|
+
* storage: [
|
|
400
|
+
* uploadthingStorage({
|
|
401
|
+
* token: process.env.UPLOADTHING_TOKEN,
|
|
402
|
+
* collections: { media: true }
|
|
403
|
+
* })
|
|
404
|
+
* ]
|
|
405
|
+
* })
|
|
406
|
+
* ```
|
|
407
|
+
*/
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create an Uploadthing storage plugin for Nextly.
|
|
411
|
+
*
|
|
412
|
+
* @param config - Uploadthing storage configuration
|
|
413
|
+
* @returns A StoragePlugin that MediaStorage can register
|
|
414
|
+
*/
|
|
415
|
+
declare function uploadthingStorage(config: UploadthingStorageConfig): StoragePlugin;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Uploadthing Storage Adapter
|
|
419
|
+
*
|
|
420
|
+
* Implements the Nextly storage adapter interface using Uploadthing's UTApi
|
|
421
|
+
* for server-side file operations. Files are served via Uploadthing's CDN.
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```typescript
|
|
425
|
+
* const adapter = new UploadthingStorageAdapter({ token: process.env.UPLOADTHING_TOKEN });
|
|
426
|
+
* const result = await adapter.upload(buffer, {
|
|
427
|
+
* filename: 'photo.jpg',
|
|
428
|
+
* mimeType: 'image/jpeg',
|
|
429
|
+
* });
|
|
430
|
+
* // result.url = 'https://utfs.io/f/abc123-photo.jpg'
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
declare class UploadthingStorageAdapter extends BaseStorageAdapter {
|
|
435
|
+
private readonly utapi;
|
|
436
|
+
constructor(config: {
|
|
437
|
+
token?: string;
|
|
438
|
+
});
|
|
439
|
+
/**
|
|
440
|
+
* Upload file to Uploadthing.
|
|
441
|
+
* Creates a File object from the buffer and uploads via UTApi.
|
|
442
|
+
*/
|
|
443
|
+
upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult>;
|
|
444
|
+
/**
|
|
445
|
+
* Delete file from Uploadthing by its file key.
|
|
446
|
+
*/
|
|
447
|
+
delete(filePath: string): Promise<void>;
|
|
448
|
+
/**
|
|
449
|
+
* Bulk delete files from Uploadthing.
|
|
450
|
+
* UTApi natively supports batch deletion.
|
|
451
|
+
*/
|
|
452
|
+
bulkDelete(filePaths: string[]): Promise<BulkDeleteResult>;
|
|
453
|
+
/**
|
|
454
|
+
* Check if file exists on Uploadthing.
|
|
455
|
+
* Uses getFileUrls - if it returns data with URLs, the file exists.
|
|
456
|
+
*/
|
|
457
|
+
exists(filePath: string): Promise<boolean>;
|
|
458
|
+
/**
|
|
459
|
+
* Get public URL for a file.
|
|
460
|
+
* Uploadthing files are served from utfs.io CDN.
|
|
461
|
+
* The URL is stored at upload time, so this reconstructs it from the key.
|
|
462
|
+
*/
|
|
463
|
+
getPublicUrl(filePath: string): string;
|
|
464
|
+
/**
|
|
465
|
+
* Get storage type identifier.
|
|
466
|
+
*/
|
|
467
|
+
getType(): string;
|
|
468
|
+
/**
|
|
469
|
+
* Keep filename sanitization local so this adapter remains stable
|
|
470
|
+
* even if upstream base adapter type declarations drift.
|
|
471
|
+
*/
|
|
472
|
+
protected sanitizeFilename(filename: string): string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export { UploadthingStorageAdapter, type UploadthingStorageConfig, uploadthingStorage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BaseStorageAdapter } from './chunk-CV4XIHXE.mjs';
|
|
2
|
+
import './chunk-3GDQP6AS.mjs';
|
|
3
|
+
import { UTApi } from 'uploadthing/server';
|
|
4
|
+
|
|
5
|
+
var UploadthingStorageAdapter = class extends BaseStorageAdapter {
|
|
6
|
+
utapi;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super();
|
|
9
|
+
this.utapi = new UTApi({
|
|
10
|
+
...config.token ? { token: config.token } : {}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Upload file to Uploadthing.
|
|
15
|
+
* Creates a File object from the buffer and uploads via UTApi.
|
|
16
|
+
*/
|
|
17
|
+
async upload(buffer, options) {
|
|
18
|
+
const sanitized = this.sanitizeFilename(options.filename);
|
|
19
|
+
const file = new File([buffer], sanitized, {
|
|
20
|
+
type: options.mimeType
|
|
21
|
+
});
|
|
22
|
+
const results = await this.utapi.uploadFiles([file], {
|
|
23
|
+
contentDisposition: options.contentDisposition ?? "attachment"
|
|
24
|
+
});
|
|
25
|
+
const result = results[0];
|
|
26
|
+
if (!result?.data) {
|
|
27
|
+
const errorMsg = result?.error?.message ?? "Unknown error";
|
|
28
|
+
throw new Error(`Uploadthing upload failed: ${errorMsg}`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
url: result.data.url,
|
|
32
|
+
// Use the file key as the storage path (needed for deletion)
|
|
33
|
+
path: result.data.key
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Delete file from Uploadthing by its file key.
|
|
38
|
+
*/
|
|
39
|
+
async delete(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
await this.utapi.deleteFiles([filePath], { keyType: "fileKey" });
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Bulk delete files from Uploadthing.
|
|
47
|
+
* UTApi natively supports batch deletion.
|
|
48
|
+
*/
|
|
49
|
+
async bulkDelete(filePaths) {
|
|
50
|
+
try {
|
|
51
|
+
await this.utapi.deleteFiles(filePaths, { keyType: "fileKey" });
|
|
52
|
+
return {
|
|
53
|
+
successful: filePaths,
|
|
54
|
+
failed: []
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : "Bulk delete failed";
|
|
58
|
+
return {
|
|
59
|
+
successful: [],
|
|
60
|
+
failed: filePaths.map((fp) => ({
|
|
61
|
+
filePath: fp,
|
|
62
|
+
error: message
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if file exists on Uploadthing.
|
|
69
|
+
* Uses getFileUrls - if it returns data with URLs, the file exists.
|
|
70
|
+
*/
|
|
71
|
+
async exists(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.utapi.getFileUrls([filePath], {
|
|
74
|
+
keyType: "fileKey"
|
|
75
|
+
});
|
|
76
|
+
const items = Array.from(result.data);
|
|
77
|
+
return items.length > 0 && !!items[0]?.url;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get public URL for a file.
|
|
84
|
+
* Uploadthing files are served from utfs.io CDN.
|
|
85
|
+
* The URL is stored at upload time, so this reconstructs it from the key.
|
|
86
|
+
*/
|
|
87
|
+
getPublicUrl(filePath) {
|
|
88
|
+
return `https://utfs.io/f/${filePath}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get storage type identifier.
|
|
92
|
+
*/
|
|
93
|
+
getType() {
|
|
94
|
+
return "uploadthing";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Keep filename sanitization local so this adapter remains stable
|
|
98
|
+
* even if upstream base adapter type declarations drift.
|
|
99
|
+
*/
|
|
100
|
+
sanitizeFilename(filename) {
|
|
101
|
+
const basename = filename.split(/[/\\]/).pop() || filename;
|
|
102
|
+
return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/plugin.ts
|
|
107
|
+
function uploadthingStorage(config) {
|
|
108
|
+
if (config.enabled === false) {
|
|
109
|
+
return {
|
|
110
|
+
name: "uploadthing-storage",
|
|
111
|
+
type: "uploadthing",
|
|
112
|
+
collections: {},
|
|
113
|
+
adapter: null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const token = config.token ?? process.env.UPLOADTHING_TOKEN;
|
|
117
|
+
if (!token) {
|
|
118
|
+
console.warn(
|
|
119
|
+
"[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config."
|
|
120
|
+
);
|
|
121
|
+
return {
|
|
122
|
+
name: "uploadthing-storage",
|
|
123
|
+
type: "uploadthing",
|
|
124
|
+
collections: {},
|
|
125
|
+
adapter: null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const adapter = new UploadthingStorageAdapter({ token });
|
|
129
|
+
return {
|
|
130
|
+
name: "uploadthing-storage",
|
|
131
|
+
type: "uploadthing",
|
|
132
|
+
collections: config.collections,
|
|
133
|
+
adapter
|
|
134
|
+
// Uploadthing supports client-side uploads via its own pattern
|
|
135
|
+
// but we don't implement getClientUploadUrl here as it requires
|
|
136
|
+
// Uploadthing's specific route handler setup
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { UploadthingStorageAdapter, uploadthingStorage };
|
|
141
|
+
//# sourceMappingURL=index.mjs.map
|
|
142
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/plugin.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Uploadthing Storage Adapter\n *\n * Implements the Nextly storage adapter interface using Uploadthing's UTApi\n * for server-side file operations. Files are served via Uploadthing's CDN.\n *\n * @example\n * ```typescript\n * const adapter = new UploadthingStorageAdapter({ token: process.env.UPLOADTHING_TOKEN });\n * const result = await adapter.upload(buffer, {\n * filename: 'photo.jpg',\n * mimeType: 'image/jpeg',\n * });\n * // result.url = 'https://utfs.io/f/abc123-photo.jpg'\n * ```\n */\n\nimport { BaseStorageAdapter } from \"nextly/storage\";\nimport type {\n UploadOptions,\n UploadResult,\n BulkDeleteResult,\n} from \"nextly/storage\";\nimport { UTApi } from \"uploadthing/server\";\n\n// ============================================================\n// Adapter Implementation\n// ============================================================\n\nexport class UploadthingStorageAdapter extends BaseStorageAdapter {\n private readonly utapi: UTApi;\n\n constructor(config: { token?: string }) {\n super();\n // UTApi reads UPLOADTHING_TOKEN from env if not provided\n this.utapi = new UTApi({\n ...(config.token ? { token: config.token } : {}),\n });\n }\n\n /**\n * Upload file to Uploadthing.\n * Creates a File object from the buffer and uploads via UTApi.\n */\n async upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult> {\n const sanitized = this.sanitizeFilename(options.filename);\n\n // UTApi.uploadFiles expects File objects\n // Cast buffer to BlobPart to satisfy TS 5.9 strict ArrayBuffer typing\n const file = new File([buffer as unknown as BlobPart], sanitized, {\n type: options.mimeType,\n });\n\n // uploadFiles returns UploadFileResult[] — one result per file.\n // Default contentDisposition flipped from \"inline\" to \"attachment\".\n // An \"inline\" disposition lets the browser render the file\n // in-context (HTML, SVG, PDF with embedded JS) which can land as\n // XSS or drive-by; \"attachment\" forces the download dialog so the\n // user has to opt in to opening it. Adopters who genuinely want\n // inline rendering can still pass `contentDisposition: \"inline\"`\n // explicitly.\n const results = await this.utapi.uploadFiles([file], {\n contentDisposition: options.contentDisposition ?? \"attachment\",\n });\n\n // results is an array of { data: { key, url, ... } | null, error: ... | null }\n const result = results[0] as {\n data: { key: string; url: string } | null;\n error: { message: string } | null;\n };\n\n if (!result?.data) {\n const errorMsg = result?.error?.message ?? \"Unknown error\";\n throw new Error(`Uploadthing upload failed: ${errorMsg}`);\n }\n\n return {\n url: result.data.url,\n // Use the file key as the storage path (needed for deletion)\n path: result.data.key,\n };\n }\n\n /**\n * Delete file from Uploadthing by its file key.\n */\n async delete(filePath: string): Promise<void> {\n try {\n await this.utapi.deleteFiles([filePath], { keyType: \"fileKey\" });\n } catch {\n // Silently ignore deletion errors (file may already be gone)\n }\n }\n\n /**\n * Bulk delete files from Uploadthing.\n * UTApi natively supports batch deletion.\n */\n async bulkDelete(filePaths: string[]): Promise<BulkDeleteResult> {\n try {\n await this.utapi.deleteFiles(filePaths, { keyType: \"fileKey\" });\n return {\n successful: filePaths,\n failed: [],\n };\n } catch (error: unknown) {\n // If bulk delete fails entirely, report all as failed\n const message =\n error instanceof Error ? error.message : \"Bulk delete failed\";\n return {\n successful: [],\n failed: filePaths.map(fp => ({\n filePath: fp,\n error: message,\n })),\n };\n }\n }\n\n /**\n * Check if file exists on Uploadthing.\n * Uses getFileUrls - if it returns data with URLs, the file exists.\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n const result = await this.utapi.getFileUrls([filePath], {\n keyType: \"fileKey\",\n });\n // getFileUrls returns { data: readonly [{ url, key }] }\n const items = Array.from(result.data);\n return items.length > 0 && !!items[0]?.url;\n } catch {\n return false;\n }\n }\n\n /**\n * Get public URL for a file.\n * Uploadthing files are served from utfs.io CDN.\n * The URL is stored at upload time, so this reconstructs it from the key.\n */\n getPublicUrl(filePath: string): string {\n // Uploadthing URLs follow the pattern: https://utfs.io/f/{fileKey}\n return `https://utfs.io/f/${filePath}`;\n }\n\n /**\n * Get storage type identifier.\n */\n getType(): string {\n return \"uploadthing\";\n }\n\n /**\n * Keep filename sanitization local so this adapter remains stable\n * even if upstream base adapter type declarations drift.\n */\n protected sanitizeFilename(filename: string): string {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n}\n","/**\n * Uploadthing Storage Plugin\n *\n * Factory function that creates a storage plugin for Uploadthing.\n * Returns a StoragePlugin that can be registered with MediaStorage.\n *\n * @example\n * ```typescript\n * import { uploadthingStorage } from '@nextlyhq/storage-uploadthing'\n * import { defineConfig } from 'nextly/config'\n *\n * export default defineConfig({\n * storage: [\n * uploadthingStorage({\n * token: process.env.UPLOADTHING_TOKEN,\n * collections: { media: true }\n * })\n * ]\n * })\n * ```\n */\n\nimport type { StoragePlugin } from \"nextly/storage\";\n\nimport { UploadthingStorageAdapter } from \"./adapter\";\nimport type { UploadthingStorageConfig } from \"./types\";\n\n/**\n * Create an Uploadthing storage plugin for Nextly.\n *\n * @param config - Uploadthing storage configuration\n * @returns A StoragePlugin that MediaStorage can register\n */\nexport function uploadthingStorage(\n config: UploadthingStorageConfig\n): StoragePlugin {\n // Handle disabled plugin\n if (config.enabled === false) {\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n // Token from config or env\n const token = config.token ?? process.env.UPLOADTHING_TOKEN;\n\n if (!token) {\n console.warn(\n \"[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config.\"\n );\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n const adapter = new UploadthingStorageAdapter({ token });\n\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: config.collections,\n adapter,\n // Uploadthing supports client-side uploads via its own pattern\n // but we don't implement getClientUploadUrl here as it requires\n // Uploadthing's specific route handler setup\n };\n}\n"]}
|