@nuvix/storage 0.1.0 → 1.0.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/dist/index.cjs.js CHANGED
@@ -42,21 +42,21 @@ class Device {
42
42
  * Works like the realpath function but works on files that does not exist
43
43
  */
44
44
  getAbsolutePath(path) {
45
- const normalizedPath = path.replace(/[/\\]/g, '/');
46
- const parts = normalizedPath.split('/').filter(part => part.length > 0);
45
+ const normalizedPath = path.replace(/[/\\]/g, "/");
46
+ const parts = normalizedPath.split("/").filter((part) => part.length > 0);
47
47
  const absolutes = [];
48
48
  for (const part of parts) {
49
- if (part === '.') {
49
+ if (part === ".") {
50
50
  continue;
51
51
  }
52
- if (part === '..') {
52
+ if (part === "..") {
53
53
  absolutes.pop();
54
54
  }
55
55
  else {
56
56
  absolutes.push(part);
57
57
  }
58
58
  }
59
- return '/' + absolutes.join('/');
59
+ return "/" + absolutes.join("/");
60
60
  }
61
61
  async getMimeType(filePath) {
62
62
  const path = await import('path');
@@ -64,59 +64,59 @@ class Device {
64
64
  const ext = path.extname(filePath).toLowerCase().slice(1);
65
65
  const mimeTypes = {
66
66
  // Text
67
- 'txt': 'text/plain',
68
- 'html': 'text/html',
69
- 'htm': 'text/html',
70
- 'css': 'text/css',
71
- 'csv': 'text/csv',
72
- 'xml': 'text/xml',
67
+ txt: "text/plain",
68
+ html: "text/html",
69
+ htm: "text/html",
70
+ css: "text/css",
71
+ csv: "text/csv",
72
+ xml: "text/xml",
73
73
  // JavaScript
74
- 'js': 'application/javascript',
75
- 'mjs': 'application/javascript',
76
- 'json': 'application/json',
74
+ js: "application/javascript",
75
+ mjs: "application/javascript",
76
+ json: "application/json",
77
77
  // Images
78
- 'png': 'image/png',
79
- 'jpg': 'image/jpeg',
80
- 'jpeg': 'image/jpeg',
81
- 'gif': 'image/gif',
82
- 'svg': 'image/svg+xml',
83
- 'webp': 'image/webp',
84
- 'ico': 'image/x-icon',
85
- 'bmp': 'image/bmp',
86
- 'tiff': 'image/tiff',
87
- 'tif': 'image/tiff',
78
+ png: "image/png",
79
+ jpg: "image/jpeg",
80
+ jpeg: "image/jpeg",
81
+ gif: "image/gif",
82
+ svg: "image/svg+xml",
83
+ webp: "image/webp",
84
+ ico: "image/x-icon",
85
+ bmp: "image/bmp",
86
+ tiff: "image/tiff",
87
+ tif: "image/tiff",
88
88
  // Documents
89
- 'pdf': 'application/pdf',
90
- 'doc': 'application/msword',
91
- 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
92
- 'xls': 'application/vnd.ms-excel',
93
- 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
94
- 'ppt': 'application/vnd.ms-powerpoint',
95
- 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
89
+ pdf: "application/pdf",
90
+ doc: "application/msword",
91
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
92
+ xls: "application/vnd.ms-excel",
93
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
94
+ ppt: "application/vnd.ms-powerpoint",
95
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
96
96
  // Audio
97
- 'mp3': 'audio/mpeg',
98
- 'wav': 'audio/wav',
99
- 'ogg': 'audio/ogg',
100
- 'm4a': 'audio/mp4',
97
+ mp3: "audio/mpeg",
98
+ wav: "audio/wav",
99
+ ogg: "audio/ogg",
100
+ m4a: "audio/mp4",
101
101
  // Video
102
- 'mp4': 'video/mp4',
103
- 'avi': 'video/x-msvideo',
104
- 'mov': 'video/quicktime',
105
- 'wmv': 'video/x-ms-wmv',
106
- 'flv': 'video/x-flv',
107
- 'webm': 'video/webm',
102
+ mp4: "video/mp4",
103
+ avi: "video/x-msvideo",
104
+ mov: "video/quicktime",
105
+ wmv: "video/x-ms-wmv",
106
+ flv: "video/x-flv",
107
+ webm: "video/webm",
108
108
  // Archives
109
- 'zip': 'application/zip',
110
- 'rar': 'application/x-rar-compressed',
111
- 'tar': 'application/x-tar',
112
- 'gz': 'application/gzip',
113
- '7z': 'application/x-7z-compressed',
109
+ zip: "application/zip",
110
+ rar: "application/x-rar-compressed",
111
+ tar: "application/x-tar",
112
+ gz: "application/gzip",
113
+ "7z": "application/x-7z-compressed",
114
114
  // Other
115
- 'bin': 'application/octet-stream',
116
- 'exe': 'application/octet-stream',
117
- 'dmg': 'application/octet-stream'
115
+ bin: "application/octet-stream",
116
+ exe: "application/octet-stream",
117
+ dmg: "application/octet-stream",
118
118
  };
119
- return mimeTypes[ext] || 'application/octet-stream';
119
+ return mimeTypes[ext] || "application/octet-stream";
120
120
  }
121
121
  }
122
122
  /**
@@ -173,11 +173,11 @@ class Storage {
173
173
  * @param system
174
174
  * @returns string
175
175
  */
176
- static human(bytes, decimals = 2, system = 'metric') {
177
- const mod = system === 'binary' ? 1024 : 1000;
176
+ static human(bytes, decimals = 2, system = "metric") {
177
+ const mod = system === "binary" ? 1024 : 1000;
178
178
  const units = {
179
- binary: ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'],
180
- metric: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
179
+ binary: ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"],
180
+ metric: ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
181
181
  };
182
182
  const factor = Math.floor((bytes.toString().length - 1) / 3);
183
183
  return `${(bytes / Math.pow(mod, factor)).toFixed(decimals)}${units[system][factor]}`;
@@ -186,10 +186,11 @@ class Storage {
186
186
  /**
187
187
  * Supported devices
188
188
  */
189
- Storage.DEVICE_LOCAL = 'local';
190
- Storage.DEVICE_S3 = 's3';
189
+ Storage.DEVICE_LOCAL = "local";
190
+ Storage.DEVICE_S3 = "s3";
191
191
  // static readonly DEVICE_DO_SPACES = 'dospaces';
192
- Storage.DEVICE_WASABI = 'wasabi';
192
+ Storage.DEVICE_WASABI = "wasabi";
193
+ Storage.DEVICE_MINIO = "minio";
193
194
  // static readonly DEVICE_BACKBLAZE = 'backblaze';
194
195
  // static readonly DEVICE_LINODE = 'linode';
195
196
  /**
@@ -201,14 +202,14 @@ Storage.devices = new Map();
201
202
 
202
203
  class Validator {
203
204
  }
204
- Validator.TYPE_STRING = 'string';
205
- Validator.TYPE_ARRAY = 'array';
206
- Validator.TYPE_INTEGER = 'integer';
207
- Validator.TYPE_BOOLEAN = 'boolean';
205
+ Validator.TYPE_STRING = "string";
206
+ Validator.TYPE_ARRAY = "array";
207
+ Validator.TYPE_INTEGER = "integer";
208
+ Validator.TYPE_BOOLEAN = "boolean";
208
209
 
209
210
  class File extends Validator {
210
211
  getDescription() {
211
- return 'File is not valid';
212
+ return "File is not valid";
212
213
  }
213
214
  /**
214
215
  * NOT MUCH RIGHT NOW.
@@ -221,26 +222,6 @@ class File extends Validator {
221
222
  isValid(name) {
222
223
  return true;
223
224
  }
224
- /**
225
- * Is array
226
- *
227
- * Function will return true if object is array.
228
- *
229
- * @return boolean
230
- */
231
- isArray() {
232
- return false;
233
- }
234
- /**
235
- * Get Type
236
- *
237
- * Returns validator type.
238
- *
239
- * @return string
240
- */
241
- getType() {
242
- return 'string';
243
- }
244
225
  }
245
226
 
246
227
  class FileExt extends Validator {
@@ -249,36 +230,30 @@ class FileExt extends Validator {
249
230
  this.allowed = allowed;
250
231
  }
251
232
  getDescription() {
252
- return 'File extension is not valid';
233
+ return "File extension is not valid";
253
234
  }
254
235
  isValid(filename) {
255
236
  const ext = this.getFileExtension(filename).toLowerCase();
256
237
  return this.allowed.includes(ext);
257
238
  }
258
- isArray() {
259
- return false;
260
- }
261
- getType() {
262
- return 'string';
263
- }
264
239
  getFileExtension(filename) {
265
- const lastDotIndex = filename.lastIndexOf('.');
266
- return lastDotIndex !== -1 ? filename.substring(lastDotIndex + 1) : '';
240
+ const lastDotIndex = filename.lastIndexOf(".");
241
+ return lastDotIndex !== -1 ? filename.substring(lastDotIndex + 1) : "";
267
242
  }
268
243
  }
269
- FileExt.TYPE_JPEG = 'jpeg';
270
- FileExt.TYPE_JPG = 'jpg';
271
- FileExt.TYPE_GIF = 'gif';
272
- FileExt.TYPE_PNG = 'png';
273
- FileExt.TYPE_GZIP = 'gz';
274
- FileExt.TYPE_ZIP = 'zip';
244
+ FileExt.TYPE_JPEG = "jpeg";
245
+ FileExt.TYPE_JPG = "jpg";
246
+ FileExt.TYPE_GIF = "gif";
247
+ FileExt.TYPE_PNG = "png";
248
+ FileExt.TYPE_GZIP = "gz";
249
+ FileExt.TYPE_ZIP = "zip";
275
250
 
276
251
  class FileName extends Validator {
277
252
  /**
278
253
  * Get Description
279
254
  */
280
255
  getDescription() {
281
- return 'Filename is not valid';
256
+ return "Filename is not valid";
282
257
  }
283
258
  /**
284
259
  * The file name can only contain "a-z", "A-Z", "0-9" and "." and not empty.
@@ -290,7 +265,7 @@ class FileName extends Validator {
290
265
  if (!name) {
291
266
  return false;
292
267
  }
293
- if (typeof name !== 'string') {
268
+ if (typeof name !== "string") {
294
269
  return false;
295
270
  }
296
271
  if (!/^[a-zA-Z0-9.]+$/.test(name)) {
@@ -298,26 +273,6 @@ class FileName extends Validator {
298
273
  }
299
274
  return true;
300
275
  }
301
- /**
302
- * Is array
303
- *
304
- * Function will return true if object is array.
305
- *
306
- * @returns boolean
307
- */
308
- isArray() {
309
- return false;
310
- }
311
- /**
312
- * Get Type
313
- *
314
- * Returns validator type.
315
- *
316
- * @returns string
317
- */
318
- getType() {
319
- return Validator.TYPE_STRING;
320
- }
321
276
  }
322
277
 
323
278
  class FileSize extends Validator {
@@ -338,30 +293,14 @@ class FileSize extends Validator {
338
293
  * Finds whether a file size is smaller than required limit.
339
294
  */
340
295
  isValid(fileSize) {
341
- if (typeof fileSize !== 'number' || !Number.isInteger(fileSize)) {
296
+ if (typeof fileSize !== "number" || !Number.isInteger(fileSize)) {
342
297
  return false;
343
298
  }
344
- if (fileSize > this.max) {
299
+ if (fileSize > this.max || fileSize < 0) {
345
300
  return false;
346
301
  }
347
302
  return true;
348
303
  }
349
- /**
350
- * Is array
351
- *
352
- * Function will return true if object is array.
353
- */
354
- isArray() {
355
- return false;
356
- }
357
- /**
358
- * Get Type
359
- *
360
- * Returns validator type.
361
- */
362
- getType() {
363
- return Validator.TYPE_INTEGER;
364
- }
365
304
  }
366
305
 
367
306
  class FileType extends Validator {
@@ -371,14 +310,14 @@ class FileType extends Validator {
371
310
  * File Type Binaries.
372
311
  */
373
312
  this.types = {
374
- [FileType.FILE_TYPE_JPEG]: '\xFF\xD8\xFF',
375
- [FileType.FILE_TYPE_GIF]: 'GIF',
376
- [FileType.FILE_TYPE_PNG]: '\x89\x50\x4e\x47\x0d\x0a',
377
- [FileType.FILE_TYPE_GZIP]: 'application/x-gzip',
313
+ [FileType.FILE_TYPE_JPEG]: "\xFF\xD8\xFF",
314
+ [FileType.FILE_TYPE_GIF]: "GIF",
315
+ [FileType.FILE_TYPE_PNG]: "\x89\x50\x4e\x47\x0d\x0a",
316
+ [FileType.FILE_TYPE_GZIP]: "application/x-gzip",
378
317
  };
379
318
  for (const key of allowed) {
380
319
  if (!(key in this.types)) {
381
- throw new Error('Unknown file mime type');
320
+ throw new Error("Unknown file mime type");
382
321
  }
383
322
  }
384
323
  this.allowed = allowed;
@@ -387,7 +326,7 @@ class FileType extends Validator {
387
326
  * Get Description
388
327
  */
389
328
  getDescription() {
390
- return 'File mime-type is not allowed ';
329
+ return "File mime-type is not allowed ";
391
330
  }
392
331
  /**
393
332
  * Is Valid.
@@ -397,7 +336,10 @@ class FileType extends Validator {
397
336
  async isValid(path) {
398
337
  try {
399
338
  const buffer = await promises.readFile(path);
400
- const bytes = buffer.toString('binary', 0, 8);
339
+ // Calculate the maximum signature length needed
340
+ const maxSignatureLength = Math.max(...Object.values(this.types).map((sig) => sig.length));
341
+ const bytesToRead = Math.min(buffer.length, Math.max(8, maxSignatureLength));
342
+ const bytes = buffer.toString("binary", 0, bytesToRead);
401
343
  for (const key of this.allowed) {
402
344
  if (bytes.indexOf(this.types[key]) === 0) {
403
345
  return true;
@@ -409,33 +351,21 @@ class FileType extends Validator {
409
351
  return false;
410
352
  }
411
353
  }
412
- /**
413
- * Is array
414
- */
415
- isArray() {
416
- return false;
417
- }
418
- /**
419
- * Get Type
420
- */
421
- getType() {
422
- return 'string';
423
- }
424
354
  }
425
355
  /**
426
356
  * File Types Constants.
427
357
  */
428
- FileType.FILE_TYPE_JPEG = 'jpeg';
429
- FileType.FILE_TYPE_GIF = 'gif';
430
- FileType.FILE_TYPE_PNG = 'png';
431
- FileType.FILE_TYPE_GZIP = 'gz';
358
+ FileType.FILE_TYPE_JPEG = "jpeg";
359
+ FileType.FILE_TYPE_GIF = "gif";
360
+ FileType.FILE_TYPE_PNG = "png";
361
+ FileType.FILE_TYPE_GZIP = "gz";
432
362
 
433
363
  class Upload extends Validator {
434
364
  /**
435
365
  * Get Description
436
366
  */
437
367
  getDescription() {
438
- return 'Not a valid upload file';
368
+ return "Not a valid upload file";
439
369
  }
440
370
  /**
441
371
  * Check if a file is a valid upload file
@@ -444,7 +374,7 @@ class Upload extends Validator {
444
374
  * @returns Promise that resolves to true if valid upload file, false otherwise
445
375
  */
446
376
  async isValid(path) {
447
- if (typeof path !== 'string') {
377
+ if (typeof path !== "string") {
448
378
  return false;
449
379
  }
450
380
  try {
@@ -455,43 +385,23 @@ class Upload extends Validator {
455
385
  return false;
456
386
  }
457
387
  }
458
- /**
459
- * Is array
460
- *
461
- * Function will return true if object is array.
462
- *
463
- * @returns false - this validator doesn't handle arrays
464
- */
465
- isArray() {
466
- return false;
467
- }
468
- /**
469
- * Get Type
470
- *
471
- * Returns validator type.
472
- *
473
- * @returns The validator type
474
- */
475
- getType() {
476
- return 'string';
477
- }
478
388
  }
479
389
 
480
390
  class Local extends Device {
481
- constructor(root = '') {
391
+ constructor(root = "") {
482
392
  super();
483
- this.root = 'temp';
393
+ this.root = "temp";
484
394
  this.MAX_PAGE_SIZE = 1000;
485
395
  this.root = root;
486
396
  }
487
397
  getName() {
488
- return 'Local Storage';
398
+ return "Local Storage";
489
399
  }
490
400
  getType() {
491
401
  return Storage.DEVICE_LOCAL;
492
402
  }
493
403
  getDescription() {
494
- return 'Adapter for Local storage that is in the physical or virtual machine or mounted to it.';
404
+ return "Adapter for Local storage that is in the physical or virtual machine or mounted to it.";
495
405
  }
496
406
  getRoot() {
497
407
  return this.root;
@@ -506,25 +416,24 @@ class Local extends Device {
506
416
  await fs.promises.rename(source, filePath);
507
417
  return chunks;
508
418
  }
509
- catch (error) {
419
+ catch {
510
420
  throw new Error(`Can't upload file ${filePath}`);
511
421
  }
512
422
  }
513
423
  const tmp = path.join(path.dirname(filePath), `tmp_${path.basename(filePath)}`, `${path.basename(filePath)}_chunks.log`);
514
424
  await this.createDirectory(path.dirname(tmp));
515
425
  const chunkFilePath = path.join(path.dirname(tmp), `${path.parse(filePath).name}.part.${chunk}`);
516
- // Skip writing chunk if the chunk was re-uploaded
517
426
  if (!(await this.exists(chunkFilePath))) {
518
427
  try {
519
428
  await fs.promises.appendFile(tmp, `${chunk}\n`);
520
429
  }
521
- catch (error) {
430
+ catch {
522
431
  throw new Error(`Can't write chunk log ${tmp}`);
523
432
  }
524
433
  }
525
434
  try {
526
- const chunkLogs = await fs.promises.readFile(tmp, 'utf8');
527
- const chunksReceived = chunkLogs.trim().split('\n').length;
435
+ const chunkLogs = await fs.promises.readFile(tmp, "utf8");
436
+ const chunksReceived = chunkLogs.trim().split("\n").length;
528
437
  await fs.promises.rename(source, chunkFilePath);
529
438
  if (chunks === chunksReceived) {
530
439
  await this.joinChunks(filePath, chunks);
@@ -532,7 +441,7 @@ class Local extends Device {
532
441
  }
533
442
  return chunksReceived;
534
443
  }
535
- catch (error) {
444
+ catch {
536
445
  throw new Error(`Failed to write chunk ${chunk}`);
537
446
  }
538
447
  }
@@ -543,7 +452,7 @@ class Local extends Device {
543
452
  await fs.promises.writeFile(filePath, data);
544
453
  return chunks;
545
454
  }
546
- catch (error) {
455
+ catch {
547
456
  throw new Error(`Can't write file ${filePath}`);
548
457
  }
549
458
  }
@@ -551,8 +460,8 @@ class Local extends Device {
551
460
  await this.createDirectory(path.dirname(tmp));
552
461
  try {
553
462
  await fs.promises.appendFile(tmp, `${chunk}\n`);
554
- const chunkLogs = await fs.promises.readFile(tmp, 'utf8');
555
- const chunksReceived = chunkLogs.trim().split('\n').length;
463
+ const chunkLogs = await fs.promises.readFile(tmp, "utf8");
464
+ const chunksReceived = chunkLogs.trim().split("\n").length;
556
465
  const chunkFilePath = path.join(path.dirname(tmp), `${path.parse(filePath).name}.part.${chunk}`);
557
466
  await fs.promises.writeFile(chunkFilePath, data);
558
467
  if (chunks === chunksReceived) {
@@ -561,7 +470,7 @@ class Local extends Device {
561
470
  }
562
471
  return chunksReceived;
563
472
  }
564
- catch (error) {
473
+ catch {
565
474
  throw new Error(`Failed to write chunk ${chunk}`);
566
475
  }
567
476
  }
@@ -574,7 +483,7 @@ class Local extends Device {
574
483
  await fs.promises.appendFile(filePath, data);
575
484
  await fs.promises.unlink(part);
576
485
  }
577
- catch (error) {
486
+ catch {
578
487
  throw new Error(`Failed to read/append chunk ${part}`);
579
488
  }
580
489
  }
@@ -583,7 +492,7 @@ class Local extends Device {
583
492
  }
584
493
  async transfer(filePath, destination, device) {
585
494
  if (!(await this.exists(filePath))) {
586
- throw new Error('File Not Found');
495
+ throw new Error("File Not Found");
587
496
  }
588
497
  const size = await this.getFileSize(filePath);
589
498
  const contentType = await this.getFileMimeType(filePath);
@@ -600,7 +509,7 @@ class Local extends Device {
600
509
  }
601
510
  return true;
602
511
  }
603
- async abort(filePath, extra = '') {
512
+ async abort(filePath, extra = "") {
604
513
  if (await this.exists(filePath)) {
605
514
  await fs.promises.unlink(filePath);
606
515
  }
@@ -612,30 +521,34 @@ class Local extends Device {
612
521
  for (const file of files) {
613
522
  await this.delete(file, true);
614
523
  }
615
- return fs.promises.rmdir(tmp).then(() => true).catch(() => false);
524
+ return fs.promises
525
+ .rmdir(tmp)
526
+ .then(() => true)
527
+ .catch(() => false);
616
528
  }
617
529
  async read(filePath, offset = 0, length) {
618
530
  if (!(await this.exists(filePath))) {
619
- throw new Error('File Not Found');
531
+ throw new Error("File Not Found");
620
532
  }
621
- const fileHandle = await fs.promises.open(filePath, 'r');
533
+ const fileHandle = await fs.promises.open(filePath, "r");
622
534
  try {
623
- const buffer = Buffer.alloc(length || (await fileHandle.stat()).size - offset);
624
- await fileHandle.read(buffer, 0, buffer.length, offset);
625
- return buffer.toString();
535
+ const size = length ?? (await fileHandle.stat()).size - offset;
536
+ const buffer = Buffer.alloc(size);
537
+ await fileHandle.read(buffer, 0, size, offset);
538
+ return buffer;
626
539
  }
627
540
  finally {
628
541
  await fileHandle.close();
629
542
  }
630
543
  }
631
- async write(filePath, data, contentType = '') {
544
+ async write(filePath, data, contentType = "") {
632
545
  try {
633
546
  await this.createDirectory(path.dirname(filePath));
634
547
  await fs.promises.writeFile(filePath, data);
635
548
  return true;
636
549
  }
637
- catch (error) {
638
- throw new Error(`Can't create directory ${path.dirname(filePath)}`);
550
+ catch {
551
+ throw new Error(`Can't write to path ${filePath}`);
639
552
  }
640
553
  }
641
554
  async move(source, target) {
@@ -647,7 +560,7 @@ class Local extends Device {
647
560
  await fs.promises.rename(source, target);
648
561
  return true;
649
562
  }
650
- catch (error) {
563
+ catch {
651
564
  return false;
652
565
  }
653
566
  }
@@ -666,7 +579,7 @@ class Local extends Device {
666
579
  }
667
580
  return true;
668
581
  }
669
- catch (error) {
582
+ catch {
670
583
  return false;
671
584
  }
672
585
  }
@@ -684,7 +597,7 @@ class Local extends Device {
684
597
  for (const file of files) {
685
598
  const stats = await fs.promises.stat(file);
686
599
  if (stats.isDirectory()) {
687
- const relativePath = file.replace(this.getRoot() + path.sep, '');
600
+ const relativePath = file.replace(this.getRoot() + path.sep, "");
688
601
  await this.deletePath(relativePath);
689
602
  }
690
603
  else {
@@ -694,7 +607,7 @@ class Local extends Device {
694
607
  await fs.promises.rmdir(fullPath);
695
608
  return true;
696
609
  }
697
- catch (error) {
610
+ catch {
698
611
  return false;
699
612
  }
700
613
  }
@@ -712,31 +625,18 @@ class Local extends Device {
712
625
  return stats.size;
713
626
  }
714
627
  async getFileMimeType(filePath) {
715
- // Note: Node.js doesn't have a built-in mime-type detection
716
- // You might want to use a library like 'mime-types' or 'file-type'
717
- const ext = path.extname(filePath);
718
- const mimeTypes = {
719
- '.txt': 'text/plain',
720
- '.html': 'text/html',
721
- '.js': 'application/javascript',
722
- '.json': 'application/json',
723
- '.png': 'image/png',
724
- '.jpg': 'image/jpeg',
725
- '.jpeg': 'image/jpeg',
726
- '.gif': 'image/gif'
727
- };
728
- return mimeTypes[ext] || 'application/octet-stream';
628
+ return this.getMimeType(filePath);
729
629
  }
730
630
  async getFileHash(filePath) {
731
631
  const data = await fs.promises.readFile(filePath);
732
- return crypto.createHash('md5').update(data).digest('hex');
632
+ return crypto.createHash("md5").update(data).digest("hex");
733
633
  }
734
634
  async createDirectory(dirPath) {
735
635
  try {
736
636
  await fs.promises.mkdir(dirPath, { recursive: true, mode: 0o755 });
737
637
  return true;
738
638
  }
739
- catch (error) {
639
+ catch {
740
640
  return false;
741
641
  }
742
642
  }
@@ -745,7 +645,7 @@ class Local extends Device {
745
645
  let size = 0;
746
646
  const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
747
647
  for (const file of files) {
748
- if (file.name.startsWith('.'))
648
+ if (file.name.startsWith("."))
749
649
  continue;
750
650
  const fullPath = path.join(dirPath, file.name);
751
651
  if (file.isDirectory()) {
@@ -758,7 +658,7 @@ class Local extends Device {
758
658
  }
759
659
  return size;
760
660
  }
761
- catch (error) {
661
+ catch {
762
662
  return -1;
763
663
  }
764
664
  }
@@ -770,31 +670,27 @@ class Local extends Device {
770
670
  const stats = await fs.promises.statfs(this.getRoot());
771
671
  return stats.blocks * stats.bsize;
772
672
  }
773
- async getFiles(dir, max = this.MAX_PAGE_SIZE, continuationToken = '') {
774
- const files = [];
673
+ async getFiles(dir, max = this.MAX_PAGE_SIZE, continuationToken = "") {
775
674
  try {
776
675
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
777
- for (const entry of entries) {
778
- files.push(path.join(dir, entry.name));
779
- }
780
- return files;
676
+ return entries.map((entry) => path.join(dir, entry.name));
781
677
  }
782
- catch (error) {
678
+ catch {
783
679
  return [];
784
680
  }
785
681
  }
786
682
  }
787
683
 
788
684
  class S3 extends Device {
789
- constructor(root, accessKey, secretKey, bucket, region = S3.US_EAST_1, acl = S3.ACL_PRIVATE, endpointUrl = '') {
685
+ constructor(root, accessKey, secretKey, bucket, region = S3.US_EAST_1, acl = S3.ACL_PRIVATE, endpointUrl = "") {
790
686
  super();
791
687
  this.acl = S3.ACL_PRIVATE;
792
- this.root = 'temp';
688
+ this.root = "temp";
793
689
  this.headers = {
794
- 'host': '',
795
- 'date': '',
796
- 'content-md5': '',
797
- 'content-type': '',
690
+ host: "",
691
+ date: "",
692
+ "content-md5": "",
693
+ "content-type": "",
798
694
  };
799
695
  this.accessKey = accessKey;
800
696
  this.secretKey = secretKey;
@@ -818,16 +714,16 @@ class S3 extends Device {
818
714
  host = `${bucket}.s3.${region}.amazonaws.com`;
819
715
  }
820
716
  }
821
- this.headers['host'] = host;
717
+ this.headers["host"] = host;
822
718
  }
823
719
  getName() {
824
- return 'S3 Storage';
720
+ return "S3 Storage";
825
721
  }
826
722
  getType() {
827
723
  return Storage.DEVICE_S3;
828
724
  }
829
725
  getDescription() {
830
- return 'S3 Bucket Storage drive for AWS or on premise solution';
726
+ return "S3 Bucket Storage drive for AWS or on premise solution";
831
727
  }
832
728
  getRoot() {
833
729
  return this.root;
@@ -847,15 +743,14 @@ class S3 extends Device {
847
743
  async upload(source, path, chunk = 1, chunks = 1, metadata = {}) {
848
744
  let data;
849
745
  let contentType;
850
- if (typeof source === 'string') {
746
+ if (typeof source === "string") {
851
747
  const fs = await import('fs');
852
- const fileData = await fs.promises.readFile(source);
853
- data = fileData.toString();
748
+ data = await fs.promises.readFile(source);
854
749
  contentType = await this.getMimeType(source);
855
750
  }
856
751
  else {
857
- data = source.toString();
858
- contentType = metadata.contentType || 'application/octet-stream';
752
+ data = source;
753
+ contentType = metadata.contentType || "application/octet-stream";
859
754
  }
860
755
  return this.uploadData(data, path, contentType, chunk, chunks, metadata);
861
756
  }
@@ -864,28 +759,29 @@ class S3 extends Device {
864
759
  await this.write(path, data, contentType);
865
760
  return 1;
866
761
  }
867
- let uploadId = metadata['uploadId'];
762
+ let uploadId = metadata["uploadId"];
868
763
  if (!uploadId) {
869
764
  uploadId = await this.createMultipartUpload(path, contentType);
870
- metadata['uploadId'] = uploadId;
765
+ metadata["uploadId"] = uploadId;
871
766
  }
872
- metadata['parts'] = metadata['parts'] || {};
873
- metadata['chunks'] = metadata['chunks'] || 0;
767
+ metadata["parts"] = metadata["parts"] || {};
768
+ metadata["chunks"] = metadata["chunks"] || 0;
874
769
  const etag = await this.uploadPart(data, path, contentType, chunk, uploadId);
875
- if (!(chunk in metadata['parts'])) {
876
- metadata['chunks']++;
770
+ const cleanETag = etag.replace(/^"|"$/g, "");
771
+ if (!(chunk in metadata["parts"])) {
772
+ metadata["chunks"]++;
877
773
  }
878
- metadata['parts'][chunk] = etag;
879
- if (metadata['chunks'] === chunks) {
880
- await this.completeMultipartUpload(path, uploadId, metadata['parts']);
774
+ metadata["parts"][chunk] = cleanETag;
775
+ if (metadata["chunks"] === chunks) {
776
+ await this.completeMultipartUpload(path, uploadId, metadata["parts"]);
881
777
  }
882
- return metadata['chunks'];
778
+ return metadata["chunks"];
883
779
  }
884
780
  async transfer(path, destination, device) {
885
781
  try {
886
782
  const response = await this.getInfo(path);
887
- const size = parseInt(response['content-length'] || '0');
888
- const contentType = response['content-type'] || '';
783
+ const size = parseInt(response["content-length"] || "0");
784
+ const contentType = response["content-type"] || "";
889
785
  if (size <= this.transferChunkSize) {
890
786
  const source = await this.read(path);
891
787
  return device.write(destination, source, contentType);
@@ -900,124 +796,136 @@ class S3 extends Device {
900
796
  return true;
901
797
  }
902
798
  catch (e) {
903
- throw new Error('File not found');
799
+ throw new Error("File not found");
904
800
  }
905
801
  }
906
802
  async createMultipartUpload(path, contentType) {
907
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/').replace(/%3F/g, '?')}` : '/';
908
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
909
- delete this.amzHeaders['x-amz-content-sha256'];
910
- this.headers['content-type'] = contentType;
911
- this.amzHeaders['x-amz-acl'] = this.acl;
912
- const response = await this.call(S3.METHOD_POST, uri, '', { uploads: '' });
913
- return response.body['UploadId'];
803
+ const uri = path !== ""
804
+ ? `/${encodeURIComponent(path).replace(/%2F/g, "/").replace(/%3F/g, "?")}`
805
+ : "/";
806
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
807
+ delete this.amzHeaders["x-amz-content-sha256"];
808
+ this.headers["content-type"] = contentType;
809
+ this.amzHeaders["x-amz-acl"] = this.acl;
810
+ const response = await this.call(S3.METHOD_POST, uri, "", { uploads: "" });
811
+ const uploadId = response.body?.InitiateMultipartUploadResult?.UploadId?.[0];
812
+ return uploadId;
914
813
  }
915
814
  async uploadPart(data, path, contentType, chunk, uploadId) {
916
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/').replace(/%3F/g, '?')}` : '/';
917
- this.headers['content-type'] = contentType;
918
- this.headers['content-md5'] = Buffer.from(this.md5(data)).toString('base64');
919
- this.amzHeaders['x-amz-content-sha256'] = this.sha256(data);
920
- delete this.amzHeaders['x-amz-acl'];
815
+ const uri = path !== ""
816
+ ? `/${encodeURIComponent(path).replace(/%2F/g, "/").replace(/%3F/g, "?")}`
817
+ : "/";
818
+ this.headers["content-type"] = contentType;
819
+ this.headers["content-md5"] = Buffer.from(this.md5(data)).toString("base64");
820
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(data);
821
+ delete this.amzHeaders["x-amz-acl"];
921
822
  const response = await this.call(S3.METHOD_PUT, uri, data, {
922
823
  partNumber: chunk.toString(),
923
824
  uploadId: uploadId,
924
825
  });
925
- return response.headers['etag'];
826
+ return response.headers["etag"];
926
827
  }
927
828
  async completeMultipartUpload(path, uploadId, parts) {
928
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/').replace(/%3F/g, '?')}` : '/';
929
- let body = '<CompleteMultipartUpload>';
829
+ const uri = path !== ""
830
+ ? `/${encodeURIComponent(path).replace(/%2F/g, "/").replace(/%3F/g, "?")}`
831
+ : "/";
832
+ let body = "<CompleteMultipartUpload>";
930
833
  for (const [key, etag] of Object.entries(parts)) {
931
834
  body += `<Part><ETag>${etag}</ETag><PartNumber>${key}</PartNumber></Part>`;
932
835
  }
933
- body += '</CompleteMultipartUpload>';
934
- this.amzHeaders['x-amz-content-sha256'] = this.sha256(body);
935
- this.headers['content-md5'] = Buffer.from(this.md5(body)).toString('base64');
836
+ body += "</CompleteMultipartUpload>";
837
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(body);
838
+ this.headers["content-md5"] = Buffer.from(this.md5(body)).toString("base64");
936
839
  await this.call(S3.METHOD_POST, uri, body, { uploadId });
937
840
  return true;
938
841
  }
939
- async abort(path, extra = '') {
940
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/')}` : '/';
941
- delete this.headers['content-type'];
942
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
943
- await this.call(S3.METHOD_DELETE, uri, '', { uploadId: extra });
842
+ async abort(path, extra = "") {
843
+ const uri = path !== "" ? `/${encodeURIComponent(path).replace(/%2F/g, "/")}` : "/";
844
+ delete this.headers["content-type"];
845
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
846
+ await this.call(S3.METHOD_DELETE, uri, "", { uploadId: extra });
944
847
  return true;
945
848
  }
946
849
  async read(path, offset = 0, length) {
947
- delete this.amzHeaders['x-amz-acl'];
948
- delete this.amzHeaders['x-amz-content-sha256'];
949
- delete this.headers['content-type'];
950
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
951
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/')}` : '/';
850
+ delete this.amzHeaders["x-amz-acl"];
851
+ delete this.amzHeaders["x-amz-content-sha256"];
852
+ delete this.headers["content-type"];
853
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
854
+ const uri = path !== "" ? `/${encodeURIComponent(path).replace(/%2F/g, "/")}` : "/";
952
855
  if (length !== undefined) {
953
856
  const end = offset + length - 1;
954
- this.headers['range'] = `bytes=${offset}-${end}`;
857
+ this.headers["range"] = `bytes=${offset}-${end}`;
955
858
  }
956
- const response = await this.call(S3.METHOD_GET, uri, '', {}, false);
957
- return response.body;
958
- }
959
- async write(path, data, contentType = '') {
960
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/').replace(/%3F/g, '?')}` : '/';
961
- this.headers['content-type'] = contentType;
962
- this.headers['content-md5'] = Buffer.from(this.md5(data)).toString('base64');
963
- this.amzHeaders['x-amz-content-sha256'] = this.sha256(data);
964
- this.amzHeaders['x-amz-acl'] = this.acl;
859
+ else {
860
+ delete this.headers["range"];
861
+ }
862
+ const response = await this.call(S3.METHOD_GET, uri, "", {}, false);
863
+ return Buffer.from(response.buffer);
864
+ }
865
+ async write(path, data, contentType = "") {
866
+ const uri = path !== ""
867
+ ? `/${encodeURIComponent(path).replace(/%2F/g, "/").replace(/%3F/g, "?")}`
868
+ : "/";
869
+ this.headers["content-type"] = contentType;
870
+ this.headers["content-md5"] = Buffer.from(this.md5(data)).toString("base64");
871
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(data);
872
+ this.amzHeaders["x-amz-acl"] = this.acl;
965
873
  await this.call(S3.METHOD_PUT, uri, data);
966
874
  return true;
967
875
  }
968
876
  async delete(path, recursive = false) {
969
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/')}` : '/';
970
- delete this.headers['content-type'];
971
- delete this.amzHeaders['x-amz-acl'];
972
- delete this.amzHeaders['x-amz-content-sha256'];
973
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
877
+ const uri = path !== "" ? `/${encodeURIComponent(path).replace(/%2F/g, "/")}` : "/";
878
+ delete this.headers["content-type"];
879
+ delete this.amzHeaders["x-amz-acl"];
880
+ delete this.amzHeaders["x-amz-content-sha256"];
881
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
974
882
  await this.call(S3.METHOD_DELETE, uri);
975
883
  return true;
976
884
  }
977
- async listObjects(prefix = '', maxKeys = S3.MAX_PAGE_SIZE, continuationToken = '') {
885
+ async listObjects(prefix = "", maxKeys = S3.MAX_PAGE_SIZE, continuationToken = "") {
978
886
  if (maxKeys > S3.MAX_PAGE_SIZE) {
979
887
  throw new Error(`Cannot list more than ${S3.MAX_PAGE_SIZE} objects`);
980
888
  }
981
- const uri = '/';
982
- prefix = prefix.replace(/^\//, '');
983
- this.headers['content-type'] = 'text/plain';
984
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
985
- delete this.amzHeaders['x-amz-content-sha256'];
986
- delete this.amzHeaders['x-amz-acl'];
889
+ const uri = "/";
890
+ prefix = prefix.replace(/^\//, "");
891
+ this.headers["content-type"] = "text/plain";
892
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
893
+ delete this.amzHeaders["x-amz-content-sha256"];
894
+ delete this.amzHeaders["x-amz-acl"];
987
895
  const parameters = {
988
- 'list-type': '2',
989
- 'prefix': prefix,
990
- 'max-keys': maxKeys.toString(),
896
+ "list-type": "2",
897
+ prefix: prefix,
898
+ "max-keys": maxKeys.toString(),
991
899
  };
992
900
  if (continuationToken) {
993
- parameters['continuation-token'] = continuationToken;
901
+ parameters["continuation-token"] = continuationToken;
994
902
  }
995
- const response = await this.call(S3.METHOD_GET, uri, '', parameters);
903
+ const response = await this.call(S3.METHOD_GET, uri, "", parameters);
996
904
  return response.body;
997
905
  }
998
906
  async deletePath(path) {
999
907
  path = `${this.getRoot()}/${path}`;
1000
- const uri = '/';
1001
- let continuationToken = '';
908
+ const uri = "/";
909
+ let continuationToken = "";
1002
910
  do {
1003
911
  const objects = await this.listObjects(path, S3.MAX_PAGE_SIZE, continuationToken);
1004
- const count = parseInt(objects['KeyCount'] || '1');
912
+ const count = parseInt(objects["KeyCount"] || "1");
1005
913
  if (count < 1)
1006
914
  break;
1007
- continuationToken = objects['NextContinuationToken'] || '';
915
+ continuationToken = objects["NextContinuationToken"] || "";
1008
916
  let body = '<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">';
1009
917
  if (count > 1) {
1010
- for (const object of objects['Contents']) {
1011
- body += `<Object><Key>${object['Key']}</Key></Object>`;
918
+ for (const object of objects["Contents"]) {
919
+ body += `<Object><Key>${object["Key"]}</Key></Object>`;
1012
920
  }
1013
921
  }
1014
922
  else {
1015
- body += `<Object><Key>${objects['Contents']['Key']}</Key></Object>`;
923
+ body += `<Object><Key>${objects["Contents"]["Key"]}</Key></Object>`;
1016
924
  }
1017
- body += '<Quiet>true</Quiet></Delete>';
1018
- this.amzHeaders['x-amz-content-sha256'] = this.sha256(body);
1019
- this.headers['content-md5'] = Buffer.from(this.md5(body)).toString('base64');
1020
- await this.call(S3.METHOD_POST, uri, body, { delete: '' });
925
+ body += "<Quiet>true</Quiet></Delete>";
926
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(body);
927
+ this.headers["content-md5"] = Buffer.from(this.md5(body)).toString("base64");
928
+ await this.call(S3.METHOD_POST, uri, body, { delete: "" });
1021
929
  } while (continuationToken);
1022
930
  return true;
1023
931
  }
@@ -1032,14 +940,14 @@ class S3 extends Device {
1032
940
  }
1033
941
  async getFileSize(path) {
1034
942
  const response = await this.getInfo(path);
1035
- return parseInt(response['content-length'] || '0');
943
+ return parseInt(response["content-length"] || "0");
1036
944
  }
1037
945
  async getFileMimeType(path) {
1038
946
  const response = await this.getInfo(path);
1039
- return response['content-type'] || '';
947
+ return response["content-type"] || "";
1040
948
  }
1041
949
  async getFileHash(path) {
1042
- const etag = (await this.getInfo(path))['etag'] || '';
950
+ const etag = (await this.getInfo(path))["etag"] || "";
1043
951
  return etag ? etag.slice(1, -1) : etag;
1044
952
  }
1045
953
  async createDirectory(path) {
@@ -1051,28 +959,28 @@ class S3 extends Device {
1051
959
  async getPartitionFreeSpace() {
1052
960
  return -1;
1053
961
  }
1054
- async getFiles(dir, max = S3.MAX_PAGE_SIZE, continuationToken = '') {
962
+ async getFiles(dir, max = S3.MAX_PAGE_SIZE, continuationToken = "") {
1055
963
  const data = await this.listObjects(dir, max, continuationToken);
1056
- data['IsTruncated'] = data['IsTruncated'] === 'true';
1057
- data['KeyCount'] = parseInt(data['KeyCount']);
1058
- data['MaxKeys'] = parseInt(data['MaxKeys']);
964
+ data["IsTruncated"] = data["IsTruncated"] === "true";
965
+ data["KeyCount"] = parseInt(data["KeyCount"]);
966
+ data["MaxKeys"] = parseInt(data["MaxKeys"]);
1059
967
  return data;
1060
968
  }
1061
969
  async getInfo(path) {
1062
- delete this.headers['content-type'];
1063
- delete this.amzHeaders['x-amz-acl'];
1064
- delete this.amzHeaders['x-amz-content-sha256'];
1065
- this.headers['content-md5'] = Buffer.from(this.md5('')).toString('base64');
1066
- const uri = path !== '' ? `/${encodeURIComponent(path).replace(/%2F/g, '/')}` : '/';
970
+ delete this.headers["content-type"];
971
+ delete this.amzHeaders["x-amz-acl"];
972
+ delete this.amzHeaders["x-amz-content-sha256"];
973
+ this.headers["content-md5"] = Buffer.from(this.md5("")).toString("base64");
974
+ const uri = path !== "" ? `/${encodeURIComponent(path).replace(/%2F/g, "/")}` : "/";
1067
975
  const response = await this.call(S3.METHOD_HEAD, uri);
1068
976
  return response.headers;
1069
977
  }
1070
978
  getSignatureV4(method, uri, parameters = {}) {
1071
- const service = 's3';
979
+ const service = "s3";
1072
980
  const region = this.region;
1073
- const algorithm = 'AWS4-HMAC-SHA256';
981
+ const algorithm = "AWS4-HMAC-SHA256";
1074
982
  const combinedHeaders = {};
1075
- const amzDateStamp = this.amzHeaders['x-amz-date'].substring(0, 8);
983
+ const amzDateStamp = this.amzHeaders["x-amz-date"].substring(0, 8);
1076
984
  // Combine headers
1077
985
  for (const [k, v] of Object.entries(this.headers)) {
1078
986
  combinedHeaders[k.toLowerCase()] = v.trim();
@@ -1088,42 +996,55 @@ class S3 extends Device {
1088
996
  }
1089
997
  // Sort parameters
1090
998
  const sortedParams = Object.keys(parameters).sort();
1091
- const queryString = sortedParams.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`).join('&');
999
+ const queryString = sortedParams
1000
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`)
1001
+ .join("&");
1092
1002
  // Create canonical request
1093
1003
  const canonicalRequest = [
1094
1004
  method,
1095
- uri.split('?')[0],
1005
+ uri.split("?")[0],
1096
1006
  queryString,
1097
1007
  ...Object.entries(sortedCombinedHeaders).map(([k, v]) => `${k}:${v}`),
1098
- '',
1099
- Object.keys(sortedCombinedHeaders).join(';'),
1100
- this.amzHeaders['x-amz-content-sha256']
1101
- ].join('\n');
1008
+ "",
1009
+ Object.keys(sortedCombinedHeaders).join(";"),
1010
+ this.amzHeaders["x-amz-content-sha256"],
1011
+ ].join("\n");
1102
1012
  // Create string to sign
1103
- const credentialScope = [amzDateStamp, region, service, 'aws4_request'].join('/');
1013
+ const credentialScope = [
1014
+ amzDateStamp,
1015
+ region,
1016
+ service,
1017
+ "aws4_request",
1018
+ ].join("/");
1104
1019
  const stringToSign = [
1105
1020
  algorithm,
1106
- this.amzHeaders['x-amz-date'],
1021
+ this.amzHeaders["x-amz-date"],
1107
1022
  credentialScope,
1108
- this.sha256(canonicalRequest)
1109
- ].join('\n');
1023
+ this.sha256(canonicalRequest),
1024
+ ].join("\n");
1110
1025
  // Calculate signature
1111
1026
  const kSecret = `AWS4${this.secretKey}`;
1112
1027
  const kDate = this.hmacSha256(amzDateStamp, kSecret);
1113
1028
  const kRegion = this.hmacSha256(region, kDate);
1114
1029
  const kService = this.hmacSha256(service, kRegion);
1115
- const kSigning = this.hmacSha256('aws4_request', kService);
1116
- const signature = this.hmacSha256(stringToSign, kSigning, 'hex');
1117
- return `${algorithm} Credential=${this.accessKey}/${credentialScope},SignedHeaders=${Object.keys(sortedCombinedHeaders).join(';')},Signature=${signature}`;
1030
+ const kSigning = this.hmacSha256("aws4_request", kService);
1031
+ const signature = this.hmacSha256(stringToSign, kSigning, "hex");
1032
+ return `${algorithm} Credential=${this.accessKey}/${credentialScope},SignedHeaders=${Object.keys(sortedCombinedHeaders).join(";")},Signature=${signature}`;
1118
1033
  }
1119
- async call(method, uri, data = '', parameters = {}, decode = true) {
1120
- await import('crypto');
1034
+ async call(method, uri, data = "", parameters = {}, decode = true) {
1121
1035
  uri = this.getAbsolutePath(uri);
1122
- const queryString = Object.keys(parameters).length ? '?' + Object.entries(parameters).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&') : '';
1123
- const url = `https://${this.headers['host']}${uri}${queryString}`;
1124
- this.amzHeaders['x-amz-date'] = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
1125
- if (!this.amzHeaders['x-amz-content-sha256']) {
1126
- this.amzHeaders['x-amz-content-sha256'] = this.sha256(data);
1036
+ const queryString = Object.keys(parameters).length
1037
+ ? "?" +
1038
+ Object.entries(parameters)
1039
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
1040
+ .join("&")
1041
+ : "";
1042
+ const url = `https://${this.headers["host"]}${uri}${queryString}`;
1043
+ this.amzHeaders["x-amz-date"] = new Date()
1044
+ .toISOString()
1045
+ .replace(/[:-]|\.\d{3}/g, "");
1046
+ if (!this.amzHeaders["x-amz-content-sha256"]) {
1047
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(data);
1127
1048
  }
1128
1049
  const headers = {};
1129
1050
  for (const [header, value] of Object.entries(this.amzHeaders)) {
@@ -1131,13 +1052,13 @@ class S3 extends Device {
1131
1052
  headers[header] = value;
1132
1053
  }
1133
1054
  }
1134
- this.headers['date'] = new Date().toUTCString();
1055
+ this.headers["date"] = new Date().toUTCString();
1135
1056
  for (const [header, value] of Object.entries(this.headers)) {
1136
1057
  if (value.length > 0) {
1137
1058
  headers[header] = value;
1138
1059
  }
1139
1060
  }
1140
- headers['Authorization'] = this.getSignatureV4(method, uri, parameters);
1061
+ headers["Authorization"] = this.getSignatureV4(method, uri, parameters);
1141
1062
  const fetchOptions = {
1142
1063
  method,
1143
1064
  headers,
@@ -1154,72 +1075,80 @@ class S3 extends Device {
1154
1075
  response.headers.forEach((value, key) => {
1155
1076
  responseHeaders[key.toLowerCase()] = value;
1156
1077
  });
1157
- let body = await response.text();
1158
- if (decode && (responseHeaders['content-type'] === 'application/xml' || (body.startsWith('<?xml') && responseHeaders['content-type'] !== 'image/svg+xml'))) {
1078
+ const buffer = await response.arrayBuffer();
1079
+ let body = new TextDecoder().decode(buffer);
1080
+ if (decode &&
1081
+ (responseHeaders["content-type"] === "application/xml" ||
1082
+ (body.startsWith("<?xml") &&
1083
+ responseHeaders["content-type"] !== "image/svg+xml"))) {
1159
1084
  const xml2js = await import('xml2js');
1160
1085
  const parser = new xml2js.Parser();
1161
1086
  body = await parser.parseStringPromise(body);
1162
1087
  }
1163
1088
  return {
1164
1089
  body,
1090
+ buffer,
1165
1091
  headers: responseHeaders,
1166
- code: response.status
1092
+ code: response.status,
1167
1093
  };
1168
1094
  }
1169
1095
  md5(data) {
1170
- return crypto.createHash('md5').update(data).digest();
1096
+ const buffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
1097
+ return crypto.createHash("md5").update(buffer).digest();
1171
1098
  }
1172
1099
  sha256(data) {
1173
- return crypto.createHash('sha256').update(data).digest('hex');
1100
+ const buffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
1101
+ return crypto.createHash("sha256").update(buffer).digest("hex");
1174
1102
  }
1175
- hmacSha256(data, key, encoding = 'binary') {
1176
- const hmac = crypto.createHmac('sha256', key);
1177
- hmac.update(data);
1178
- return encoding === 'hex' ? hmac.digest('hex') : hmac.digest();
1103
+ hmacSha256(data, key, encoding = "binary") {
1104
+ const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
1105
+ const hmac = crypto.createHmac("sha256", key);
1106
+ hmac.update(dataBuffer);
1107
+ return encoding === "hex" ? hmac.digest("hex") : hmac.digest();
1179
1108
  }
1180
1109
  }
1181
1110
  // HTTP Methods
1182
- S3.METHOD_GET = 'GET';
1183
- S3.METHOD_POST = 'POST';
1184
- S3.METHOD_PUT = 'PUT';
1185
- S3.METHOD_PATCH = 'PATCH';
1186
- S3.METHOD_DELETE = 'DELETE';
1187
- S3.METHOD_HEAD = 'HEAD';
1188
- S3.METHOD_OPTIONS = 'OPTIONS';
1189
- S3.METHOD_CONNECT = 'CONNECT';
1190
- S3.METHOD_TRACE = 'TRACE';
1111
+ S3.METHOD_GET = "GET";
1112
+ S3.METHOD_POST = "POST";
1113
+ S3.METHOD_PUT = "PUT";
1114
+ S3.METHOD_PATCH = "PATCH";
1115
+ S3.METHOD_DELETE = "DELETE";
1116
+ S3.METHOD_HEAD = "HEAD";
1117
+ S3.METHOD_OPTIONS = "OPTIONS";
1118
+ S3.METHOD_CONNECT = "CONNECT";
1119
+ S3.METHOD_TRACE = "TRACE";
1191
1120
  // AWS Regions
1192
- S3.US_EAST_1 = 'us-east-1';
1193
- S3.US_EAST_2 = 'us-east-2';
1194
- S3.US_WEST_1 = 'us-west-1';
1195
- S3.US_WEST_2 = 'us-west-2';
1196
- S3.AF_SOUTH_1 = 'af-south-1';
1197
- S3.AP_EAST_1 = 'ap-east-1';
1198
- S3.AP_SOUTH_1 = 'ap-south-1';
1199
- S3.AP_NORTHEAST_3 = 'ap-northeast-3';
1200
- S3.AP_NORTHEAST_2 = 'ap-northeast-2';
1201
- S3.AP_NORTHEAST_1 = 'ap-northeast-1';
1202
- S3.AP_SOUTHEAST_1 = 'ap-southeast-1';
1203
- S3.AP_SOUTHEAST_2 = 'ap-southeast-2';
1204
- S3.CA_CENTRAL_1 = 'ca-central-1';
1205
- S3.EU_CENTRAL_1 = 'eu-central-1';
1206
- S3.EU_WEST_1 = 'eu-west-1';
1207
- S3.EU_SOUTH_1 = 'eu-south-1';
1208
- S3.EU_WEST_2 = 'eu-west-2';
1209
- S3.EU_WEST_3 = 'eu-west-3';
1210
- S3.EU_NORTH_1 = 'eu-north-1';
1211
- S3.SA_EAST_1 = 'eu-north-1';
1212
- S3.CN_NORTH_1 = 'cn-north-1';
1213
- S3.CN_NORTH_4 = 'cn-north-4';
1214
- S3.CN_NORTHWEST_1 = 'cn-northwest-1';
1215
- S3.ME_SOUTH_1 = 'me-south-1';
1216
- S3.US_GOV_EAST_1 = 'us-gov-east-1';
1217
- S3.US_GOV_WEST_1 = 'us-gov-west-1';
1121
+ S3.US_EAST_1 = "us-east-1";
1122
+ S3.US_EAST_2 = "us-east-2";
1123
+ S3.US_WEST_1 = "us-west-1";
1124
+ S3.US_WEST_2 = "us-west-2";
1125
+ S3.AF_SOUTH_1 = "af-south-1";
1126
+ S3.AP_EAST_1 = "ap-east-1";
1127
+ S3.AP_SOUTH_1 = "ap-south-1";
1128
+ S3.AP_NORTHEAST_3 = "ap-northeast-3";
1129
+ S3.AP_NORTHEAST_2 = "ap-northeast-2";
1130
+ S3.AP_NORTHEAST_1 = "ap-northeast-1";
1131
+ S3.AP_SOUTHEAST_1 = "ap-southeast-1";
1132
+ S3.AP_SOUTHEAST_2 = "ap-southeast-2";
1133
+ S3.CA_CENTRAL_1 = "ca-central-1";
1134
+ S3.EU_CENTRAL_1 = "eu-central-1";
1135
+ S3.EU_WEST_1 = "eu-west-1";
1136
+ S3.EU_SOUTH_1 = "eu-south-1";
1137
+ S3.EU_WEST_2 = "eu-west-2";
1138
+ S3.EU_WEST_3 = "eu-west-3";
1139
+ S3.EU_NORTH_1 = "eu-north-1";
1140
+ S3.SA_EAST_1 = "eu-north-1";
1141
+ S3.CN_NORTH_1 = "cn-north-1";
1142
+ S3.CN_NORTH_4 = "cn-north-4";
1143
+ S3.CN_NORTHWEST_1 = "cn-northwest-1";
1144
+ S3.ME_SOUTH_1 = "me-south-1";
1145
+ S3.US_GOV_EAST_1 = "us-gov-east-1";
1146
+ S3.US_GOV_WEST_1 = "us-gov-west-1";
1218
1147
  // ACL Flags
1219
- S3.ACL_PRIVATE = 'private';
1220
- S3.ACL_PUBLIC_READ = 'public-read';
1221
- S3.ACL_PUBLIC_READ_WRITE = 'public-read-write';
1222
- S3.ACL_AUTHENTICATED_READ = 'authenticated-read';
1148
+ S3.ACL_PRIVATE = "private";
1149
+ S3.ACL_PUBLIC_READ = "public-read";
1150
+ S3.ACL_PUBLIC_READ_WRITE = "public-read-write";
1151
+ S3.ACL_AUTHENTICATED_READ = "authenticated-read";
1223
1152
  S3.MAX_PAGE_SIZE = 1000;
1224
1153
  S3.retryAttempts = 3;
1225
1154
  S3.retryDelay = 500;
@@ -1230,13 +1159,13 @@ class Wasabi extends S3 {
1230
1159
  */
1231
1160
  constructor(root, accessKey, secretKey, bucket, region = Wasabi.EU_CENTRAL_1, acl = S3.ACL_PRIVATE) {
1232
1161
  super(root, accessKey, secretKey, bucket, region, acl);
1233
- this.headers['host'] = `${bucket}.s3.${region}.wasabisys.com`;
1162
+ this.headers["host"] = `${bucket}.s3.${region}.wasabisys.com`;
1234
1163
  }
1235
1164
  getName() {
1236
- return 'Wasabi Storage';
1165
+ return "Wasabi Storage";
1237
1166
  }
1238
1167
  getDescription() {
1239
- return 'Wasabi Storage';
1168
+ return "Wasabi Storage";
1240
1169
  }
1241
1170
  getType() {
1242
1171
  return Storage.DEVICE_WASABI;
@@ -1245,16 +1174,112 @@ class Wasabi extends S3 {
1245
1174
  /**
1246
1175
  * Regions constants
1247
1176
  */
1248
- Wasabi.US_WEST_1 = 'us-west-1';
1249
- Wasabi.AP_NORTHEAST_1 = 'ap-northeast-1';
1250
- Wasabi.AP_NORTHEAST_2 = 'ap-northeast-2';
1251
- Wasabi.EU_CENTRAL_1 = 'eu-central-1';
1252
- Wasabi.EU_CENTRAL_2 = 'eu-central-2';
1253
- Wasabi.EU_WEST_1 = 'eu-west-1';
1254
- Wasabi.EU_WEST_2 = 'eu-west-2';
1255
- Wasabi.US_CENTRAL_1 = 'us-central-1';
1256
- Wasabi.US_EAST_1 = 'us-east-1';
1257
- Wasabi.US_EAST_2 = 'us-east-2';
1177
+ Wasabi.US_WEST_1 = "us-west-1";
1178
+ Wasabi.AP_NORTHEAST_1 = "ap-northeast-1";
1179
+ Wasabi.AP_NORTHEAST_2 = "ap-northeast-2";
1180
+ Wasabi.EU_CENTRAL_1 = "eu-central-1";
1181
+ Wasabi.EU_CENTRAL_2 = "eu-central-2";
1182
+ Wasabi.EU_WEST_1 = "eu-west-1";
1183
+ Wasabi.EU_WEST_2 = "eu-west-2";
1184
+ Wasabi.US_CENTRAL_1 = "us-central-1";
1185
+ Wasabi.US_EAST_1 = "us-east-1";
1186
+ Wasabi.US_EAST_2 = "us-east-2";
1187
+
1188
+ class MinIO extends S3 {
1189
+ /**
1190
+ * MinIO Constructor
1191
+ */
1192
+ constructor(root, accessKey, secretKey, bucket, endpoint = "localhost:9000", acl = S3.ACL_PRIVATE, useSSL = false) {
1193
+ const protocol = useSSL ? "https" : "http";
1194
+ // Remove protocol from endpoint if it exists
1195
+ const cleanEndpoint = endpoint.replace(/^https?:\/\//, "");
1196
+ super(root, accessKey, secretKey, bucket, "us-east-1", acl, cleanEndpoint);
1197
+ // Override the host for MinIO
1198
+ this.headers["host"] = `${bucket}.${cleanEndpoint}`;
1199
+ // Store the full endpoint URL for MinIO-specific operations
1200
+ this.endpointUrl = `${protocol}://${cleanEndpoint}`;
1201
+ }
1202
+ getName() {
1203
+ return "MinIO Storage";
1204
+ }
1205
+ getDescription() {
1206
+ return "MinIO S3-compatible object storage server";
1207
+ }
1208
+ getType() {
1209
+ return Storage.DEVICE_MINIO;
1210
+ }
1211
+ /**
1212
+ * Override the call method to use the custom endpoint
1213
+ */
1214
+ async call(method, uri, data = "", parameters = {}, decode = true) {
1215
+ // Temporarily modify the host header to not include the bucket for MinIO
1216
+ const originalHost = this.headers["host"];
1217
+ // For MinIO, we need to handle path-style URLs differently
1218
+ const bucketName = originalHost.split(".")[0];
1219
+ const endpoint = originalHost.split(".").slice(1).join(".");
1220
+ // Use path-style URLs for MinIO
1221
+ this.headers["host"] = endpoint;
1222
+ const queryString = Object.keys(parameters).length
1223
+ ? "?" +
1224
+ Object.entries(parameters)
1225
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
1226
+ .join("&")
1227
+ : "";
1228
+ const pathStyleUri = `/${bucketName}${this.getAbsolutePath(uri)}`;
1229
+ const url = `${this.endpointUrl}${pathStyleUri}${queryString}`;
1230
+ this.amzHeaders["x-amz-date"] = new Date()
1231
+ .toISOString()
1232
+ .replace(/[:-]|\.\d{3}/g, "");
1233
+ if (!this.amzHeaders["x-amz-content-sha256"]) {
1234
+ this.amzHeaders["x-amz-content-sha256"] = this.sha256(data);
1235
+ }
1236
+ const headers = {};
1237
+ for (const [header, value] of Object.entries(this.amzHeaders)) {
1238
+ if (value.length > 0) {
1239
+ headers[header] = value;
1240
+ }
1241
+ }
1242
+ this.headers["date"] = new Date().toUTCString();
1243
+ for (const [header, value] of Object.entries(this.headers)) {
1244
+ if (value.length > 0) {
1245
+ headers[header] = value;
1246
+ }
1247
+ }
1248
+ headers["Authorization"] = this.getSignatureV4(method, pathStyleUri, parameters);
1249
+ const fetchOptions = {
1250
+ method,
1251
+ headers,
1252
+ };
1253
+ if (method === S3.METHOD_PUT || method === S3.METHOD_POST) {
1254
+ fetchOptions.body = data;
1255
+ }
1256
+ const response = await fetch(url, fetchOptions);
1257
+ // Restore original host
1258
+ this.headers["host"] = originalHost;
1259
+ if (!response.ok) {
1260
+ const errorBody = await response.text();
1261
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
1262
+ }
1263
+ const responseHeaders = {};
1264
+ response.headers.forEach((value, key) => {
1265
+ responseHeaders[key.toLowerCase()] = value;
1266
+ });
1267
+ let body = await response.text();
1268
+ if (decode &&
1269
+ (responseHeaders["content-type"] === "application/xml" ||
1270
+ (body.startsWith("<?xml") &&
1271
+ responseHeaders["content-type"] !== "image/svg+xml"))) {
1272
+ const xml2js = await import('xml2js');
1273
+ const parser = new xml2js.Parser();
1274
+ body = await parser.parseStringPromise(body);
1275
+ }
1276
+ return {
1277
+ body,
1278
+ headers: responseHeaders,
1279
+ code: response.status,
1280
+ };
1281
+ }
1282
+ }
1258
1283
 
1259
1284
  exports.Device = Device;
1260
1285
  exports.File = File;
@@ -1263,6 +1288,7 @@ exports.FileName = FileName;
1263
1288
  exports.FileSize = FileSize;
1264
1289
  exports.FileType = FileType;
1265
1290
  exports.Local = Local;
1291
+ exports.MinIO = MinIO;
1266
1292
  exports.S3 = S3;
1267
1293
  exports.Storage = Storage;
1268
1294
  exports.Upload = Upload;