@od-oneapp/storage 2026.1.1301

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.
Files changed (69) hide show
  1. package/README.md +854 -0
  2. package/dist/client-next.d.mts +61 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +111 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client-utils-Dx6W25iz.d.mts +43 -0
  7. package/dist/client-utils-Dx6W25iz.d.mts.map +1 -0
  8. package/dist/client.d.mts +28 -0
  9. package/dist/client.d.mts.map +1 -0
  10. package/dist/client.mjs +183 -0
  11. package/dist/client.mjs.map +1 -0
  12. package/dist/env-BVHLmQdh.mjs +128 -0
  13. package/dist/env-BVHLmQdh.mjs.map +1 -0
  14. package/dist/env.mjs +3 -0
  15. package/dist/health-check-D7LnnDec.mjs +746 -0
  16. package/dist/health-check-D7LnnDec.mjs.map +1 -0
  17. package/dist/health-check-im_huJ59.d.mts +116 -0
  18. package/dist/health-check-im_huJ59.d.mts.map +1 -0
  19. package/dist/index.d.mts +60 -0
  20. package/dist/index.d.mts.map +1 -0
  21. package/dist/index.mjs +3 -0
  22. package/dist/keys.d.mts +37 -0
  23. package/dist/keys.d.mts.map +1 -0
  24. package/dist/keys.mjs +253 -0
  25. package/dist/keys.mjs.map +1 -0
  26. package/dist/server-edge.d.mts +28 -0
  27. package/dist/server-edge.d.mts.map +1 -0
  28. package/dist/server-edge.mjs +88 -0
  29. package/dist/server-edge.mjs.map +1 -0
  30. package/dist/server-next.d.mts +183 -0
  31. package/dist/server-next.d.mts.map +1 -0
  32. package/dist/server-next.mjs +1353 -0
  33. package/dist/server-next.mjs.map +1 -0
  34. package/dist/server.d.mts +70 -0
  35. package/dist/server.d.mts.map +1 -0
  36. package/dist/server.mjs +384 -0
  37. package/dist/server.mjs.map +1 -0
  38. package/dist/types.d.mts +321 -0
  39. package/dist/types.d.mts.map +1 -0
  40. package/dist/types.mjs +3 -0
  41. package/dist/validation.d.mts +101 -0
  42. package/dist/validation.d.mts.map +1 -0
  43. package/dist/validation.mjs +590 -0
  44. package/dist/validation.mjs.map +1 -0
  45. package/dist/vercel-blob-07Sx0Akn.d.mts +31 -0
  46. package/dist/vercel-blob-07Sx0Akn.d.mts.map +1 -0
  47. package/dist/vercel-blob-DA8HaYuw.mjs +158 -0
  48. package/dist/vercel-blob-DA8HaYuw.mjs.map +1 -0
  49. package/package.json +111 -0
  50. package/src/actions/blob-upload.ts +171 -0
  51. package/src/actions/index.ts +23 -0
  52. package/src/actions/mediaActions.ts +1071 -0
  53. package/src/actions/productMediaActions.ts +538 -0
  54. package/src/auth-helpers.ts +386 -0
  55. package/src/capabilities.ts +225 -0
  56. package/src/client-next.ts +184 -0
  57. package/src/client-utils.ts +292 -0
  58. package/src/client.ts +102 -0
  59. package/src/constants.ts +88 -0
  60. package/src/health-check.ts +81 -0
  61. package/src/multi-storage.ts +230 -0
  62. package/src/multipart.ts +497 -0
  63. package/src/retry-utils.test.ts +118 -0
  64. package/src/retry-utils.ts +59 -0
  65. package/src/server-edge.ts +129 -0
  66. package/src/server-next.ts +14 -0
  67. package/src/server.ts +666 -0
  68. package/src/validation.test.ts +312 -0
  69. package/src/validation.ts +827 -0
