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