@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 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.uploadFile('/documents/file.pdf', buffer);
212
-
213
- // Upload with public access
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
- ### ๐ŸŽฌ Streaming & Express.js Integration
254
+ ### ๐Ÿ“ค File Upload Middleware
260
255
 
261
- #### Stream File Download
256
+ #### Client Side
262
257
  ```typescript
263
- import express from 'express';
264
-
265
- const app = express();
266
-
267
- // Stream single file
268
- app.get('/download/:file',
269
- await s3.getStreamFileCtrl({
270
- filePath: '/documents/file.pdf',
271
- filename: 'download.pdf',
272
- forDownloading: true
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
- #### Stream Video with Range Support
294
- ```typescript
295
- // Video streaming with range requests
296
- app.get('/video/:id',
297
- await s3.getStreamVideoFileCtrl({
298
- fileKey: '/videos/movie.mp4',
299
- contentType: 'video/mp4',
300
- bufferMB: 5,
301
- streamTimeoutMS: 30000,
302
- allowedWhitelist: ['https://myapp.com']
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
- #### View Image
401
+ #### Server side (express.js)
402
+
308
403
  ```typescript
309
- // Serve image with caching
310
- app.get('/image',
311
- s3.getImageFileViewCtrl({
312
- queryField: 'path', // ?path=/images/photo.jpg
313
- cachingAge: 31536000 // 1 year
314
- })
315
- );
316
-
317
- // With fixed file path
318
- app.get('/logo',
319
- s3.getImageFileViewCtrl({
320
- fileKey: '/public/logo.png'
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
- app.get('/pdf',
328
- s3.getPdfFileViewCtrl({
329
- queryField: 'document',
330
- cachingAge: 86400 // 1 day
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
- ### ๐Ÿ“ค File Upload Middleware
537
+ ### ๐ŸŽฌ Streaming Files
336
538
 
337
- #### Single File Upload
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
- #### Multiple Files Upload
364
- ```typescript
365
- app.post('/upload-multiple',
366
- s3.uploadMultipleFiles('photos', '/uploads/gallery', {
367
- maxFileSize: '10MB',
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
- #### Upload with Custom Filename
548
+ #### Server side (Express.js)
549
+
379
550
  ```typescript
380
- app.post('/upload',
381
- s3.uploadSingleFile('file', '/uploads', {
382
- filename: async (req, file) => {
383
- const timestamp = Date.now();
384
- const ext = path.extname(file.originalname);
385
- return `${req.user.id}-${timestamp}${ext}`;
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
- #### Upload with Custom Metadata
395
- ```typescript
396
- app.post('/upload',
397
- s3.uploadSingleFile('file', '/uploads', {
398
- metadata: async (req, file) => ({
399
- userId: req.user.id,
400
- uploadDate: new Date().toISOString(),
401
- originalName: file.originalname
402
- })
403
- }),
404
- (req, res) => {
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
- #### Upload Any Files (Mixed Fields)
583
+ Server Side
411
584
  ```typescript
412
- app.post('/upload-any',
413
- s3.uploadAnyFiles('/uploads', 10, {
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