@lossless.org/client 1.15.0 → 1.17.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/.smartconfig.json +20 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/nosqldb/classes.collection.d.ts +6 -1
- package/dist_ts/nosqldb/classes.collection.js +13 -2
- package/dist_ts/nosqldb/classes.doc.d.ts +90 -20
- package/dist_ts/nosqldb/classes.doc.js +412 -39
- package/dist_ts/objectstorage/classes.bucket.d.ts +16 -0
- package/dist_ts/objectstorage/classes.bucket.js +77 -3
- package/dist_ts/objectstorage/classes.objectrangeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.objectrangeerror.js +9 -0
- package/dist_ts/objectstorage/index.d.ts +1 -0
- package/dist_ts/objectstorage/index.js +2 -1
- package/dist_ts/objectstorage/interfaces.d.ts +33 -0
- package/dist_ts/objectstorage/internal.bucketrequests.d.ts +4 -0
- package/dist_ts/objectstorage/internal.bucketrequests.js +2 -2
- package/licenses/SOURCES.md +13 -0
- package/licenses/aws-sdk-js-v3.txt +201 -0
- package/licenses/nodable-entities.txt +21 -0
- package/package.json +3 -1
- package/readme.md +8 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/nosqldb/classes.collection.ts +15 -1
- package/ts/nosqldb/classes.doc.ts +633 -69
- package/ts/objectstorage/classes.bucket.ts +122 -2
- package/ts/objectstorage/classes.objectrangeerror.ts +11 -0
- package/ts/objectstorage/index.ts +1 -0
- package/ts/objectstorage/interfaces.ts +37 -0
- package/ts/objectstorage/internal.bucketrequests.ts +1 -1
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
purgeExactPath as purgeExactPathOperation,
|
|
19
19
|
} from './internal.exactpathpurge.operations.js';
|
|
20
20
|
import { beginBasicBucketOperation } from './internal.exactupload.capability.js';
|
|
21
|
-
import { headBucketExists, runAccountRequest } from './internal.bucketrequests.js';
|
|
21
|
+
import { getProviderStatus, headBucketExists, runAccountRequest } from './internal.bucketrequests.js';
|
|
22
|
+
import { ObjectRangeError } from './classes.objectrangeerror.js';
|
|
22
23
|
|
|
23
24
|
export interface IListObjectKeysPageOptions {
|
|
24
25
|
prefix?: string;
|
|
@@ -196,6 +197,58 @@ const toObjectListingEntry = (
|
|
|
196
197
|
};
|
|
197
198
|
};
|
|
198
199
|
|
|
200
|
+
const assertByteRangeOptions = (
|
|
201
|
+
optionsArg: interfaces.IFastGetRangeStreamOptions,
|
|
202
|
+
): interfaces.IObjectByteRange => {
|
|
203
|
+
const refuse = (messageArg: string): never => {
|
|
204
|
+
throw new ObjectRangeError('invalid_argument', messageArg);
|
|
205
|
+
};
|
|
206
|
+
if (!optionsArg || typeof optionsArg !== 'object' || Array.isArray(optionsArg)) {
|
|
207
|
+
return refuse('fastGetRangeStream options must be a plain object.');
|
|
208
|
+
}
|
|
209
|
+
const range: unknown = optionsArg.range;
|
|
210
|
+
if (!range || typeof range !== 'object' || Array.isArray(range)) {
|
|
211
|
+
return refuse('range must be an object with a start and an optional end.');
|
|
212
|
+
}
|
|
213
|
+
const { start, end } = range as { start?: unknown; end?: unknown };
|
|
214
|
+
if (typeof start !== 'number' || !Number.isSafeInteger(start) || start < 0) {
|
|
215
|
+
return refuse('range.start must be a non-negative safe integer.');
|
|
216
|
+
}
|
|
217
|
+
if (end === undefined) {
|
|
218
|
+
return { start };
|
|
219
|
+
}
|
|
220
|
+
if (typeof end !== 'number' || !Number.isSafeInteger(end) || end < start) {
|
|
221
|
+
return refuse('range.end must be a safe integer that is not below range.start.');
|
|
222
|
+
}
|
|
223
|
+
return { start, end };
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reads the served range and the object size from `Content-Range`, and
|
|
228
|
+
* refuses a response that does not serve exactly the requested range, so a
|
|
229
|
+
* provider that ignored the Range header never hands out the whole object.
|
|
230
|
+
*/
|
|
231
|
+
const readServedByteRange = (
|
|
232
|
+
responseArg: plugins.s3.GetObjectCommandOutput,
|
|
233
|
+
requestedArg: interfaces.IObjectByteRange,
|
|
234
|
+
): { range: { start: number; end: number }; size: number } => {
|
|
235
|
+
const match = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(responseArg.ContentRange ?? '');
|
|
236
|
+
const [start, end, size] = match ? match.slice(1).map(Number) : [];
|
|
237
|
+
if (
|
|
238
|
+
start === undefined || end === undefined || size === undefined
|
|
239
|
+
|| ![start, end, size].every(Number.isSafeInteger)
|
|
240
|
+
|| start !== requestedArg.start
|
|
241
|
+
|| end !== Math.min(requestedArg.end ?? Number.MAX_SAFE_INTEGER, size - 1)
|
|
242
|
+
|| (responseArg.ContentLength !== undefined && responseArg.ContentLength !== end - start + 1)
|
|
243
|
+
) {
|
|
244
|
+
throw new ObjectRangeError(
|
|
245
|
+
'invalid_response',
|
|
246
|
+
'The provider did not answer the byte-range request with a matching Content-Range.',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return { range: { start, end }, size };
|
|
250
|
+
};
|
|
251
|
+
|
|
199
252
|
const destroyReadableAndWaitForSettlement = async (
|
|
200
253
|
readableArg: plugins.stream.Readable,
|
|
201
254
|
): Promise<void> => {
|
|
@@ -460,6 +513,64 @@ export class Bucket {
|
|
|
460
513
|
optionsArg: interfaces.IFastGetStreamOptions,
|
|
461
514
|
typeArg: 'webstream' | 'nodestream' = 'nodestream'
|
|
462
515
|
): Promise<ReadableStream | plugins.stream.Readable> {
|
|
516
|
+
const { stream } = await this.openObjectReadStream(optionsArg, typeArg, undefined, () => undefined);
|
|
517
|
+
return stream;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Streams one inclusive byte range of an object with a single GetObject
|
|
522
|
+
* request and reports the range served and the object's total size from the
|
|
523
|
+
* provider's `Content-Range`, so an HTTP 206 answer needs no extra HEAD.
|
|
524
|
+
* An invalid range is refused with `ObjectRangeError` code
|
|
525
|
+
* `invalid_argument` before any request; a range that starts at or beyond
|
|
526
|
+
* the end of the object rejects with `range_not_satisfiable`.
|
|
527
|
+
*/
|
|
528
|
+
public fastGetRangeStream(
|
|
529
|
+
optionsArg: interfaces.IFastGetRangeStreamOptions,
|
|
530
|
+
typeArg: 'webstream'
|
|
531
|
+
): Promise<interfaces.IFastGetRangeStreamResult<ReadableStream>>;
|
|
532
|
+
public async fastGetRangeStream(
|
|
533
|
+
optionsArg: interfaces.IFastGetRangeStreamOptions,
|
|
534
|
+
typeArg?: 'nodestream'
|
|
535
|
+
): Promise<interfaces.IFastGetRangeStreamResult<plugins.stream.Readable>>;
|
|
536
|
+
|
|
537
|
+
public async fastGetRangeStream(
|
|
538
|
+
optionsArg: interfaces.IFastGetRangeStreamOptions,
|
|
539
|
+
typeArg: 'webstream' | 'nodestream' = 'nodestream'
|
|
540
|
+
): Promise<interfaces.IFastGetRangeStreamResult<ReadableStream | plugins.stream.Readable>> {
|
|
541
|
+
const requested = assertByteRangeOptions(optionsArg);
|
|
542
|
+
try {
|
|
543
|
+
const { stream, info } = await this.openObjectReadStream(
|
|
544
|
+
optionsArg,
|
|
545
|
+
typeArg,
|
|
546
|
+
`bytes=${requested.start}-${requested.end ?? ''}`,
|
|
547
|
+
(responseArg) => readServedByteRange(responseArg, requested),
|
|
548
|
+
);
|
|
549
|
+
return { stream, range: info.range, size: info.size };
|
|
550
|
+
} catch (error) {
|
|
551
|
+
const { name, httpStatusCode } = getProviderStatus(error);
|
|
552
|
+
if (httpStatusCode === 416 || name === 'InvalidRange') {
|
|
553
|
+
throw new ObjectRangeError(
|
|
554
|
+
'range_not_satisfiable',
|
|
555
|
+
`The byte range starting at ${requested.start} is not satisfiable for '${optionsArg.path}'.`,
|
|
556
|
+
{ cause: error },
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Sends one GetObject, reads the response headers through `readResponseArg`
|
|
565
|
+
* before any byte is handed out, and returns the body as a stream that
|
|
566
|
+
* `optionsArg.signal` and `SmartBucket.close()` cancel.
|
|
567
|
+
*/
|
|
568
|
+
private async openObjectReadStream<TInfo>(
|
|
569
|
+
optionsArg: interfaces.IFastGetStreamOptions,
|
|
570
|
+
typeArg: 'webstream' | 'nodestream',
|
|
571
|
+
rangeHeaderArg: string | undefined,
|
|
572
|
+
readResponseArg: (responseArg: plugins.s3.GetObjectCommandOutput) => TInfo,
|
|
573
|
+
): Promise<{ stream: ReadableStream | plugins.stream.Readable; info: TInfo }> {
|
|
463
574
|
const operation = beginBasicBucketOperation(this, optionsArg.signal);
|
|
464
575
|
const disposeBody = async (responseBodyArg: unknown): Promise<void> => {
|
|
465
576
|
try {
|
|
@@ -494,6 +605,7 @@ export class Bucket {
|
|
|
494
605
|
new plugins.s3.GetObjectCommand({
|
|
495
606
|
Bucket: operation.bucketName,
|
|
496
607
|
Key: optionsArg.path,
|
|
608
|
+
...(rangeHeaderArg === undefined ? {} : { Range: rangeHeaderArg }),
|
|
497
609
|
}),
|
|
498
610
|
{ abortSignal: operation.signal },
|
|
499
611
|
),
|
|
@@ -511,6 +623,14 @@ export class Bucket {
|
|
|
511
623
|
const disposeResponseBody = async (): Promise<void> => {
|
|
512
624
|
await disposeBody(responseBody);
|
|
513
625
|
};
|
|
626
|
+
let info: TInfo;
|
|
627
|
+
try {
|
|
628
|
+
info = readResponseArg(response);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
await disposeResponseBody();
|
|
631
|
+
operation.finish();
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
514
634
|
let sourceStream: plugins.stream.Readable | null = null;
|
|
515
635
|
try {
|
|
516
636
|
sourceStream = responseBody instanceof plugins.stream.Readable
|
|
@@ -644,7 +764,7 @@ export class Bucket {
|
|
|
644
764
|
operation.finish();
|
|
645
765
|
throw error;
|
|
646
766
|
}
|
|
647
|
-
return result;
|
|
767
|
+
return { stream: result, info };
|
|
648
768
|
}
|
|
649
769
|
|
|
650
770
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TObjectRangeErrorCode } from './interfaces.js';
|
|
2
|
+
|
|
3
|
+
export class ObjectRangeError extends Error {
|
|
4
|
+
public readonly code: TObjectRangeErrorCode;
|
|
5
|
+
|
|
6
|
+
constructor(codeArg: TObjectRangeErrorCode, messageArg: string, optionsArg?: ErrorOptions) {
|
|
7
|
+
super(messageArg, optionsArg);
|
|
8
|
+
this.name = 'ObjectRangeError';
|
|
9
|
+
this.code = codeArg;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -2,6 +2,7 @@ export * from './classes.smartbucket.js';
|
|
|
2
2
|
export * from './classes.bucket.js';
|
|
3
3
|
export * from './classes.exactuploaderror.js';
|
|
4
4
|
export * from './classes.exactpathpurgeerror.js';
|
|
5
|
+
export * from './classes.objectrangeerror.js';
|
|
5
6
|
export * from './classes.directory.js';
|
|
6
7
|
export * from './classes.file.js';
|
|
7
8
|
export * from './classes.listcursor.js';
|
|
@@ -13,6 +13,43 @@ export interface IFastGetStreamOptions {
|
|
|
13
13
|
signal?: AbortSignal;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A byte range of an object, with HTTP semantics: `start` and `end` are
|
|
18
|
+
* zero-based and inclusive. Without `end` the range runs to the last byte.
|
|
19
|
+
*/
|
|
20
|
+
export interface IObjectByteRange {
|
|
21
|
+
start: number;
|
|
22
|
+
end?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface IFastGetRangeStreamOptions extends IFastGetStreamOptions {
|
|
26
|
+
range: IObjectByteRange;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One ranged download: the stream carries exactly the bytes of `range`.
|
|
31
|
+
* - `range`: the inclusive byte range the provider served, from its
|
|
32
|
+
* `Content-Range`; `end` is clamped to the last byte of the object.
|
|
33
|
+
* - `size`: the byte length of the whole object, from the same header.
|
|
34
|
+
*/
|
|
35
|
+
export interface IFastGetRangeStreamResult<TStream> {
|
|
36
|
+
stream: TStream;
|
|
37
|
+
range: { start: number; end: number };
|
|
38
|
+
size: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* - `invalid_argument`: the request was refused before it was sent.
|
|
43
|
+
* - `range_not_satisfiable`: the provider answered HTTP 416; the range starts
|
|
44
|
+
* at or beyond the end of the object.
|
|
45
|
+
* - `invalid_response`: the provider answered without a `Content-Range` that
|
|
46
|
+
* matches the requested range, so its bytes were not handed out.
|
|
47
|
+
*/
|
|
48
|
+
export type TObjectRangeErrorCode =
|
|
49
|
+
| 'invalid_argument'
|
|
50
|
+
| 'range_not_satisfiable'
|
|
51
|
+
| 'invalid_response';
|
|
52
|
+
|
|
16
53
|
export interface IFastPutStreamOptions {
|
|
17
54
|
path: string;
|
|
18
55
|
readableStream: Readable | ReadableStream;
|
|
@@ -19,7 +19,7 @@ export const runAccountRequest = async <T>(
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const getProviderStatus = (errorArg: unknown): { name?: string; httpStatusCode?: number } => {
|
|
22
|
+
export const getProviderStatus = (errorArg: unknown): { name?: string; httpStatusCode?: number } => {
|
|
23
23
|
if (typeof errorArg !== 'object' || errorArg === null) {
|
|
24
24
|
return {};
|
|
25
25
|
}
|