package/src/server.ts ADDED
@@ -0,0 +1,666 @@
1
+ /**
2
+ * @fileoverview Server-side storage exports (non-Next.js)
3
+ *
4
+ * This file provides server-side storage functionality for non-Next.js environments.
5
+ * For Next.js applications, use '@repo/storage/server/next' instead.
6
+ *
7
+ * Features:
8
+ * - Multi-provider storage support (Vercel Blob, Cloudflare R2, Cloudflare Images)
9
+ * - Storage provider abstraction
10
+ * - Upload, download, list, and delete operations
11
+ * - Presigned URL generation
12
+ * - Multipart upload support
13
+ *
14
+ * @module @repo/storage/server
15
+ */
16
+
17
+ import { logWarn } from '@repo/shared/logs/server/next';
18
+
19
+ import { safeEnv } from '../env';
20
+ import { CloudflareImagesProvider } from '../providers/cloudflare-images';
21
+ import { CloudflareR2Provider } from '../providers/cloudflare-r2';
22
+ import { VercelBlobProvider } from '../providers/vercel-blob';
23
+ import {
24
+ type ListOptions,
25
+ type MultiStorageConfig,
26
+ type StorageCapabilities,
27
+ type StorageConfig,
28
+ type StorageObject,
29
+ type StorageProvider,
30
+ type StorageProviderType,
31
+ type UploadOptions,
32
+ } from '../types';
33
+
34
+ import { DEFAULT_STORAGE_CAPABILITIES } from './constants';
35
+ import { MultiStorageManager } from './multi-storage';
36
+
37
+
38
+ export { env, safeEnv } from '../env';
39
+ export { CloudflareImagesProvider } from '../providers/cloudflare-images';
40
+ export { CloudflareR2Provider } from '../providers/cloudflare-r2';
41
+ export { VercelBlobProvider } from '../providers/vercel-blob';
42
+ export * from '../types';
43
+ export { MultiStorageManager } from './multi-storage';
44
+
45
+ // Export multipart utilities
46
+ export {
47
+ createMultipartUploadManager,
48
+ getOptimalPartSize,
49
+ hasMultipartSupport,
50
+ MultipartUploadManager,
51
+ type MultipartUploadResult,
52
+ type MultipartUploadState,
53
+ } from './multipart';
54
+
55
+ // Export validation utilities
56
+ export {
57
+ ConfigError,
58
+ createStorageError,
59
+ DownloadError,
60
+ formatFileSize,
61
+ getErrorCode,
62
+ getQuotaInfo,
63
+ isQuotaExceeded,
64
+ isRetryableError,
65
+ NetworkError,
66
+ parseFileSize,
67
+ ProviderError,
68
+ StorageError,
69
+ StorageErrorCode,
70
+ UploadError,
71
+ validateFileSize,
72
+ validateMimeType,
73
+ validateStorageKey,
74
+ validateUploadOptions,
75
+ ValidationError,
76
+ type QuotaInfo,
77
+ type ValidationOptions,
78
+ } from './validation';
79
+
80
+ // Export capabilities utilities
81
+ export {
82
+ checkProviderSuitability,
83
+ describeProviderCapabilities,
84
+ getBestProvider,
85
+ getCapabilityMatrix,
86
+ getProviderCapabilities,
87
+ hasAllCapabilities,
88
+ hasAnyCapability,
89
+ hasCapability,
90
+ validateProviderCapabilities,
91
+ } from './capabilities';
92
+
93
+ // Export health check utilities
94
+ export { checkProviderHealth, storageHealthCheck } from './health-check';
95
+ export type { HealthCheckResult } from './health-check';
96
+
97
+ /**
98
+ * Configuration validation helpers
99
+ */
100
+
101
+ /**
102
+ * Validates storage configuration and returns helpful error messages
103
+ * @param config - Storage configuration to validate
104
+ * @returns Validation result with errors if any
105
+ */
106
+ export function validateStorageConfig(config: StorageConfig): {
107
+ valid: boolean;
108
+ errors: string[];
109
+ warnings: string[];
110
+ } {
111
+ const errors: string[] = [];
112
+ const warnings: string[] = [];
113
+
114
+ // Check provider type
115
+ if (!config.provider) {
116
+ errors.push('Storage provider is required');
117
+ }
118
+
119
+ // Validate provider-specific configuration
120
+ switch (config.provider) {
121
+ case 'multi':
122
+ errors.push('Multi provider configuration should not be validated here');
123
+ break;
124
+
125
+ case 'vercel-blob':
126
+ if (!config.vercelBlob?.token) {
127
+ errors.push('VERCEL_BLOB_READ_WRITE_TOKEN is required for Vercel Blob provider');
128
+ }
129
+ break;
130
+
131
+ case 'cloudflare-r2':
132
+ if (!config.cloudflareR2) {
133
+ errors.push('Cloudflare R2 configuration is required');
134
+ } else {
135
+ const r2Config = Array.isArray(config.cloudflareR2)
136
+ ? config.cloudflareR2[0]
137
+ : config.cloudflareR2;
138
+ if (!r2Config) {
139
+ errors.push('Cloudflare R2 configuration is required');
140
+ } else {
141
+ if (!r2Config.accessKeyId) {
142
+ errors.push('R2_ACCESS_KEY_ID is required for Cloudflare R2 provider');
143
+ }
144
+ if (!r2Config.secretAccessKey) {
145
+ errors.push('R2_SECRET_ACCESS_KEY is required for Cloudflare R2 provider');
146
+ }
147
+ if (!r2Config.bucket) {
148
+ errors.push('R2_BUCKET is required for Cloudflare R2 provider');
149
+ }
150
+ if (!r2Config.accountId) {
151
+ errors.push('R2_ACCOUNT_ID is required for Cloudflare R2 provider');
152
+ }
153
+ }
154
+ }
155
+ break;
156
+
157
+ case 'cloudflare-images':
158
+ if (!config.cloudflareImages) {
159
+ errors.push('Cloudflare Images configuration is required');
160
+ } else {
161
+ if (!config.cloudflareImages.accountId) {
162
+ errors.push('CLOUDFLARE_IMAGES_ACCOUNT_ID is required for Cloudflare Images provider');
163
+ }
164
+ if (!config.cloudflareImages.apiToken) {
165
+ errors.push('CLOUDFLARE_IMAGES_API_TOKEN is required for Cloudflare Images provider');
166
+ }
167
+ }
168
+ break;
169
+
170
+ default:
171
+ errors.push(`Unknown storage provider: ${config.provider}`);
172
+ }
173
+
174
+ // Check for common configuration issues
175
+ if (config.cloudflareR2 && config.cloudflareImages) {
176
+ warnings.push('Multiple providers configured - consider using multi-provider setup');
177
+ }
178
+
179
+ return {
180
+ valid: errors.length === 0,
181
+ errors,
182
+ warnings,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Determine storage provider capabilities based on the provided storage configuration.
188
+ *
189
+ * @param config - The storage configuration used to infer provider capabilities
190
+ * @returns A `StorageCapabilities` object describing which features the configured provider supports
191
+ */
192
+ export function getProviderCapabilitiesFromConfig(config: StorageConfig): StorageCapabilities {
193
+ switch (config.provider) {
194
+ case 'multi':
195
+ return { ...DEFAULT_STORAGE_CAPABILITIES };
196
+
197
+ case 'vercel-blob':
198
+ return {
199
+ multipart: true,
200
+ presignedUrls: false,
201
+ progressTracking: true,
202
+ abortSignal: true,
203
+ metadata: true,
204
+ customDomains: true,
205
+ edgeCompatible: true,
206
+ versioning: false,
207
+ encryption: false,
208
+ directoryListing: false,
209
+ };
210
+
211
+ case 'cloudflare-r2':
212
+ return {
213
+ multipart: true,
214
+ presignedUrls: true,
215
+ progressTracking: false,
216
+ abortSignal: false,
217
+ metadata: true,
218
+ customDomains: true,
219
+ edgeCompatible: false,
220
+ versioning: false,
221
+ encryption: false,
222
+ directoryListing: false,
223
+ };
224
+
225
+ case 'cloudflare-images':
226
+ return {
227
+ multipart: false,
228
+ presignedUrls: false,
229
+ progressTracking: false,
230
+ abortSignal: false,
231
+ metadata: true,
232
+ customDomains: true,
233
+ edgeCompatible: false,
234
+ versioning: false,
235
+ encryption: false,
236
+ directoryListing: false,
237
+ };
238
+
239
+ default:
240
+ return {
241
+ multipart: false,
242
+ presignedUrls: false,
243
+ progressTracking: false,
244
+ abortSignal: false,
245
+ metadata: false,
246
+ customDomains: false,
247
+ edgeCompatible: false,
248
+ versioning: false,
249
+ encryption: false,
250
+ directoryListing: false,
251
+ };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Create a StorageProvider instance based on the provided StorageConfig.
257
+ *
258
+ * @param config - Storage configuration specifying `provider` and the provider-specific settings required to instantiate that provider
259
+ * @returns The instantiated StorageProvider corresponding to `config.provider`
260
+ * @throws Error if the chosen provider's configuration is missing or incomplete, if a `cloudflare-r2` array is empty, or if `provider` is unrecognized
261
+ */
262
+ export function createStorageProvider(config: StorageConfig): StorageProvider {
263
+ switch (config.provider) {
264
+ case 'multi':
265
+ throw new Error('Multi provider should be created via MultiStorageManager');
266
+
267
+ case 'cloudflare-r2':
268
+ if (!config.cloudflareR2) {
269
+ throw new Error('Storage provider configuration is incomplete');
270
+ }
271
+ // Support single R2 config
272
+ if (!Array.isArray(config.cloudflareR2)) {
273
+ return new CloudflareR2Provider(config.cloudflareR2);
274
+ }
275
+ // For array, use first one (backward compatibility)
276
+ if (config.cloudflareR2.length === 0) {
277
+ throw new Error('No R2 configurations provided');
278
+ }
279
+ const firstR2Config = config.cloudflareR2[0];
280
+ if (!firstR2Config) {
281
+ throw new Error('First R2 configuration is undefined');
282
+ }
283
+ return new CloudflareR2Provider(firstR2Config);
284
+
285
+ case 'cloudflare-images':
286
+ if (!config.cloudflareImages) {
287
+ throw new Error('Storage provider configuration is incomplete');
288
+ }
289
+ return new CloudflareImagesProvider(config.cloudflareImages);
290
+
291
+ case 'vercel-blob':
292
+ if (!config.vercelBlob?.token) {
293
+ throw new Error('Storage provider configuration is incomplete');
294
+ }
295
+ return new VercelBlobProvider(config.vercelBlob.token);
296
+
297
+ default:
298
+ throw new Error(`Unknown storage provider: ${config.provider}`);
299
+ }
300
+ }
301
+
302
+ // Singleton storage instance
303
+ let storageInstance: MultiStorageManager | null | StorageProvider = null;
304
+ let hasLoggedWarning = false;
305
+ let multiStorageInstance: MultiStorageManager | null = null;
306
+
307
+ /**
308
+ * Test helper to reset singleton state
309
+ * @internal
310
+ */
311
+ export function resetStorageState(): void {
312
+ storageInstance = null;
313
+ multiStorageInstance = null;
314
+ hasLoggedWarning = false;
315
+ }
316
+
317
+ // Mock storage provider for development
318
+ class MockStorageProvider implements StorageProvider {
319
+ private storage = new Map<
320
+ string,
321
+ { data: ArrayBuffer | Blob | Buffer | File | ReadableStream; metadata: StorageObject }
322
+ >();
323
+
324
+ async delete(key: string): Promise<void> {
325
+ this.storage.delete(key);
326
+ }
327
+
328
+ async download(_key: string): Promise<Blob> {
329
+ return new Blob(['mock data'], { type: 'text/plain' });
330
+ }
331
+
332
+ async exists(key: string): Promise<boolean> {
333
+ return this.storage.has(key);
334
+ }
335
+
336
+ async getMetadata(key: string): Promise<StorageObject> {
337
+ const item = this.storage.get(key);
338
+ if (!item) {
339
+ throw new Error(`Object with key ${key} not found`);
340
+ }
341
+ return item.metadata;
342
+ }
343
+
344
+ async getUrl(key: string, _options?: { expiresIn?: number }): Promise<string> {
345
+ return `https://mock-storage.example.com/${key}`;
346
+ }
347
+
348
+ async list(_options?: ListOptions): Promise<StorageObject[]> {
349
+ return Array.from(this.storage.values()).map(item => item.metadata);
350
+ }
351
+
352
+ async upload(
353
+ key: string,
354
+ data: ArrayBuffer | Blob | Buffer | File | ReadableStream,
355
+ options?: UploadOptions,
356
+ ): Promise<StorageObject> {
357
+ const mockObject: StorageObject = {
358
+ contentType: options?.contentType ?? 'application/octet-stream',
359
+ key,
360
+ lastModified: new Date(),
361
+ size: data instanceof Buffer ? data.length : 1024,
362
+ url: `https://mock-storage.example.com/${key}`,
363
+ };
364
+ this.storage.set(key, { data, metadata: mockObject });
365
+ return mockObject;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Return the configured StorageProvider, initializing it on first access if needed.
371
+ *
372
+ * @returns The configured StorageProvider; a mock provider when no real provider is configured
373
+ */
374
+ export function getStorage(): StorageProvider {
375
+ if (!storageInstance) {
376
+ return initializeStorage();
377
+ }
378
+ return storageInstance;
379
+ }
380
+
381
+ /**
382
+ * Initialize and return the application's storage provider from environment configuration.
383
+ *
384
+ * Builds a StorageConfig from environment variables, validates it, and instantiates the corresponding StorageProvider. If no provider is configured, a MockStorageProvider is returned.
385
+ *
386
+ * @returns The instantiated StorageProvider (a MockStorageProvider when no provider is configured).
387
+ * @throws Error when required provider environment variables are missing (message: "Storage provider configuration is incomplete").
388
+ * @throws Error when configuration validation fails (message: "Storage configuration validation failed: <errors>").
389
+ */
390
+ export function initializeStorage(): StorageProvider {
391
+ if (storageInstance) {
392
+ return storageInstance;
393
+ }
394
+
395
+ const env = safeEnv();
396
+ const provider = env.STORAGE_PROVIDER;
397
+
398
+ // Return mock provider if no provider is configured
399
+ if (!provider) {
400
+ if (!hasLoggedWarning) {
401
+ // Storage service runs in mock mode when not configured
402
+ hasLoggedWarning = true;
403
+ }
404
+ storageInstance = new MockStorageProvider();
405
+ return storageInstance;
406
+ }
407
+
408
+ const config: StorageConfig = {
409
+ provider: provider as StorageProviderType,
410
+ };
411
+
412
+ // Configure based on provider
413
+ switch (provider) {
414
+ case 'multi':
415
+ throw new Error('Multi provider configuration not supported in getStorage()');
416
+
417
+ case 'cloudflare-images':
418
+ throw new Error('Cloudflare Images provider not supported as primary storage provider');
419
+
420
+ case 'cloudflare-r2': {
421
+ if (
422
+ !env.R2_ACCOUNT_ID ||
423
+ !env.R2_ACCESS_KEY_ID ||
424
+ !env.R2_SECRET_ACCESS_KEY ||
425
+ !env.R2_BUCKET
426
+ ) {
427
+ throw new Error('Storage provider configuration is incomplete');
428
+ }
429
+ config.cloudflareR2 = {
430
+ accessKeyId: env.R2_ACCESS_KEY_ID,
431
+ accountId: env.R2_ACCOUNT_ID,
432
+ bucket: env.R2_BUCKET,
433
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY,
434
+ };
435
+ break;
436
+ }
437
+ case 'vercel-blob': {
438
+ if (!env.VERCEL_BLOB_READ_WRITE_TOKEN) {
439
+ throw new Error('Storage provider configuration is incomplete');
440
+ }
441
+ config.vercelBlob = {
442
+ token: env.VERCEL_BLOB_READ_WRITE_TOKEN,
443
+ };
444
+ break;
445
+ }
446
+ }
447
+
448
+ // Validate configuration before creating provider
449
+ const validation = validateStorageConfig(config);
450
+ if (!validation.valid) {
451
+ throw new Error(`Storage configuration validation failed: ${validation.errors.join(', ')}`);
452
+ }
453
+
454
+ // Log warnings if any
455
+ if (validation.warnings.length > 0) {
456
+ logWarn('Storage configuration warnings:', {
457
+ warnings: validation.warnings,
458
+ provider: config.provider,
459
+ });
460
+ }
461
+
462
+ storageInstance = createStorageProvider(config);
463
+ return storageInstance;
464
+ }
465
+
466
+ /**
467
+ * Initialize multi-storage manager with environment variables
468
+ * @returns Configured multi-storage manager instance
469
+ */
470
+ export function initializeMultiStorage(): MultiStorageManager {
471
+ if (multiStorageInstance) {
472
+ return multiStorageInstance;
473
+ }
474
+
475
+ const env = safeEnv();
476
+
477
+ // Check if we have a full storage config JSON
478
+ if (env.STORAGE_CONFIG) {
479
+ const config =
480
+ typeof env.STORAGE_CONFIG === 'string' ? JSON.parse(env.STORAGE_CONFIG) : env.STORAGE_CONFIG;
481
+ multiStorageInstance = new MultiStorageManager(config);
482
+ return multiStorageInstance;
483
+ }
484
+
485
+ // Build config from individual env vars
486
+ const config: MultiStorageConfig = {
487
+ providers: {},
488
+ };
489
+
490
+ // Add R2 providers from JSON array
491
+ if (env.R2_CREDENTIALS && Array.isArray(env.R2_CREDENTIALS)) {
492
+ const r2Configs = env.R2_CREDENTIALS;
493
+ r2Configs.forEach((r2Config, index: number) => {
494
+ const name = r2Config.name ?? `r2-${index}`;
495
+ config.providers[name] = {
496
+ provider: 'cloudflare-r2',
497
+ cloudflareR2: r2Config,
498
+ };
499
+ });
500
+ }
501
+
502
+ // Add legacy R2 config if present
503
+ if (env.R2_ACCESS_KEY_ID && env.R2_SECRET_ACCESS_KEY && env.R2_BUCKET && env.R2_ACCOUNT_ID) {
504
+ config.providers['r2-legacy'] = {
505
+ provider: 'cloudflare-r2',
506
+ cloudflareR2: {
507
+ accessKeyId: env.R2_ACCESS_KEY_ID,
508
+ accountId: env.R2_ACCOUNT_ID,
509
+ bucket: env.R2_BUCKET,
510
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY,
511
+ },
512
+ };
513
+ }
514
+
515
+ // Add Cloudflare Images if configured
516
+ if (env.CLOUDFLARE_IMAGES_API_TOKEN && env.CLOUDFLARE_IMAGES_ACCOUNT_ID) {
517
+ config.providers['images'] = {
518
+ provider: 'cloudflare-images',
519
+ cloudflareImages: {
520
+ accountId: env.CLOUDFLARE_IMAGES_ACCOUNT_ID,
521
+ apiToken: env.CLOUDFLARE_IMAGES_API_TOKEN,
522
+ deliveryUrl: env.CLOUDFLARE_IMAGES_DELIVERY_URL,
523
+ signingKey: env.CLOUDFLARE_IMAGES_SIGNING_KEY,
524
+ },
525
+ };
526
+ }
527
+
528
+ // Add Vercel Blob if configured
529
+ if (env.VERCEL_BLOB_READ_WRITE_TOKEN) {
530
+ config.providers['blob'] = {
531
+ provider: 'vercel-blob',
532
+ vercelBlob: {
533
+ token: env.VERCEL_BLOB_READ_WRITE_TOKEN,
534
+ },
535
+ };
536
+ }
537
+
538
+ // Set up routing
539
+ config.routing = {
540
+ images: 'images', // Route image files to Cloudflare Images if available
541
+ documents: config.providers['r2-0'] ? 'r2-0' : 'r2-legacy', // Route documents to first R2
542
+ };
543
+
544
+ if (Object.keys(config.providers).length === 0) {
545
+ // Return mock multi-storage
546
+ config.providers['mock'] = {
547
+ provider: 'vercel-blob', // Using mock provider
548
+ vercelBlob: { token: 'mock' },
549
+ };
550
+ multiStorageInstance = new MultiStorageManager(config);
551
+
552
+ if (!hasLoggedWarning) {
553
+ // Multi-storage service runs in mock mode when not configured
554
+ hasLoggedWarning = true;
555
+ }
556
+ return multiStorageInstance;
557
+ }
558
+
559
+ multiStorageInstance = new MultiStorageManager(config);
560
+ return multiStorageInstance;
561
+ }
562
+
563
+ /**
564
+ * Get multi-storage manager instance with lazy initialization
565
+ * @returns Multi-storage manager instance
566
+ */
567
+ export function getMultiStorage(): MultiStorageManager {
568
+ if (!multiStorageInstance) {
569
+ return initializeMultiStorage();
570
+ }
571
+ return multiStorageInstance;
572
+ }
573
+
574
+ /**
575
+ * Helper functions for common storage operations
576
+ * Provides direct access to the primary storage provider methods
577
+ */
578
+ export const storage = {
579
+ delete: (key: string) => getStorage().delete(key),
580
+ download: (key: string) => getStorage().download(key),
581
+ exists: (key: string) => getStorage().exists(key),
582
+ getMetadata: (key: string) => getStorage().getMetadata(key),
583
+ getUrl: (key: string, options?: { expiresIn?: number }) => getStorage().getUrl(key, options),
584
+ list: (options?: ListOptions) => getStorage().list(options),
585
+ upload: (
586
+ key: string,
587
+ data: ArrayBuffer | Blob | Buffer | File | ReadableStream,
588
+ options?: UploadOptions,
589
+ ) => getStorage().upload(key, data, options),
590
+
591
+ // Multipart upload helpers
592
+ createMultipartUpload: (key: string, options?: UploadOptions) => {
593
+ const provider = getStorage();
594
+ if (!provider.createMultipartUpload) {
595
+ throw new Error('Provider does not support multipart uploads');
596
+ }
597
+ return provider.createMultipartUpload(key, options);
598
+ },
599
+
600
+ uploadPart: (
601
+ uploadId: string,
602
+ partNumber: number,
603
+ data: ArrayBuffer | Blob | Buffer,
604
+ options?: UploadOptions,
605
+ ) => {
606
+ const provider = getStorage();
607
+ if (!provider.uploadPart) {
608
+ throw new Error('Provider does not support multipart uploads');
609
+ }
610
+ return provider.uploadPart(uploadId, partNumber, data, options);
611
+ },
612
+
613
+ completeMultipartUpload: (
614
+ uploadId: string,
615
+ parts: Array<{ etag: string; partNumber: number }>,
616
+ ) => {
617
+ const provider = getStorage();
618
+ if (!provider.completeMultipartUpload) {
619
+ throw new Error('Provider does not support multipart uploads');
620
+ }
621
+ return provider.completeMultipartUpload(uploadId, parts);
622
+ },
623
+
624
+ abortMultipartUpload: (uploadId: string) => {
625
+ const provider = getStorage();
626
+ if (!provider.abortMultipartUpload) {
627
+ throw new Error('Provider does not support multipart uploads');
628
+ }
629
+ return provider.abortMultipartUpload(uploadId);
630
+ },
631
+
632
+ // Presigned URL helper
633
+ getPresignedUploadUrl: (key: string, options?: { expiresIn?: number; contentType?: string }) => {
634
+ const provider = getStorage();
635
+ if (!provider.getPresignedUploadUrl) {
636
+ throw new Error('Provider does not support presigned URLs');
637
+ }
638
+ return provider.getPresignedUploadUrl(key, options);
639
+ },
640
+
641
+ // Capabilities helper
642
+ getCapabilities: () => {
643
+ const provider = getStorage();
644
+ return provider.getCapabilities?.() ?? { ...DEFAULT_STORAGE_CAPABILITIES };
645
+ },
646
+ };
647
+
648
+ /**
649
+ * Multi-storage helper functions
650
+ * Provides access to multi-storage manager methods with automatic routing
651
+ */
652
+ export const multiStorage = {
653
+ delete: (key: string) => getMultiStorage().delete(key),
654
+ download: (key: string) => getMultiStorage().download(key),
655
+ exists: (key: string) => getMultiStorage().exists(key),
656
+ getMetadata: (key: string) => getMultiStorage().getMetadata(key),
657
+ getProvider: (name: string) => getMultiStorage().getProvider(name),
658
+ getProviderNames: () => getMultiStorage().getProviderNames(),
659
+ getUrl: (key: string, options?: { expiresIn?: number }) => getMultiStorage().getUrl(key, options),
660
+ list: (options?: ListOptions & { provider?: string }) => getMultiStorage().list(options),
661
+ upload: (
662
+ key: string,
663
+ data: ArrayBuffer | Blob | Buffer | File | ReadableStream,
664
+ options?: UploadOptions & { provider?: string },
665
+ ) => getMultiStorage().upload(key, data, options),
666
+ };