@chidchanun/bcp 0.1.25 → 0.1.26
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 +169 -263
- package/docs/README.md +148 -210
- package/docs/file-upload.md +194 -32
- package/docs/releases/0.1.26.md +281 -0
- package/docs/s3-storage.md +276 -0
- package/docs/storage.md +278 -162
- package/package.json +5 -2
- package/packages/client/src/database.mjs +236 -0
- package/packages/client/src/server.ts +22 -0
- package/packages/server/src/file-delivery.ts +8 -24
- package/packages/server/src/storage-s3.ts +1159 -0
- package/packages/server/src/storage.ts +740 -54
- package/packages/server/src/upload-stream.ts +846 -0
|
@@ -20,12 +20,15 @@ import {
|
|
|
20
20
|
|
|
21
21
|
const METADATA_DIRECTORY =
|
|
22
22
|
".bcp-storage-meta";
|
|
23
|
+
const STREAM_CHUNK_BYTES =
|
|
24
|
+
64 * 1024;
|
|
23
25
|
|
|
24
26
|
export type StorageErrorCode =
|
|
25
27
|
| "INVALID_KEY"
|
|
26
28
|
| "OBJECT_NOT_FOUND"
|
|
27
29
|
| "OBJECT_EXISTS"
|
|
28
|
-
| "RANGE_NOT_SATISFIABLE"
|
|
30
|
+
| "RANGE_NOT_SATISFIABLE"
|
|
31
|
+
| "STREAM_TOO_LARGE";
|
|
29
32
|
|
|
30
33
|
export class StorageError extends Error {
|
|
31
34
|
readonly code:
|
|
@@ -62,18 +65,49 @@ export interface StoragePutOptions {
|
|
|
62
65
|
overwrite?: boolean;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
export interface StoragePutStreamOptions
|
|
69
|
+
extends StoragePutOptions {
|
|
70
|
+
maxBytes?: number;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
export interface StorageReadOptions {
|
|
66
75
|
start?: number;
|
|
67
76
|
end?: number;
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
export interface StorageAdapterCapabilities {
|
|
80
|
+
streamingRead: boolean;
|
|
81
|
+
streamingWrite: boolean;
|
|
82
|
+
ranges: boolean;
|
|
83
|
+
signedUrls: boolean;
|
|
84
|
+
listing: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type StorageReadableStream =
|
|
88
|
+
ReadableStream<
|
|
89
|
+
Uint8Array<ArrayBuffer>
|
|
90
|
+
>;
|
|
91
|
+
|
|
70
92
|
export interface StorageAdapter {
|
|
93
|
+
capabilities?:
|
|
94
|
+
Partial<
|
|
95
|
+
StorageAdapterCapabilities
|
|
96
|
+
>;
|
|
97
|
+
|
|
71
98
|
put(
|
|
72
99
|
key: string,
|
|
73
100
|
value: StorageWriteValue,
|
|
74
101
|
options?: StoragePutOptions
|
|
75
102
|
): Promise<StorageObjectMetadata>;
|
|
76
103
|
|
|
104
|
+
putStream?(
|
|
105
|
+
key: string,
|
|
106
|
+
stream:
|
|
107
|
+
ReadableStream<Uint8Array>,
|
|
108
|
+
options?: StoragePutStreamOptions
|
|
109
|
+
): Promise<StorageObjectMetadata>;
|
|
110
|
+
|
|
77
111
|
stat(
|
|
78
112
|
key: string
|
|
79
113
|
): Promise<StorageObjectMetadata | null>;
|
|
@@ -83,6 +117,11 @@ export interface StorageAdapter {
|
|
|
83
117
|
options?: StorageReadOptions
|
|
84
118
|
): Promise<Uint8Array>;
|
|
85
119
|
|
|
120
|
+
readStream?(
|
|
121
|
+
key: string,
|
|
122
|
+
options?: StorageReadOptions
|
|
123
|
+
): Promise<StorageReadableStream>;
|
|
124
|
+
|
|
86
125
|
exists(
|
|
87
126
|
key: string
|
|
88
127
|
): Promise<boolean>;
|
|
@@ -137,7 +176,433 @@ export function createLocalStorage(
|
|
|
137
176
|
METADATA_DIRECTORY
|
|
138
177
|
);
|
|
139
178
|
|
|
179
|
+
const statObject =
|
|
180
|
+
async (
|
|
181
|
+
key: string
|
|
182
|
+
): Promise<StorageObjectMetadata | null> => {
|
|
183
|
+
const normalizedKey =
|
|
184
|
+
normalizeStorageKey(
|
|
185
|
+
key
|
|
186
|
+
);
|
|
187
|
+
const destination =
|
|
188
|
+
resolveStoragePath(
|
|
189
|
+
rootDirectory,
|
|
190
|
+
normalizedKey
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
let fileStat;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
fileStat =
|
|
197
|
+
await stat(
|
|
198
|
+
destination
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (
|
|
202
|
+
isNodeErrorCode(
|
|
203
|
+
error,
|
|
204
|
+
"ENOENT"
|
|
205
|
+
)
|
|
206
|
+
) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!fileStat.isFile()) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const record =
|
|
218
|
+
await readMetadataRecord(
|
|
219
|
+
metadataDirectory,
|
|
220
|
+
normalizedKey
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return createObjectMetadata(
|
|
224
|
+
normalizedKey,
|
|
225
|
+
fileStat.size,
|
|
226
|
+
fileStat.mtime,
|
|
227
|
+
record?.contentType ??
|
|
228
|
+
"application/octet-stream",
|
|
229
|
+
record?.checksumSha256
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const putStream =
|
|
234
|
+
async (
|
|
235
|
+
key: string,
|
|
236
|
+
stream:
|
|
237
|
+
ReadableStream<Uint8Array>,
|
|
238
|
+
putOptions:
|
|
239
|
+
StoragePutStreamOptions = {}
|
|
240
|
+
): Promise<StorageObjectMetadata> => {
|
|
241
|
+
const normalizedKey =
|
|
242
|
+
normalizeStorageKey(
|
|
243
|
+
key
|
|
244
|
+
);
|
|
245
|
+
const destination =
|
|
246
|
+
resolveStoragePath(
|
|
247
|
+
rootDirectory,
|
|
248
|
+
normalizedKey
|
|
249
|
+
);
|
|
250
|
+
const contentType =
|
|
251
|
+
normalizeContentType(
|
|
252
|
+
putOptions.contentType ??
|
|
253
|
+
""
|
|
254
|
+
);
|
|
255
|
+
const maxBytes =
|
|
256
|
+
normalizeOptionalByteLimit(
|
|
257
|
+
putOptions.maxBytes,
|
|
258
|
+
"stream maxBytes"
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await mkdir(
|
|
262
|
+
path.dirname(
|
|
263
|
+
destination
|
|
264
|
+
),
|
|
265
|
+
{
|
|
266
|
+
recursive: true,
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const reader =
|
|
271
|
+
stream.getReader();
|
|
272
|
+
let handle:
|
|
273
|
+
Awaited<
|
|
274
|
+
ReturnType<typeof open>
|
|
275
|
+
> | null =
|
|
276
|
+
null;
|
|
277
|
+
let destinationOpened =
|
|
278
|
+
false;
|
|
279
|
+
let completed =
|
|
280
|
+
false;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
putOptions.signal
|
|
284
|
+
?.throwIfAborted();
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
handle =
|
|
288
|
+
await open(
|
|
289
|
+
destination,
|
|
290
|
+
putOptions.overwrite
|
|
291
|
+
? "w"
|
|
292
|
+
: "wx"
|
|
293
|
+
);
|
|
294
|
+
destinationOpened =
|
|
295
|
+
true;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (
|
|
298
|
+
isNodeErrorCode(
|
|
299
|
+
error,
|
|
300
|
+
"EEXIST"
|
|
301
|
+
)
|
|
302
|
+
) {
|
|
303
|
+
throw new StorageError(
|
|
304
|
+
"OBJECT_EXISTS",
|
|
305
|
+
`BCP Framework: storage object "${normalizedKey}" already exists.`,
|
|
306
|
+
409
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const hash =
|
|
314
|
+
createHash(
|
|
315
|
+
"sha256"
|
|
316
|
+
);
|
|
317
|
+
let totalBytes =
|
|
318
|
+
0;
|
|
319
|
+
|
|
320
|
+
while (true) {
|
|
321
|
+
putOptions.signal
|
|
322
|
+
?.throwIfAborted();
|
|
323
|
+
|
|
324
|
+
const result =
|
|
325
|
+
await reader.read();
|
|
326
|
+
|
|
327
|
+
if (result.done) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const chunk =
|
|
332
|
+
result.value;
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
!(chunk instanceof
|
|
336
|
+
Uint8Array)
|
|
337
|
+
) {
|
|
338
|
+
throw new TypeError(
|
|
339
|
+
"BCP Framework: storage streams must emit Uint8Array chunks."
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const nextTotal =
|
|
344
|
+
totalBytes +
|
|
345
|
+
chunk.byteLength;
|
|
346
|
+
|
|
347
|
+
if (
|
|
348
|
+
maxBytes !== undefined &&
|
|
349
|
+
nextTotal > maxBytes
|
|
350
|
+
) {
|
|
351
|
+
throw new StorageError(
|
|
352
|
+
"STREAM_TOO_LARGE",
|
|
353
|
+
`BCP Framework: storage stream exceeded ${maxBytes} bytes.`,
|
|
354
|
+
413
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await writeAll(
|
|
359
|
+
handle,
|
|
360
|
+
chunk
|
|
361
|
+
);
|
|
362
|
+
hash.update(
|
|
363
|
+
chunk
|
|
364
|
+
);
|
|
365
|
+
totalBytes =
|
|
366
|
+
nextTotal;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await handle.close();
|
|
370
|
+
handle =
|
|
371
|
+
null;
|
|
372
|
+
|
|
373
|
+
const checksumSha256 =
|
|
374
|
+
hash.digest(
|
|
375
|
+
"hex"
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
await writeMetadataRecord(
|
|
379
|
+
metadataDirectory,
|
|
380
|
+
{
|
|
381
|
+
version: 1,
|
|
382
|
+
key:
|
|
383
|
+
normalizedKey,
|
|
384
|
+
contentType,
|
|
385
|
+
checksumSha256,
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const fileStat =
|
|
390
|
+
await stat(
|
|
391
|
+
destination
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
completed =
|
|
395
|
+
true;
|
|
396
|
+
|
|
397
|
+
return createObjectMetadata(
|
|
398
|
+
normalizedKey,
|
|
399
|
+
fileStat.size,
|
|
400
|
+
fileStat.mtime,
|
|
401
|
+
contentType,
|
|
402
|
+
checksumSha256
|
|
403
|
+
);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
try {
|
|
406
|
+
await reader.cancel(
|
|
407
|
+
error
|
|
408
|
+
);
|
|
409
|
+
} catch {
|
|
410
|
+
// Ignore source cancellation failures.
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
throw error;
|
|
414
|
+
} finally {
|
|
415
|
+
reader.releaseLock();
|
|
416
|
+
|
|
417
|
+
if (handle) {
|
|
418
|
+
try {
|
|
419
|
+
await handle.close();
|
|
420
|
+
} catch {
|
|
421
|
+
// Preserve the original failure.
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
destinationOpened &&
|
|
427
|
+
!completed
|
|
428
|
+
) {
|
|
429
|
+
await rm(
|
|
430
|
+
destination,
|
|
431
|
+
{
|
|
432
|
+
force: true,
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const readStream =
|
|
440
|
+
async (
|
|
441
|
+
key: string,
|
|
442
|
+
readOptions:
|
|
443
|
+
StorageReadOptions = {}
|
|
444
|
+
): Promise<StorageReadableStream> => {
|
|
445
|
+
const normalizedKey =
|
|
446
|
+
normalizeStorageKey(
|
|
447
|
+
key
|
|
448
|
+
);
|
|
449
|
+
const destination =
|
|
450
|
+
resolveStoragePath(
|
|
451
|
+
rootDirectory,
|
|
452
|
+
normalizedKey
|
|
453
|
+
);
|
|
454
|
+
const metadata =
|
|
455
|
+
await statObject(
|
|
456
|
+
normalizedKey
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (!metadata) {
|
|
460
|
+
throw new StorageError(
|
|
461
|
+
"OBJECT_NOT_FOUND",
|
|
462
|
+
`BCP Framework: storage object "${normalizedKey}" was not found.`,
|
|
463
|
+
404
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const range =
|
|
468
|
+
normalizeReadRange(
|
|
469
|
+
metadata.size,
|
|
470
|
+
readOptions
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
metadata.size === 0 &&
|
|
475
|
+
range === null
|
|
476
|
+
) {
|
|
477
|
+
return new ReadableStream<
|
|
478
|
+
Uint8Array<ArrayBuffer>
|
|
479
|
+
>({
|
|
480
|
+
start(controller) {
|
|
481
|
+
controller.close();
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const start =
|
|
487
|
+
range?.start ??
|
|
488
|
+
0;
|
|
489
|
+
const end =
|
|
490
|
+
range?.end ??
|
|
491
|
+
metadata.size - 1;
|
|
492
|
+
const handle =
|
|
493
|
+
await open(
|
|
494
|
+
destination,
|
|
495
|
+
"r"
|
|
496
|
+
);
|
|
497
|
+
let position =
|
|
498
|
+
start;
|
|
499
|
+
let closed =
|
|
500
|
+
false;
|
|
501
|
+
|
|
502
|
+
const closeHandle =
|
|
503
|
+
async () => {
|
|
504
|
+
if (closed) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
closed =
|
|
509
|
+
true;
|
|
510
|
+
await handle.close();
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
return new ReadableStream<
|
|
514
|
+
Uint8Array<ArrayBuffer>
|
|
515
|
+
>({
|
|
516
|
+
async pull(controller) {
|
|
517
|
+
try {
|
|
518
|
+
if (
|
|
519
|
+
position > end
|
|
520
|
+
) {
|
|
521
|
+
await closeHandle();
|
|
522
|
+
controller.close();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const remaining =
|
|
527
|
+
end -
|
|
528
|
+
position +
|
|
529
|
+
1;
|
|
530
|
+
const requested =
|
|
531
|
+
Math.min(
|
|
532
|
+
STREAM_CHUNK_BYTES,
|
|
533
|
+
remaining
|
|
534
|
+
);
|
|
535
|
+
const buffer =
|
|
536
|
+
Buffer.allocUnsafe(
|
|
537
|
+
requested
|
|
538
|
+
);
|
|
539
|
+
const result =
|
|
540
|
+
await handle.read(
|
|
541
|
+
buffer,
|
|
542
|
+
0,
|
|
543
|
+
requested,
|
|
544
|
+
position
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
if (
|
|
548
|
+
result.bytesRead === 0
|
|
549
|
+
) {
|
|
550
|
+
await closeHandle();
|
|
551
|
+
controller.close();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const chunk =
|
|
556
|
+
new Uint8Array(
|
|
557
|
+
result.bytesRead
|
|
558
|
+
);
|
|
559
|
+
chunk.set(
|
|
560
|
+
buffer.subarray(
|
|
561
|
+
0,
|
|
562
|
+
result.bytesRead
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
position +=
|
|
566
|
+
result.bytesRead;
|
|
567
|
+
|
|
568
|
+
controller.enqueue(
|
|
569
|
+
chunk
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
if (
|
|
573
|
+
position > end
|
|
574
|
+
) {
|
|
575
|
+
await closeHandle();
|
|
576
|
+
controller.close();
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
try {
|
|
580
|
+
await closeHandle();
|
|
581
|
+
} catch {
|
|
582
|
+
// Preserve the read failure.
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
controller.error(
|
|
586
|
+
error
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
async cancel() {
|
|
592
|
+
await closeHandle();
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
|
|
140
597
|
return {
|
|
598
|
+
capabilities: {
|
|
599
|
+
streamingRead: true,
|
|
600
|
+
streamingWrite: true,
|
|
601
|
+
ranges: true,
|
|
602
|
+
signedUrls: false,
|
|
603
|
+
listing: false,
|
|
604
|
+
},
|
|
605
|
+
|
|
141
606
|
async put(
|
|
142
607
|
key,
|
|
143
608
|
value,
|
|
@@ -234,56 +699,10 @@ export function createLocalStorage(
|
|
|
234
699
|
);
|
|
235
700
|
},
|
|
236
701
|
|
|
237
|
-
|
|
238
|
-
const normalizedKey =
|
|
239
|
-
normalizeStorageKey(
|
|
240
|
-
key
|
|
241
|
-
);
|
|
242
|
-
const destination =
|
|
243
|
-
resolveStoragePath(
|
|
244
|
-
rootDirectory,
|
|
245
|
-
normalizedKey
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
let fileStat;
|
|
249
|
-
|
|
250
|
-
try {
|
|
251
|
-
fileStat =
|
|
252
|
-
await stat(
|
|
253
|
-
destination
|
|
254
|
-
);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
if (
|
|
257
|
-
isNodeErrorCode(
|
|
258
|
-
error,
|
|
259
|
-
"ENOENT"
|
|
260
|
-
)
|
|
261
|
-
) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
702
|
+
putStream,
|
|
267
703
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const record =
|
|
273
|
-
await readMetadataRecord(
|
|
274
|
-
metadataDirectory,
|
|
275
|
-
normalizedKey
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
return createObjectMetadata(
|
|
279
|
-
normalizedKey,
|
|
280
|
-
fileStat.size,
|
|
281
|
-
fileStat.mtime,
|
|
282
|
-
record?.contentType ??
|
|
283
|
-
"application/octet-stream",
|
|
284
|
-
record?.checksumSha256
|
|
285
|
-
);
|
|
286
|
-
},
|
|
704
|
+
stat:
|
|
705
|
+
statObject,
|
|
287
706
|
|
|
288
707
|
async read(
|
|
289
708
|
key,
|
|
@@ -299,7 +718,7 @@ export function createLocalStorage(
|
|
|
299
718
|
normalizedKey
|
|
300
719
|
);
|
|
301
720
|
const metadata =
|
|
302
|
-
await
|
|
721
|
+
await statObject(
|
|
303
722
|
normalizedKey
|
|
304
723
|
);
|
|
305
724
|
|
|
@@ -359,9 +778,11 @@ export function createLocalStorage(
|
|
|
359
778
|
}
|
|
360
779
|
},
|
|
361
780
|
|
|
781
|
+
readStream,
|
|
782
|
+
|
|
362
783
|
async exists(key) {
|
|
363
784
|
return (
|
|
364
|
-
await
|
|
785
|
+
await statObject(
|
|
365
786
|
key
|
|
366
787
|
)
|
|
367
788
|
) !== null;
|
|
@@ -410,6 +831,113 @@ export function createLocalStorage(
|
|
|
410
831
|
};
|
|
411
832
|
}
|
|
412
833
|
|
|
834
|
+
export function getStorageCapabilities(
|
|
835
|
+
storage: StorageAdapter
|
|
836
|
+
): StorageAdapterCapabilities {
|
|
837
|
+
return {
|
|
838
|
+
streamingRead:
|
|
839
|
+
storage.capabilities
|
|
840
|
+
?.streamingRead ??
|
|
841
|
+
typeof storage.readStream ===
|
|
842
|
+
"function",
|
|
843
|
+
streamingWrite:
|
|
844
|
+
storage.capabilities
|
|
845
|
+
?.streamingWrite ??
|
|
846
|
+
typeof storage.putStream ===
|
|
847
|
+
"function",
|
|
848
|
+
ranges:
|
|
849
|
+
storage.capabilities
|
|
850
|
+
?.ranges ??
|
|
851
|
+
true,
|
|
852
|
+
signedUrls:
|
|
853
|
+
storage.capabilities
|
|
854
|
+
?.signedUrls ??
|
|
855
|
+
false,
|
|
856
|
+
listing:
|
|
857
|
+
storage.capabilities
|
|
858
|
+
?.listing ??
|
|
859
|
+
false,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export async function putStorageStream(
|
|
864
|
+
storage: StorageAdapter,
|
|
865
|
+
key: string,
|
|
866
|
+
stream:
|
|
867
|
+
ReadableStream<Uint8Array>,
|
|
868
|
+
options:
|
|
869
|
+
StoragePutStreamOptions = {}
|
|
870
|
+
): Promise<StorageObjectMetadata> {
|
|
871
|
+
if (storage.putStream) {
|
|
872
|
+
return storage.putStream(
|
|
873
|
+
key,
|
|
874
|
+
stream,
|
|
875
|
+
options
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const bytes =
|
|
880
|
+
await collectStorageStream(
|
|
881
|
+
stream,
|
|
882
|
+
options
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
return storage.put(
|
|
886
|
+
key,
|
|
887
|
+
bytes,
|
|
888
|
+
{
|
|
889
|
+
contentType:
|
|
890
|
+
options.contentType,
|
|
891
|
+
overwrite:
|
|
892
|
+
options.overwrite,
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export async function readStorageStream(
|
|
898
|
+
storage: StorageAdapter,
|
|
899
|
+
key: string,
|
|
900
|
+
options:
|
|
901
|
+
StorageReadOptions = {}
|
|
902
|
+
): Promise<StorageReadableStream> {
|
|
903
|
+
if (storage.readStream) {
|
|
904
|
+
return storage.readStream(
|
|
905
|
+
key,
|
|
906
|
+
options
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const bytes =
|
|
911
|
+
await storage.read(
|
|
912
|
+
key,
|
|
913
|
+
options
|
|
914
|
+
);
|
|
915
|
+
const chunk =
|
|
916
|
+
new Uint8Array(
|
|
917
|
+
bytes.byteLength
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
chunk.set(
|
|
921
|
+
bytes
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
return new ReadableStream<
|
|
925
|
+
Uint8Array<ArrayBuffer>
|
|
926
|
+
>({
|
|
927
|
+
start(controller) {
|
|
928
|
+
if (
|
|
929
|
+
chunk.byteLength > 0
|
|
930
|
+
) {
|
|
931
|
+
controller.enqueue(
|
|
932
|
+
chunk
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
controller.close();
|
|
937
|
+
},
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
413
941
|
export async function storeUploadedFile(
|
|
414
942
|
file: File,
|
|
415
943
|
options: StoreUploadedFileOptions
|
|
@@ -428,15 +956,19 @@ export async function storeUploadedFile(
|
|
|
428
956
|
options.key
|
|
429
957
|
);
|
|
430
958
|
const metadata =
|
|
431
|
-
await
|
|
959
|
+
await putStorageStream(
|
|
960
|
+
options.storage,
|
|
432
961
|
key,
|
|
433
|
-
file,
|
|
962
|
+
file.stream(),
|
|
434
963
|
{
|
|
435
964
|
overwrite:
|
|
436
965
|
options.overwrite,
|
|
437
966
|
contentType:
|
|
438
967
|
file.type ||
|
|
439
968
|
"application/octet-stream",
|
|
969
|
+
maxBytes:
|
|
970
|
+
options.constraints
|
|
971
|
+
?.maxBytes,
|
|
440
972
|
}
|
|
441
973
|
);
|
|
442
974
|
|
|
@@ -583,6 +1115,160 @@ async function toStorageBytes(
|
|
|
583
1115
|
);
|
|
584
1116
|
}
|
|
585
1117
|
|
|
1118
|
+
async function collectStorageStream(
|
|
1119
|
+
stream:
|
|
1120
|
+
ReadableStream<Uint8Array>,
|
|
1121
|
+
options:
|
|
1122
|
+
Pick<
|
|
1123
|
+
StoragePutStreamOptions,
|
|
1124
|
+
"maxBytes" |
|
|
1125
|
+
"signal"
|
|
1126
|
+
>
|
|
1127
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
1128
|
+
const maxBytes =
|
|
1129
|
+
normalizeOptionalByteLimit(
|
|
1130
|
+
options.maxBytes,
|
|
1131
|
+
"stream maxBytes"
|
|
1132
|
+
);
|
|
1133
|
+
const chunks:
|
|
1134
|
+
Uint8Array<ArrayBuffer>[] = [];
|
|
1135
|
+
let totalBytes =
|
|
1136
|
+
0;
|
|
1137
|
+
const reader =
|
|
1138
|
+
stream.getReader();
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
while (true) {
|
|
1142
|
+
options.signal
|
|
1143
|
+
?.throwIfAborted();
|
|
1144
|
+
|
|
1145
|
+
const result =
|
|
1146
|
+
await reader.read();
|
|
1147
|
+
|
|
1148
|
+
if (result.done) {
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const chunk =
|
|
1153
|
+
result.value;
|
|
1154
|
+
const nextTotal =
|
|
1155
|
+
totalBytes +
|
|
1156
|
+
chunk.byteLength;
|
|
1157
|
+
|
|
1158
|
+
if (
|
|
1159
|
+
maxBytes !== undefined &&
|
|
1160
|
+
nextTotal > maxBytes
|
|
1161
|
+
) {
|
|
1162
|
+
throw new StorageError(
|
|
1163
|
+
"STREAM_TOO_LARGE",
|
|
1164
|
+
`BCP Framework: storage stream exceeded ${maxBytes} bytes.`,
|
|
1165
|
+
413
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const ownedChunk =
|
|
1170
|
+
new Uint8Array(
|
|
1171
|
+
chunk.byteLength
|
|
1172
|
+
);
|
|
1173
|
+
ownedChunk.set(
|
|
1174
|
+
chunk
|
|
1175
|
+
);
|
|
1176
|
+
chunks.push(
|
|
1177
|
+
ownedChunk
|
|
1178
|
+
);
|
|
1179
|
+
totalBytes =
|
|
1180
|
+
nextTotal;
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
try {
|
|
1184
|
+
await reader.cancel(
|
|
1185
|
+
error
|
|
1186
|
+
);
|
|
1187
|
+
} catch {
|
|
1188
|
+
// Ignore source cancellation failures.
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
throw error;
|
|
1192
|
+
} finally {
|
|
1193
|
+
reader.releaseLock();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const bytes =
|
|
1197
|
+
new Uint8Array(
|
|
1198
|
+
totalBytes
|
|
1199
|
+
);
|
|
1200
|
+
let offset =
|
|
1201
|
+
0;
|
|
1202
|
+
|
|
1203
|
+
for (const chunk of chunks) {
|
|
1204
|
+
bytes.set(
|
|
1205
|
+
chunk,
|
|
1206
|
+
offset
|
|
1207
|
+
);
|
|
1208
|
+
offset +=
|
|
1209
|
+
chunk.byteLength;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return bytes;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function writeAll(
|
|
1216
|
+
handle:
|
|
1217
|
+
Awaited<
|
|
1218
|
+
ReturnType<typeof open>
|
|
1219
|
+
>,
|
|
1220
|
+
bytes: Uint8Array
|
|
1221
|
+
): Promise<void> {
|
|
1222
|
+
let offset =
|
|
1223
|
+
0;
|
|
1224
|
+
|
|
1225
|
+
while (
|
|
1226
|
+
offset < bytes.byteLength
|
|
1227
|
+
) {
|
|
1228
|
+
const result =
|
|
1229
|
+
await handle.write(
|
|
1230
|
+
bytes,
|
|
1231
|
+
offset,
|
|
1232
|
+
bytes.byteLength -
|
|
1233
|
+
offset,
|
|
1234
|
+
null
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
if (
|
|
1238
|
+
result.bytesWritten <= 0
|
|
1239
|
+
) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
"BCP Framework: storage stream write made no progress."
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
offset +=
|
|
1246
|
+
result.bytesWritten;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function normalizeOptionalByteLimit(
|
|
1251
|
+
value: number | undefined,
|
|
1252
|
+
label: string
|
|
1253
|
+
): number | undefined {
|
|
1254
|
+
if (value === undefined) {
|
|
1255
|
+
return undefined;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (
|
|
1259
|
+
!Number.isSafeInteger(
|
|
1260
|
+
value
|
|
1261
|
+
) ||
|
|
1262
|
+
value < 0
|
|
1263
|
+
) {
|
|
1264
|
+
throw new TypeError(
|
|
1265
|
+
`BCP Framework: storage ${label} must be a non-negative safe integer.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return value;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
586
1272
|
function normalizeReadRange(
|
|
587
1273
|
size: number,
|
|
588
1274
|
options: StorageReadOptions
|