@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/README.md +125 -68
- package/dist/index.cjs.js +454 -428
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +38 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +454 -429
- package/dist/index.esm.js.map +1 -1
- package/dist/src/device/local.d.ts +3 -3
- package/dist/src/device/local.d.ts.map +1 -1
- package/dist/src/device/minio.d.ts +16 -0
- package/dist/src/device/minio.d.ts.map +1 -0
- package/dist/src/device/s3.d.ts +14 -9
- package/dist/src/device/s3.d.ts.map +1 -1
- package/dist/src/device/wasabi.d.ts.map +1 -1
- package/dist/src/device.d.ts +3 -3
- package/dist/src/device.d.ts.map +1 -1
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.d.ts.map +1 -1
- package/dist/src/validator/file-ext.d.ts +0 -2
- package/dist/src/validator/file-ext.d.ts.map +1 -1
- package/dist/src/validator/file-name.d.ts +1 -17
- package/dist/src/validator/file-name.d.ts.map +1 -1
- package/dist/src/validator/file-size.d.ts +1 -13
- package/dist/src/validator/file-size.d.ts.map +1 -1
- package/dist/src/validator/file-type.d.ts +1 -9
- package/dist/src/validator/file-type.d.ts.map +1 -1
- package/dist/src/validator/file.d.ts +0 -16
- package/dist/src/validator/file.d.ts.map +1 -1
- package/dist/src/validator/upload.d.ts +0 -16
- package/dist/src/validator/upload.d.ts.map +1 -1
- package/dist/src/validator/validator.d.ts.map +1 -1
- package/package.json +7 -6
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(
|
|
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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
js: "application/javascript",
|
|
73
|
+
mjs: "application/javascript",
|
|
74
|
+
json: "application/json",
|
|
75
75
|
// Images
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
mp3: "audio/mpeg",
|
|
96
|
+
wav: "audio/wav",
|
|
97
|
+
ogg: "audio/ogg",
|
|
98
|
+
m4a: "audio/mp4",
|
|
99
99
|
// Video
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
bin: "application/octet-stream",
|
|
114
|
+
exe: "application/octet-stream",
|
|
115
|
+
dmg: "application/octet-stream",
|
|
116
116
|
};
|
|
117
|
-
return mimeTypes[ext] ||
|
|
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 =
|
|
175
|
-
const mod = system ===
|
|
174
|
+
static human(bytes, decimals = 2, system = "metric") {
|
|
175
|
+
const mod = system === "binary" ? 1024 : 1000;
|
|
176
176
|
const units = {
|
|
177
|
-
binary: [
|
|
178
|
-
metric: [
|
|
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 =
|
|
188
|
-
Storage.DEVICE_S3 =
|
|
187
|
+
Storage.DEVICE_LOCAL = "local";
|
|
188
|
+
Storage.DEVICE_S3 = "s3";
|
|
189
189
|
// static readonly DEVICE_DO_SPACES = 'dospaces';
|
|
190
|
-
Storage.DEVICE_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 =
|
|
203
|
-
Validator.TYPE_ARRAY =
|
|
204
|
-
Validator.TYPE_INTEGER =
|
|
205
|
-
Validator.TYPE_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
|
|
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
|
|
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 =
|
|
268
|
-
FileExt.TYPE_JPG =
|
|
269
|
-
FileExt.TYPE_GIF =
|
|
270
|
-
FileExt.TYPE_PNG =
|
|
271
|
-
FileExt.TYPE_GZIP =
|
|
272
|
-
FileExt.TYPE_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
|
|
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 !==
|
|
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 !==
|
|
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]:
|
|
373
|
-
[FileType.FILE_TYPE_GIF]:
|
|
374
|
-
[FileType.FILE_TYPE_PNG]:
|
|
375
|
-
[FileType.FILE_TYPE_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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
427
|
-
FileType.FILE_TYPE_GIF =
|
|
428
|
-
FileType.FILE_TYPE_PNG =
|
|
429
|
-
FileType.FILE_TYPE_GZIP =
|
|
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
|
|
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 !==
|
|
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 =
|
|
391
|
+
this.root = "temp";
|
|
482
392
|
this.MAX_PAGE_SIZE = 1000;
|
|
483
393
|
this.root = root;
|
|
484
394
|
}
|
|
485
395
|
getName() {
|
|
486
|
-
return
|
|
396
|
+
return "Local Storage";
|
|
487
397
|
}
|
|
488
398
|
getType() {
|
|
489
399
|
return Storage.DEVICE_LOCAL;
|
|
490
400
|
}
|
|
491
401
|
getDescription() {
|
|
492
|
-
return
|
|
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
|
|
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
|
|
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,
|
|
525
|
-
const chunksReceived = chunkLogs.trim().split(
|
|
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
|
|
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
|
|
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,
|
|
553
|
-
const chunksReceived = chunkLogs.trim().split(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
529
|
+
throw new Error("File Not Found");
|
|
618
530
|
}
|
|
619
|
-
const fileHandle = await promises.open(filePath,
|
|
531
|
+
const fileHandle = await promises.open(filePath, "r");
|
|
620
532
|
try {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
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
|
|
636
|
-
throw new Error(`Can't
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
686
|
+
this.root = "temp";
|
|
791
687
|
this.headers = {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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[
|
|
715
|
+
this.headers["host"] = host;
|
|
820
716
|
}
|
|
821
717
|
getName() {
|
|
822
|
-
return
|
|
718
|
+
return "S3 Storage";
|
|
823
719
|
}
|
|
824
720
|
getType() {
|
|
825
721
|
return Storage.DEVICE_S3;
|
|
826
722
|
}
|
|
827
723
|
getDescription() {
|
|
828
|
-
return
|
|
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 ===
|
|
744
|
+
if (typeof source === "string") {
|
|
849
745
|
const fs = await import('fs');
|
|
850
|
-
|
|
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
|
|
856
|
-
contentType = metadata.contentType ||
|
|
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[
|
|
760
|
+
let uploadId = metadata["uploadId"];
|
|
866
761
|
if (!uploadId) {
|
|
867
762
|
uploadId = await this.createMultipartUpload(path, contentType);
|
|
868
|
-
metadata[
|
|
763
|
+
metadata["uploadId"] = uploadId;
|
|
869
764
|
}
|
|
870
|
-
metadata[
|
|
871
|
-
metadata[
|
|
765
|
+
metadata["parts"] = metadata["parts"] || {};
|
|
766
|
+
metadata["chunks"] = metadata["chunks"] || 0;
|
|
872
767
|
const etag = await this.uploadPart(data, path, contentType, chunk, uploadId);
|
|
873
|
-
|
|
874
|
-
|
|
768
|
+
const cleanETag = etag.replace(/^"|"$/g, "");
|
|
769
|
+
if (!(chunk in metadata["parts"])) {
|
|
770
|
+
metadata["chunks"]++;
|
|
875
771
|
}
|
|
876
|
-
metadata[
|
|
877
|
-
if (metadata[
|
|
878
|
-
await this.completeMultipartUpload(path, uploadId, metadata[
|
|
772
|
+
metadata["parts"][chunk] = cleanETag;
|
|
773
|
+
if (metadata["chunks"] === chunks) {
|
|
774
|
+
await this.completeMultipartUpload(path, uploadId, metadata["parts"]);
|
|
879
775
|
}
|
|
880
|
-
return metadata[
|
|
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[
|
|
886
|
-
const contentType = response[
|
|
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(
|
|
797
|
+
throw new Error("File not found");
|
|
902
798
|
}
|
|
903
799
|
}
|
|
904
800
|
async createMultipartUpload(path, contentType) {
|
|
905
|
-
const uri = path !==
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
this.headers[
|
|
909
|
-
this.amzHeaders[
|
|
910
|
-
|
|
911
|
-
|
|
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 !==
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
this.
|
|
918
|
-
|
|
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[
|
|
824
|
+
return response.headers["etag"];
|
|
924
825
|
}
|
|
925
826
|
async completeMultipartUpload(path, uploadId, parts) {
|
|
926
|
-
const uri = path !==
|
|
927
|
-
|
|
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 +=
|
|
932
|
-
this.amzHeaders[
|
|
933
|
-
this.headers[
|
|
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 !==
|
|
939
|
-
delete this.headers[
|
|
940
|
-
this.headers[
|
|
941
|
-
await this.call(S3.METHOD_DELETE, uri,
|
|
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[
|
|
946
|
-
delete this.amzHeaders[
|
|
947
|
-
delete this.headers[
|
|
948
|
-
this.headers[
|
|
949
|
-
const uri = path !==
|
|
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[
|
|
855
|
+
this.headers["range"] = `bytes=${offset}-${end}`;
|
|
953
856
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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 !==
|
|
968
|
-
delete this.headers[
|
|
969
|
-
delete this.amzHeaders[
|
|
970
|
-
delete this.amzHeaders[
|
|
971
|
-
this.headers[
|
|
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 =
|
|
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[
|
|
982
|
-
this.headers[
|
|
983
|
-
delete this.amzHeaders[
|
|
984
|
-
delete this.amzHeaders[
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
894
|
+
"list-type": "2",
|
|
895
|
+
prefix: prefix,
|
|
896
|
+
"max-keys": maxKeys.toString(),
|
|
989
897
|
};
|
|
990
898
|
if (continuationToken) {
|
|
991
|
-
parameters[
|
|
899
|
+
parameters["continuation-token"] = continuationToken;
|
|
992
900
|
}
|
|
993
|
-
const response = await this.call(S3.METHOD_GET, uri,
|
|
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[
|
|
910
|
+
const count = parseInt(objects["KeyCount"] || "1");
|
|
1003
911
|
if (count < 1)
|
|
1004
912
|
break;
|
|
1005
|
-
continuationToken = objects[
|
|
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[
|
|
1009
|
-
body += `<Object><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[
|
|
921
|
+
body += `<Object><Key>${objects["Contents"]["Key"]}</Key></Object>`;
|
|
1014
922
|
}
|
|
1015
|
-
body +=
|
|
1016
|
-
this.amzHeaders[
|
|
1017
|
-
this.headers[
|
|
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[
|
|
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[
|
|
945
|
+
return response["content-type"] || "";
|
|
1038
946
|
}
|
|
1039
947
|
async getFileHash(path) {
|
|
1040
|
-
const etag = (await this.getInfo(path))[
|
|
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[
|
|
1055
|
-
data[
|
|
1056
|
-
data[
|
|
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[
|
|
1061
|
-
delete this.amzHeaders[
|
|
1062
|
-
delete this.amzHeaders[
|
|
1063
|
-
this.headers[
|
|
1064
|
-
const uri = path !==
|
|
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 =
|
|
977
|
+
const service = "s3";
|
|
1070
978
|
const region = this.region;
|
|
1071
|
-
const algorithm =
|
|
979
|
+
const algorithm = "AWS4-HMAC-SHA256";
|
|
1072
980
|
const combinedHeaders = {};
|
|
1073
|
-
const amzDateStamp = this.amzHeaders[
|
|
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
|
|
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(
|
|
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[
|
|
1099
|
-
].join(
|
|
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 = [
|
|
1011
|
+
const credentialScope = [
|
|
1012
|
+
amzDateStamp,
|
|
1013
|
+
region,
|
|
1014
|
+
service,
|
|
1015
|
+
"aws4_request",
|
|
1016
|
+
].join("/");
|
|
1102
1017
|
const stringToSign = [
|
|
1103
1018
|
algorithm,
|
|
1104
|
-
this.amzHeaders[
|
|
1019
|
+
this.amzHeaders["x-amz-date"],
|
|
1105
1020
|
credentialScope,
|
|
1106
|
-
this.sha256(canonicalRequest)
|
|
1107
|
-
].join(
|
|
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(
|
|
1114
|
-
const signature = this.hmacSha256(stringToSign, kSigning,
|
|
1115
|
-
return `${algorithm} Credential=${this.accessKey}/${credentialScope},SignedHeaders=${Object.keys(sortedCombinedHeaders).join(
|
|
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 =
|
|
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
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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[
|
|
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[
|
|
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
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1174
|
-
const
|
|
1175
|
-
hmac.
|
|
1176
|
-
|
|
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 =
|
|
1181
|
-
S3.METHOD_POST =
|
|
1182
|
-
S3.METHOD_PUT =
|
|
1183
|
-
S3.METHOD_PATCH =
|
|
1184
|
-
S3.METHOD_DELETE =
|
|
1185
|
-
S3.METHOD_HEAD =
|
|
1186
|
-
S3.METHOD_OPTIONS =
|
|
1187
|
-
S3.METHOD_CONNECT =
|
|
1188
|
-
S3.METHOD_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 =
|
|
1191
|
-
S3.US_EAST_2 =
|
|
1192
|
-
S3.US_WEST_1 =
|
|
1193
|
-
S3.US_WEST_2 =
|
|
1194
|
-
S3.AF_SOUTH_1 =
|
|
1195
|
-
S3.AP_EAST_1 =
|
|
1196
|
-
S3.AP_SOUTH_1 =
|
|
1197
|
-
S3.AP_NORTHEAST_3 =
|
|
1198
|
-
S3.AP_NORTHEAST_2 =
|
|
1199
|
-
S3.AP_NORTHEAST_1 =
|
|
1200
|
-
S3.AP_SOUTHEAST_1 =
|
|
1201
|
-
S3.AP_SOUTHEAST_2 =
|
|
1202
|
-
S3.CA_CENTRAL_1 =
|
|
1203
|
-
S3.EU_CENTRAL_1 =
|
|
1204
|
-
S3.EU_WEST_1 =
|
|
1205
|
-
S3.EU_SOUTH_1 =
|
|
1206
|
-
S3.EU_WEST_2 =
|
|
1207
|
-
S3.EU_WEST_3 =
|
|
1208
|
-
S3.EU_NORTH_1 =
|
|
1209
|
-
S3.SA_EAST_1 =
|
|
1210
|
-
S3.CN_NORTH_1 =
|
|
1211
|
-
S3.CN_NORTH_4 =
|
|
1212
|
-
S3.CN_NORTHWEST_1 =
|
|
1213
|
-
S3.ME_SOUTH_1 =
|
|
1214
|
-
S3.US_GOV_EAST_1 =
|
|
1215
|
-
S3.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 =
|
|
1218
|
-
S3.ACL_PUBLIC_READ =
|
|
1219
|
-
S3.ACL_PUBLIC_READ_WRITE =
|
|
1220
|
-
S3.ACL_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[
|
|
1160
|
+
this.headers["host"] = `${bucket}.s3.${region}.wasabisys.com`;
|
|
1232
1161
|
}
|
|
1233
1162
|
getName() {
|
|
1234
|
-
return
|
|
1163
|
+
return "Wasabi Storage";
|
|
1235
1164
|
}
|
|
1236
1165
|
getDescription() {
|
|
1237
|
-
return
|
|
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 =
|
|
1247
|
-
Wasabi.AP_NORTHEAST_1 =
|
|
1248
|
-
Wasabi.AP_NORTHEAST_2 =
|
|
1249
|
-
Wasabi.EU_CENTRAL_1 =
|
|
1250
|
-
Wasabi.EU_CENTRAL_2 =
|
|
1251
|
-
Wasabi.EU_WEST_1 =
|
|
1252
|
-
Wasabi.EU_WEST_2 =
|
|
1253
|
-
Wasabi.US_CENTRAL_1 =
|
|
1254
|
-
Wasabi.US_EAST_1 =
|
|
1255
|
-
Wasabi.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
|