@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.
- package/README.md +854 -0
- package/dist/client-next.d.mts +61 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +111 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client-utils-Dx6W25iz.d.mts +43 -0
- package/dist/client-utils-Dx6W25iz.d.mts.map +1 -0
- package/dist/client.d.mts +28 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +183 -0
- package/dist/client.mjs.map +1 -0
- package/dist/env-BVHLmQdh.mjs +128 -0
- package/dist/env-BVHLmQdh.mjs.map +1 -0
- package/dist/env.mjs +3 -0
- package/dist/health-check-D7LnnDec.mjs +746 -0
- package/dist/health-check-D7LnnDec.mjs.map +1 -0
- package/dist/health-check-im_huJ59.d.mts +116 -0
- package/dist/health-check-im_huJ59.d.mts.map +1 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/keys.d.mts +37 -0
- package/dist/keys.d.mts.map +1 -0
- package/dist/keys.mjs +253 -0
- package/dist/keys.mjs.map +1 -0
- package/dist/server-edge.d.mts +28 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +88 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +183 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +1353 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +70 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +384 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types.d.mts +321 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +3 -0
- package/dist/validation.d.mts +101 -0
- package/dist/validation.d.mts.map +1 -0
- package/dist/validation.mjs +590 -0
- package/dist/validation.mjs.map +1 -0
- package/dist/vercel-blob-07Sx0Akn.d.mts +31 -0
- package/dist/vercel-blob-07Sx0Akn.d.mts.map +1 -0
- package/dist/vercel-blob-DA8HaYuw.mjs +158 -0
- package/dist/vercel-blob-DA8HaYuw.mjs.map +1 -0
- package/package.json +111 -0
- package/src/actions/blob-upload.ts +171 -0
- package/src/actions/index.ts +23 -0
- package/src/actions/mediaActions.ts +1071 -0
- package/src/actions/productMediaActions.ts +538 -0
- package/src/auth-helpers.ts +386 -0
- package/src/capabilities.ts +225 -0
- package/src/client-next.ts +184 -0
- package/src/client-utils.ts +292 -0
- package/src/client.ts +102 -0
- package/src/constants.ts +88 -0
- package/src/health-check.ts +81 -0
- package/src/multi-storage.ts +230 -0
- package/src/multipart.ts +497 -0
- package/src/retry-utils.test.ts +118 -0
- package/src/retry-utils.ts +59 -0
- package/src/server-edge.ts +129 -0
- package/src/server-next.ts +14 -0
- package/src/server.ts +666 -0
- package/src/validation.test.ts +312 -0
- package/src/validation.ts +827 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side storage exports for Next.js
|
|
3
|
+
*
|
|
4
|
+
* This file provides client-side storage functionality specifically for Next.js applications.
|
|
5
|
+
* NO REACT HOOKS - keep in consuming apps to avoid React peer dependency.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - File upload with progress tracking
|
|
9
|
+
* - Next.js-specific upload helpers
|
|
10
|
+
* - Client-side validation
|
|
11
|
+
*
|
|
12
|
+
* @module @repo/storage/client/next
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use client';
|
|
16
|
+
|
|
17
|
+
// Import for internal use
|
|
18
|
+
import { uploadFile } from './client';
|
|
19
|
+
import { validateMimeType } from './validation';
|
|
20
|
+
|
|
21
|
+
// Re-export all client functionality
|
|
22
|
+
export * from './client';
|
|
23
|
+
|
|
24
|
+
// Next.js-specific upload helpers (no hooks)
|
|
25
|
+
export async function uploadFileWithProgress(
|
|
26
|
+
pathname: string,
|
|
27
|
+
file: File | Blob,
|
|
28
|
+
options?: {
|
|
29
|
+
access?: 'public' | 'private';
|
|
30
|
+
addRandomSuffix?: boolean;
|
|
31
|
+
allowOverwrite?: boolean;
|
|
32
|
+
cacheControlMaxAge?: number;
|
|
33
|
+
clientPayload?: string;
|
|
34
|
+
contentType?: string;
|
|
35
|
+
multipart?: boolean;
|
|
36
|
+
onUploadProgress?: (event: { loaded: number; total?: number; percentage?: number }) => void;
|
|
37
|
+
handleUploadUrl?: string;
|
|
38
|
+
},
|
|
39
|
+
): Promise<{ url: string; pathname: string }> {
|
|
40
|
+
// Use the base uploadFile function
|
|
41
|
+
return await uploadFile(pathname, file, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Upload multiple files with progress tracking
|
|
46
|
+
*
|
|
47
|
+
* @param files - Array of files to upload
|
|
48
|
+
* @param getPathname - Function to generate pathname for each file
|
|
49
|
+
* @param options - Upload options
|
|
50
|
+
* @returns Array of upload results
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const results = await uploadMultipleFiles(
|
|
55
|
+
* files,
|
|
56
|
+
* (file, index) => `uploads/${index}-${file.name}`,
|
|
57
|
+
* {
|
|
58
|
+
* onUploadProgress: (event) => {
|
|
59
|
+
* logInfo(`Uploaded ${event.percentage}%`);
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export async function uploadMultipleFiles(
|
|
66
|
+
files: File[],
|
|
67
|
+
getPathname: (file: File, index: number) => string,
|
|
68
|
+
options?: {
|
|
69
|
+
access?: 'public' | 'private';
|
|
70
|
+
addRandomSuffix?: boolean;
|
|
71
|
+
allowOverwrite?: boolean;
|
|
72
|
+
cacheControlMaxAge?: number;
|
|
73
|
+
clientPayload?: string;
|
|
74
|
+
contentType?: string;
|
|
75
|
+
multipart?: boolean;
|
|
76
|
+
onUploadProgress?: (event: { loaded: number; total?: number; percentage?: number }) => void;
|
|
77
|
+
handleUploadUrl?: string;
|
|
78
|
+
onFileProgress?: (
|
|
79
|
+
fileIndex: number,
|
|
80
|
+
progress: { loaded: number; total?: number; percentage?: number },
|
|
81
|
+
) => void;
|
|
82
|
+
},
|
|
83
|
+
): Promise<Array<{ url: string; pathname: string; success: boolean; error?: string }>> {
|
|
84
|
+
const results: Array<{ url: string; pathname: string; success: boolean; error?: string }> = [];
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < files.length; i++) {
|
|
87
|
+
const file = files[i];
|
|
88
|
+
if (!file) continue;
|
|
89
|
+
const pathname = getPathname(file, i);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const result = await uploadFile(pathname, file, {
|
|
93
|
+
...options,
|
|
94
|
+
onUploadProgress: options?.onFileProgress
|
|
95
|
+
? (event: { loaded: number; total?: number; percentage?: number }) =>
|
|
96
|
+
options.onFileProgress?.(i, event)
|
|
97
|
+
: options?.onUploadProgress,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
results.push({
|
|
101
|
+
...result,
|
|
102
|
+
success: true,
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
results.push({
|
|
106
|
+
url: '',
|
|
107
|
+
pathname,
|
|
108
|
+
success: false,
|
|
109
|
+
error: error instanceof Error ? error.message : String(error),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate file before upload
|
|
119
|
+
*
|
|
120
|
+
* @param file - File to validate
|
|
121
|
+
* @param options - Validation options
|
|
122
|
+
* @returns Validation result
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* const validation = await validateFileForUpload(file, {
|
|
127
|
+
* maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
128
|
+
* allowedMimeTypes: ['image/jpeg', 'image/png'],
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* if (!validation.valid) {
|
|
132
|
+
* logError('Validation failed:', validation.errors);
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export async function validateFileForUpload(
|
|
137
|
+
file: File,
|
|
138
|
+
options: {
|
|
139
|
+
maxFileSize?: number;
|
|
140
|
+
allowedMimeTypes?: string[];
|
|
141
|
+
allowedExtensions?: string[];
|
|
142
|
+
} = {},
|
|
143
|
+
): Promise<{ valid: boolean; errors: string[] }> {
|
|
144
|
+
const errors: string[] = [];
|
|
145
|
+
|
|
146
|
+
// Validate file size
|
|
147
|
+
if (options.maxFileSize && file.size > options.maxFileSize) {
|
|
148
|
+
errors.push(`File size ${file.size} bytes exceeds maximum ${options.maxFileSize} bytes`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate MIME type
|
|
152
|
+
if (options.allowedMimeTypes && options.allowedMimeTypes.length > 0) {
|
|
153
|
+
const mimeValidation = validateMimeType(file.type, options.allowedMimeTypes);
|
|
154
|
+
if (!mimeValidation.valid) {
|
|
155
|
+
if (mimeValidation.error) {
|
|
156
|
+
errors.push(mimeValidation.error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate file extension
|
|
162
|
+
if (options.allowedExtensions && options.allowedExtensions.length > 0) {
|
|
163
|
+
const extension = file.name.match(/\.[^/.]+$/)?.[0]?.toLowerCase();
|
|
164
|
+
if (extension && !options.allowedExtensions.includes(extension)) {
|
|
165
|
+
errors.push(
|
|
166
|
+
`File extension ${extension} is not allowed. Allowed: ${options.allowedExtensions.join(', ')}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
valid: errors.length === 0,
|
|
173
|
+
errors,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Re-export validation utilities for convenience
|
|
178
|
+
export {
|
|
179
|
+
formatFileSize,
|
|
180
|
+
parseFileSize,
|
|
181
|
+
validateFileSize,
|
|
182
|
+
validateMimeType,
|
|
183
|
+
validateStorageKey,
|
|
184
|
+
} from './validation';
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side utilities for storage operations
|
|
3
|
+
*
|
|
4
|
+
* These work with server-side APIs to enable client uploads without exposing credentials.
|
|
5
|
+
* Provides presigned URL uploads and progress tracking.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Presigned URL uploads
|
|
9
|
+
* - Upload progress tracking
|
|
10
|
+
* - Client-side file handling
|
|
11
|
+
*
|
|
12
|
+
* @module @repo/storage/client-utils
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PresignedUploadUrl as BasePresignedUploadUrl } from '../types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extended presigned upload URL configuration for client-side use
|
|
19
|
+
* Includes the storage key for tracking uploads
|
|
20
|
+
*/
|
|
21
|
+
export interface ClientPresignedUploadUrl extends BasePresignedUploadUrl {
|
|
22
|
+
key: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Upload progress information for client-side tracking
|
|
27
|
+
* Uses 'percentage' to align with types.ts UploadProgress
|
|
28
|
+
*/
|
|
29
|
+
export interface ClientUploadProgress {
|
|
30
|
+
loaded: number;
|
|
31
|
+
total: number;
|
|
32
|
+
percentage: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Upload a file using a presigned URL (works with R2, S3, etc.)
|
|
37
|
+
*/
|
|
38
|
+
export async function uploadWithPresignedUrl(
|
|
39
|
+
presignedData: ClientPresignedUploadUrl,
|
|
40
|
+
file: File | Blob,
|
|
41
|
+
options?: {
|
|
42
|
+
onProgress?: (progress: ClientUploadProgress) => void;
|
|
43
|
+
},
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const formData = new FormData();
|
|
46
|
+
|
|
47
|
+
// Add any required fields first (for POST uploads)
|
|
48
|
+
if (presignedData.fields) {
|
|
49
|
+
Object.entries(presignedData.fields).forEach(([key, value]) => {
|
|
50
|
+
formData.append(key, value);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add the file last
|
|
55
|
+
formData.append('file', file);
|
|
56
|
+
|
|
57
|
+
// For progress tracking, we need XMLHttpRequest
|
|
58
|
+
if (options?.onProgress) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const xhr = new XMLHttpRequest();
|
|
61
|
+
|
|
62
|
+
xhr.upload.addEventListener('progress', event => {
|
|
63
|
+
if (event.lengthComputable && options.onProgress) {
|
|
64
|
+
options.onProgress({
|
|
65
|
+
loaded: event.loaded,
|
|
66
|
+
total: event.total,
|
|
67
|
+
percentage: (event.loaded / event.total) * 100,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
xhr.addEventListener('load', () => {
|
|
73
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
74
|
+
resolve();
|
|
75
|
+
} else {
|
|
76
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
xhr.addEventListener('error', () => {
|
|
81
|
+
reject(new Error('Upload failed'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
xhr.open('POST', presignedData.url);
|
|
85
|
+
xhr.send(formData);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Simple fetch for no progress tracking
|
|
90
|
+
const response = await fetch(presignedData.url, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: formData,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`Upload failed with status ${response.status}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Upload directly to a URL (PUT method, typically for presigned PUT URLs)
|
|
102
|
+
*/
|
|
103
|
+
export async function uploadDirectToUrl(
|
|
104
|
+
url: string,
|
|
105
|
+
file: File | Blob,
|
|
106
|
+
options?: {
|
|
107
|
+
headers?: Record<string, string>;
|
|
108
|
+
onProgress?: (progress: ClientUploadProgress) => void;
|
|
109
|
+
},
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
// For progress tracking, we need XMLHttpRequest
|
|
112
|
+
if (options?.onProgress) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const xhr = new XMLHttpRequest();
|
|
115
|
+
|
|
116
|
+
xhr.upload.addEventListener('progress', event => {
|
|
117
|
+
if (event.lengthComputable && options.onProgress) {
|
|
118
|
+
options.onProgress({
|
|
119
|
+
loaded: event.loaded,
|
|
120
|
+
total: event.total,
|
|
121
|
+
percentage: (event.loaded / event.total) * 100,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
xhr.addEventListener('load', () => {
|
|
127
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
128
|
+
resolve();
|
|
129
|
+
} else {
|
|
130
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
xhr.addEventListener('error', () => {
|
|
135
|
+
reject(new Error('Upload failed'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
xhr.open('PUT', url);
|
|
139
|
+
|
|
140
|
+
// Set headers
|
|
141
|
+
if (options?.headers) {
|
|
142
|
+
Object.entries(options.headers).forEach(([key, value]) => {
|
|
143
|
+
xhr.setRequestHeader(key, value);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
xhr.send(file);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Simple fetch for no progress tracking
|
|
152
|
+
const response = await fetch(url, {
|
|
153
|
+
method: 'PUT',
|
|
154
|
+
body: file,
|
|
155
|
+
headers: options?.headers,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`Upload failed with status ${response.status}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Client-side multipart upload coordinator
|
|
165
|
+
* Works with server-side APIs to manage multipart uploads
|
|
166
|
+
*/
|
|
167
|
+
export class ClientMultipartUpload {
|
|
168
|
+
private uploadId: string;
|
|
169
|
+
private key: string;
|
|
170
|
+
private parts: Array<{ PartNumber: number; ETag: string }> = [];
|
|
171
|
+
|
|
172
|
+
constructor(uploadId: string, key: string) {
|
|
173
|
+
this.uploadId = uploadId;
|
|
174
|
+
this.key = key;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async uploadPart(
|
|
178
|
+
partNumber: number,
|
|
179
|
+
presignedUrl: string,
|
|
180
|
+
data: Blob,
|
|
181
|
+
_onProgress?: (progress: ClientUploadProgress) => void,
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
// Note: onProgress tracking for presigned URL uploads requires XMLHttpRequest
|
|
184
|
+
// This is a simplified implementation using fetch
|
|
185
|
+
const response = await fetch(presignedUrl, {
|
|
186
|
+
method: 'PUT',
|
|
187
|
+
body: data,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`Part upload failed with status ${response.status}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const etag = response.headers.get('ETag') ?? '';
|
|
195
|
+
this.parts.push({ PartNumber: partNumber, ETag: etag });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getParts() {
|
|
199
|
+
return this.parts.sort((a, b) => a.PartNumber - b.PartNumber);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getUploadId() {
|
|
203
|
+
return this.uploadId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getKey() {
|
|
207
|
+
return this.key;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Split a file into chunks for multipart upload
|
|
213
|
+
*/
|
|
214
|
+
export async function* splitFileIntoChunks(
|
|
215
|
+
file: File | Blob,
|
|
216
|
+
chunkSize: number = 5 * 1024 * 1024, // 5MB default
|
|
217
|
+
): AsyncGenerator<{ chunk: Blob; partNumber: number; start: number; end: number }> {
|
|
218
|
+
const totalSize = file.size;
|
|
219
|
+
let partNumber = 1;
|
|
220
|
+
|
|
221
|
+
for (let start = 0; start < totalSize; start += chunkSize) {
|
|
222
|
+
const end = Math.min(start + chunkSize, totalSize);
|
|
223
|
+
const chunk = file.slice(start, end);
|
|
224
|
+
|
|
225
|
+
yield {
|
|
226
|
+
chunk,
|
|
227
|
+
partNumber,
|
|
228
|
+
start,
|
|
229
|
+
end,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
partNumber++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Download a file from a URL with progress tracking
|
|
238
|
+
*/
|
|
239
|
+
export async function downloadFromUrl(
|
|
240
|
+
url: string,
|
|
241
|
+
options?: {
|
|
242
|
+
onProgress?: (progress: ClientUploadProgress) => void;
|
|
243
|
+
},
|
|
244
|
+
): Promise<Blob> {
|
|
245
|
+
const response = await fetch(url);
|
|
246
|
+
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(`Download failed with status ${response.status}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If no progress tracking needed, just return blob
|
|
252
|
+
if (!options?.onProgress || !response.body) {
|
|
253
|
+
return response.blob();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Stream with progress tracking
|
|
257
|
+
const contentLength = response.headers.get('content-length');
|
|
258
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
259
|
+
|
|
260
|
+
const reader = response.body.getReader();
|
|
261
|
+
const chunks: Uint8Array[] = [];
|
|
262
|
+
let loaded = 0;
|
|
263
|
+
|
|
264
|
+
while (true) {
|
|
265
|
+
const { done, value } = await reader.read();
|
|
266
|
+
|
|
267
|
+
if (done) break;
|
|
268
|
+
|
|
269
|
+
chunks.push(value);
|
|
270
|
+
loaded += value.length;
|
|
271
|
+
|
|
272
|
+
if (total > 0) {
|
|
273
|
+
options.onProgress({
|
|
274
|
+
loaded,
|
|
275
|
+
total,
|
|
276
|
+
percentage: (loaded / total) * 100,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const blob = new Blob(
|
|
282
|
+
chunks.map(
|
|
283
|
+
chunk =>
|
|
284
|
+
chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength) as ArrayBuffer,
|
|
285
|
+
),
|
|
286
|
+
{
|
|
287
|
+
type: response.headers.get('content-type') ?? 'application/octet-stream',
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return blob;
|
|
292
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Client-side storage exports (non-Next.js)
|
|
3
|
+
*
|
|
4
|
+
* This file provides client-side storage functionality for non-Next.js environments.
|
|
5
|
+
* For Next.js applications, use '@od-oneapp/storage/client/next' instead.
|
|
6
|
+
*
|
|
7
|
+
* CLEAN BREAK: Removed legacy wrappers, direct Vercel SDK re-exports only.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - File upload utilities
|
|
11
|
+
* - Client-side validation
|
|
12
|
+
* - Key generation utilities
|
|
13
|
+
*
|
|
14
|
+
* @module @od-oneapp/storage/client
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Re-export core Vercel Blob client utilities
|
|
18
|
+
// Import for internal use
|
|
19
|
+
import { upload as vercelUpload } from '@vercel/blob/client';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
generateClientTokenFromReadWriteToken,
|
|
23
|
+
handleUpload,
|
|
24
|
+
upload,
|
|
25
|
+
} from '@vercel/blob/client';
|
|
26
|
+
|
|
27
|
+
// Export client-side validation utilities
|
|
28
|
+
export {
|
|
29
|
+
formatFileSize,
|
|
30
|
+
parseFileSize,
|
|
31
|
+
validateFileSize,
|
|
32
|
+
validateMimeType,
|
|
33
|
+
validateStorageKey,
|
|
34
|
+
type ValidationOptions,
|
|
35
|
+
} from './validation';
|
|
36
|
+
|
|
37
|
+
// Export key generation utilities (validateStorageKey is exported from './validation')
|
|
38
|
+
export {
|
|
39
|
+
generateMultipleKeys,
|
|
40
|
+
generateStorageKey,
|
|
41
|
+
isKeyPattern,
|
|
42
|
+
normalizeStorageKey,
|
|
43
|
+
parseStorageKey,
|
|
44
|
+
sanitizeStorageKey,
|
|
45
|
+
type KeyGenerationOptions,
|
|
46
|
+
type KeyPattern,
|
|
47
|
+
} from '../keys';
|
|
48
|
+
|
|
49
|
+
// Export client-side utilities for presigned URLs (R2, S3, etc.)
|
|
50
|
+
export * from './client-utils';
|
|
51
|
+
|
|
52
|
+
// Type-safe wrapper for Vercel's upload function
|
|
53
|
+
export async function uploadFile(
|
|
54
|
+
pathname: string,
|
|
55
|
+
file: File | Blob,
|
|
56
|
+
options?: {
|
|
57
|
+
access?: 'public' | 'private';
|
|
58
|
+
addRandomSuffix?: boolean;
|
|
59
|
+
allowOverwrite?: boolean;
|
|
60
|
+
cacheControlMaxAge?: number;
|
|
61
|
+
clientPayload?: string;
|
|
62
|
+
contentType?: string;
|
|
63
|
+
multipart?: boolean;
|
|
64
|
+
onUploadProgress?: (event: { loaded: number; total?: number; percentage?: number }) => void;
|
|
65
|
+
handleUploadUrl?: string;
|
|
66
|
+
},
|
|
67
|
+
): Promise<{ url: string; pathname: string }> {
|
|
68
|
+
const { handleUploadUrl, ...vercelOptions } = options ?? {};
|
|
69
|
+
|
|
70
|
+
if (handleUploadUrl) {
|
|
71
|
+
// Use handleUpload pattern for server-side processing
|
|
72
|
+
return await vercelUpload(pathname, file, {
|
|
73
|
+
...vercelOptions,
|
|
74
|
+
access: 'public' as const,
|
|
75
|
+
handleUploadUrl,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
// Use direct upload with client token
|
|
79
|
+
return await vercelUpload(pathname, file, {
|
|
80
|
+
...vercelOptions,
|
|
81
|
+
access: 'public' as const,
|
|
82
|
+
handleUploadUrl: '/api/upload', // Default upload URL
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Export types that are safe for client-side use
|
|
88
|
+
export type {
|
|
89
|
+
BlobListResponse,
|
|
90
|
+
ClientUploadOptions,
|
|
91
|
+
CloudflareImagesBatchToken,
|
|
92
|
+
CloudflareImagesListOptions,
|
|
93
|
+
CloudflareImagesTransformOptions,
|
|
94
|
+
CloudflareImagesVariant,
|
|
95
|
+
DirectUploadResponse,
|
|
96
|
+
ListOptions,
|
|
97
|
+
PresignedUploadUrl,
|
|
98
|
+
StorageObject,
|
|
99
|
+
UploadOptions,
|
|
100
|
+
UploadProgress,
|
|
101
|
+
VercelBlobOptions,
|
|
102
|
+
} from '../types';
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Storage Package Constants
|
|
3
|
+
*
|
|
4
|
+
* Centralized constants for storage operations to avoid magic numbers
|
|
5
|
+
* and improve maintainability.
|
|
6
|
+
*
|
|
7
|
+
* Includes:
|
|
8
|
+
* - URL expiration times
|
|
9
|
+
* - File size limits
|
|
10
|
+
* - Multipart upload thresholds
|
|
11
|
+
* - Retry configuration
|
|
12
|
+
* - Rate limiting settings
|
|
13
|
+
*
|
|
14
|
+
* @module @repo/storage/constants
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const STORAGE_CONSTANTS = {
|
|
18
|
+
// URL Expiration Times (seconds)
|
|
19
|
+
DEFAULT_URL_EXPIRY_SECONDS: 3600, // 1 hour
|
|
20
|
+
PRODUCT_URL_EXPIRY_SECONDS: 3600, // 1 hour for product photos
|
|
21
|
+
UPLOAD_URL_EXPIRY_SECONDS: 1800, // 30 minutes for uploads
|
|
22
|
+
ADMIN_URL_EXPIRY_SECONDS: 7200, // 2 hours for admin operations
|
|
23
|
+
|
|
24
|
+
// File Size Limits (bytes)
|
|
25
|
+
MULTIPART_THRESHOLD_BYTES: 100 * 1024 * 1024, // 100MB - use multipart above this
|
|
26
|
+
DEFAULT_PART_SIZE_BYTES: 5 * 1024 * 1024, // 5MB default part size
|
|
27
|
+
DEFAULT_MAX_PART_SIZE_BYTES: 25 * 1024 * 1024, // 25MB max part size
|
|
28
|
+
DEFAULT_QUEUE_SIZE: 4, // Concurrent upload parts
|
|
29
|
+
|
|
30
|
+
// Batch Operations
|
|
31
|
+
DEFAULT_BATCH_SIZE: 5, // Process 5 items concurrently
|
|
32
|
+
MAX_BATCH_SIZE: 50, // Maximum batch size
|
|
33
|
+
|
|
34
|
+
// Timeouts (milliseconds)
|
|
35
|
+
DEFAULT_REQUEST_TIMEOUT_MS: 30000, // 30 seconds
|
|
36
|
+
DEFAULT_UPLOAD_TIMEOUT_MS: 300000, // 5 minutes for uploads
|
|
37
|
+
DEFAULT_DOWNLOAD_TIMEOUT_MS: 60000, // 1 minute for downloads
|
|
38
|
+
|
|
39
|
+
// Retry Configuration
|
|
40
|
+
DEFAULT_MAX_RETRIES: 3,
|
|
41
|
+
RETRY_BASE_DELAY_MS: 1000, // 1 second base delay
|
|
42
|
+
|
|
43
|
+
// Key Validation
|
|
44
|
+
MAX_KEY_LENGTH: 1024,
|
|
45
|
+
MAX_FILENAME_LENGTH: 255,
|
|
46
|
+
|
|
47
|
+
// Rate Limiting (requests per window)
|
|
48
|
+
DEFAULT_RATE_LIMIT_REQUESTS: 100,
|
|
49
|
+
DEFAULT_RATE_LIMIT_WINDOW_MS: 60 * 1000, // 1 minute
|
|
50
|
+
|
|
51
|
+
// Health Check
|
|
52
|
+
HEALTH_CHECK_KEY: '__health_check__',
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* File size thresholds for multipart upload decisions
|
|
57
|
+
*/
|
|
58
|
+
export const MULTIPART_THRESHOLDS = {
|
|
59
|
+
SMALL_FILE: 100 * 1024 * 1024, // < 100MB - use simple upload
|
|
60
|
+
MEDIUM_FILE: 1024 * 1024 * 1024, // < 1GB - use 10MB parts
|
|
61
|
+
LARGE_FILE: Infinity, // >= 1GB - use 25MB parts
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Part sizes based on file size
|
|
66
|
+
*/
|
|
67
|
+
export const PART_SIZES = {
|
|
68
|
+
SMALL: 5 * 1024 * 1024, // 5MB for files < 100MB
|
|
69
|
+
MEDIUM: 10 * 1024 * 1024, // 10MB for files < 1GB
|
|
70
|
+
LARGE: 25 * 1024 * 1024, // 25MB for files >= 1GB
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Default storage capabilities for providers that don't implement getCapabilities()
|
|
75
|
+
* Single source of truth for capability defaults
|
|
76
|
+
*/
|
|
77
|
+
export const DEFAULT_STORAGE_CAPABILITIES = {
|
|
78
|
+
multipart: false,
|
|
79
|
+
presignedUrls: false,
|
|
80
|
+
progressTracking: false,
|
|
81
|
+
abortSignal: false,
|
|
82
|
+
metadata: false,
|
|
83
|
+
customDomains: false,
|
|
84
|
+
edgeCompatible: false,
|
|
85
|
+
versioning: false,
|
|
86
|
+
encryption: false,
|
|
87
|
+
directoryListing: false,
|
|
88
|
+
} as const;
|