@kumix/storage 0.1.0
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 +291 -0
- package/dist/cloudinary.d.ts +93 -0
- package/dist/cloudinary.d.ts.map +1 -0
- package/dist/cloudinary.js +516 -0
- package/dist/cloudinary.js.map +1 -0
- package/dist/config-J6AdDh_c.d.ts +594 -0
- package/dist/config-J6AdDh_c.d.ts.map +1 -0
- package/dist/config-t-NVJICl.js +410 -0
- package/dist/config-t-NVJICl.js.map +1 -0
- package/dist/helpers.d.ts +764 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +1042 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/s3.d.ts +109 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +779 -0
- package/dist/s3.js.map +1 -0
- package/package.json +94 -0
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
import { lookup } from "mime-types";
|
|
2
|
+
//#region src/helpers.ts
|
|
3
|
+
/**
|
|
4
|
+
* Storage helper utilities
|
|
5
|
+
* Provides utility functions for file operations, MIME type detection, and path manipulation
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate a unique file key with timestamp and random string
|
|
9
|
+
* @param originalName Original file name
|
|
10
|
+
* @param prefix Optional prefix for the key
|
|
11
|
+
* @returns Unique file key
|
|
12
|
+
* @public
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { generateFileKey } from '@kumix/storage/helpers';
|
|
17
|
+
*
|
|
18
|
+
* const key1 = generateFileKey('document.pdf');
|
|
19
|
+
* // Returns: "document-1703123456789-abc123.pdf"
|
|
20
|
+
*
|
|
21
|
+
* const key2 = generateFileKey('photo.jpg', 'uploads/images');
|
|
22
|
+
* // Returns: "uploads/images/photo-1703123456789-def456.jpg"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function generateFileKey(originalName, prefix) {
|
|
26
|
+
const timestamp = Date.now();
|
|
27
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
28
|
+
const extension = originalName.split(".").pop();
|
|
29
|
+
const fileName = `${originalName.split(".").slice(0, -1).join(".")}-${timestamp}-${random}${extension ? `.${extension}` : ""}`;
|
|
30
|
+
return prefix ? `${prefix}/${fileName}` : fileName;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get MIME type from file name or extension
|
|
34
|
+
* @param fileName File name or path
|
|
35
|
+
* @returns MIME type string
|
|
36
|
+
* @public
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { getMimeType } from '@kumix/storage/helpers';
|
|
41
|
+
*
|
|
42
|
+
* const type1 = getMimeType('document.pdf');
|
|
43
|
+
* // Returns: "application/pdf"
|
|
44
|
+
*
|
|
45
|
+
* const type2 = getMimeType('photo.jpg');
|
|
46
|
+
* // Returns: "image/jpeg"
|
|
47
|
+
*
|
|
48
|
+
* const type3 = getMimeType('unknown.xyz');
|
|
49
|
+
* // Returns: "application/octet-stream"
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
function getMimeType(fileName) {
|
|
53
|
+
return lookup(fileName) || "application/octet-stream";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate file size against maximum allowed size
|
|
57
|
+
* @param fileSize File size in bytes
|
|
58
|
+
* @param maxSize Maximum allowed size in bytes
|
|
59
|
+
* @returns Validation result with error message if invalid
|
|
60
|
+
* @public
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { validateFileSize } from '@kumix/storage/helpers';
|
|
65
|
+
*
|
|
66
|
+
* const result1 = validateFileSize(1024 * 1024, 5 * 1024 * 1024); // 1MB vs 5MB limit
|
|
67
|
+
* // Returns: { valid: true }
|
|
68
|
+
*
|
|
69
|
+
* const result2 = validateFileSize(10 * 1024 * 1024, 5 * 1024 * 1024); // 10MB vs 5MB limit
|
|
70
|
+
* // Returns: { valid: false, error: "File size 10 MB exceeds maximum allowed size 5 MB" }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
function validateFileSize(fileSize, maxSize) {
|
|
74
|
+
if (fileSize > maxSize) return {
|
|
75
|
+
valid: false,
|
|
76
|
+
error: `File size ${formatFileSize(fileSize)} exceeds maximum allowed size ${formatFileSize(maxSize)}`
|
|
77
|
+
};
|
|
78
|
+
return { valid: true };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validate file type against allowed types (MIME types or extensions)
|
|
82
|
+
* @param fileName File name or path
|
|
83
|
+
* @param allowedTypes Array of allowed MIME types or file extensions
|
|
84
|
+
* @returns Validation result with error message if invalid
|
|
85
|
+
* @public
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* import { validateFileType } from '@kumix/storage/helpers';
|
|
90
|
+
*
|
|
91
|
+
* // Using MIME types
|
|
92
|
+
* const result1 = validateFileType('photo.jpg', ['image/*', 'application/pdf']);
|
|
93
|
+
* // Returns: { valid: true }
|
|
94
|
+
*
|
|
95
|
+
* // Using file extensions
|
|
96
|
+
* const result2 = validateFileType('document.txt', ['.pdf', '.doc', '.docx']);
|
|
97
|
+
* // Returns: { valid: false, error: "File type not allowed. Allowed types: .pdf, .doc, .docx" }
|
|
98
|
+
*
|
|
99
|
+
* // Mixed MIME types and extensions
|
|
100
|
+
* const result3 = validateFileType('video.mp4', ['image/*', '.pdf', 'video/mp4']);
|
|
101
|
+
* // Returns: { valid: true }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
function validateFileType(fileName, allowedTypes) {
|
|
105
|
+
const mimeType = getMimeType(fileName);
|
|
106
|
+
const extension = fileName.split(".").pop()?.toLowerCase();
|
|
107
|
+
const isValidMime = allowedTypes.some((type) => {
|
|
108
|
+
if (type.includes("*")) {
|
|
109
|
+
const baseType = type.split("/")[0];
|
|
110
|
+
return mimeType.startsWith(baseType);
|
|
111
|
+
}
|
|
112
|
+
return mimeType === type;
|
|
113
|
+
});
|
|
114
|
+
const isValidExtension = extension && allowedTypes.includes(`.${extension}`);
|
|
115
|
+
if (!isValidMime && !isValidExtension) return {
|
|
116
|
+
valid: false,
|
|
117
|
+
error: `File type not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
118
|
+
};
|
|
119
|
+
return { valid: true };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format file size in human readable format
|
|
123
|
+
* @param bytes File size in bytes
|
|
124
|
+
* @returns Formatted file size string
|
|
125
|
+
* @public
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* import { formatFileSize } from '@kumix/storage/helpers';
|
|
130
|
+
*
|
|
131
|
+
* const size1 = formatFileSize(1024);
|
|
132
|
+
* // Returns: "1 KB"
|
|
133
|
+
*
|
|
134
|
+
* const size2 = formatFileSize(1048576);
|
|
135
|
+
* // Returns: "1 MB"
|
|
136
|
+
*
|
|
137
|
+
* const size3 = formatFileSize(1073741824);
|
|
138
|
+
* // Returns: "1 GB"
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
function formatFileSize(bytes) {
|
|
142
|
+
if (bytes === 0) return "0 Bytes";
|
|
143
|
+
const k = 1024;
|
|
144
|
+
const sizes = [
|
|
145
|
+
"Bytes",
|
|
146
|
+
"KB",
|
|
147
|
+
"MB",
|
|
148
|
+
"GB",
|
|
149
|
+
"TB"
|
|
150
|
+
];
|
|
151
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
152
|
+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Sanitize file name for S3 key by removing special characters
|
|
156
|
+
* @param fileName Original file name
|
|
157
|
+
* @returns Sanitized file name safe for S3 keys
|
|
158
|
+
* @public
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* import { sanitizeFileName } from '@kumix/storage/helpers';
|
|
163
|
+
*
|
|
164
|
+
* const safe1 = sanitizeFileName('My Document (2023).pdf');
|
|
165
|
+
* // Returns: "my_document_2023.pdf"
|
|
166
|
+
*
|
|
167
|
+
* const safe2 = sanitizeFileName('file@#$%name.txt');
|
|
168
|
+
* // Returns: "file_name.txt"
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
function sanitizeFileName(fileName) {
|
|
172
|
+
return fileName.replace(/[^a-zA-Z0-9.-]/g, "_").replace(/_{2,}/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract file extension from file name
|
|
176
|
+
* @param fileName File name or path
|
|
177
|
+
* @returns File extension with dot (e.g., '.pdf')
|
|
178
|
+
* @public
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* import { getFileExtension } from '@kumix/storage/helpers';
|
|
183
|
+
*
|
|
184
|
+
* const ext1 = getFileExtension('document.pdf');
|
|
185
|
+
* // Returns: ".pdf"
|
|
186
|
+
*
|
|
187
|
+
* const ext2 = getFileExtension('photo.JPEG');
|
|
188
|
+
* // Returns: ".jpeg"
|
|
189
|
+
*
|
|
190
|
+
* const ext3 = getFileExtension('noextension');
|
|
191
|
+
* // Returns: ""
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
function getFileExtension(fileName) {
|
|
195
|
+
const extension = fileName.split(".").pop();
|
|
196
|
+
return extension ? `.${extension.toLowerCase()}` : "";
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Generate cache control header for HTTP responses
|
|
200
|
+
* @param maxAge Cache max age in seconds (default: 1 year)
|
|
201
|
+
* @param isPublic Whether cache should be public or private
|
|
202
|
+
* @returns Cache control header string
|
|
203
|
+
* @public
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* import { generateCacheControl } from '@kumix/storage/helpers';
|
|
208
|
+
*
|
|
209
|
+
* const cache1 = generateCacheControl();
|
|
210
|
+
* // Returns: "public, max-age=31536000"
|
|
211
|
+
*
|
|
212
|
+
* const cache2 = generateCacheControl(3600, false);
|
|
213
|
+
* // Returns: "private, max-age=3600"
|
|
214
|
+
*
|
|
215
|
+
* const cache3 = generateCacheControl(86400); // 1 day
|
|
216
|
+
* // Returns: "public, max-age=86400"
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
function generateCacheControl(maxAge = 31536e3, isPublic = true) {
|
|
220
|
+
return `${isPublic ? "public" : "private"}, max-age=${maxAge}`;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Parse S3 URL to extract bucket and key
|
|
224
|
+
* @param url S3 URL to parse
|
|
225
|
+
* @returns Object containing bucket and key
|
|
226
|
+
* @public
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* import { parseS3Url } from '@kumix/storage/helpers';
|
|
231
|
+
*
|
|
232
|
+
* // Virtual hosted-style URL
|
|
233
|
+
* const result1 = parseS3Url('https://my-bucket.s3.us-east-1.amazonaws.com/folder/file.pdf');
|
|
234
|
+
* // Returns: { bucket: "my-bucket", key: "folder/file.pdf" }
|
|
235
|
+
*
|
|
236
|
+
* // Path-style URL
|
|
237
|
+
* const result2 = parseS3Url('https://s3.us-east-1.amazonaws.com/my-bucket/folder/file.pdf');
|
|
238
|
+
* // Returns: { bucket: "my-bucket", key: "folder/file.pdf" }
|
|
239
|
+
*
|
|
240
|
+
* // Custom endpoint (R2, MinIO)
|
|
241
|
+
* const result3 = parseS3Url('https://my-endpoint.com/my-bucket/folder/file.pdf');
|
|
242
|
+
* // Returns: { bucket: "my-bucket", key: "folder/file.pdf" }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
function parseS3Url(url) {
|
|
246
|
+
try {
|
|
247
|
+
const urlObj = new URL(url);
|
|
248
|
+
if (urlObj.hostname.includes("amazonaws.com")) if (urlObj.hostname.startsWith("s3")) {
|
|
249
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
250
|
+
return {
|
|
251
|
+
bucket: pathParts[0],
|
|
252
|
+
key: pathParts.slice(1).join("/")
|
|
253
|
+
};
|
|
254
|
+
} else return {
|
|
255
|
+
bucket: urlObj.hostname.split(".")[0],
|
|
256
|
+
key: urlObj.pathname.substring(1)
|
|
257
|
+
};
|
|
258
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
259
|
+
return {
|
|
260
|
+
bucket: pathParts[0],
|
|
261
|
+
key: pathParts.slice(1).join("/")
|
|
262
|
+
};
|
|
263
|
+
} catch {
|
|
264
|
+
return {};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Build public URL for a file
|
|
269
|
+
* @param baseUrl Base URL of the storage service
|
|
270
|
+
* @param bucket Bucket name
|
|
271
|
+
* @param key File key/path
|
|
272
|
+
* @returns Complete public URL
|
|
273
|
+
* @public
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* import { buildPublicUrl } from '@kumix/storage/helpers';
|
|
278
|
+
*
|
|
279
|
+
* const url1 = buildPublicUrl('https://cdn.example.com', 'my-bucket', 'folder/file.pdf');
|
|
280
|
+
* // Returns: "https://cdn.example.com/my-bucket/folder/file.pdf"
|
|
281
|
+
*
|
|
282
|
+
* const url2 = buildPublicUrl('https://storage.example.com/', 'assets', 'images/logo.png');
|
|
283
|
+
* // Returns: "https://storage.example.com/assets/images/logo.png"
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
function buildPublicUrl(baseUrl, bucket, key, supabase) {
|
|
287
|
+
let cleanBaseUrl;
|
|
288
|
+
cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
289
|
+
const cleanKey = key.replace(/^\//, "");
|
|
290
|
+
if (supabase) {
|
|
291
|
+
cleanBaseUrl = cleanBaseUrl.replace(/\.storage/, "").replace(/\/storage\/v1\/s3$/, "");
|
|
292
|
+
cleanBaseUrl = `${cleanBaseUrl}/storage/v1/object/public`;
|
|
293
|
+
}
|
|
294
|
+
return `${cleanBaseUrl}/${bucket === "" ? "" : `${bucket}/`}${cleanKey}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Validate S3 key format according to AWS S3 naming rules
|
|
298
|
+
* @param key S3 object key to validate
|
|
299
|
+
* @returns Validation result with error message if invalid
|
|
300
|
+
* @public
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* import { validateS3Key } from '@kumix/storage/helpers';
|
|
305
|
+
*
|
|
306
|
+
* const result1 = validateS3Key('documents/report.pdf');
|
|
307
|
+
* // Returns: { valid: true }
|
|
308
|
+
*
|
|
309
|
+
* const result2 = validateS3Key('/invalid/key/');
|
|
310
|
+
* // Returns: { valid: false, error: "Key cannot start or end with forward slash" }
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
function validateS3Key(key) {
|
|
314
|
+
if (!key || key.length === 0) return {
|
|
315
|
+
valid: false,
|
|
316
|
+
error: "Key cannot be empty"
|
|
317
|
+
};
|
|
318
|
+
if (key.length > 1024) return {
|
|
319
|
+
valid: false,
|
|
320
|
+
error: "Key cannot exceed 1024 characters"
|
|
321
|
+
};
|
|
322
|
+
if (key.startsWith("/") || key.endsWith("/")) return {
|
|
323
|
+
valid: false,
|
|
324
|
+
error: "Key cannot start or end with forward slash"
|
|
325
|
+
};
|
|
326
|
+
if (/[^\w\-_./]/.test(key)) return {
|
|
327
|
+
valid: false,
|
|
328
|
+
error: "Key contains invalid characters"
|
|
329
|
+
};
|
|
330
|
+
return { valid: true };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Convert File object to Buffer for upload
|
|
334
|
+
* @param file File object from browser input
|
|
335
|
+
* @returns Promise that resolves to Buffer
|
|
336
|
+
* @public
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* import { fileToBuffer } from '@kumix/storage/helpers';
|
|
341
|
+
*
|
|
342
|
+
* // In browser with file input
|
|
343
|
+
* const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
344
|
+
* const file = fileInput.files?.[0];
|
|
345
|
+
*
|
|
346
|
+
* if (file) {
|
|
347
|
+
* const buffer = await fileToBuffer(file);
|
|
348
|
+
* // Use buffer for upload
|
|
349
|
+
* await storage.uploadFile('uploads/file.pdf', buffer);
|
|
350
|
+
* }
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
async function fileToBuffer(file) {
|
|
354
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
355
|
+
return new Uint8Array(arrayBuffer);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get file information from File object
|
|
359
|
+
* @param file File object from browser input
|
|
360
|
+
* @returns Object containing file metadata
|
|
361
|
+
* @public
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```typescript
|
|
365
|
+
* import { getFileInfo } from '@kumix/storage/helpers';
|
|
366
|
+
*
|
|
367
|
+
* // In browser with file input
|
|
368
|
+
* const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
369
|
+
* const file = fileInput.files?.[0];
|
|
370
|
+
*
|
|
371
|
+
* if (file) {
|
|
372
|
+
* const info = getFileInfo(file);
|
|
373
|
+
* console.log(info);
|
|
374
|
+
* // Returns: {
|
|
375
|
+
* // name: "document.pdf",
|
|
376
|
+
* // size: 1048576,
|
|
377
|
+
* // type: "application/pdf",
|
|
378
|
+
* // lastModified: Date
|
|
379
|
+
* // }
|
|
380
|
+
* }
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
function getFileInfo(file) {
|
|
384
|
+
return {
|
|
385
|
+
name: file.name,
|
|
386
|
+
size: file.size,
|
|
387
|
+
type: file.type || getMimeType(file.name),
|
|
388
|
+
lastModified: new Date(file.lastModified)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Generate unique key with prefix and timestamp
|
|
393
|
+
* @param fileName Original file name
|
|
394
|
+
* @param prefix Optional prefix for the key
|
|
395
|
+
* @param includeTimestamp Whether to include timestamp for uniqueness
|
|
396
|
+
* @returns Unique file key
|
|
397
|
+
* @public
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* import { generateUniqueKey } from '@kumix/storage/helpers';
|
|
402
|
+
*
|
|
403
|
+
* const key1 = generateUniqueKey('report.pdf', 'documents');
|
|
404
|
+
* // Returns: "documents/report-1703123456789-abc123.pdf"
|
|
405
|
+
*
|
|
406
|
+
* const key2 = generateUniqueKey('photo.jpg', 'images', false);
|
|
407
|
+
* // Returns: "images/photo.jpg"
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
function generateUniqueKey(fileName, prefix, includeTimestamp = true) {
|
|
411
|
+
const sanitized = sanitizeFileName(fileName);
|
|
412
|
+
const extension = getFileExtension(sanitized);
|
|
413
|
+
const baseName = sanitized.replace(extension, "");
|
|
414
|
+
let uniquePart = "";
|
|
415
|
+
if (includeTimestamp) uniquePart = `-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
|
|
416
|
+
const finalName = `${baseName}${uniquePart}${extension}`;
|
|
417
|
+
return prefix ? `${prefix}/${finalName}` : finalName;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Extract bucket, key, and region from S3 URL
|
|
421
|
+
* @param url S3 URL to parse
|
|
422
|
+
* @returns Object containing bucket, key, and region information
|
|
423
|
+
* @public
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* import { extractS3Info } from '@kumix/storage/helpers';
|
|
428
|
+
*
|
|
429
|
+
* // AWS S3 virtual hosted-style
|
|
430
|
+
* const result1 = extractS3Info('https://my-bucket.s3.us-west-2.amazonaws.com/folder/file.pdf');
|
|
431
|
+
* // Returns: { bucket: "my-bucket", key: "folder/file.pdf", region: "us-west-2" }
|
|
432
|
+
*
|
|
433
|
+
* // AWS S3 path-style
|
|
434
|
+
* const result2 = extractS3Info('https://s3.eu-central-1.amazonaws.com/my-bucket/file.pdf');
|
|
435
|
+
* // Returns: { bucket: "my-bucket", key: "file.pdf", region: "eu-central-1" }
|
|
436
|
+
*
|
|
437
|
+
* // Custom endpoint
|
|
438
|
+
* const result3 = extractS3Info('https://minio.example.com/my-bucket/folder/file.pdf');
|
|
439
|
+
* // Returns: { bucket: "my-bucket", key: "folder/file.pdf" }
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
function extractS3Info(url) {
|
|
443
|
+
try {
|
|
444
|
+
const urlObj = new URL(url);
|
|
445
|
+
if (urlObj.hostname.includes("amazonaws.com")) if (urlObj.hostname.startsWith("s3")) {
|
|
446
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
447
|
+
const region = urlObj.hostname.split(".")[1];
|
|
448
|
+
return {
|
|
449
|
+
bucket: pathParts[0],
|
|
450
|
+
key: pathParts.slice(1).join("/"),
|
|
451
|
+
region
|
|
452
|
+
};
|
|
453
|
+
} else {
|
|
454
|
+
const hostParts = urlObj.hostname.split(".");
|
|
455
|
+
const bucket = hostParts[0];
|
|
456
|
+
const region = hostParts[2];
|
|
457
|
+
return {
|
|
458
|
+
bucket,
|
|
459
|
+
key: urlObj.pathname.substring(1),
|
|
460
|
+
region
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
464
|
+
return {
|
|
465
|
+
bucket: pathParts[0],
|
|
466
|
+
key: pathParts.slice(1).join("/")
|
|
467
|
+
};
|
|
468
|
+
} catch {
|
|
469
|
+
return {};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Validate S3 configuration object
|
|
474
|
+
* @param config S3 configuration object to validate
|
|
475
|
+
* @returns Validation result with list of errors
|
|
476
|
+
* @public
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* import { validateS3Config } from '@kumix/storage/helpers';
|
|
481
|
+
*
|
|
482
|
+
* const config = {
|
|
483
|
+
* provider: 'aws',
|
|
484
|
+
* region: 'us-east-1',
|
|
485
|
+
* bucket: 'my-bucket',
|
|
486
|
+
* accessKeyId: 'AKIA...',
|
|
487
|
+
* secretAccessKey: 'secret...'
|
|
488
|
+
* };
|
|
489
|
+
*
|
|
490
|
+
* const result = validateS3Config(config);
|
|
491
|
+
* // Returns: { valid: true, errors: [] }
|
|
492
|
+
*
|
|
493
|
+
* const invalidConfig = { provider: 'aws' }; // Missing required fields
|
|
494
|
+
* const result2 = validateS3Config(invalidConfig);
|
|
495
|
+
* // Returns: { valid: false, errors: ["Region is required", "Bucket is required", ...] }
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
function validateS3Config(config) {
|
|
499
|
+
const errors = [];
|
|
500
|
+
if (!config.provider) errors.push("Provider is required");
|
|
501
|
+
if (!config.region) errors.push("Region is required");
|
|
502
|
+
if (!config.bucket) errors.push("Bucket is required");
|
|
503
|
+
if (!config.accessKeyId) errors.push("Access Key ID is required");
|
|
504
|
+
if (!config.secretAccessKey) errors.push("Secret Access Key is required");
|
|
505
|
+
if (config.bucket && typeof config.bucket === "string") {
|
|
506
|
+
const bucketValidation = validateBucketName(config.bucket);
|
|
507
|
+
if (!bucketValidation.valid) errors.push(`Invalid bucket name: ${bucketValidation.error}`);
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
valid: errors.length === 0,
|
|
511
|
+
errors
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Validate S3 bucket name according to AWS naming rules
|
|
516
|
+
* @param bucketName Bucket name to validate
|
|
517
|
+
* @returns Validation result with error message if invalid
|
|
518
|
+
* @public
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```typescript
|
|
522
|
+
* import { validateBucketName } from '@kumix/storage/helpers';
|
|
523
|
+
*
|
|
524
|
+
* const result1 = validateBucketName('my-valid-bucket');
|
|
525
|
+
* // Returns: { valid: true }
|
|
526
|
+
*
|
|
527
|
+
* const result2 = validateBucketName('My-Invalid-Bucket');
|
|
528
|
+
* // Returns: { valid: false, error: "Bucket name can only contain lowercase letters, numbers, dots, and hyphens" }
|
|
529
|
+
*
|
|
530
|
+
* const result3 = validateBucketName('ab');
|
|
531
|
+
* // Returns: { valid: false, error: "Bucket name must be between 3 and 63 characters" }
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
function validateBucketName(bucketName) {
|
|
535
|
+
if (!bucketName || bucketName.length === 0) return {
|
|
536
|
+
valid: false,
|
|
537
|
+
error: "Bucket name cannot be empty"
|
|
538
|
+
};
|
|
539
|
+
if (bucketName.length < 3 || bucketName.length > 63) return {
|
|
540
|
+
valid: false,
|
|
541
|
+
error: "Bucket name must be between 3 and 63 characters"
|
|
542
|
+
};
|
|
543
|
+
if (!/^[a-z0-9.-]+$/.test(bucketName)) return {
|
|
544
|
+
valid: false,
|
|
545
|
+
error: "Bucket name can only contain lowercase letters, numbers, dots, and hyphens"
|
|
546
|
+
};
|
|
547
|
+
if (bucketName.startsWith(".") || bucketName.endsWith(".")) return {
|
|
548
|
+
valid: false,
|
|
549
|
+
error: "Bucket name cannot start or end with a dot"
|
|
550
|
+
};
|
|
551
|
+
if (bucketName.startsWith("-") || bucketName.endsWith("-")) return {
|
|
552
|
+
valid: false,
|
|
553
|
+
error: "Bucket name cannot start or end with a hyphen"
|
|
554
|
+
};
|
|
555
|
+
if (bucketName.includes("..")) return {
|
|
556
|
+
valid: false,
|
|
557
|
+
error: "Bucket name cannot contain consecutive dots"
|
|
558
|
+
};
|
|
559
|
+
return { valid: true };
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Generate content disposition header for file downloads
|
|
563
|
+
* @param fileName File name for the download
|
|
564
|
+
* @param disposition Whether to display inline or as attachment
|
|
565
|
+
* @returns Content disposition header string
|
|
566
|
+
* @public
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```typescript
|
|
570
|
+
* import { getContentDisposition } from '@kumix/storage/helpers';
|
|
571
|
+
*
|
|
572
|
+
* const header1 = getContentDisposition('document.pdf');
|
|
573
|
+
* // Returns: 'attachment; filename="document.pdf"'
|
|
574
|
+
*
|
|
575
|
+
* const header2 = getContentDisposition('image.jpg', 'inline');
|
|
576
|
+
* // Returns: 'inline; filename="image.jpg"'
|
|
577
|
+
*
|
|
578
|
+
* const header3 = getContentDisposition('file@#$name.txt');
|
|
579
|
+
* // Returns: 'attachment; filename="filename.txt"'
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
function getContentDisposition(fileName, disposition = "attachment") {
|
|
583
|
+
return `${disposition}; filename="${fileName.replace(/[^\w\s.-]/gi, "")}"`;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Check if file is an image based on MIME type
|
|
587
|
+
* @param fileName File name or path
|
|
588
|
+
* @returns True if file is an image
|
|
589
|
+
* @public
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* ```typescript
|
|
593
|
+
* import { isImageFile } from '@kumix/storage/helpers';
|
|
594
|
+
*
|
|
595
|
+
* const result1 = isImageFile('photo.jpg');
|
|
596
|
+
* // Returns: true
|
|
597
|
+
*
|
|
598
|
+
* const result2 = isImageFile('document.pdf');
|
|
599
|
+
* // Returns: false
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
function isImageFile(fileName) {
|
|
603
|
+
return getMimeType(fileName).startsWith("image/");
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Check if file is a video based on MIME type
|
|
607
|
+
* @param fileName File name or path
|
|
608
|
+
* @returns True if file is a video
|
|
609
|
+
* @public
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* import { isVideoFile } from '@kumix/storage/helpers';
|
|
614
|
+
*
|
|
615
|
+
* const result1 = isVideoFile('movie.mp4');
|
|
616
|
+
* // Returns: true
|
|
617
|
+
*
|
|
618
|
+
* const result2 = isVideoFile('document.pdf');
|
|
619
|
+
* // Returns: false
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
function isVideoFile(fileName) {
|
|
623
|
+
return getMimeType(fileName).startsWith("video/");
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Check if file is an audio file based on MIME type
|
|
627
|
+
* @param fileName File name or path
|
|
628
|
+
* @returns True if file is an audio file
|
|
629
|
+
* @public
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```typescript
|
|
633
|
+
* import { isAudioFile } from '@kumix/storage/helpers';
|
|
634
|
+
*
|
|
635
|
+
* const result1 = isAudioFile('song.mp3');
|
|
636
|
+
* // Returns: true
|
|
637
|
+
*
|
|
638
|
+
* const result2 = isAudioFile('document.pdf');
|
|
639
|
+
* // Returns: false
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
function isAudioFile(fileName) {
|
|
643
|
+
return getMimeType(fileName).startsWith("audio/");
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Check if file is a document based on MIME type
|
|
647
|
+
* @param fileName File name or path
|
|
648
|
+
* @returns True if file is a document
|
|
649
|
+
* @public
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* ```typescript
|
|
653
|
+
* import { isDocumentFile } from '@kumix/storage/helpers';
|
|
654
|
+
*
|
|
655
|
+
* const result1 = isDocumentFile('report.pdf');
|
|
656
|
+
* // Returns: true
|
|
657
|
+
*
|
|
658
|
+
* const result2 = isDocumentFile('spreadsheet.xlsx');
|
|
659
|
+
* // Returns: true
|
|
660
|
+
*
|
|
661
|
+
* const result3 = isDocumentFile('photo.jpg');
|
|
662
|
+
* // Returns: false
|
|
663
|
+
* ```
|
|
664
|
+
*/
|
|
665
|
+
function isDocumentFile(fileName) {
|
|
666
|
+
const mimeType = getMimeType(fileName);
|
|
667
|
+
return [
|
|
668
|
+
"application/pdf",
|
|
669
|
+
"application/msword",
|
|
670
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
671
|
+
"application/vnd.ms-excel",
|
|
672
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
673
|
+
"application/vnd.ms-powerpoint",
|
|
674
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
675
|
+
"text/plain",
|
|
676
|
+
"text/csv"
|
|
677
|
+
].includes(mimeType);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Normalize path separators and remove redundant slashes
|
|
681
|
+
* @param path Path to normalize
|
|
682
|
+
* @returns Normalized path
|
|
683
|
+
* @public
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* import { normalizePath } from '@kumix/storage/helpers';
|
|
688
|
+
*
|
|
689
|
+
* const path1 = normalizePath('folder//subfolder///file.txt');
|
|
690
|
+
* // Returns: "folder/subfolder/file.txt"
|
|
691
|
+
*
|
|
692
|
+
* const path2 = normalizePath('\\folder\\file.txt');
|
|
693
|
+
* // Returns: "folder/file.txt"
|
|
694
|
+
*
|
|
695
|
+
* const path3 = normalizePath('/folder/file.txt/');
|
|
696
|
+
* // Returns: "folder/file.txt"
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
function normalizePath(path) {
|
|
700
|
+
return path.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Join path segments safely with proper separators
|
|
704
|
+
* @param segments Path segments to join
|
|
705
|
+
* @returns Joined path
|
|
706
|
+
* @public
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```typescript
|
|
710
|
+
* import { joinPath } from '@kumix/storage/helpers';
|
|
711
|
+
*
|
|
712
|
+
* const path1 = joinPath('folder', 'subfolder', 'file.txt');
|
|
713
|
+
* // Returns: "folder/subfolder/file.txt"
|
|
714
|
+
*
|
|
715
|
+
* const path2 = joinPath('/folder/', '/subfolder/', '/file.txt/');
|
|
716
|
+
* // Returns: "folder/subfolder/file.txt"
|
|
717
|
+
*
|
|
718
|
+
* const path3 = joinPath('', 'folder', '', 'file.txt');
|
|
719
|
+
* // Returns: "folder/file.txt"
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
function joinPath(...segments) {
|
|
723
|
+
return segments.filter((segment) => segment && segment.trim() !== "").map((segment) => segment.replace(/^\/+|\/+$/g, "")).filter((segment) => segment !== "").join("/");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Get parent directory path from a file key
|
|
727
|
+
* @param key File key or path
|
|
728
|
+
* @returns Parent directory path
|
|
729
|
+
* @public
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```typescript
|
|
733
|
+
* import { getParentPath } from '@kumix/storage/helpers';
|
|
734
|
+
*
|
|
735
|
+
* const parent1 = getParentPath('folder/subfolder/file.txt');
|
|
736
|
+
* // Returns: "folder/subfolder"
|
|
737
|
+
*
|
|
738
|
+
* const parent2 = getParentPath('file.txt');
|
|
739
|
+
* // Returns: ""
|
|
740
|
+
*
|
|
741
|
+
* const parent3 = getParentPath('folder/file.txt');
|
|
742
|
+
* // Returns: "folder"
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
function getParentPath(key) {
|
|
746
|
+
const normalizedKey = normalizePath(key);
|
|
747
|
+
const lastSlashIndex = normalizedKey.lastIndexOf("/");
|
|
748
|
+
return lastSlashIndex === -1 ? "" : normalizedKey.substring(0, lastSlashIndex);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Get filename from a file key or path
|
|
752
|
+
* @param key File key or path
|
|
753
|
+
* @returns Filename without path
|
|
754
|
+
* @public
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* ```typescript
|
|
758
|
+
* import { getFileName } from '@kumix/storage/helpers';
|
|
759
|
+
*
|
|
760
|
+
* const name1 = getFileName('folder/subfolder/document.pdf');
|
|
761
|
+
* // Returns: "document.pdf"
|
|
762
|
+
*
|
|
763
|
+
* const name2 = getFileName('file.txt');
|
|
764
|
+
* // Returns: "file.txt"
|
|
765
|
+
*
|
|
766
|
+
* const name3 = getFileName('folder/subfolder/');
|
|
767
|
+
* // Returns: ""
|
|
768
|
+
* ```
|
|
769
|
+
*/
|
|
770
|
+
function getFileName(key) {
|
|
771
|
+
const normalizedKey = normalizePath(key);
|
|
772
|
+
const lastSlashIndex = normalizedKey.lastIndexOf("/");
|
|
773
|
+
return lastSlashIndex === -1 ? normalizedKey : normalizedKey.substring(lastSlashIndex + 1);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Convert base64 string to Buffer
|
|
777
|
+
* @param base64 Base64 encoded string
|
|
778
|
+
* @returns Buffer containing the decoded data
|
|
779
|
+
* @public
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```typescript
|
|
783
|
+
* import { base64ToBuffer } from '@kumix/storage/helpers';
|
|
784
|
+
*
|
|
785
|
+
* const base64 = 'SGVsbG8gV29ybGQ='; // "Hello World" in base64
|
|
786
|
+
* const buffer = base64ToBuffer(base64);
|
|
787
|
+
* console.log(buffer.toString()); // "Hello World"
|
|
788
|
+
*
|
|
789
|
+
* // For file uploads from base64 data
|
|
790
|
+
* const imageBase64 = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...';
|
|
791
|
+
* const cleanBase64 = imageBase64.split(',')[1]; // Remove data URL prefix
|
|
792
|
+
* const imageBuffer = base64ToBuffer(cleanBase64);
|
|
793
|
+
* await storage.uploadFile('images/photo.jpg', imageBuffer);
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
796
|
+
function base64ToBuffer(base64) {
|
|
797
|
+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
|
|
798
|
+
if (typeof Buffer !== "undefined") return Buffer.from(cleanBase64, "base64");
|
|
799
|
+
const binary = atob(cleanBase64);
|
|
800
|
+
const bytes = new Uint8Array(binary.length);
|
|
801
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
802
|
+
return bytes;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Convert Buffer to base64 string
|
|
806
|
+
* @param buffer Buffer to encode
|
|
807
|
+
* @param includeDataUrl Whether to include data URL prefix
|
|
808
|
+
* @param mimeType MIME type for data URL (required if includeDataUrl is true)
|
|
809
|
+
* @returns Base64 encoded string
|
|
810
|
+
* @public
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```typescript
|
|
814
|
+
* import { bufferToBase64 } from '@kumix/storage/helpers';
|
|
815
|
+
*
|
|
816
|
+
* const buffer = Buffer.from('Hello World');
|
|
817
|
+
* const base64 = bufferToBase64(buffer);
|
|
818
|
+
* // Returns: "SGVsbG8gV29ybGQ="
|
|
819
|
+
*
|
|
820
|
+
* // With data URL for images
|
|
821
|
+
* const imageBuffer = await storage.downloadFile('images/photo.jpg');
|
|
822
|
+
* const dataUrl = bufferToBase64(imageBuffer.content!, true, 'image/jpeg');
|
|
823
|
+
* // Returns: "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..."
|
|
824
|
+
*
|
|
825
|
+
* // Use in HTML img tag
|
|
826
|
+
* document.querySelector('img').src = dataUrl;
|
|
827
|
+
* ```
|
|
828
|
+
*/
|
|
829
|
+
function bufferToBase64(buffer, includeDataUrl = false, mimeType) {
|
|
830
|
+
let base64;
|
|
831
|
+
if (typeof Buffer !== "undefined" && buffer instanceof Buffer) base64 = buffer.toString("base64");
|
|
832
|
+
else {
|
|
833
|
+
let binary = "";
|
|
834
|
+
for (let i = 0; i < buffer.length; i++) binary += String.fromCharCode(buffer[i]);
|
|
835
|
+
base64 = btoa(binary);
|
|
836
|
+
}
|
|
837
|
+
if (includeDataUrl) {
|
|
838
|
+
if (!mimeType) throw new Error("MIME type is required when includeDataUrl is true");
|
|
839
|
+
return `data:${mimeType};base64,${base64}`;
|
|
840
|
+
}
|
|
841
|
+
return base64;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Generate file hash/checksum for integrity verification
|
|
845
|
+
* @param content File content as Buffer or string
|
|
846
|
+
* @param algorithm Hash algorithm to use
|
|
847
|
+
* @returns Hash string
|
|
848
|
+
* @public
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* ```typescript
|
|
852
|
+
* import { generateFileHash } from '@kumix/storage/helpers';
|
|
853
|
+
*
|
|
854
|
+
* const content = Buffer.from('Hello World');
|
|
855
|
+
*
|
|
856
|
+
* const md5Hash = generateFileHash(content, 'md5');
|
|
857
|
+
* // Returns: "b10a8db164e0754105b7a99be72e3fe5"
|
|
858
|
+
*
|
|
859
|
+
* const sha256Hash = generateFileHash(content, 'sha256');
|
|
860
|
+
* // Returns: "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
|
|
861
|
+
*
|
|
862
|
+
* // For file integrity checking
|
|
863
|
+
* const fileBuffer = await storage.downloadFile('document.pdf');
|
|
864
|
+
* const currentHash = generateFileHash(fileBuffer.content!, 'sha256');
|
|
865
|
+
* const expectedHash = 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e';
|
|
866
|
+
*
|
|
867
|
+
* if (currentHash === expectedHash) {
|
|
868
|
+
* console.log('File integrity verified');
|
|
869
|
+
* } else {
|
|
870
|
+
* console.log('File may be corrupted');
|
|
871
|
+
* }
|
|
872
|
+
* ```
|
|
873
|
+
*/
|
|
874
|
+
async function generateFileHash(content, algorithm = "md5") {
|
|
875
|
+
if (algorithm !== "md5" && typeof crypto !== "undefined" && crypto.subtle) {
|
|
876
|
+
const rawData = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
877
|
+
const hashBuffer = await crypto.subtle.digest(algorithm === "sha1" ? "SHA-1" : "SHA-256", rawData);
|
|
878
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
879
|
+
}
|
|
880
|
+
const hash = (await import("node:crypto")).createHash(algorithm);
|
|
881
|
+
hash.update(content);
|
|
882
|
+
return hash.digest("hex");
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Generate multiple unique keys at once for batch operations
|
|
886
|
+
* @param fileNames Array of file names
|
|
887
|
+
* @param prefix Optional prefix for all keys
|
|
888
|
+
* @returns Array of unique keys
|
|
889
|
+
* @public
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* ```typescript
|
|
893
|
+
* import { generateBatchKeys } from '@kumix/storage/helpers';
|
|
894
|
+
*
|
|
895
|
+
* const fileNames = ['document1.pdf', 'document2.pdf', 'image.jpg'];
|
|
896
|
+
* const keys = generateBatchKeys(fileNames, 'uploads');
|
|
897
|
+
* // Returns: [
|
|
898
|
+
* // "uploads/document1-1703123456789-abc123.pdf",
|
|
899
|
+
* // "uploads/document2-1703123456790-def456.pdf",
|
|
900
|
+
* // "uploads/image-1703123456791-ghi789.jpg"
|
|
901
|
+
* // ]
|
|
902
|
+
*
|
|
903
|
+
* // Use for batch uploads
|
|
904
|
+
* const files = [file1, file2, file3];
|
|
905
|
+
* const keys = generateBatchKeys(files.map(f => f.name), 'batch-upload');
|
|
906
|
+
*
|
|
907
|
+
* for (let i = 0; i < files.length; i++) {
|
|
908
|
+
* await storage.uploadFile(keys[i], files[i]);
|
|
909
|
+
* }
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
function generateBatchKeys(fileNames, prefix) {
|
|
913
|
+
return fileNames.map((fileName) => generateFileKey(fileName, prefix));
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Validate multiple files at once with consistent options
|
|
917
|
+
* @param files Array of File objects to validate
|
|
918
|
+
* @param options Validation options
|
|
919
|
+
* @returns Array of validation results
|
|
920
|
+
* @public
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* ```typescript
|
|
924
|
+
* import { validateBatchFiles } from '@kumix/storage/helpers';
|
|
925
|
+
*
|
|
926
|
+
* const files = [file1, file2, file3]; // File objects from input
|
|
927
|
+
* const options = {
|
|
928
|
+
* maxSize: 5 * 1024 * 1024, // 5MB
|
|
929
|
+
* allowedTypes: ['image/*', 'application/pdf'],
|
|
930
|
+
* maxFiles: 10
|
|
931
|
+
* };
|
|
932
|
+
*
|
|
933
|
+
* const results = validateBatchFiles(files, options);
|
|
934
|
+
* // Returns: [
|
|
935
|
+
* // { valid: true, fileName: "image.jpg" },
|
|
936
|
+
* // { valid: false, fileName: "large.pdf", errors: ["File size exceeds limit"] },
|
|
937
|
+
* // { valid: true, fileName: "document.pdf" }
|
|
938
|
+
* // ]
|
|
939
|
+
*
|
|
940
|
+
* const validFiles = files.filter((_, index) => results[index].valid);
|
|
941
|
+
* const invalidFiles = files.filter((_, index) => !results[index].valid);
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
944
|
+
function validateBatchFiles(files, options) {
|
|
945
|
+
const results = [];
|
|
946
|
+
if (options.maxFiles && files.length > options.maxFiles) return files.map((file) => ({
|
|
947
|
+
valid: false,
|
|
948
|
+
fileName: file.name,
|
|
949
|
+
errors: [`Maximum ${options.maxFiles} files allowed, but ${files.length} files provided`]
|
|
950
|
+
}));
|
|
951
|
+
for (const file of files) {
|
|
952
|
+
const errors = [];
|
|
953
|
+
if (options.maxSize) {
|
|
954
|
+
const sizeResult = validateFileSize(file.size, options.maxSize);
|
|
955
|
+
if (!sizeResult.valid && sizeResult.error) errors.push(sizeResult.error);
|
|
956
|
+
}
|
|
957
|
+
if (options.allowedTypes) {
|
|
958
|
+
const typeResult = validateFileType(file.name, options.allowedTypes);
|
|
959
|
+
if (!typeResult.valid && typeResult.error) errors.push(typeResult.error);
|
|
960
|
+
}
|
|
961
|
+
results.push({
|
|
962
|
+
valid: errors.length === 0,
|
|
963
|
+
fileName: file.name,
|
|
964
|
+
errors: errors.length > 0 ? errors : void 0
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return results;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Detect file type from content using magic numbers/file signatures
|
|
971
|
+
* @param buffer File content buffer
|
|
972
|
+
* @returns Detected MIME type or 'application/octet-stream' if unknown
|
|
973
|
+
* @public
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```typescript
|
|
977
|
+
* import { detectFileTypeFromContent } from '@kumix/storage/helpers';
|
|
978
|
+
*
|
|
979
|
+
* // Read file content
|
|
980
|
+
* const fileBuffer = await storage.downloadFile('unknown-file');
|
|
981
|
+
* const detectedType = detectFileTypeFromContent(fileBuffer.content!);
|
|
982
|
+
*
|
|
983
|
+
* console.log(detectedType);
|
|
984
|
+
* // Returns: "image/jpeg" for JPEG files
|
|
985
|
+
* // Returns: "application/pdf" for PDF files
|
|
986
|
+
* // Returns: "image/png" for PNG files
|
|
987
|
+
* // Returns: "application/octet-stream" for unknown types
|
|
988
|
+
*
|
|
989
|
+
* // Verify file type matches extension
|
|
990
|
+
* const fileName = 'document.pdf';
|
|
991
|
+
* const expectedType = getMimeType(fileName);
|
|
992
|
+
* const actualType = detectFileTypeFromContent(fileBuffer.content!);
|
|
993
|
+
*
|
|
994
|
+
* if (expectedType !== actualType) {
|
|
995
|
+
* console.warn('File extension does not match content type');
|
|
996
|
+
* }
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
function detectFileTypeFromContent(buffer) {
|
|
1000
|
+
if (buffer.length === 0) return "application/octet-stream";
|
|
1001
|
+
const header = buffer.subarray(0, 16);
|
|
1002
|
+
if (header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), "") === "%PDF") return "application/pdf";
|
|
1003
|
+
if (header[0] === 255 && header[1] === 216 && header[2] === 255) return "image/jpeg";
|
|
1004
|
+
const pngSig = new Uint8Array([
|
|
1005
|
+
137,
|
|
1006
|
+
80,
|
|
1007
|
+
78,
|
|
1008
|
+
71,
|
|
1009
|
+
13,
|
|
1010
|
+
10,
|
|
1011
|
+
26,
|
|
1012
|
+
10
|
|
1013
|
+
]);
|
|
1014
|
+
if (header.length >= 8 && header.slice(0, 8).every((b, i) => b === pngSig[i])) return "image/png";
|
|
1015
|
+
const gifHead = header.subarray(0, 6).reduce((s, b) => s + String.fromCharCode(b), "");
|
|
1016
|
+
if (gifHead === "GIF87a" || gifHead === "GIF89a") return "image/gif";
|
|
1017
|
+
if (header.subarray(0, 4).reduce((s, b) => s + String.fromCharCode(b), "") === "RIFF" && header.subarray(8, 12).reduce((s, b) => s + String.fromCharCode(b), "") === "WEBP") return "image/webp";
|
|
1018
|
+
if (header[0] === 80 && header[1] === 75 && (header[2] === 3 || header[2] === 5)) {
|
|
1019
|
+
const zipContent = new TextDecoder().decode(buffer.slice(0, Math.min(buffer.length, 1e3)));
|
|
1020
|
+
if (zipContent.includes("word/")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
1021
|
+
if (zipContent.includes("xl/")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
1022
|
+
if (zipContent.includes("ppt/")) return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1023
|
+
return "application/zip";
|
|
1024
|
+
}
|
|
1025
|
+
if (new TextDecoder().decode(header.subarray(4, 8)) === "ftyp") return "video/mp4";
|
|
1026
|
+
if (header[0] === 255 && (header[1] & 224) === 224 || new TextDecoder().decode(header.subarray(0, 3)) === "ID3") return "audio/mpeg";
|
|
1027
|
+
let isText = true;
|
|
1028
|
+
const sampleSize = Math.min(buffer.length, 512);
|
|
1029
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
1030
|
+
const byte = buffer[i];
|
|
1031
|
+
if (byte === 0 || byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
|
|
1032
|
+
isText = false;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (isText) return "text/plain";
|
|
1037
|
+
return "application/octet-stream";
|
|
1038
|
+
}
|
|
1039
|
+
//#endregion
|
|
1040
|
+
export { base64ToBuffer, bufferToBase64, buildPublicUrl, detectFileTypeFromContent, extractS3Info, fileToBuffer, formatFileSize, generateBatchKeys, generateCacheControl, generateFileHash, generateFileKey, generateUniqueKey, getContentDisposition, getFileExtension, getFileInfo, getFileName, getMimeType, getParentPath, isAudioFile, isDocumentFile, isImageFile, isVideoFile, joinPath, normalizePath, parseS3Url, sanitizeFileName, validateBatchFiles, validateBucketName, validateFileSize, validateFileType, validateS3Config, validateS3Key };
|
|
1041
|
+
|
|
1042
|
+
//# sourceMappingURL=helpers.js.map
|