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