@hdriel/aws-utils 1.1.7 โ 1.2.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 +317 -163
- package/dist/index.cjs +157 -119
- package/dist/index.d.cts +41 -23
- package/dist/index.d.ts +41 -23
- package/dist/index.js +158 -120
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,16 +205,12 @@ await s3.deleteDirectory('/uploads/temp'); // Delete directory and all contents
|
|
|
205
205
|
```typescript
|
|
206
206
|
// CREATE
|
|
207
207
|
// > Upload File
|
|
208
|
-
import { ACLs } from '@hdriel/aws-utils';
|
|
208
|
+
import type { ACLs } from '@hdriel/aws-utils';
|
|
209
209
|
|
|
210
|
-
// Upload buffer
|
|
211
|
-
await s3.
|
|
212
|
-
|
|
213
|
-
// Upload with
|
|
214
|
-
await s3.uploadFile('/public/image.jpg', buffer, ACLs.public_read);
|
|
215
|
-
|
|
216
|
-
// Upload with version tag
|
|
217
|
-
await s3.uploadFile('/docs/v2.pdf', buffer, ACLs.private, '2.0.0');
|
|
210
|
+
await s3.uploadFileContent('/documents/file.pdf', buffer); // Upload buffer
|
|
211
|
+
await s3.uploadFileContent('/documents/file.pdf', [{ type: 'food', value: 'apple' }], { prettier: true /* default true */ }); // Upload object/array data
|
|
212
|
+
await s3.uploadFileContent('/public/image.jpg', buffer, {acl: ACLs.public_read}); // Upload with public access
|
|
213
|
+
await s3.uploadFileContent('/docs/v2.pdf', buffer, {acl: ACLs.private, version: '2.0.0'}); // Upload with version tag
|
|
218
214
|
|
|
219
215
|
// > Generate Presigned URL
|
|
220
216
|
const url = await s3.fileUrl('/private/document.pdf'); // Expires in 15 minutes (default)
|
|
@@ -250,189 +246,347 @@ const gb = await s3.sizeOf('/large-file.zip', 'GB');
|
|
|
250
246
|
// > File Tagging
|
|
251
247
|
await s3.taggingFile('/documents/file.pdf', {Key: 'version', Value: '1.0.0'}); // Tag file with version
|
|
252
248
|
|
|
253
|
-
|
|
254
249
|
// DELETE
|
|
255
250
|
await s3.deleteFile('/documents/old-file.pdf');
|
|
256
251
|
|
|
257
252
|
```
|
|
258
253
|
|
|
259
|
-
###
|
|
254
|
+
### ๐ค File Upload Middleware
|
|
260
255
|
|
|
261
|
-
####
|
|
256
|
+
#### Client Side
|
|
262
257
|
```typescript
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
);
|
|
275
|
-
```
|
|
258
|
+
class S3Service {
|
|
259
|
+
private api: Axios;
|
|
260
|
+
|
|
261
|
+
constructor() {
|
|
262
|
+
this.api = axios.create({
|
|
263
|
+
baseURL: this.baseURL,
|
|
264
|
+
timeout: 30_000,
|
|
265
|
+
headers: {'Content-Type': 'application/json'},
|
|
266
|
+
withCredentials: true,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
276
269
|
|
|
277
|
-
#### Stream Zip Archive
|
|
278
|
-
```typescript
|
|
279
|
-
// Download multiple files as zip
|
|
280
|
-
app.get('/download-all',
|
|
281
|
-
await s3.getStreamZipFileCtr({
|
|
282
|
-
filePath: [
|
|
283
|
-
'/documents/file1.pdf',
|
|
284
|
-
'/documents/file2.pdf',
|
|
285
|
-
'/images/photo.jpg'
|
|
286
|
-
],
|
|
287
|
-
filename: 'archive.zip',
|
|
288
|
-
compressionLevel: 5 // 0-9, lower = faster
|
|
289
|
-
})
|
|
290
|
-
);
|
|
291
|
-
```
|
|
292
270
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
271
|
+
async uploadFile(
|
|
272
|
+
file: File,
|
|
273
|
+
directoryPath: string,
|
|
274
|
+
type?: FILE_TYPE,
|
|
275
|
+
onProgress?: (progress: number) => void
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
if (!file) return;
|
|
279
|
+
|
|
280
|
+
if (this.uploadAbortController) {
|
|
281
|
+
this.uploadAbortController.abort();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.uploadAbortController = new AbortController();
|
|
285
|
+
|
|
286
|
+
if (file.size === 0) {
|
|
287
|
+
const {data: response} = await this.api.post('/files/content', {
|
|
288
|
+
path: directoryPath + file.name,
|
|
289
|
+
data: '',
|
|
290
|
+
signal: this.uploadAbortController.signal,
|
|
291
|
+
});
|
|
292
|
+
return response;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.uploadAbortController.abort();
|
|
296
|
+
this.uploadAbortController = null;
|
|
297
|
+
this.uploadAbortController = new AbortController();
|
|
298
|
+
|
|
299
|
+
const formData = new FormData();
|
|
300
|
+
formData.append('file', file);
|
|
301
|
+
|
|
302
|
+
// Encode directory and filename to handle non-Latin characters
|
|
303
|
+
const encodedDirectory = encodeURIComponent(directoryPath);
|
|
304
|
+
const encodedFilename = encodeURIComponent(file.name);
|
|
305
|
+
|
|
306
|
+
const {data: response} = await this.api.post(`/files/upload/${type || ''}`, formData, {
|
|
307
|
+
headers: {
|
|
308
|
+
'Content-Type': 'multipart/form-data',
|
|
309
|
+
'X-Upload-Directory': encodedDirectory,
|
|
310
|
+
'X-Upload-Filename': encodedFilename,
|
|
311
|
+
},
|
|
312
|
+
timeout: 1_000_000,
|
|
313
|
+
signal: this.uploadAbortController.signal,
|
|
314
|
+
onUploadProgress: onProgress
|
|
315
|
+
? (progressEvent: AxiosProgressEvent) => {
|
|
316
|
+
const percentage = progressEvent.total
|
|
317
|
+
? (progressEvent.loaded / progressEvent.total) * 100
|
|
318
|
+
: 0;
|
|
319
|
+
onProgress(percentage);
|
|
320
|
+
}
|
|
321
|
+
: undefined,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
this.uploadAbortController = null;
|
|
325
|
+
return response;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
this.uploadAbortController = null;
|
|
328
|
+
|
|
329
|
+
console.error('Failed to upload file:', error);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async uploadFiles(
|
|
335
|
+
files: File[],
|
|
336
|
+
directory: string,
|
|
337
|
+
type?: FILE_TYPE,
|
|
338
|
+
onProgress?: (progress: number) => void
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
try {
|
|
341
|
+
if (!files) return;
|
|
342
|
+
|
|
343
|
+
if (this.uploadAbortController) {
|
|
344
|
+
this.uploadAbortController.abort();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.uploadAbortController = new AbortController();
|
|
348
|
+
|
|
349
|
+
await Promise.allSettled(
|
|
350
|
+
files
|
|
351
|
+
.filter((file) => file.size === 0)
|
|
352
|
+
.map(async (file) => {
|
|
353
|
+
const { data: response } = await this.api.post('/files/content', {
|
|
354
|
+
path: [directory.replace(/\/$/, ''), file.name].join('/'),
|
|
355
|
+
data: '',
|
|
356
|
+
});
|
|
357
|
+
return response;
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
files = files.filter((file) => file.size !== 0);
|
|
362
|
+
|
|
363
|
+
const formData = new FormData();
|
|
364
|
+
files.forEach((file) => {
|
|
365
|
+
const copyFile = new File([file], encodeURIComponent(file.name), { type: file.type });
|
|
366
|
+
formData.append('files', copyFile);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const encodedDirectory = encodeURIComponent(directory);
|
|
370
|
+
|
|
371
|
+
const { data: response } = await this.api.post(`/files/multi-upload/${type || ''}`, formData, {
|
|
372
|
+
headers: {
|
|
373
|
+
'Content-Type': 'multipart/form-data',
|
|
374
|
+
'X-Upload-Directory': encodedDirectory,
|
|
375
|
+
},
|
|
376
|
+
timeout: 1_000_000,
|
|
377
|
+
signal: this.uploadAbortController.signal,
|
|
378
|
+
onUploadProgress: onProgress
|
|
379
|
+
? (progressEvent: AxiosProgressEvent) => {
|
|
380
|
+
const percentage = progressEvent.total
|
|
381
|
+
? (progressEvent.loaded / progressEvent.total) * 100
|
|
382
|
+
: 0;
|
|
383
|
+
onProgress(percentage);
|
|
384
|
+
}
|
|
385
|
+
: undefined,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
this.uploadAbortController = null;
|
|
389
|
+
|
|
390
|
+
return response;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
this.uploadAbortController = null;
|
|
393
|
+
console.error('Failed to upload file:', error);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
}
|
|
305
399
|
```
|
|
306
400
|
|
|
307
|
-
####
|
|
401
|
+
#### Server side (express.js)
|
|
402
|
+
|
|
308
403
|
```typescript
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
404
|
+
# file.route.ts
|
|
405
|
+
router.post(['/upload/:fileType', '/upload'], uploadSingleFileMW, uploadSingleFileCtrl);
|
|
406
|
+
router.post(['/multi-upload/:fileType', '/multi-upload'], uploadMultiFilesMW, uploadMultiFilesCtrl);
|
|
407
|
+
|
|
408
|
+
###########################################################################################################
|
|
409
|
+
|
|
410
|
+
# streamimg.mw.ts
|
|
411
|
+
import { NextFunction, Request, Response } from 'express';
|
|
412
|
+
import { FILE_TYPE, type S3Util, UploadedS3File } from '../shared';
|
|
413
|
+
import logger from '../logger';
|
|
414
|
+
|
|
415
|
+
export const uploadSingleFileMW = (req: Request & { s3File?: UploadedS3File }, res: Response, next: NextFunction) => {
|
|
416
|
+
try {
|
|
417
|
+
const fileType = req.params?.fileType as FILE_TYPE;
|
|
418
|
+
|
|
419
|
+
if (!req.headers.hasOwnProperty('x-upload-directory')) {
|
|
420
|
+
return res.status(400).json({ error: 'Directory header is required' });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const directory = (req.headers['x-upload-directory'] as string) || '';
|
|
424
|
+
const filename = req.headers['x-upload-filename'] as string;
|
|
425
|
+
|
|
426
|
+
logger.info(req.id, 'uploading single file', { filename, directory });
|
|
427
|
+
|
|
428
|
+
const s3UploadOptions: S3UploadOptions = {
|
|
429
|
+
...(fileType && { fileType }),
|
|
430
|
+
...(filename && { filename }),
|
|
431
|
+
}
|
|
432
|
+
const uploadMiddleware = s3.uploadSingleFileMW('file', directory, s3UploadOptions);
|
|
433
|
+
|
|
434
|
+
return uploadMiddleware(req, res, next);
|
|
435
|
+
} catch (err: any) {
|
|
436
|
+
logger.error(req.id, 'failed on uploadMultiFilesCtrl', { errMsg: err.message });
|
|
437
|
+
next(err);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export const uploadMultiFilesMW = (
|
|
442
|
+
req: Request & { s3Files?: UploadedS3File[] },
|
|
443
|
+
res: Response,
|
|
444
|
+
next: NextFunction
|
|
445
|
+
) => {
|
|
446
|
+
try {
|
|
447
|
+
const fileType = req.params?.fileType as FILE_TYPE;
|
|
448
|
+
if (!req.headers.hasOwnProperty('x-upload-directory')) {
|
|
449
|
+
return res.status(400).json({ error: 'Directory header is required' });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const directory = (req.headers['x-upload-directory'] as string) || '/';
|
|
453
|
+
logger.info(req.id, 'uploading multiple files', { directory });
|
|
454
|
+
|
|
455
|
+
const s3UploadOptions: S3UploadOptions = {
|
|
456
|
+
...(fileType && { fileType }),
|
|
457
|
+
}
|
|
458
|
+
const uploadMiddleware = s3.uploadMultipleFilesMW('files', directory, s3UploadOptions);
|
|
459
|
+
|
|
460
|
+
return uploadMiddleware(req, res, next);
|
|
461
|
+
} catch (err: any) {
|
|
462
|
+
logger.warn(req.id, 'failed to upload files', { message: err.message });
|
|
463
|
+
next(err);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
###########################################################################################################
|
|
468
|
+
|
|
469
|
+
# file.controller.ts
|
|
470
|
+
export const uploadSingleFileCtrl = (
|
|
471
|
+
req: Request & { s3File?: UploadedS3File },
|
|
472
|
+
res: Response,
|
|
473
|
+
_next: NextFunction
|
|
474
|
+
) => {
|
|
475
|
+
const s3File = req.s3File;
|
|
476
|
+
|
|
477
|
+
if (s3File) {
|
|
478
|
+
const file = {
|
|
479
|
+
key: s3File.key,
|
|
480
|
+
location: s3File.location,
|
|
481
|
+
bucket: s3File.bucket,
|
|
482
|
+
etag: s3File.etag,
|
|
483
|
+
// @ts-ignore
|
|
484
|
+
size: s3File.size,
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// todo: store your fileKey in your database
|
|
488
|
+
|
|
489
|
+
logger.info(req.id, 'file uploaded', file);
|
|
490
|
+
return res.json({ success: true, file });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
export const uploadMultiFilesCtrl = (
|
|
497
|
+
req: Request & { s3Files?: UploadedS3File[] },
|
|
498
|
+
res: Response,
|
|
499
|
+
_next: NextFunction
|
|
500
|
+
) => {
|
|
501
|
+
const s3Files = req.s3Files;
|
|
502
|
+
|
|
503
|
+
if (s3Files?.length) {
|
|
504
|
+
const files = s3Files.map((s3File) => ({
|
|
505
|
+
key: s3File.key,
|
|
506
|
+
location: s3File.location,
|
|
507
|
+
bucket: s3File.bucket,
|
|
508
|
+
etag: s3File.etag,
|
|
509
|
+
}));
|
|
510
|
+
|
|
511
|
+
// todo: store your fileKeys in your database
|
|
512
|
+
|
|
513
|
+
logger.info(req.id, 'files uploaded', files);
|
|
514
|
+
return res.json({ success: true, files });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
518
|
+
};
|
|
323
519
|
```
|
|
520
|
+
### Upload Options
|
|
324
521
|
|
|
325
|
-
#### View PDF
|
|
326
522
|
```typescript
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
523
|
+
interface S3UploadOptions {
|
|
524
|
+
acl?: ACLs; // 'private' | 'public-read' | 'public-read-write';
|
|
525
|
+
maxFileSize?: ByteUnitStringValue | number; // '5MB', '1GB', or bytes
|
|
526
|
+
filename?: string | ((req: Request, file: File) => string | Promise<string>);
|
|
527
|
+
fileType?: FILE_TYPE | FILE_TYPE[]; // 'image' | 'video' | 'audio' | 'application' | 'text'
|
|
528
|
+
fileExt?: FILE_EXT | FILE_EXT[]; // 'jpg', 'png', 'pdf', etc...
|
|
529
|
+
metadata?:
|
|
530
|
+
| Record<string, string>
|
|
531
|
+
| ((req: Request, file: File) => Record<string, string> | Promise<Record<string, string>>);
|
|
532
|
+
|
|
533
|
+
maxFilesCount?: undefined | number | null; // For multiple file uploads
|
|
534
|
+
}
|
|
333
535
|
```
|
|
334
536
|
|
|
335
|
-
###
|
|
537
|
+
### ๐ฌ Streaming Files
|
|
336
538
|
|
|
337
|
-
####
|
|
338
|
-
```typescript
|
|
339
|
-
import express from 'express';
|
|
340
|
-
|
|
341
|
-
const app = express();
|
|
342
|
-
|
|
343
|
-
app.post('/upload',
|
|
344
|
-
s3.uploadSingleFile('file', '/uploads', {
|
|
345
|
-
maxFileSize: '5MB',
|
|
346
|
-
fileType: ['image', 'application'],
|
|
347
|
-
fileExt: ['jpg', 'png', 'pdf']
|
|
348
|
-
}),
|
|
349
|
-
(req, res) => {
|
|
350
|
-
console.log(req.s3File);
|
|
351
|
-
// {
|
|
352
|
-
// key: '/uploads/photo.jpg',
|
|
353
|
-
// location: 'https://...',
|
|
354
|
-
// size: 12345,
|
|
355
|
-
// mimetype: 'image/jpeg',
|
|
356
|
-
// ...
|
|
357
|
-
// }
|
|
358
|
-
res.json({ file: req.s3File });
|
|
359
|
-
}
|
|
360
|
-
);
|
|
361
|
-
```
|
|
539
|
+
#### Client side
|
|
362
540
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
maxFilesCount: 5,
|
|
369
|
-
fileType: ['image']
|
|
370
|
-
}),
|
|
371
|
-
(req, res) => {
|
|
372
|
-
console.log(req.s3Files); // Array of uploaded files
|
|
373
|
-
res.json({ files: req.s3Files });
|
|
374
|
-
}
|
|
375
|
-
);
|
|
541
|
+
```html
|
|
542
|
+
<!-- videoURL = `${s3Service.baseURL}/files/stream?file=${encodedFileKey}` -->
|
|
543
|
+
<video controls src={videoURL}>
|
|
544
|
+
Your browser does not support the video tag.
|
|
545
|
+
</video>
|
|
376
546
|
```
|
|
377
547
|
|
|
378
|
-
####
|
|
548
|
+
#### Server side (Express.js)
|
|
549
|
+
|
|
379
550
|
```typescript
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
551
|
+
# file.route.ts
|
|
552
|
+
router.get('/stream', streamVideoFilesCtrl);
|
|
553
|
+
// or directly from s3 util like (need to provided file key from query.file or params.file or header field , or change it in the options like: {queryField: 'fileKey'} )
|
|
554
|
+
router.get('/stream', s3.streamVideoFilesCtrl());
|
|
555
|
+
|
|
556
|
+
# file.control.ts
|
|
557
|
+
export const streamVideoFilesCtrl = async (req: Request, res: Response, next: NextFunction) => {
|
|
558
|
+
try {
|
|
559
|
+
const fileKey = req.query?.file as string;
|
|
560
|
+
const mw = await s3.streamVideoFileCtrl({ fileKey });
|
|
561
|
+
|
|
562
|
+
return mw(req, res, next);
|
|
563
|
+
} catch (err: any) {
|
|
564
|
+
logger.error(req.id, 'failed on streamVideoFilesCtrl', { errMsg: err.message });
|
|
565
|
+
next(err);
|
|
386
566
|
}
|
|
387
|
-
|
|
388
|
-
(req, res) => {
|
|
389
|
-
res.json({ file: req.s3File });
|
|
390
|
-
}
|
|
391
|
-
);
|
|
567
|
+
};
|
|
392
568
|
```
|
|
393
569
|
|
|
394
|
-
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
res.json({ file: req.s3File });
|
|
406
|
-
}
|
|
407
|
-
);
|
|
570
|
+
##### Streaming Image/PDF files
|
|
571
|
+
```html
|
|
572
|
+
<!-- imageURL = `${s3Service.baseURL}/files/image?file=${encodedFileKey}` -->
|
|
573
|
+
<img src={imageURL} alt={file?.name} />
|
|
574
|
+
|
|
575
|
+
<!-- pdfURL = `${s3Service.baseURL}/files/pdf?file=${encodedFileKey}` -->
|
|
576
|
+
<iframe
|
|
577
|
+
src={pdfURL}
|
|
578
|
+
style={{ width: '100%', height: '600px', border: 'none' }}
|
|
579
|
+
title="PDF Preview"
|
|
580
|
+
/>
|
|
408
581
|
```
|
|
409
582
|
|
|
410
|
-
|
|
583
|
+
Server Side
|
|
411
584
|
```typescript
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
maxFileSize: '20MB'
|
|
415
|
-
}),
|
|
416
|
-
(req, res) => {
|
|
417
|
-
console.log(req.s3AllFiles); // All uploaded files
|
|
418
|
-
res.json({ files: req.s3AllFiles });
|
|
419
|
-
}
|
|
420
|
-
);
|
|
585
|
+
router.get('/image', s3.streamImageFileCtrl());
|
|
586
|
+
router.get('/pdf', s3.streamPdfFileCtrl());
|
|
421
587
|
```
|
|
422
588
|
|
|
423
|
-
### Upload Options
|
|
424
589
|
|
|
425
|
-
```typescript
|
|
426
|
-
interface S3UploadOptions {
|
|
427
|
-
acl?: 'private' | 'public-read' | 'public-read-write';
|
|
428
|
-
maxFileSize?: string | number; // '5MB', '1GB', or bytes
|
|
429
|
-
maxFilesCount?: number; // For multiple file uploads
|
|
430
|
-
filename?: string | ((req, file) => string | Promise<string>);
|
|
431
|
-
fileType?: Array<'image' | 'video' | 'audio' | 'application' | 'text'>;
|
|
432
|
-
fileExt?: string[]; // ['jpg', 'png', 'pdf']
|
|
433
|
-
metadata?: object | ((req, file) => object | Promise<object>);
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
590
|
|
|
437
591
|
## ๐งช LocalStack Support
|
|
438
592
|
|
|
@@ -524,7 +678,6 @@ The utility includes optimized HTTP/HTTPS agents:
|
|
|
524
678
|
// - socketTimeout: 30000ms
|
|
525
679
|
```
|
|
526
680
|
|
|
527
|
-
|
|
528
681
|
## ๐ Complete Express.js Example
|
|
529
682
|
# FULL DEMO PROJECT EXAMPLE:
|
|
530
683
|
please see this project code before using: [aws-utils-demo github link!](https://github.com/hdriel/aws-utils-demo)
|
|
@@ -542,8 +695,9 @@ This package is written in TypeScript and includes full type definitions
|
|
|
542
695
|
## ๐ Links
|
|
543
696
|
|
|
544
697
|
- [AWS S3 Documentation](https://docs.aws.amazon.com/s3/)
|
|
698
|
+
- [AWS S3 SDK V3 Documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/)
|
|
545
699
|
- [LocalStack Documentation](https://docs.localstack.cloud/user-guide/aws/s3/)
|
|
546
|
-
- [GitHub Repository](
|
|
700
|
+
- [GitHub Demo Repository](https://github.com/hdriel/aws-utils-demo)
|
|
547
701
|
|
|
548
702
|
---
|
|
549
703
|
|