@chidchanun/bcp 0.1.26 → 0.1.28
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 +151 -523
- package/docs/README.md +69 -82
- package/docs/production-hardening.md +152 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/releases/0.1.28.md +134 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +42 -2
- package/packages/server/src/hardening-proxy.ts +393 -0
- package/packages/server/src/production-hardening.ts +374 -0
- package/packages/server/src/standalone-production-runtime-v7.ts +299 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
} from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
rm,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createLocalStorage as createBaseLocalStorage,
|
|
15
|
+
getStorageCapabilities,
|
|
16
|
+
normalizeStorageKey,
|
|
17
|
+
putStorageStream,
|
|
18
|
+
readStorageStream,
|
|
19
|
+
StorageError,
|
|
20
|
+
type LocalStorageOptions,
|
|
21
|
+
type StorageAdapter,
|
|
22
|
+
type StorageAdapterCapabilities,
|
|
23
|
+
type StorageObjectMetadata,
|
|
24
|
+
type StoragePutOptions,
|
|
25
|
+
type StoragePutStreamOptions,
|
|
26
|
+
type StorageWriteValue,
|
|
27
|
+
} from "./storage.js";
|
|
28
|
+
|
|
29
|
+
const LOCAL_METADATA_DIRECTORY =
|
|
30
|
+
".bcp-storage-meta";
|
|
31
|
+
const USER_METADATA_PREFIX =
|
|
32
|
+
"user-";
|
|
33
|
+
const DEFAULT_LIST_LIMIT =
|
|
34
|
+
100;
|
|
35
|
+
const MAX_LIST_LIMIT =
|
|
36
|
+
1000;
|
|
37
|
+
|
|
38
|
+
export type StorageUserMetadata =
|
|
39
|
+
Record<string, string>;
|
|
40
|
+
|
|
41
|
+
export interface StorageEcosystemPutOptions
|
|
42
|
+
extends StoragePutOptions {
|
|
43
|
+
metadata?: StorageUserMetadata;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StorageEcosystemPutStreamOptions
|
|
47
|
+
extends StoragePutStreamOptions {
|
|
48
|
+
metadata?: StorageUserMetadata;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StorageListOptions {
|
|
52
|
+
prefix?: string;
|
|
53
|
+
limit?: number;
|
|
54
|
+
cursor?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StorageListResult {
|
|
58
|
+
objects: StorageObjectMetadata[];
|
|
59
|
+
cursor?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface StorageCopyOptions {
|
|
63
|
+
overwrite?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StorageSignedUrlOptions {
|
|
67
|
+
expiresIn?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StorageSignedWriteUrlOptions
|
|
71
|
+
extends StorageSignedUrlOptions {
|
|
72
|
+
contentType?: string;
|
|
73
|
+
metadata?: StorageUserMetadata;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StorageDeleteManyFailure {
|
|
77
|
+
key: string;
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface StorageDeleteManyResult {
|
|
82
|
+
deleted: string[];
|
|
83
|
+
missing: string[];
|
|
84
|
+
failed: StorageDeleteManyFailure[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface StorageEcosystemCapabilities
|
|
88
|
+
extends StorageAdapterCapabilities {
|
|
89
|
+
signedReadUrls: boolean;
|
|
90
|
+
signedWriteUrls: boolean;
|
|
91
|
+
copy: boolean;
|
|
92
|
+
move: boolean;
|
|
93
|
+
metadata: boolean;
|
|
94
|
+
bulkDelete: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface StorageEcosystemAdapter
|
|
98
|
+
extends StorageAdapter {
|
|
99
|
+
put(
|
|
100
|
+
key: string,
|
|
101
|
+
value: StorageWriteValue,
|
|
102
|
+
options?: StorageEcosystemPutOptions
|
|
103
|
+
): Promise<StorageObjectMetadata>;
|
|
104
|
+
|
|
105
|
+
putStream?(
|
|
106
|
+
key: string,
|
|
107
|
+
stream: ReadableStream<Uint8Array>,
|
|
108
|
+
options?: StorageEcosystemPutStreamOptions
|
|
109
|
+
): Promise<StorageObjectMetadata>;
|
|
110
|
+
|
|
111
|
+
list?(
|
|
112
|
+
options?: StorageListOptions
|
|
113
|
+
): Promise<StorageListResult>;
|
|
114
|
+
|
|
115
|
+
copy?(
|
|
116
|
+
sourceKey: string,
|
|
117
|
+
destinationKey: string,
|
|
118
|
+
options?: StorageCopyOptions
|
|
119
|
+
): Promise<StorageObjectMetadata>;
|
|
120
|
+
|
|
121
|
+
move?(
|
|
122
|
+
sourceKey: string,
|
|
123
|
+
destinationKey: string,
|
|
124
|
+
options?: StorageCopyOptions
|
|
125
|
+
): Promise<StorageObjectMetadata>;
|
|
126
|
+
|
|
127
|
+
deleteMany?(
|
|
128
|
+
keys: readonly string[]
|
|
129
|
+
): Promise<StorageDeleteManyResult>;
|
|
130
|
+
|
|
131
|
+
getMetadata?(
|
|
132
|
+
key: string
|
|
133
|
+
): Promise<StorageUserMetadata>;
|
|
134
|
+
|
|
135
|
+
setMetadata?(
|
|
136
|
+
key: string,
|
|
137
|
+
metadata: StorageUserMetadata
|
|
138
|
+
): Promise<StorageUserMetadata>;
|
|
139
|
+
|
|
140
|
+
createSignedReadUrl?(
|
|
141
|
+
key: string,
|
|
142
|
+
options?: StorageSignedUrlOptions
|
|
143
|
+
): Promise<string>;
|
|
144
|
+
|
|
145
|
+
createSignedWriteUrl?(
|
|
146
|
+
key: string,
|
|
147
|
+
options?: StorageSignedWriteUrlOptions
|
|
148
|
+
): Promise<string>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type StorageEcosystemErrorCode =
|
|
152
|
+
"UNSUPPORTED_OPERATION" |
|
|
153
|
+
"INVALID_CURSOR" |
|
|
154
|
+
"INVALID_METADATA";
|
|
155
|
+
|
|
156
|
+
export class StorageEcosystemError
|
|
157
|
+
extends Error {
|
|
158
|
+
readonly code:
|
|
159
|
+
StorageEcosystemErrorCode;
|
|
160
|
+
readonly status:
|
|
161
|
+
number;
|
|
162
|
+
|
|
163
|
+
constructor(
|
|
164
|
+
code: StorageEcosystemErrorCode,
|
|
165
|
+
message: string,
|
|
166
|
+
status = 400
|
|
167
|
+
) {
|
|
168
|
+
super(message);
|
|
169
|
+
this.name =
|
|
170
|
+
"StorageEcosystemError";
|
|
171
|
+
this.code =
|
|
172
|
+
code;
|
|
173
|
+
this.status =
|
|
174
|
+
status;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface LocalUserMetadataRecord {
|
|
179
|
+
version: 1;
|
|
180
|
+
key: string;
|
|
181
|
+
metadata: StorageUserMetadata;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function createLocalStorage(
|
|
185
|
+
options: LocalStorageOptions
|
|
186
|
+
): StorageEcosystemAdapter {
|
|
187
|
+
const rootDirectory =
|
|
188
|
+
path.resolve(
|
|
189
|
+
assertNonEmptyString(
|
|
190
|
+
options.directory,
|
|
191
|
+
"directory"
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
const metadataDirectory =
|
|
195
|
+
path.join(
|
|
196
|
+
rootDirectory,
|
|
197
|
+
LOCAL_METADATA_DIRECTORY
|
|
198
|
+
);
|
|
199
|
+
const base =
|
|
200
|
+
createBaseLocalStorage(
|
|
201
|
+
options
|
|
202
|
+
);
|
|
203
|
+
const basePut =
|
|
204
|
+
base.put.bind(
|
|
205
|
+
base
|
|
206
|
+
);
|
|
207
|
+
const basePutStream =
|
|
208
|
+
base.putStream?.bind(
|
|
209
|
+
base
|
|
210
|
+
);
|
|
211
|
+
const baseDelete =
|
|
212
|
+
base.delete.bind(
|
|
213
|
+
base
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const adapter =
|
|
217
|
+
base as StorageEcosystemAdapter;
|
|
218
|
+
|
|
219
|
+
adapter.capabilities = {
|
|
220
|
+
...base.capabilities,
|
|
221
|
+
listing: true,
|
|
222
|
+
signedUrls: false,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
adapter.put =
|
|
226
|
+
async (
|
|
227
|
+
key,
|
|
228
|
+
value,
|
|
229
|
+
putOptions = {}
|
|
230
|
+
) => {
|
|
231
|
+
const normalizedKey =
|
|
232
|
+
normalizeStorageKey(
|
|
233
|
+
key
|
|
234
|
+
);
|
|
235
|
+
const stored =
|
|
236
|
+
await basePut(
|
|
237
|
+
normalizedKey,
|
|
238
|
+
value,
|
|
239
|
+
putOptions
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
await replaceLocalUserMetadata(
|
|
243
|
+
metadataDirectory,
|
|
244
|
+
normalizedKey,
|
|
245
|
+
putOptions.metadata
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return stored;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (basePutStream) {
|
|
252
|
+
adapter.putStream =
|
|
253
|
+
async (
|
|
254
|
+
key,
|
|
255
|
+
stream,
|
|
256
|
+
putOptions = {}
|
|
257
|
+
) => {
|
|
258
|
+
const normalizedKey =
|
|
259
|
+
normalizeStorageKey(
|
|
260
|
+
key
|
|
261
|
+
);
|
|
262
|
+
const stored =
|
|
263
|
+
await basePutStream(
|
|
264
|
+
normalizedKey,
|
|
265
|
+
stream,
|
|
266
|
+
putOptions
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
await replaceLocalUserMetadata(
|
|
270
|
+
metadataDirectory,
|
|
271
|
+
normalizedKey,
|
|
272
|
+
putOptions.metadata
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return stored;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
adapter.getMetadata =
|
|
280
|
+
async (key) => {
|
|
281
|
+
const normalizedKey =
|
|
282
|
+
normalizeStorageKey(
|
|
283
|
+
key
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
await requireStorageObject(
|
|
287
|
+
adapter,
|
|
288
|
+
normalizedKey
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return readLocalUserMetadata(
|
|
292
|
+
metadataDirectory,
|
|
293
|
+
normalizedKey
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
adapter.setMetadata =
|
|
298
|
+
async (
|
|
299
|
+
key,
|
|
300
|
+
metadata
|
|
301
|
+
) => {
|
|
302
|
+
const normalizedKey =
|
|
303
|
+
normalizeStorageKey(
|
|
304
|
+
key
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await requireStorageObject(
|
|
308
|
+
adapter,
|
|
309
|
+
normalizedKey
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const normalizedMetadata =
|
|
313
|
+
normalizeStorageUserMetadata(
|
|
314
|
+
metadata
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
await writeLocalUserMetadata(
|
|
318
|
+
metadataDirectory,
|
|
319
|
+
normalizedKey,
|
|
320
|
+
normalizedMetadata
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
...normalizedMetadata,
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
adapter.list =
|
|
329
|
+
async (
|
|
330
|
+
listOptions = {}
|
|
331
|
+
) => {
|
|
332
|
+
const prefix =
|
|
333
|
+
normalizeStorageListPrefix(
|
|
334
|
+
listOptions.prefix
|
|
335
|
+
);
|
|
336
|
+
const limit =
|
|
337
|
+
normalizeStorageListLimit(
|
|
338
|
+
listOptions.limit
|
|
339
|
+
);
|
|
340
|
+
const afterKey =
|
|
341
|
+
decodeLocalCursor(
|
|
342
|
+
listOptions.cursor
|
|
343
|
+
);
|
|
344
|
+
const keys =
|
|
345
|
+
await collectLocalObjectKeys(
|
|
346
|
+
rootDirectory
|
|
347
|
+
);
|
|
348
|
+
const matching =
|
|
349
|
+
keys
|
|
350
|
+
.filter(
|
|
351
|
+
(key) =>
|
|
352
|
+
prefix.length === 0 ||
|
|
353
|
+
key.startsWith(
|
|
354
|
+
prefix
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
.filter(
|
|
358
|
+
(key) =>
|
|
359
|
+
afterKey === null ||
|
|
360
|
+
key > afterKey
|
|
361
|
+
)
|
|
362
|
+
.sort();
|
|
363
|
+
const selected =
|
|
364
|
+
matching.slice(
|
|
365
|
+
0,
|
|
366
|
+
limit
|
|
367
|
+
);
|
|
368
|
+
const objects =
|
|
369
|
+
(
|
|
370
|
+
await Promise.all(
|
|
371
|
+
selected.map(
|
|
372
|
+
(key) =>
|
|
373
|
+
adapter.stat(
|
|
374
|
+
key
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
).filter(
|
|
379
|
+
(
|
|
380
|
+
value
|
|
381
|
+
): value is StorageObjectMetadata =>
|
|
382
|
+
value !== null
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
objects,
|
|
387
|
+
...(matching.length > selected.length &&
|
|
388
|
+
selected.length > 0
|
|
389
|
+
? {
|
|
390
|
+
cursor:
|
|
391
|
+
encodeLocalCursor(
|
|
392
|
+
selected[
|
|
393
|
+
selected.length - 1
|
|
394
|
+
]!
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
adapter.copy =
|
|
402
|
+
async (
|
|
403
|
+
sourceKey,
|
|
404
|
+
destinationKey,
|
|
405
|
+
copyOptions = {}
|
|
406
|
+
) =>
|
|
407
|
+
copyStorageObjectFallback(
|
|
408
|
+
adapter,
|
|
409
|
+
sourceKey,
|
|
410
|
+
destinationKey,
|
|
411
|
+
copyOptions
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
adapter.move =
|
|
415
|
+
async (
|
|
416
|
+
sourceKey,
|
|
417
|
+
destinationKey,
|
|
418
|
+
copyOptions = {}
|
|
419
|
+
) => {
|
|
420
|
+
const source =
|
|
421
|
+
normalizeStorageKey(
|
|
422
|
+
sourceKey
|
|
423
|
+
);
|
|
424
|
+
const destination =
|
|
425
|
+
normalizeStorageKey(
|
|
426
|
+
destinationKey
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (source === destination) {
|
|
430
|
+
return requireStorageObject(
|
|
431
|
+
adapter,
|
|
432
|
+
source
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const stored =
|
|
437
|
+
await copyStorageObjectFallback(
|
|
438
|
+
adapter,
|
|
439
|
+
source,
|
|
440
|
+
destination,
|
|
441
|
+
copyOptions
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
await adapter.delete(
|
|
445
|
+
source
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return stored;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
adapter.delete =
|
|
452
|
+
async (key) => {
|
|
453
|
+
const normalizedKey =
|
|
454
|
+
normalizeStorageKey(
|
|
455
|
+
key
|
|
456
|
+
);
|
|
457
|
+
const deleted =
|
|
458
|
+
await baseDelete(
|
|
459
|
+
normalizedKey
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (deleted) {
|
|
463
|
+
await rm(
|
|
464
|
+
localUserMetadataFile(
|
|
465
|
+
metadataDirectory,
|
|
466
|
+
normalizedKey
|
|
467
|
+
),
|
|
468
|
+
{
|
|
469
|
+
force: true,
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return deleted;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
adapter.deleteMany =
|
|
478
|
+
async (keys) =>
|
|
479
|
+
deleteStorageObjectsFallback(
|
|
480
|
+
adapter,
|
|
481
|
+
keys
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return adapter;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function getStorageEcosystemCapabilities(
|
|
488
|
+
storage: StorageAdapter
|
|
489
|
+
): StorageEcosystemCapabilities {
|
|
490
|
+
const base =
|
|
491
|
+
getStorageCapabilities(
|
|
492
|
+
storage
|
|
493
|
+
);
|
|
494
|
+
const ecosystem =
|
|
495
|
+
storage as StorageEcosystemAdapter;
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
...base,
|
|
499
|
+
signedUrls:
|
|
500
|
+
base.signedUrls ||
|
|
501
|
+
typeof ecosystem.createSignedReadUrl ===
|
|
502
|
+
"function" ||
|
|
503
|
+
typeof ecosystem.createSignedWriteUrl ===
|
|
504
|
+
"function",
|
|
505
|
+
listing:
|
|
506
|
+
base.listing ||
|
|
507
|
+
typeof ecosystem.list ===
|
|
508
|
+
"function",
|
|
509
|
+
signedReadUrls:
|
|
510
|
+
typeof ecosystem.createSignedReadUrl ===
|
|
511
|
+
"function",
|
|
512
|
+
signedWriteUrls:
|
|
513
|
+
typeof ecosystem.createSignedWriteUrl ===
|
|
514
|
+
"function",
|
|
515
|
+
copy:
|
|
516
|
+
typeof ecosystem.copy ===
|
|
517
|
+
"function",
|
|
518
|
+
move:
|
|
519
|
+
typeof ecosystem.move ===
|
|
520
|
+
"function",
|
|
521
|
+
metadata:
|
|
522
|
+
typeof ecosystem.getMetadata ===
|
|
523
|
+
"function" &&
|
|
524
|
+
typeof ecosystem.setMetadata ===
|
|
525
|
+
"function",
|
|
526
|
+
bulkDelete:
|
|
527
|
+
typeof ecosystem.deleteMany ===
|
|
528
|
+
"function",
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export async function listStorageObjects(
|
|
533
|
+
storage: StorageAdapter,
|
|
534
|
+
options: StorageListOptions = {}
|
|
535
|
+
): Promise<StorageListResult> {
|
|
536
|
+
const ecosystem =
|
|
537
|
+
storage as StorageEcosystemAdapter;
|
|
538
|
+
|
|
539
|
+
if (!ecosystem.list) {
|
|
540
|
+
throw unsupportedOperation(
|
|
541
|
+
"listing"
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return ecosystem.list(
|
|
546
|
+
options
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function copyStorageObject(
|
|
551
|
+
storage: StorageAdapter,
|
|
552
|
+
sourceKey: string,
|
|
553
|
+
destinationKey: string,
|
|
554
|
+
options: StorageCopyOptions = {}
|
|
555
|
+
): Promise<StorageObjectMetadata> {
|
|
556
|
+
const ecosystem =
|
|
557
|
+
storage as StorageEcosystemAdapter;
|
|
558
|
+
|
|
559
|
+
if (ecosystem.copy) {
|
|
560
|
+
return ecosystem.copy(
|
|
561
|
+
sourceKey,
|
|
562
|
+
destinationKey,
|
|
563
|
+
options
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return copyStorageObjectFallback(
|
|
568
|
+
ecosystem,
|
|
569
|
+
sourceKey,
|
|
570
|
+
destinationKey,
|
|
571
|
+
options
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function moveStorageObject(
|
|
576
|
+
storage: StorageAdapter,
|
|
577
|
+
sourceKey: string,
|
|
578
|
+
destinationKey: string,
|
|
579
|
+
options: StorageCopyOptions = {}
|
|
580
|
+
): Promise<StorageObjectMetadata> {
|
|
581
|
+
const ecosystem =
|
|
582
|
+
storage as StorageEcosystemAdapter;
|
|
583
|
+
|
|
584
|
+
if (ecosystem.move) {
|
|
585
|
+
return ecosystem.move(
|
|
586
|
+
sourceKey,
|
|
587
|
+
destinationKey,
|
|
588
|
+
options
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const source =
|
|
593
|
+
normalizeStorageKey(
|
|
594
|
+
sourceKey
|
|
595
|
+
);
|
|
596
|
+
const destination =
|
|
597
|
+
normalizeStorageKey(
|
|
598
|
+
destinationKey
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
if (source === destination) {
|
|
602
|
+
return requireStorageObject(
|
|
603
|
+
storage,
|
|
604
|
+
source
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const stored =
|
|
609
|
+
await copyStorageObjectFallback(
|
|
610
|
+
ecosystem,
|
|
611
|
+
source,
|
|
612
|
+
destination,
|
|
613
|
+
options
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
await storage.delete(
|
|
617
|
+
source
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
return stored;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function deleteStorageObjects(
|
|
624
|
+
storage: StorageAdapter,
|
|
625
|
+
keys: readonly string[]
|
|
626
|
+
): Promise<StorageDeleteManyResult> {
|
|
627
|
+
const ecosystem =
|
|
628
|
+
storage as StorageEcosystemAdapter;
|
|
629
|
+
|
|
630
|
+
if (ecosystem.deleteMany) {
|
|
631
|
+
return ecosystem.deleteMany(
|
|
632
|
+
keys
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return deleteStorageObjectsFallback(
|
|
637
|
+
ecosystem,
|
|
638
|
+
keys
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function getStorageMetadata(
|
|
643
|
+
storage: StorageAdapter,
|
|
644
|
+
key: string
|
|
645
|
+
): Promise<StorageUserMetadata> {
|
|
646
|
+
const ecosystem =
|
|
647
|
+
storage as StorageEcosystemAdapter;
|
|
648
|
+
|
|
649
|
+
if (!ecosystem.getMetadata) {
|
|
650
|
+
throw unsupportedOperation(
|
|
651
|
+
"metadata reads"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return ecosystem.getMetadata(
|
|
656
|
+
key
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export async function setStorageMetadata(
|
|
661
|
+
storage: StorageAdapter,
|
|
662
|
+
key: string,
|
|
663
|
+
metadata: StorageUserMetadata
|
|
664
|
+
): Promise<StorageUserMetadata> {
|
|
665
|
+
const ecosystem =
|
|
666
|
+
storage as StorageEcosystemAdapter;
|
|
667
|
+
|
|
668
|
+
if (!ecosystem.setMetadata) {
|
|
669
|
+
throw unsupportedOperation(
|
|
670
|
+
"metadata writes"
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return ecosystem.setMetadata(
|
|
675
|
+
key,
|
|
676
|
+
metadata
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export async function createStorageSignedReadUrl(
|
|
681
|
+
storage: StorageAdapter,
|
|
682
|
+
key: string,
|
|
683
|
+
options: StorageSignedUrlOptions = {}
|
|
684
|
+
): Promise<string> {
|
|
685
|
+
const ecosystem =
|
|
686
|
+
storage as StorageEcosystemAdapter;
|
|
687
|
+
|
|
688
|
+
if (!ecosystem.createSignedReadUrl) {
|
|
689
|
+
throw unsupportedOperation(
|
|
690
|
+
"signed read URLs"
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return ecosystem.createSignedReadUrl(
|
|
695
|
+
key,
|
|
696
|
+
options
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export async function createStorageSignedWriteUrl(
|
|
701
|
+
storage: StorageAdapter,
|
|
702
|
+
key: string,
|
|
703
|
+
options: StorageSignedWriteUrlOptions = {}
|
|
704
|
+
): Promise<string> {
|
|
705
|
+
const ecosystem =
|
|
706
|
+
storage as StorageEcosystemAdapter;
|
|
707
|
+
|
|
708
|
+
if (!ecosystem.createSignedWriteUrl) {
|
|
709
|
+
throw unsupportedOperation(
|
|
710
|
+
"signed write URLs"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return ecosystem.createSignedWriteUrl(
|
|
715
|
+
key,
|
|
716
|
+
options
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function normalizeStorageUserMetadata(
|
|
721
|
+
metadata:
|
|
722
|
+
StorageUserMetadata |
|
|
723
|
+
undefined
|
|
724
|
+
): StorageUserMetadata {
|
|
725
|
+
if (metadata === undefined) {
|
|
726
|
+
return {};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (
|
|
730
|
+
metadata === null ||
|
|
731
|
+
typeof metadata !==
|
|
732
|
+
"object" ||
|
|
733
|
+
Array.isArray(
|
|
734
|
+
metadata
|
|
735
|
+
)
|
|
736
|
+
) {
|
|
737
|
+
throw new StorageEcosystemError(
|
|
738
|
+
"INVALID_METADATA",
|
|
739
|
+
"BCP Framework: storage metadata must be a string-to-string object."
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const normalized:
|
|
744
|
+
StorageUserMetadata = {};
|
|
745
|
+
|
|
746
|
+
for (
|
|
747
|
+
const [rawKey, rawValue]
|
|
748
|
+
of Object.entries(
|
|
749
|
+
metadata
|
|
750
|
+
)
|
|
751
|
+
) {
|
|
752
|
+
if (
|
|
753
|
+
typeof rawValue !==
|
|
754
|
+
"string"
|
|
755
|
+
) {
|
|
756
|
+
throw new StorageEcosystemError(
|
|
757
|
+
"INVALID_METADATA",
|
|
758
|
+
`BCP Framework: storage metadata value for "${rawKey}" must be a string.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const key =
|
|
763
|
+
rawKey
|
|
764
|
+
.trim()
|
|
765
|
+
.toLowerCase();
|
|
766
|
+
const value =
|
|
767
|
+
rawValue.trim();
|
|
768
|
+
|
|
769
|
+
if (
|
|
770
|
+
key.length === 0 ||
|
|
771
|
+
!/^[a-z0-9][a-z0-9_.-]{0,127}$/.test(
|
|
772
|
+
key
|
|
773
|
+
) ||
|
|
774
|
+
key === "bcp_sha256" ||
|
|
775
|
+
/[\r\n\u0000]/.test(
|
|
776
|
+
value
|
|
777
|
+
)
|
|
778
|
+
) {
|
|
779
|
+
throw new StorageEcosystemError(
|
|
780
|
+
"INVALID_METADATA",
|
|
781
|
+
`BCP Framework: invalid storage metadata entry "${rawKey}".`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
normalized[
|
|
786
|
+
key
|
|
787
|
+
] = value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return normalized;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function normalizeStorageListPrefix(
|
|
794
|
+
value: string | undefined
|
|
795
|
+
): string {
|
|
796
|
+
if (value === undefined) {
|
|
797
|
+
return "";
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const normalized =
|
|
801
|
+
value
|
|
802
|
+
.trim()
|
|
803
|
+
.replace(
|
|
804
|
+
/\\/g,
|
|
805
|
+
"/"
|
|
806
|
+
)
|
|
807
|
+
.replace(
|
|
808
|
+
/^\/+/,
|
|
809
|
+
""
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
if (normalized.length === 0) {
|
|
813
|
+
return "";
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const hasTrailingSlash =
|
|
817
|
+
normalized.endsWith(
|
|
818
|
+
"/"
|
|
819
|
+
);
|
|
820
|
+
const withoutTrailingSlash =
|
|
821
|
+
normalized.replace(
|
|
822
|
+
/\/+$/,
|
|
823
|
+
""
|
|
824
|
+
);
|
|
825
|
+
const safe =
|
|
826
|
+
normalizeStorageKey(
|
|
827
|
+
withoutTrailingSlash
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
return hasTrailingSlash
|
|
831
|
+
? `${safe}/`
|
|
832
|
+
: safe;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export function normalizeStorageListLimit(
|
|
836
|
+
value: number | undefined
|
|
837
|
+
): number {
|
|
838
|
+
if (value === undefined) {
|
|
839
|
+
return DEFAULT_LIST_LIMIT;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (
|
|
843
|
+
!Number.isSafeInteger(
|
|
844
|
+
value
|
|
845
|
+
) ||
|
|
846
|
+
value < 1 ||
|
|
847
|
+
value > MAX_LIST_LIMIT
|
|
848
|
+
) {
|
|
849
|
+
throw new TypeError(
|
|
850
|
+
`BCP Framework: storage list limit must be an integer between 1 and ${MAX_LIST_LIMIT}.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return value;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function normalizeStorageSignedUrlExpiry(
|
|
858
|
+
value: number | undefined
|
|
859
|
+
): number {
|
|
860
|
+
if (value === undefined) {
|
|
861
|
+
return 15 * 60;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (
|
|
865
|
+
!Number.isSafeInteger(
|
|
866
|
+
value
|
|
867
|
+
) ||
|
|
868
|
+
value < 1 ||
|
|
869
|
+
value > 7 * 24 * 60 * 60
|
|
870
|
+
) {
|
|
871
|
+
throw new TypeError(
|
|
872
|
+
"BCP Framework: signed URL expiresIn must be an integer between 1 and 604800 seconds."
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return value;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function copyStorageObjectFallback(
|
|
880
|
+
storage: StorageEcosystemAdapter,
|
|
881
|
+
sourceKey: string,
|
|
882
|
+
destinationKey: string,
|
|
883
|
+
options: StorageCopyOptions
|
|
884
|
+
): Promise<StorageObjectMetadata> {
|
|
885
|
+
const source =
|
|
886
|
+
normalizeStorageKey(
|
|
887
|
+
sourceKey
|
|
888
|
+
);
|
|
889
|
+
const destination =
|
|
890
|
+
normalizeStorageKey(
|
|
891
|
+
destinationKey
|
|
892
|
+
);
|
|
893
|
+
const sourceMetadata =
|
|
894
|
+
await requireStorageObject(
|
|
895
|
+
storage,
|
|
896
|
+
source
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
if (source === destination) {
|
|
900
|
+
return sourceMetadata;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (
|
|
904
|
+
!options.overwrite &&
|
|
905
|
+
await storage.exists(
|
|
906
|
+
destination
|
|
907
|
+
)
|
|
908
|
+
) {
|
|
909
|
+
throw new StorageError(
|
|
910
|
+
"OBJECT_EXISTS",
|
|
911
|
+
`BCP Framework: storage object "${destination}" already exists.`,
|
|
912
|
+
409
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const userMetadata =
|
|
917
|
+
storage.getMetadata
|
|
918
|
+
? await storage.getMetadata(
|
|
919
|
+
source
|
|
920
|
+
)
|
|
921
|
+
: {};
|
|
922
|
+
const stream =
|
|
923
|
+
await readStorageStream(
|
|
924
|
+
storage,
|
|
925
|
+
source
|
|
926
|
+
);
|
|
927
|
+
const stored =
|
|
928
|
+
await putStorageStream(
|
|
929
|
+
storage,
|
|
930
|
+
destination,
|
|
931
|
+
stream,
|
|
932
|
+
{
|
|
933
|
+
overwrite:
|
|
934
|
+
options.overwrite,
|
|
935
|
+
contentType:
|
|
936
|
+
sourceMetadata.contentType,
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
if (
|
|
941
|
+
storage.setMetadata &&
|
|
942
|
+
Object.keys(
|
|
943
|
+
userMetadata
|
|
944
|
+
).length > 0
|
|
945
|
+
) {
|
|
946
|
+
await storage.setMetadata(
|
|
947
|
+
destination,
|
|
948
|
+
userMetadata
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return stored;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function deleteStorageObjectsFallback(
|
|
956
|
+
storage: StorageAdapter,
|
|
957
|
+
keys: readonly string[]
|
|
958
|
+
): Promise<StorageDeleteManyResult> {
|
|
959
|
+
const deleted:
|
|
960
|
+
string[] = [];
|
|
961
|
+
const missing:
|
|
962
|
+
string[] = [];
|
|
963
|
+
const failed:
|
|
964
|
+
StorageDeleteManyFailure[] = [];
|
|
965
|
+
const uniqueKeys =
|
|
966
|
+
[
|
|
967
|
+
...new Set(
|
|
968
|
+
keys.map(
|
|
969
|
+
(key) =>
|
|
970
|
+
normalizeStorageKey(
|
|
971
|
+
key
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
),
|
|
975
|
+
];
|
|
976
|
+
|
|
977
|
+
for (const key of uniqueKeys) {
|
|
978
|
+
try {
|
|
979
|
+
if (
|
|
980
|
+
await storage.delete(
|
|
981
|
+
key
|
|
982
|
+
)
|
|
983
|
+
) {
|
|
984
|
+
deleted.push(
|
|
985
|
+
key
|
|
986
|
+
);
|
|
987
|
+
} else {
|
|
988
|
+
missing.push(
|
|
989
|
+
key
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
} catch (error) {
|
|
993
|
+
failed.push({
|
|
994
|
+
key,
|
|
995
|
+
message:
|
|
996
|
+
error instanceof Error
|
|
997
|
+
? error.message
|
|
998
|
+
: String(
|
|
999
|
+
error
|
|
1000
|
+
),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return {
|
|
1006
|
+
deleted,
|
|
1007
|
+
missing,
|
|
1008
|
+
failed,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function requireStorageObject(
|
|
1013
|
+
storage: StorageAdapter,
|
|
1014
|
+
key: string
|
|
1015
|
+
): Promise<StorageObjectMetadata> {
|
|
1016
|
+
const metadata =
|
|
1017
|
+
await storage.stat(
|
|
1018
|
+
key
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
if (!metadata) {
|
|
1022
|
+
throw new StorageError(
|
|
1023
|
+
"OBJECT_NOT_FOUND",
|
|
1024
|
+
`BCP Framework: storage object "${key}" was not found.`,
|
|
1025
|
+
404
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
return metadata;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function collectLocalObjectKeys(
|
|
1033
|
+
rootDirectory: string
|
|
1034
|
+
): Promise<string[]> {
|
|
1035
|
+
const keys:
|
|
1036
|
+
string[] = [];
|
|
1037
|
+
|
|
1038
|
+
const visit =
|
|
1039
|
+
async (
|
|
1040
|
+
directory: string,
|
|
1041
|
+
prefix: string
|
|
1042
|
+
): Promise<void> => {
|
|
1043
|
+
let entries;
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
entries =
|
|
1047
|
+
await readdir(
|
|
1048
|
+
directory,
|
|
1049
|
+
{
|
|
1050
|
+
withFileTypes: true,
|
|
1051
|
+
}
|
|
1052
|
+
);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
if (
|
|
1055
|
+
isNodeErrorCode(
|
|
1056
|
+
error,
|
|
1057
|
+
"ENOENT"
|
|
1058
|
+
)
|
|
1059
|
+
) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
for (const entry of entries) {
|
|
1067
|
+
if (
|
|
1068
|
+
prefix.length === 0 &&
|
|
1069
|
+
entry.name ===
|
|
1070
|
+
LOCAL_METADATA_DIRECTORY
|
|
1071
|
+
) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const relative =
|
|
1076
|
+
prefix.length === 0
|
|
1077
|
+
? entry.name
|
|
1078
|
+
: `${prefix}/${entry.name}`;
|
|
1079
|
+
const fullPath =
|
|
1080
|
+
path.join(
|
|
1081
|
+
directory,
|
|
1082
|
+
entry.name
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
if (entry.isDirectory()) {
|
|
1086
|
+
await visit(
|
|
1087
|
+
fullPath,
|
|
1088
|
+
relative
|
|
1089
|
+
);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (entry.isFile()) {
|
|
1094
|
+
keys.push(
|
|
1095
|
+
normalizeStorageKey(
|
|
1096
|
+
relative
|
|
1097
|
+
)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
await visit(
|
|
1104
|
+
rootDirectory,
|
|
1105
|
+
""
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
return keys;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function replaceLocalUserMetadata(
|
|
1112
|
+
metadataDirectory: string,
|
|
1113
|
+
key: string,
|
|
1114
|
+
metadata:
|
|
1115
|
+
StorageUserMetadata |
|
|
1116
|
+
undefined
|
|
1117
|
+
): Promise<void> {
|
|
1118
|
+
if (metadata === undefined) {
|
|
1119
|
+
await rm(
|
|
1120
|
+
localUserMetadataFile(
|
|
1121
|
+
metadataDirectory,
|
|
1122
|
+
key
|
|
1123
|
+
),
|
|
1124
|
+
{
|
|
1125
|
+
force: true,
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
await writeLocalUserMetadata(
|
|
1132
|
+
metadataDirectory,
|
|
1133
|
+
key,
|
|
1134
|
+
normalizeStorageUserMetadata(
|
|
1135
|
+
metadata
|
|
1136
|
+
)
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function writeLocalUserMetadata(
|
|
1141
|
+
metadataDirectory: string,
|
|
1142
|
+
key: string,
|
|
1143
|
+
metadata: StorageUserMetadata
|
|
1144
|
+
): Promise<void> {
|
|
1145
|
+
await mkdir(
|
|
1146
|
+
metadataDirectory,
|
|
1147
|
+
{
|
|
1148
|
+
recursive: true,
|
|
1149
|
+
}
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const record:
|
|
1153
|
+
LocalUserMetadataRecord = {
|
|
1154
|
+
version: 1,
|
|
1155
|
+
key,
|
|
1156
|
+
metadata,
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
await writeFile(
|
|
1160
|
+
localUserMetadataFile(
|
|
1161
|
+
metadataDirectory,
|
|
1162
|
+
key
|
|
1163
|
+
),
|
|
1164
|
+
JSON.stringify(
|
|
1165
|
+
record
|
|
1166
|
+
),
|
|
1167
|
+
"utf8"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function readLocalUserMetadata(
|
|
1172
|
+
metadataDirectory: string,
|
|
1173
|
+
key: string
|
|
1174
|
+
): Promise<StorageUserMetadata> {
|
|
1175
|
+
try {
|
|
1176
|
+
const raw =
|
|
1177
|
+
await readFile(
|
|
1178
|
+
localUserMetadataFile(
|
|
1179
|
+
metadataDirectory,
|
|
1180
|
+
key
|
|
1181
|
+
),
|
|
1182
|
+
"utf8"
|
|
1183
|
+
);
|
|
1184
|
+
const parsed =
|
|
1185
|
+
JSON.parse(
|
|
1186
|
+
raw
|
|
1187
|
+
) as Partial<
|
|
1188
|
+
LocalUserMetadataRecord
|
|
1189
|
+
>;
|
|
1190
|
+
|
|
1191
|
+
if (
|
|
1192
|
+
parsed.version !== 1 ||
|
|
1193
|
+
parsed.key !== key ||
|
|
1194
|
+
parsed.metadata === undefined
|
|
1195
|
+
) {
|
|
1196
|
+
return {};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return normalizeStorageUserMetadata(
|
|
1200
|
+
parsed.metadata
|
|
1201
|
+
);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
if (
|
|
1204
|
+
isNodeErrorCode(
|
|
1205
|
+
error,
|
|
1206
|
+
"ENOENT"
|
|
1207
|
+
) ||
|
|
1208
|
+
error instanceof SyntaxError
|
|
1209
|
+
) {
|
|
1210
|
+
return {};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function localUserMetadataFile(
|
|
1218
|
+
metadataDirectory: string,
|
|
1219
|
+
key: string
|
|
1220
|
+
): string {
|
|
1221
|
+
const digest =
|
|
1222
|
+
createHash(
|
|
1223
|
+
"sha256"
|
|
1224
|
+
)
|
|
1225
|
+
.update(
|
|
1226
|
+
key
|
|
1227
|
+
)
|
|
1228
|
+
.digest(
|
|
1229
|
+
"hex"
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
return path.join(
|
|
1233
|
+
metadataDirectory,
|
|
1234
|
+
`${USER_METADATA_PREFIX}${digest}.json`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function encodeLocalCursor(
|
|
1239
|
+
key: string
|
|
1240
|
+
): string {
|
|
1241
|
+
return Buffer.from(
|
|
1242
|
+
key,
|
|
1243
|
+
"utf8"
|
|
1244
|
+
).toString(
|
|
1245
|
+
"base64url"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function decodeLocalCursor(
|
|
1250
|
+
cursor: string | undefined
|
|
1251
|
+
): string | null {
|
|
1252
|
+
if (cursor === undefined) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
try {
|
|
1257
|
+
const key =
|
|
1258
|
+
Buffer.from(
|
|
1259
|
+
cursor,
|
|
1260
|
+
"base64url"
|
|
1261
|
+
).toString(
|
|
1262
|
+
"utf8"
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
if (
|
|
1266
|
+
key.length === 0 ||
|
|
1267
|
+
encodeLocalCursor(
|
|
1268
|
+
key
|
|
1269
|
+
) !== cursor
|
|
1270
|
+
) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
"invalid cursor"
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return normalizeStorageKey(
|
|
1277
|
+
key
|
|
1278
|
+
);
|
|
1279
|
+
} catch {
|
|
1280
|
+
throw new StorageEcosystemError(
|
|
1281
|
+
"INVALID_CURSOR",
|
|
1282
|
+
"BCP Framework: invalid storage listing cursor."
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function unsupportedOperation(
|
|
1288
|
+
operation: string
|
|
1289
|
+
): StorageEcosystemError {
|
|
1290
|
+
return new StorageEcosystemError(
|
|
1291
|
+
"UNSUPPORTED_OPERATION",
|
|
1292
|
+
`BCP Framework: this storage adapter does not support ${operation}.`,
|
|
1293
|
+
501
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function assertNonEmptyString(
|
|
1298
|
+
value: string,
|
|
1299
|
+
label: string
|
|
1300
|
+
): string {
|
|
1301
|
+
if (
|
|
1302
|
+
typeof value !== "string" ||
|
|
1303
|
+
value.trim().length === 0
|
|
1304
|
+
) {
|
|
1305
|
+
throw new TypeError(
|
|
1306
|
+
`BCP Framework: storage ${label} must be a non-empty string.`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return value.trim();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function isNodeErrorCode(
|
|
1314
|
+
error: unknown,
|
|
1315
|
+
code: string
|
|
1316
|
+
): boolean {
|
|
1317
|
+
return (
|
|
1318
|
+
error instanceof Error &&
|
|
1319
|
+
"code" in error &&
|
|
1320
|
+
(
|
|
1321
|
+
error as Error & {
|
|
1322
|
+
code?: string;
|
|
1323
|
+
}
|
|
1324
|
+
).code === code
|
|
1325
|
+
);
|
|
1326
|
+
}
|