s3db.js 6.2.0 → 7.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/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +30057 -18387
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +30043 -18384
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +29730 -18061
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -69
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +142 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import EventEmitter from "events";
|
|
3
|
+
import { chunk } from "lodash-es";
|
|
4
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
5
|
+
|
|
6
|
+
import { idGenerator } from "./concerns/id.js";
|
|
7
|
+
import tryFn from "./concerns/try-fn.js";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
S3Client,
|
|
11
|
+
PutObjectCommand,
|
|
12
|
+
GetObjectCommand,
|
|
13
|
+
CopyObjectCommand,
|
|
14
|
+
HeadObjectCommand,
|
|
15
|
+
DeleteObjectCommand,
|
|
16
|
+
DeleteObjectsCommand,
|
|
17
|
+
ListObjectsV2Command,
|
|
18
|
+
} from '@aws-sdk/client-s3';
|
|
19
|
+
import { md5 } from 'hash-wasm';
|
|
20
|
+
|
|
21
|
+
import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "./errors.js";
|
|
22
|
+
import { ConnectionString } from "./connection-string.class.js";
|
|
23
|
+
|
|
24
|
+
export class Client extends EventEmitter {
|
|
25
|
+
constructor({
|
|
26
|
+
verbose = false,
|
|
27
|
+
id = null,
|
|
28
|
+
AwsS3Client,
|
|
29
|
+
connectionString,
|
|
30
|
+
parallelism = 10,
|
|
31
|
+
}) {
|
|
32
|
+
super();
|
|
33
|
+
this.verbose = verbose;
|
|
34
|
+
this.id = id ?? idGenerator();
|
|
35
|
+
this.parallelism = parallelism;
|
|
36
|
+
this.config = new ConnectionString(connectionString);
|
|
37
|
+
this.client = AwsS3Client || this.createClient()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
createClient() {
|
|
41
|
+
let options = {
|
|
42
|
+
region: this.config.region,
|
|
43
|
+
endpoint: this.config.endpoint,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.config.forcePathStyle) options.forcePathStyle = true
|
|
47
|
+
|
|
48
|
+
if (this.config.accessKeyId) {
|
|
49
|
+
options.credentials = {
|
|
50
|
+
accessKeyId: this.config.accessKeyId,
|
|
51
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = new S3Client(options);
|
|
56
|
+
|
|
57
|
+
// Adiciona middleware para Content-MD5 em DeleteObjectsCommand
|
|
58
|
+
client.middlewareStack.add(
|
|
59
|
+
(next, context) => async (args) => {
|
|
60
|
+
if (context.commandName === 'DeleteObjectsCommand') {
|
|
61
|
+
const body = args.request.body;
|
|
62
|
+
if (body && typeof body === 'string') {
|
|
63
|
+
const contentMd5 = Buffer.from(await md5(body), 'hex').toString('base64');
|
|
64
|
+
args.request.headers['Content-MD5'] = contentMd5;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return next(args);
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
step: 'build',
|
|
71
|
+
name: 'addContentMd5ForDeleteObjects',
|
|
72
|
+
priority: 'high',
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return client;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async sendCommand(command) {
|
|
80
|
+
this.emit("command.request", command.constructor.name, command.input);
|
|
81
|
+
const [ok, err, response] = await tryFn(() => this.client.send(command));
|
|
82
|
+
if (!ok) {
|
|
83
|
+
const bucket = this.config.bucket;
|
|
84
|
+
const key = command.input && command.input.Key;
|
|
85
|
+
throw mapAwsError(err, {
|
|
86
|
+
bucket,
|
|
87
|
+
key,
|
|
88
|
+
commandName: command.constructor.name,
|
|
89
|
+
commandInput: command.input,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
this.emit("command.response", command.constructor.name, response, command.input);
|
|
93
|
+
return response;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async putObject({ key, metadata, contentType, body, contentEncoding, contentLength }) {
|
|
97
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
98
|
+
const fullKey = keyPrefix ? path.join(keyPrefix, key) : key;
|
|
99
|
+
|
|
100
|
+
// Ensure all metadata values are strings and keys are valid
|
|
101
|
+
const stringMetadata = {};
|
|
102
|
+
if (metadata) {
|
|
103
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
104
|
+
// Ensure key is a valid string and value is a string
|
|
105
|
+
const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
106
|
+
stringMetadata[validKey] = String(v);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const options = {
|
|
111
|
+
Bucket: this.config.bucket,
|
|
112
|
+
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
113
|
+
Metadata: stringMetadata,
|
|
114
|
+
Body: body || Buffer.alloc(0),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (contentType !== undefined) options.ContentType = contentType
|
|
118
|
+
if (contentEncoding !== undefined) options.ContentEncoding = contentEncoding
|
|
119
|
+
if (contentLength !== undefined) options.ContentLength = contentLength
|
|
120
|
+
|
|
121
|
+
let response, error;
|
|
122
|
+
try {
|
|
123
|
+
response = await this.sendCommand(new PutObjectCommand(options));
|
|
124
|
+
return response;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
error = err;
|
|
127
|
+
throw mapAwsError(err, {
|
|
128
|
+
bucket: this.config.bucket,
|
|
129
|
+
key,
|
|
130
|
+
commandName: 'PutObjectCommand',
|
|
131
|
+
commandInput: options,
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
this.emit('putObject', error || response, { key, metadata, contentType, body, contentEncoding, contentLength });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async getObject(key) {
|
|
139
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
140
|
+
const options = {
|
|
141
|
+
Bucket: this.config.bucket,
|
|
142
|
+
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
143
|
+
};
|
|
144
|
+
let response, error;
|
|
145
|
+
try {
|
|
146
|
+
response = await this.sendCommand(new GetObjectCommand(options));
|
|
147
|
+
return response;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
error = err;
|
|
150
|
+
throw mapAwsError(err, {
|
|
151
|
+
bucket: this.config.bucket,
|
|
152
|
+
key,
|
|
153
|
+
commandName: 'GetObjectCommand',
|
|
154
|
+
commandInput: options,
|
|
155
|
+
});
|
|
156
|
+
} finally {
|
|
157
|
+
this.emit('getObject', error || response, { key });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async headObject(key) {
|
|
162
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
163
|
+
const options = {
|
|
164
|
+
Bucket: this.config.bucket,
|
|
165
|
+
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
166
|
+
};
|
|
167
|
+
let response, error;
|
|
168
|
+
try {
|
|
169
|
+
response = await this.sendCommand(new HeadObjectCommand(options));
|
|
170
|
+
return response;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
error = err;
|
|
173
|
+
throw mapAwsError(err, {
|
|
174
|
+
bucket: this.config.bucket,
|
|
175
|
+
key,
|
|
176
|
+
commandName: 'HeadObjectCommand',
|
|
177
|
+
commandInput: options,
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
this.emit('headObject', error || response, { key });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async copyObject({ from, to }) {
|
|
185
|
+
const options = {
|
|
186
|
+
Bucket: this.config.bucket,
|
|
187
|
+
Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, to) : to,
|
|
188
|
+
CopySource: path.join(this.config.bucket, this.config.keyPrefix ? path.join(this.config.keyPrefix, from) : from),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
let response, error;
|
|
192
|
+
try {
|
|
193
|
+
response = await this.sendCommand(new CopyObjectCommand(options));
|
|
194
|
+
return response;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
error = err;
|
|
197
|
+
throw mapAwsError(err, {
|
|
198
|
+
bucket: this.config.bucket,
|
|
199
|
+
key: to,
|
|
200
|
+
commandName: 'CopyObjectCommand',
|
|
201
|
+
commandInput: options,
|
|
202
|
+
});
|
|
203
|
+
} finally {
|
|
204
|
+
this.emit('copyObject', error || response, { from, to });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async exists(key) {
|
|
209
|
+
const [ok, err] = await tryFn(() => this.headObject(key));
|
|
210
|
+
if (ok) return true;
|
|
211
|
+
if (err.name === "NoSuchKey" || err.name === "NotFound") return false;
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteObject(key) {
|
|
216
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
217
|
+
const fullKey = keyPrefix ? path.join(keyPrefix, key) : key;
|
|
218
|
+
const options = {
|
|
219
|
+
Bucket: this.config.bucket,
|
|
220
|
+
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
let response, error;
|
|
224
|
+
try {
|
|
225
|
+
response = await this.sendCommand(new DeleteObjectCommand(options));
|
|
226
|
+
return response;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
error = err;
|
|
229
|
+
throw mapAwsError(err, {
|
|
230
|
+
bucket: this.config.bucket,
|
|
231
|
+
key,
|
|
232
|
+
commandName: 'DeleteObjectCommand',
|
|
233
|
+
commandInput: options,
|
|
234
|
+
});
|
|
235
|
+
} finally {
|
|
236
|
+
this.emit('deleteObject', error || response, { key });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async deleteObjects(keys) {
|
|
241
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
242
|
+
const packages = chunk(keys, 1000);
|
|
243
|
+
|
|
244
|
+
const { results, errors } = await PromisePool.for(packages)
|
|
245
|
+
.withConcurrency(this.parallelism)
|
|
246
|
+
.process(async (keys) => {
|
|
247
|
+
// Log existence before deletion
|
|
248
|
+
for (const key of keys) {
|
|
249
|
+
const resolvedKey = keyPrefix ? path.join(keyPrefix, key) : key;
|
|
250
|
+
const bucket = this.config.bucket;
|
|
251
|
+
const existsBefore = await this.exists(key);
|
|
252
|
+
}
|
|
253
|
+
const options = {
|
|
254
|
+
Bucket: this.config.bucket,
|
|
255
|
+
Delete: {
|
|
256
|
+
Objects: keys.map((key) => ({
|
|
257
|
+
Key: keyPrefix ? path.join(keyPrefix, key) : key,
|
|
258
|
+
})),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Debug log
|
|
263
|
+
let response;
|
|
264
|
+
const [ok, err, res] = await tryFn(() => this.sendCommand(new DeleteObjectsCommand(options)));
|
|
265
|
+
if (!ok) throw err;
|
|
266
|
+
response = res;
|
|
267
|
+
if (response && response.Errors && response.Errors.length > 0) {
|
|
268
|
+
// console.error('[Client][ERROR] DeleteObjectsCommand errors:', response.Errors);
|
|
269
|
+
}
|
|
270
|
+
if (response && response.Deleted && response.Deleted.length !== keys.length) {
|
|
271
|
+
// console.error('[Client][ERROR] Nem todos os objetos foram deletados:', response.Deleted, 'esperado:', keys);
|
|
272
|
+
}
|
|
273
|
+
return response;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const report = {
|
|
277
|
+
deleted: results,
|
|
278
|
+
notFound: errors,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.emit("deleteObjects", report, keys);
|
|
282
|
+
return report;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Delete all objects under a specific prefix using efficient pagination
|
|
287
|
+
* @param {Object} options - Delete options
|
|
288
|
+
* @param {string} options.prefix - S3 prefix to delete
|
|
289
|
+
* @returns {Promise<number>} Number of objects deleted
|
|
290
|
+
*/
|
|
291
|
+
async deleteAll({ prefix } = {}) {
|
|
292
|
+
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
293
|
+
let continuationToken;
|
|
294
|
+
let totalDeleted = 0;
|
|
295
|
+
|
|
296
|
+
do {
|
|
297
|
+
const listCommand = new ListObjectsV2Command({
|
|
298
|
+
Bucket: this.config.bucket,
|
|
299
|
+
Prefix: keyPrefix ? path.join(keyPrefix, prefix || "") : prefix || "",
|
|
300
|
+
ContinuationToken: continuationToken,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const listResponse = await this.client.send(listCommand);
|
|
304
|
+
|
|
305
|
+
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
|
306
|
+
const deleteCommand = new DeleteObjectsCommand({
|
|
307
|
+
Bucket: this.config.bucket,
|
|
308
|
+
Delete: {
|
|
309
|
+
Objects: listResponse.Contents.map(obj => ({ Key: obj.Key }))
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const deleteResponse = await this.client.send(deleteCommand);
|
|
314
|
+
const deletedCount = deleteResponse.Deleted ? deleteResponse.Deleted.length : 0;
|
|
315
|
+
totalDeleted += deletedCount;
|
|
316
|
+
|
|
317
|
+
this.emit("deleteAll", {
|
|
318
|
+
prefix,
|
|
319
|
+
batch: deletedCount,
|
|
320
|
+
total: totalDeleted
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
|
|
325
|
+
} while (continuationToken);
|
|
326
|
+
|
|
327
|
+
this.emit("deleteAllComplete", {
|
|
328
|
+
prefix,
|
|
329
|
+
totalDeleted
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return totalDeleted;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async moveObject({ from, to }) {
|
|
336
|
+
const [ok, err] = await tryFn(async () => {
|
|
337
|
+
await this.copyObject({ from, to });
|
|
338
|
+
await this.deleteObject(from);
|
|
339
|
+
});
|
|
340
|
+
if (!ok) {
|
|
341
|
+
throw new UnknownError("Unknown error in moveObject", { bucket: this.config.bucket, from, to, original: err });
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async listObjects({
|
|
347
|
+
prefix,
|
|
348
|
+
maxKeys = 1000,
|
|
349
|
+
continuationToken,
|
|
350
|
+
} = {}) {
|
|
351
|
+
const options = {
|
|
352
|
+
Bucket: this.config.bucket,
|
|
353
|
+
MaxKeys: maxKeys,
|
|
354
|
+
ContinuationToken: continuationToken,
|
|
355
|
+
Prefix: this.config.keyPrefix
|
|
356
|
+
? path.join(this.config.keyPrefix, prefix || "")
|
|
357
|
+
: prefix || "",
|
|
358
|
+
};
|
|
359
|
+
const [ok, err, response] = await tryFn(() => this.sendCommand(new ListObjectsV2Command(options)));
|
|
360
|
+
if (!ok) {
|
|
361
|
+
throw new UnknownError("Unknown error in listObjects", { prefix, bucket: this.config.bucket, original: err });
|
|
362
|
+
}
|
|
363
|
+
this.emit("listObjects", response, options);
|
|
364
|
+
return response;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async count({ prefix } = {}) {
|
|
368
|
+
let count = 0;
|
|
369
|
+
let truncated = true;
|
|
370
|
+
let continuationToken;
|
|
371
|
+
while (truncated) {
|
|
372
|
+
const options = {
|
|
373
|
+
prefix,
|
|
374
|
+
continuationToken,
|
|
375
|
+
};
|
|
376
|
+
const response = await this.listObjects(options);
|
|
377
|
+
count += response.KeyCount || 0;
|
|
378
|
+
truncated = response.IsTruncated || false;
|
|
379
|
+
continuationToken = response.NextContinuationToken;
|
|
380
|
+
}
|
|
381
|
+
this.emit("count", count, { prefix });
|
|
382
|
+
return count;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async getAllKeys({ prefix } = {}) {
|
|
386
|
+
let keys = [];
|
|
387
|
+
let truncated = true;
|
|
388
|
+
let continuationToken;
|
|
389
|
+
while (truncated) {
|
|
390
|
+
const options = {
|
|
391
|
+
prefix,
|
|
392
|
+
continuationToken,
|
|
393
|
+
};
|
|
394
|
+
const response = await this.listObjects(options);
|
|
395
|
+
if (response.Contents) {
|
|
396
|
+
keys = keys.concat(response.Contents.map((x) => x.Key));
|
|
397
|
+
}
|
|
398
|
+
truncated = response.IsTruncated || false;
|
|
399
|
+
continuationToken = response.NextContinuationToken;
|
|
400
|
+
}
|
|
401
|
+
if (this.config.keyPrefix) {
|
|
402
|
+
keys = keys
|
|
403
|
+
.map((x) => x.replace(this.config.keyPrefix, ""))
|
|
404
|
+
.map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x));
|
|
405
|
+
}
|
|
406
|
+
this.emit("getAllKeys", keys, { prefix });
|
|
407
|
+
return keys;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async getContinuationTokenAfterOffset(params = {}) {
|
|
411
|
+
const {
|
|
412
|
+
prefix,
|
|
413
|
+
offset = 1000,
|
|
414
|
+
} = params
|
|
415
|
+
if (offset === 0) return null;
|
|
416
|
+
let truncated = true;
|
|
417
|
+
let continuationToken;
|
|
418
|
+
let skipped = 0;
|
|
419
|
+
while (truncated) {
|
|
420
|
+
let maxKeys =
|
|
421
|
+
offset < 1000
|
|
422
|
+
? offset
|
|
423
|
+
: offset - skipped > 1000
|
|
424
|
+
? 1000
|
|
425
|
+
: offset - skipped;
|
|
426
|
+
const options = {
|
|
427
|
+
prefix,
|
|
428
|
+
maxKeys,
|
|
429
|
+
continuationToken,
|
|
430
|
+
};
|
|
431
|
+
const res = await this.listObjects(options);
|
|
432
|
+
if (res.Contents) {
|
|
433
|
+
skipped += res.Contents.length;
|
|
434
|
+
}
|
|
435
|
+
truncated = res.IsTruncated || false;
|
|
436
|
+
continuationToken = res.NextContinuationToken;
|
|
437
|
+
if (skipped >= offset) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
this.emit("getContinuationTokenAfterOffset", continuationToken || null, params);
|
|
442
|
+
return continuationToken || null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async getKeysPage(params = {}) {
|
|
446
|
+
const {
|
|
447
|
+
prefix,
|
|
448
|
+
offset = 0,
|
|
449
|
+
amount = 100,
|
|
450
|
+
} = params
|
|
451
|
+
let keys = [];
|
|
452
|
+
let truncated = true;
|
|
453
|
+
let continuationToken;
|
|
454
|
+
if (offset > 0) {
|
|
455
|
+
continuationToken = await this.getContinuationTokenAfterOffset({
|
|
456
|
+
prefix,
|
|
457
|
+
offset,
|
|
458
|
+
});
|
|
459
|
+
if (!continuationToken) {
|
|
460
|
+
this.emit("getKeysPage", [], params);
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
while (truncated) {
|
|
465
|
+
const options = {
|
|
466
|
+
prefix,
|
|
467
|
+
continuationToken,
|
|
468
|
+
};
|
|
469
|
+
const res = await this.listObjects(options);
|
|
470
|
+
if (res.Contents) {
|
|
471
|
+
keys = keys.concat(res.Contents.map((x) => x.Key));
|
|
472
|
+
}
|
|
473
|
+
truncated = res.IsTruncated || false;
|
|
474
|
+
continuationToken = res.NextContinuationToken;
|
|
475
|
+
if (keys.length >= amount) {
|
|
476
|
+
keys = keys.slice(0, amount);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (this.config.keyPrefix) {
|
|
481
|
+
keys = keys
|
|
482
|
+
.map((x) => x.replace(this.config.keyPrefix, ""))
|
|
483
|
+
.map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x));
|
|
484
|
+
}
|
|
485
|
+
this.emit("getKeysPage", keys, params);
|
|
486
|
+
return keys;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async moveAllObjects({ prefixFrom, prefixTo }) {
|
|
490
|
+
const keys = await this.getAllKeys({ prefix: prefixFrom });
|
|
491
|
+
const { results, errors } = await PromisePool
|
|
492
|
+
.for(keys)
|
|
493
|
+
.withConcurrency(this.parallelism)
|
|
494
|
+
.process(async (key) => {
|
|
495
|
+
const to = key.replace(prefixFrom, prefixTo)
|
|
496
|
+
const [ok, err] = await tryFn(async () => {
|
|
497
|
+
await this.moveObject({
|
|
498
|
+
from: key,
|
|
499
|
+
to,
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
if (!ok) {
|
|
503
|
+
throw new UnknownError("Unknown error in moveAllObjects", { bucket: this.config.bucket, from: key, to, original: err });
|
|
504
|
+
}
|
|
505
|
+
return to;
|
|
506
|
+
});
|
|
507
|
+
this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
508
|
+
if (errors.length > 0) {
|
|
509
|
+
throw new Error("Some objects could not be moved");
|
|
510
|
+
}
|
|
511
|
+
return results;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export default Client;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
2
|
+
const base = alphabet.length;
|
|
3
|
+
const charToValue = Object.fromEntries([...alphabet].map((c, i) => [c, i]));
|
|
4
|
+
|
|
5
|
+
export const encode = n => {
|
|
6
|
+
if (typeof n !== 'number' || isNaN(n)) return 'undefined';
|
|
7
|
+
if (!isFinite(n)) return 'undefined';
|
|
8
|
+
if (n === 0) return alphabet[0];
|
|
9
|
+
if (n < 0) return '-' + encode(-Math.floor(n));
|
|
10
|
+
n = Math.floor(n);
|
|
11
|
+
let s = '';
|
|
12
|
+
while (n) {
|
|
13
|
+
s = alphabet[n % base] + s;
|
|
14
|
+
n = Math.floor(n / base);
|
|
15
|
+
}
|
|
16
|
+
return s;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const decode = s => {
|
|
20
|
+
if (typeof s !== 'string') return NaN;
|
|
21
|
+
if (s === '') return 0;
|
|
22
|
+
let negative = false;
|
|
23
|
+
if (s[0] === '-') {
|
|
24
|
+
negative = true;
|
|
25
|
+
s = s.slice(1);
|
|
26
|
+
}
|
|
27
|
+
let r = 0;
|
|
28
|
+
for (let i = 0; i < s.length; i++) {
|
|
29
|
+
const idx = charToValue[s[i]];
|
|
30
|
+
if (idx === undefined) return NaN;
|
|
31
|
+
r = r * base + idx;
|
|
32
|
+
}
|
|
33
|
+
return negative ? -r : r;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const encodeDecimal = n => {
|
|
37
|
+
if (typeof n !== 'number' || isNaN(n)) return 'undefined';
|
|
38
|
+
if (!isFinite(n)) return 'undefined';
|
|
39
|
+
const negative = n < 0;
|
|
40
|
+
n = Math.abs(n);
|
|
41
|
+
const [intPart, decPart] = n.toString().split('.');
|
|
42
|
+
const encodedInt = encode(Number(intPart));
|
|
43
|
+
if (decPart) {
|
|
44
|
+
return (negative ? '-' : '') + encodedInt + '.' + decPart;
|
|
45
|
+
}
|
|
46
|
+
return (negative ? '-' : '') + encodedInt;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const decodeDecimal = s => {
|
|
50
|
+
if (typeof s !== 'string') return NaN;
|
|
51
|
+
let negative = false;
|
|
52
|
+
if (s[0] === '-') {
|
|
53
|
+
negative = true;
|
|
54
|
+
s = s.slice(1);
|
|
55
|
+
}
|
|
56
|
+
const [intPart, decPart] = s.split('.');
|
|
57
|
+
const decodedInt = decode(intPart);
|
|
58
|
+
if (isNaN(decodedInt)) return NaN;
|
|
59
|
+
const num = decPart ? Number(decodedInt + '.' + decPart) : decodedInt;
|
|
60
|
+
return negative ? -num : num;
|
|
61
|
+
};
|