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