@opengeni/storage 0.2.75 → 0.2.93
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/dist/bounded-object-read.d.ts +76 -0
- package/dist/bounded-object-write.d.ts +53 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +810 -4
- package/dist/index.js.map +1 -1
- package/dist/object-storage-bounded.d.ts +21 -0
- package/package.json +3 -3
- package/src/bounded-object-read.ts +323 -0
- package/src/bounded-object-write.ts +269 -0
- package/src/index.ts +299 -2
- package/src/object-storage-bounded.ts +297 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import { RETAINED_OUTPUT_MAX_PAGE_BYTES, type FileAsset } from "@opengeni/contracts";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { pipeline } from "node:stream/promises";
|
|
3
5
|
import {
|
|
4
6
|
BlobSASPermissions,
|
|
5
7
|
BlobServiceClient,
|
|
@@ -23,14 +25,22 @@ import {
|
|
|
23
25
|
type StorageOptions,
|
|
24
26
|
} from "@google-cloud/storage";
|
|
25
27
|
|
|
28
|
+
export * from "./bounded-object-read";
|
|
29
|
+
export * from "./bounded-object-write";
|
|
30
|
+
export * from "./object-storage-bounded";
|
|
31
|
+
|
|
26
32
|
export const MAX_SINGLE_PUT_SIZE_BYTES = 5_000_000_000;
|
|
27
33
|
export const UPLOAD_URL_TTL_SECONDS = 15 * 60;
|
|
28
34
|
export const DOWNLOAD_URL_TTL_SECONDS = 5 * 60;
|
|
35
|
+
const INTERNAL_STREAM_BUFFER_BYTES = 1024 * 1024;
|
|
36
|
+
const INTERNAL_STREAM_CONCURRENCY = 2;
|
|
29
37
|
|
|
30
38
|
export type ObjectHead = {
|
|
31
39
|
ContentLength?: number;
|
|
32
40
|
ContentType?: string;
|
|
33
41
|
Metadata?: Record<string, string>;
|
|
42
|
+
/** Opaque provider generation/etag used only for conditional internal reads. */
|
|
43
|
+
VersionToken?: string;
|
|
34
44
|
};
|
|
35
45
|
|
|
36
46
|
export type ObjectStorage = {
|
|
@@ -42,10 +52,14 @@ export type ObjectStorage = {
|
|
|
42
52
|
contentType: string;
|
|
43
53
|
sha256?: string | null;
|
|
44
54
|
expiresInSeconds?: number;
|
|
55
|
+
/** Select the network endpoint embedded in the signed URL. */
|
|
56
|
+
audience?: "public" | "sandbox";
|
|
45
57
|
}) => Promise<{ url: string; requiredHeaders: Record<string, string>; expiresAt: Date }>;
|
|
46
58
|
createGetUrl: (args: {
|
|
47
59
|
key: string;
|
|
48
60
|
expiresInSeconds?: number;
|
|
61
|
+
/** Select the network endpoint embedded in the signed URL. */
|
|
62
|
+
audience?: "public" | "sandbox";
|
|
49
63
|
}) => Promise<{ url: string; expiresAt: Date }>;
|
|
50
64
|
headFile: (file: FileAsset) => Promise<ObjectHead>;
|
|
51
65
|
/** Check provider existence without downloading object bytes. */
|
|
@@ -61,6 +75,14 @@ export type ObjectStorage = {
|
|
|
61
75
|
) => Promise<Uint8Array | null>;
|
|
62
76
|
/** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */
|
|
63
77
|
getObjectBytes: (key: string) => Promise<{ bytes: Uint8Array; contentType?: string } | null>;
|
|
78
|
+
/** Provider-versioned raw-key primitives used by bounded immutable adapters. */
|
|
79
|
+
headObject?: (key: string) => Promise<ObjectHead | null>;
|
|
80
|
+
getObjectRange?: (args: {
|
|
81
|
+
key: string;
|
|
82
|
+
start: number;
|
|
83
|
+
endInclusive: number;
|
|
84
|
+
expectedVersionToken: string;
|
|
85
|
+
}) => Promise<{ bytes: Uint8Array; versionToken: string } | null>;
|
|
64
86
|
/**
|
|
65
87
|
* SERVER-SIDE authenticated direct PUT (no presign + browser fetch). For an
|
|
66
88
|
* in-process upload from a trusted holder of the storage credentials (e.g. the
|
|
@@ -79,6 +101,25 @@ export type ObjectStorage = {
|
|
|
79
101
|
body: Uint8Array;
|
|
80
102
|
sha256?: string | null;
|
|
81
103
|
}) => Promise<void>;
|
|
104
|
+
/** Atomic create-only raw PUT. Returns false when the key already exists. */
|
|
105
|
+
putObjectIfAbsent?: (args: {
|
|
106
|
+
key: string;
|
|
107
|
+
contentType: string;
|
|
108
|
+
body: Uint8Array;
|
|
109
|
+
sha256: string;
|
|
110
|
+
}) => Promise<boolean>;
|
|
111
|
+
/**
|
|
112
|
+
* Atomic create-only raw upload from a bounded asynchronous byte stream.
|
|
113
|
+
* Providers may buffer a small fixed number of chunks, never the whole body.
|
|
114
|
+
*/
|
|
115
|
+
putObjectStreamIfAbsent?: (args: {
|
|
116
|
+
key: string;
|
|
117
|
+
contentType: string;
|
|
118
|
+
chunks: AsyncIterable<Uint8Array>;
|
|
119
|
+
byteSize: number;
|
|
120
|
+
sha256: string;
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
}) => Promise<boolean>;
|
|
82
123
|
/**
|
|
83
124
|
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
84
125
|
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
@@ -126,6 +167,12 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
126
167
|
...sharedClientConfig,
|
|
127
168
|
...(settings.objectStorageEndpoint ? { endpoint: settings.objectStorageEndpoint } : {}),
|
|
128
169
|
});
|
|
170
|
+
const sandboxPresignClient = settings.objectStorageSandboxEndpoint
|
|
171
|
+
? new S3Client({
|
|
172
|
+
...sharedClientConfig,
|
|
173
|
+
endpoint: settings.objectStorageSandboxEndpoint,
|
|
174
|
+
})
|
|
175
|
+
: presignClient;
|
|
129
176
|
const requestClient = settings.objectStorageInternalEndpoint
|
|
130
177
|
? new S3Client({
|
|
131
178
|
...sharedClientConfig,
|
|
@@ -148,7 +195,13 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
148
195
|
Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,
|
|
149
196
|
});
|
|
150
197
|
return {
|
|
151
|
-
url: await getSignedUrl(
|
|
198
|
+
url: await getSignedUrl(
|
|
199
|
+
args.audience === "sandbox" ? sandboxPresignClient : presignClient,
|
|
200
|
+
command,
|
|
201
|
+
{
|
|
202
|
+
expiresIn,
|
|
203
|
+
},
|
|
204
|
+
),
|
|
152
205
|
requiredHeaders,
|
|
153
206
|
expiresAt: new Date(Date.now() + expiresIn * 1000),
|
|
154
207
|
};
|
|
@@ -157,7 +210,7 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
157
210
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
158
211
|
return {
|
|
159
212
|
url: await getSignedUrl(
|
|
160
|
-
presignClient,
|
|
213
|
+
args.audience === "sandbox" ? sandboxPresignClient : presignClient,
|
|
161
214
|
new GetObjectCommand({
|
|
162
215
|
Bucket: settings.objectStorageBucket,
|
|
163
216
|
Key: args.key,
|
|
@@ -182,6 +235,44 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
182
235
|
}),
|
|
183
236
|
);
|
|
184
237
|
},
|
|
238
|
+
async putObjectIfAbsent(args) {
|
|
239
|
+
try {
|
|
240
|
+
await requestClient.send(
|
|
241
|
+
new PutObjectCommand({
|
|
242
|
+
Bucket: settings.objectStorageBucket,
|
|
243
|
+
Key: args.key,
|
|
244
|
+
ContentType: args.contentType,
|
|
245
|
+
Body: args.body,
|
|
246
|
+
Metadata: { sha256: args.sha256 },
|
|
247
|
+
IfNoneMatch: "*",
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
return true;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (isS3VersionMismatch(error)) return false;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
async putObjectStreamIfAbsent(args) {
|
|
257
|
+
try {
|
|
258
|
+
await requestClient.send(
|
|
259
|
+
new PutObjectCommand({
|
|
260
|
+
Bucket: settings.objectStorageBucket,
|
|
261
|
+
Key: args.key,
|
|
262
|
+
ContentType: args.contentType,
|
|
263
|
+
ContentLength: args.byteSize,
|
|
264
|
+
Body: Readable.from(args.chunks),
|
|
265
|
+
Metadata: { sha256: args.sha256 },
|
|
266
|
+
IfNoneMatch: "*",
|
|
267
|
+
}),
|
|
268
|
+
args.signal ? { abortSignal: args.signal } : undefined,
|
|
269
|
+
);
|
|
270
|
+
return true;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (isS3VersionMismatch(error)) return false;
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
185
276
|
async headFile(file) {
|
|
186
277
|
const head = await requestClient.send(
|
|
187
278
|
new HeadObjectCommand({
|
|
@@ -234,6 +325,43 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
234
325
|
throw error;
|
|
235
326
|
}
|
|
236
327
|
},
|
|
328
|
+
async headObject(key) {
|
|
329
|
+
try {
|
|
330
|
+
const head = await requestClient.send(
|
|
331
|
+
new HeadObjectCommand({ Bucket: settings.objectStorageBucket, Key: key }),
|
|
332
|
+
);
|
|
333
|
+
return objectHead({
|
|
334
|
+
contentLength: head.ContentLength,
|
|
335
|
+
contentType: head.ContentType,
|
|
336
|
+
metadata: head.Metadata,
|
|
337
|
+
versionToken: head.ETag,
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (isS3NotFound(error)) return null;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
async getObjectRange(args) {
|
|
345
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
346
|
+
try {
|
|
347
|
+
const result = await requestClient.send(
|
|
348
|
+
new GetObjectCommand({
|
|
349
|
+
Bucket: settings.objectStorageBucket,
|
|
350
|
+
Key: args.key,
|
|
351
|
+
Range: `bytes=${args.start}-${args.endInclusive}`,
|
|
352
|
+
IfMatch: args.expectedVersionToken,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
if (!result.ETag || result.ETag !== args.expectedVersionToken) return null;
|
|
356
|
+
return {
|
|
357
|
+
bytes: await s3BodyToBoundedBytes(result.Body, args.key, length),
|
|
358
|
+
versionToken: result.ETag,
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (isS3NotFound(error) || isS3VersionMismatch(error)) return null;
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
},
|
|
237
365
|
async getObjectBytes(key) {
|
|
238
366
|
try {
|
|
239
367
|
const result = await requestClient.send(
|
|
@@ -319,6 +447,15 @@ function isS3NotFound(error: unknown): boolean {
|
|
|
319
447
|
return metadata?.httpStatusCode === 404;
|
|
320
448
|
}
|
|
321
449
|
|
|
450
|
+
function isS3VersionMismatch(error: unknown): boolean {
|
|
451
|
+
if (!error || typeof error !== "object") return false;
|
|
452
|
+
const metadata =
|
|
453
|
+
"$metadata" in error
|
|
454
|
+
? (error as { $metadata?: { httpStatusCode?: number } }).$metadata
|
|
455
|
+
: undefined;
|
|
456
|
+
return metadata?.httpStatusCode === 412;
|
|
457
|
+
}
|
|
458
|
+
|
|
322
459
|
function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
323
460
|
const client = new GcsClient(gcsClientOptions(settings));
|
|
324
461
|
const bucket = client.bucket(settings.objectStorageBucket);
|
|
@@ -365,6 +502,39 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
365
502
|
...(args.sha256 ? { metadata: { metadata: { sha256: args.sha256 } } } : {}),
|
|
366
503
|
});
|
|
367
504
|
},
|
|
505
|
+
async putObjectIfAbsent(args) {
|
|
506
|
+
try {
|
|
507
|
+
await bucket.file(args.key).save(Buffer.from(args.body), {
|
|
508
|
+
contentType: args.contentType,
|
|
509
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
510
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
511
|
+
});
|
|
512
|
+
return true;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
async putObjectStreamIfAbsent(args) {
|
|
519
|
+
const destination = bucket.file(args.key).createWriteStream({
|
|
520
|
+
resumable: false,
|
|
521
|
+
contentType: args.contentType,
|
|
522
|
+
highWaterMark: INTERNAL_STREAM_BUFFER_BYTES,
|
|
523
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
524
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
525
|
+
});
|
|
526
|
+
try {
|
|
527
|
+
if (args.signal) {
|
|
528
|
+
await pipeline(Readable.from(args.chunks), destination, { signal: args.signal });
|
|
529
|
+
} else {
|
|
530
|
+
await pipeline(Readable.from(args.chunks), destination);
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
},
|
|
368
538
|
async headFile(file) {
|
|
369
539
|
const [metadata] = await bucket.file(file.objectKey).getMetadata();
|
|
370
540
|
return objectHead({
|
|
@@ -398,6 +568,35 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
398
568
|
throw error;
|
|
399
569
|
}
|
|
400
570
|
},
|
|
571
|
+
async headObject(key) {
|
|
572
|
+
try {
|
|
573
|
+
const [metadata] = await bucket.file(key).getMetadata();
|
|
574
|
+
return objectHead({
|
|
575
|
+
contentLength: parseContentLength(metadata.size),
|
|
576
|
+
contentType: metadata.contentType,
|
|
577
|
+
metadata: stringMetadata(metadata.metadata),
|
|
578
|
+
versionToken: metadata.generation === undefined ? undefined : String(metadata.generation),
|
|
579
|
+
});
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (isGcsNotFound(error)) return null;
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
async getObjectRange(args) {
|
|
586
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
587
|
+
try {
|
|
588
|
+
const [bytes] = await bucket
|
|
589
|
+
.file(args.key, { generation: args.expectedVersionToken })
|
|
590
|
+
.download({ start: args.start, end: args.endInclusive });
|
|
591
|
+
return {
|
|
592
|
+
bytes: exactRangeBytes(bytes, args.key, length),
|
|
593
|
+
versionToken: args.expectedVersionToken,
|
|
594
|
+
};
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (isGcsNotFound(error) || isGcsVersionMismatch(error)) return null;
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
},
|
|
401
600
|
async getObjectBytes(key) {
|
|
402
601
|
try {
|
|
403
602
|
const [bytes] = await bucket.file(key).download();
|
|
@@ -420,6 +619,10 @@ function isGcsNotFound(error: unknown): boolean {
|
|
|
420
619
|
return Boolean(error) && typeof error === "object" && (error as { code?: unknown }).code === 404;
|
|
421
620
|
}
|
|
422
621
|
|
|
622
|
+
function isGcsVersionMismatch(error: unknown): boolean {
|
|
623
|
+
return Boolean(error) && typeof error === "object" && (error as { code?: unknown }).code === 412;
|
|
624
|
+
}
|
|
625
|
+
|
|
423
626
|
function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null {
|
|
424
627
|
const sharedKey = azureSharedKeyCredential(settings);
|
|
425
628
|
const requestServiceClient = settings.objectStorageAzureConnectionString
|
|
@@ -490,6 +693,41 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
490
693
|
...(args.sha256 ? { metadata: { sha256: args.sha256 } } : {}),
|
|
491
694
|
});
|
|
492
695
|
},
|
|
696
|
+
async putObjectIfAbsent(args) {
|
|
697
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
698
|
+
const body = Buffer.from(args.body);
|
|
699
|
+
try {
|
|
700
|
+
await blobClient.upload(body, body.byteLength, {
|
|
701
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
702
|
+
metadata: { sha256: args.sha256 },
|
|
703
|
+
conditions: { ifNoneMatch: "*" },
|
|
704
|
+
});
|
|
705
|
+
return true;
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
async putObjectStreamIfAbsent(args) {
|
|
712
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
713
|
+
try {
|
|
714
|
+
await blobClient.uploadStream(
|
|
715
|
+
Readable.from(args.chunks),
|
|
716
|
+
INTERNAL_STREAM_BUFFER_BYTES,
|
|
717
|
+
INTERNAL_STREAM_CONCURRENCY,
|
|
718
|
+
{
|
|
719
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
720
|
+
metadata: { sha256: args.sha256 },
|
|
721
|
+
conditions: { ifNoneMatch: "*" },
|
|
722
|
+
...(args.signal ? { abortSignal: args.signal } : {}),
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
return true;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
},
|
|
493
731
|
async headFile(file) {
|
|
494
732
|
return azureHeadToObjectHead(
|
|
495
733
|
await requestContainerClient.getBlobClient(file.objectKey).getProperties(),
|
|
@@ -522,6 +760,36 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
522
760
|
throw error;
|
|
523
761
|
}
|
|
524
762
|
},
|
|
763
|
+
async headObject(key) {
|
|
764
|
+
try {
|
|
765
|
+
const properties = await requestContainerClient.getBlobClient(key).getProperties();
|
|
766
|
+
return objectHead({
|
|
767
|
+
contentLength: properties.contentLength,
|
|
768
|
+
contentType: properties.contentType,
|
|
769
|
+
metadata: properties.metadata,
|
|
770
|
+
versionToken: properties.etag,
|
|
771
|
+
});
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (isAzureNotFound(error)) return null;
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
async getObjectRange(args) {
|
|
778
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
779
|
+
try {
|
|
780
|
+
const download = await requestContainerClient
|
|
781
|
+
.getBlobClient(args.key)
|
|
782
|
+
.download(args.start, length, { conditions: { ifMatch: args.expectedVersionToken } });
|
|
783
|
+
if (!download.etag || download.etag !== args.expectedVersionToken) return null;
|
|
784
|
+
return {
|
|
785
|
+
bytes: await azureDownloadToBoundedBytes(download, args.key, length),
|
|
786
|
+
versionToken: download.etag,
|
|
787
|
+
};
|
|
788
|
+
} catch (error) {
|
|
789
|
+
if (isAzureNotFound(error) || isAzureVersionMismatch(error)) return null;
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
},
|
|
525
793
|
async getObjectBytes(key) {
|
|
526
794
|
try {
|
|
527
795
|
const download = await requestContainerClient.getBlobClient(key).download();
|
|
@@ -549,6 +817,14 @@ function isAzureNotFound(error: unknown): boolean {
|
|
|
549
817
|
);
|
|
550
818
|
}
|
|
551
819
|
|
|
820
|
+
function isAzureVersionMismatch(error: unknown): boolean {
|
|
821
|
+
return (
|
|
822
|
+
Boolean(error) &&
|
|
823
|
+
typeof error === "object" &&
|
|
824
|
+
(error as { statusCode?: unknown }).statusCode === 412
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
552
828
|
function azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredential {
|
|
553
829
|
if (settings.objectStorageAzureConnectionString) {
|
|
554
830
|
const parsed = parseConnectionString(settings.objectStorageAzureConnectionString);
|
|
@@ -712,6 +988,25 @@ function assertFileByteRange(
|
|
|
712
988
|
return length;
|
|
713
989
|
}
|
|
714
990
|
|
|
991
|
+
function assertRawObjectByteRange(key: string, start: number, endInclusive: number): number {
|
|
992
|
+
if (
|
|
993
|
+
typeof key !== "string" ||
|
|
994
|
+
key.length < 1 ||
|
|
995
|
+
key.length > 2048 ||
|
|
996
|
+
!Number.isSafeInteger(start) ||
|
|
997
|
+
!Number.isSafeInteger(endInclusive) ||
|
|
998
|
+
start < 0 ||
|
|
999
|
+
endInclusive < start
|
|
1000
|
+
) {
|
|
1001
|
+
throw new RangeError("Invalid raw object byte range");
|
|
1002
|
+
}
|
|
1003
|
+
const length = endInclusive - start + 1;
|
|
1004
|
+
if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
1005
|
+
throw new RangeError("Raw object byte range exceeds the bounded page limit");
|
|
1006
|
+
}
|
|
1007
|
+
return length;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
715
1010
|
function exactRangeBytes(bytes: Uint8Array, objectKey: string, expectedBytes: number): Uint8Array {
|
|
716
1011
|
if (bytes.byteLength !== expectedBytes) {
|
|
717
1012
|
throw new Error(
|
|
@@ -725,11 +1020,13 @@ function objectHead(input: {
|
|
|
725
1020
|
contentLength?: number | undefined;
|
|
726
1021
|
contentType?: string | undefined;
|
|
727
1022
|
metadata?: Record<string, string> | undefined;
|
|
1023
|
+
versionToken?: string | undefined;
|
|
728
1024
|
}): ObjectHead {
|
|
729
1025
|
return {
|
|
730
1026
|
...(input.contentLength !== undefined ? { ContentLength: input.contentLength } : {}),
|
|
731
1027
|
...(input.contentType !== undefined ? { ContentType: input.contentType } : {}),
|
|
732
1028
|
...(input.metadata !== undefined ? { Metadata: input.metadata } : {}),
|
|
1029
|
+
...(input.versionToken !== undefined ? { VersionToken: input.versionToken } : {}),
|
|
733
1030
|
};
|
|
734
1031
|
}
|
|
735
1032
|
|