@opengeni/storage 0.2.68 → 0.2.87
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 +35 -0
- package/dist/index.js +815 -13
- 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 +300 -11
- 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 = {
|
|
@@ -61,6 +71,14 @@ export type ObjectStorage = {
|
|
|
61
71
|
) => Promise<Uint8Array | null>;
|
|
62
72
|
/** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */
|
|
63
73
|
getObjectBytes: (key: string) => Promise<{ bytes: Uint8Array; contentType?: string } | null>;
|
|
74
|
+
/** Provider-versioned raw-key primitives used by bounded immutable adapters. */
|
|
75
|
+
headObject?: (key: string) => Promise<ObjectHead | null>;
|
|
76
|
+
getObjectRange?: (args: {
|
|
77
|
+
key: string;
|
|
78
|
+
start: number;
|
|
79
|
+
endInclusive: number;
|
|
80
|
+
expectedVersionToken: string;
|
|
81
|
+
}) => Promise<{ bytes: Uint8Array; versionToken: string } | null>;
|
|
64
82
|
/**
|
|
65
83
|
* SERVER-SIDE authenticated direct PUT (no presign + browser fetch). For an
|
|
66
84
|
* in-process upload from a trusted holder of the storage credentials (e.g. the
|
|
@@ -79,6 +97,25 @@ export type ObjectStorage = {
|
|
|
79
97
|
body: Uint8Array;
|
|
80
98
|
sha256?: string | null;
|
|
81
99
|
}) => Promise<void>;
|
|
100
|
+
/** Atomic create-only raw PUT. Returns false when the key already exists. */
|
|
101
|
+
putObjectIfAbsent?: (args: {
|
|
102
|
+
key: string;
|
|
103
|
+
contentType: string;
|
|
104
|
+
body: Uint8Array;
|
|
105
|
+
sha256: string;
|
|
106
|
+
}) => Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Atomic create-only raw upload from a bounded asynchronous byte stream.
|
|
109
|
+
* Providers may buffer a small fixed number of chunks, never the whole body.
|
|
110
|
+
*/
|
|
111
|
+
putObjectStreamIfAbsent?: (args: {
|
|
112
|
+
key: string;
|
|
113
|
+
contentType: string;
|
|
114
|
+
chunks: AsyncIterable<Uint8Array>;
|
|
115
|
+
byteSize: number;
|
|
116
|
+
sha256: string;
|
|
117
|
+
signal?: AbortSignal;
|
|
118
|
+
}) => Promise<boolean>;
|
|
82
119
|
/**
|
|
83
120
|
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
84
121
|
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
@@ -182,6 +219,44 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
182
219
|
}),
|
|
183
220
|
);
|
|
184
221
|
},
|
|
222
|
+
async putObjectIfAbsent(args) {
|
|
223
|
+
try {
|
|
224
|
+
await requestClient.send(
|
|
225
|
+
new PutObjectCommand({
|
|
226
|
+
Bucket: settings.objectStorageBucket,
|
|
227
|
+
Key: args.key,
|
|
228
|
+
ContentType: args.contentType,
|
|
229
|
+
Body: args.body,
|
|
230
|
+
Metadata: { sha256: args.sha256 },
|
|
231
|
+
IfNoneMatch: "*",
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (isS3VersionMismatch(error)) return false;
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async putObjectStreamIfAbsent(args) {
|
|
241
|
+
try {
|
|
242
|
+
await requestClient.send(
|
|
243
|
+
new PutObjectCommand({
|
|
244
|
+
Bucket: settings.objectStorageBucket,
|
|
245
|
+
Key: args.key,
|
|
246
|
+
ContentType: args.contentType,
|
|
247
|
+
ContentLength: args.byteSize,
|
|
248
|
+
Body: Readable.from(args.chunks),
|
|
249
|
+
Metadata: { sha256: args.sha256 },
|
|
250
|
+
IfNoneMatch: "*",
|
|
251
|
+
}),
|
|
252
|
+
args.signal ? { abortSignal: args.signal } : undefined,
|
|
253
|
+
);
|
|
254
|
+
return true;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (isS3VersionMismatch(error)) return false;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
185
260
|
async headFile(file) {
|
|
186
261
|
const head = await requestClient.send(
|
|
187
262
|
new HeadObjectCommand({
|
|
@@ -234,6 +309,43 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
234
309
|
throw error;
|
|
235
310
|
}
|
|
236
311
|
},
|
|
312
|
+
async headObject(key) {
|
|
313
|
+
try {
|
|
314
|
+
const head = await requestClient.send(
|
|
315
|
+
new HeadObjectCommand({ Bucket: settings.objectStorageBucket, Key: key }),
|
|
316
|
+
);
|
|
317
|
+
return objectHead({
|
|
318
|
+
contentLength: head.ContentLength,
|
|
319
|
+
contentType: head.ContentType,
|
|
320
|
+
metadata: head.Metadata,
|
|
321
|
+
versionToken: head.ETag,
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (isS3NotFound(error)) return null;
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
async getObjectRange(args) {
|
|
329
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
330
|
+
try {
|
|
331
|
+
const result = await requestClient.send(
|
|
332
|
+
new GetObjectCommand({
|
|
333
|
+
Bucket: settings.objectStorageBucket,
|
|
334
|
+
Key: args.key,
|
|
335
|
+
Range: `bytes=${args.start}-${args.endInclusive}`,
|
|
336
|
+
IfMatch: args.expectedVersionToken,
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
if (!result.ETag || result.ETag !== args.expectedVersionToken) return null;
|
|
340
|
+
return {
|
|
341
|
+
bytes: await s3BodyToBoundedBytes(result.Body, args.key, length),
|
|
342
|
+
versionToken: result.ETag,
|
|
343
|
+
};
|
|
344
|
+
} catch (error) {
|
|
345
|
+
if (isS3NotFound(error) || isS3VersionMismatch(error)) return null;
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
237
349
|
async getObjectBytes(key) {
|
|
238
350
|
try {
|
|
239
351
|
const result = await requestClient.send(
|
|
@@ -319,6 +431,15 @@ function isS3NotFound(error: unknown): boolean {
|
|
|
319
431
|
return metadata?.httpStatusCode === 404;
|
|
320
432
|
}
|
|
321
433
|
|
|
434
|
+
function isS3VersionMismatch(error: unknown): boolean {
|
|
435
|
+
if (!error || typeof error !== "object") return false;
|
|
436
|
+
const metadata =
|
|
437
|
+
"$metadata" in error
|
|
438
|
+
? (error as { $metadata?: { httpStatusCode?: number } }).$metadata
|
|
439
|
+
: undefined;
|
|
440
|
+
return metadata?.httpStatusCode === 412;
|
|
441
|
+
}
|
|
442
|
+
|
|
322
443
|
function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
323
444
|
const client = new GcsClient(gcsClientOptions(settings));
|
|
324
445
|
const bucket = client.bucket(settings.objectStorageBucket);
|
|
@@ -365,6 +486,39 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
365
486
|
...(args.sha256 ? { metadata: { metadata: { sha256: args.sha256 } } } : {}),
|
|
366
487
|
});
|
|
367
488
|
},
|
|
489
|
+
async putObjectIfAbsent(args) {
|
|
490
|
+
try {
|
|
491
|
+
await bucket.file(args.key).save(Buffer.from(args.body), {
|
|
492
|
+
contentType: args.contentType,
|
|
493
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
494
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
495
|
+
});
|
|
496
|
+
return true;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
async putObjectStreamIfAbsent(args) {
|
|
503
|
+
const destination = bucket.file(args.key).createWriteStream({
|
|
504
|
+
resumable: false,
|
|
505
|
+
contentType: args.contentType,
|
|
506
|
+
highWaterMark: INTERNAL_STREAM_BUFFER_BYTES,
|
|
507
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
508
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
509
|
+
});
|
|
510
|
+
try {
|
|
511
|
+
if (args.signal) {
|
|
512
|
+
await pipeline(Readable.from(args.chunks), destination, { signal: args.signal });
|
|
513
|
+
} else {
|
|
514
|
+
await pipeline(Readable.from(args.chunks), destination);
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
},
|
|
368
522
|
async headFile(file) {
|
|
369
523
|
const [metadata] = await bucket.file(file.objectKey).getMetadata();
|
|
370
524
|
return objectHead({
|
|
@@ -398,6 +552,35 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
398
552
|
throw error;
|
|
399
553
|
}
|
|
400
554
|
},
|
|
555
|
+
async headObject(key) {
|
|
556
|
+
try {
|
|
557
|
+
const [metadata] = await bucket.file(key).getMetadata();
|
|
558
|
+
return objectHead({
|
|
559
|
+
contentLength: parseContentLength(metadata.size),
|
|
560
|
+
contentType: metadata.contentType,
|
|
561
|
+
metadata: stringMetadata(metadata.metadata),
|
|
562
|
+
versionToken: metadata.generation === undefined ? undefined : String(metadata.generation),
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (isGcsNotFound(error)) return null;
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
async getObjectRange(args) {
|
|
570
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
571
|
+
try {
|
|
572
|
+
const [bytes] = await bucket
|
|
573
|
+
.file(args.key, { generation: args.expectedVersionToken })
|
|
574
|
+
.download({ start: args.start, end: args.endInclusive });
|
|
575
|
+
return {
|
|
576
|
+
bytes: exactRangeBytes(bytes, args.key, length),
|
|
577
|
+
versionToken: args.expectedVersionToken,
|
|
578
|
+
};
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (isGcsNotFound(error) || isGcsVersionMismatch(error)) return null;
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
},
|
|
401
584
|
async getObjectBytes(key) {
|
|
402
585
|
try {
|
|
403
586
|
const [bytes] = await bucket.file(key).download();
|
|
@@ -420,12 +603,24 @@ function isGcsNotFound(error: unknown): boolean {
|
|
|
420
603
|
return Boolean(error) && typeof error === "object" && (error as { code?: unknown }).code === 404;
|
|
421
604
|
}
|
|
422
605
|
|
|
606
|
+
function isGcsVersionMismatch(error: unknown): boolean {
|
|
607
|
+
return Boolean(error) && typeof error === "object" && (error as { code?: unknown }).code === 412;
|
|
608
|
+
}
|
|
609
|
+
|
|
423
610
|
function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null {
|
|
424
611
|
const sharedKey = azureSharedKeyCredential(settings);
|
|
425
|
-
const
|
|
612
|
+
const requestServiceClient = settings.objectStorageAzureConnectionString
|
|
426
613
|
? BlobServiceClient.fromConnectionString(settings.objectStorageAzureConnectionString)
|
|
427
614
|
: new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey);
|
|
428
|
-
const
|
|
615
|
+
const presignServiceClient = settings.objectStorageAzureEndpoint
|
|
616
|
+
? new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey)
|
|
617
|
+
: requestServiceClient;
|
|
618
|
+
const requestContainerClient = requestServiceClient.getContainerClient(
|
|
619
|
+
settings.objectStorageBucket,
|
|
620
|
+
);
|
|
621
|
+
const presignContainerClient = presignServiceClient.getContainerClient(
|
|
622
|
+
settings.objectStorageBucket,
|
|
623
|
+
);
|
|
429
624
|
|
|
430
625
|
return {
|
|
431
626
|
bucket: settings.objectStorageBucket,
|
|
@@ -434,7 +629,7 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
434
629
|
async createPutUrl(args) {
|
|
435
630
|
const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;
|
|
436
631
|
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
437
|
-
const blobClient =
|
|
632
|
+
const blobClient = presignContainerClient.getBlockBlobClient(args.key);
|
|
438
633
|
const sas = generateBlobSASQueryParameters(
|
|
439
634
|
{
|
|
440
635
|
containerName: settings.objectStorageBucket,
|
|
@@ -458,7 +653,7 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
458
653
|
async createGetUrl(args) {
|
|
459
654
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
460
655
|
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
461
|
-
const blobClient =
|
|
656
|
+
const blobClient = presignContainerClient.getBlobClient(args.key);
|
|
462
657
|
const sas = generateBlobSASQueryParameters(
|
|
463
658
|
{
|
|
464
659
|
containerName: settings.objectStorageBucket,
|
|
@@ -475,21 +670,56 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
475
670
|
},
|
|
476
671
|
async putObject(args) {
|
|
477
672
|
// Authenticated in-process upload via the shared-key Azure client (no SAS).
|
|
478
|
-
const blobClient =
|
|
673
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
479
674
|
const body = Buffer.from(args.body);
|
|
480
675
|
await blobClient.upload(body, body.byteLength, {
|
|
481
676
|
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
482
677
|
...(args.sha256 ? { metadata: { sha256: args.sha256 } } : {}),
|
|
483
678
|
});
|
|
484
679
|
},
|
|
680
|
+
async putObjectIfAbsent(args) {
|
|
681
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
682
|
+
const body = Buffer.from(args.body);
|
|
683
|
+
try {
|
|
684
|
+
await blobClient.upload(body, body.byteLength, {
|
|
685
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
686
|
+
metadata: { sha256: args.sha256 },
|
|
687
|
+
conditions: { ifNoneMatch: "*" },
|
|
688
|
+
});
|
|
689
|
+
return true;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
async putObjectStreamIfAbsent(args) {
|
|
696
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
697
|
+
try {
|
|
698
|
+
await blobClient.uploadStream(
|
|
699
|
+
Readable.from(args.chunks),
|
|
700
|
+
INTERNAL_STREAM_BUFFER_BYTES,
|
|
701
|
+
INTERNAL_STREAM_CONCURRENCY,
|
|
702
|
+
{
|
|
703
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
704
|
+
metadata: { sha256: args.sha256 },
|
|
705
|
+
conditions: { ifNoneMatch: "*" },
|
|
706
|
+
...(args.signal ? { abortSignal: args.signal } : {}),
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
return true;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
},
|
|
485
715
|
async headFile(file) {
|
|
486
716
|
return azureHeadToObjectHead(
|
|
487
|
-
await
|
|
717
|
+
await requestContainerClient.getBlobClient(file.objectKey).getProperties(),
|
|
488
718
|
);
|
|
489
719
|
},
|
|
490
720
|
async fileExists(file) {
|
|
491
721
|
try {
|
|
492
|
-
await
|
|
722
|
+
await requestContainerClient.getBlobClient(file.objectKey).getProperties();
|
|
493
723
|
return true;
|
|
494
724
|
} catch (error) {
|
|
495
725
|
if (isAzureNotFound(error)) return false;
|
|
@@ -498,14 +728,14 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
498
728
|
},
|
|
499
729
|
async getFileBytes(file) {
|
|
500
730
|
return await azureDownloadToBytes(
|
|
501
|
-
await
|
|
731
|
+
await requestContainerClient.getBlobClient(file.objectKey).download(),
|
|
502
732
|
);
|
|
503
733
|
},
|
|
504
734
|
async getFileRange(file, range) {
|
|
505
735
|
const length = assertFileByteRange(file, range);
|
|
506
736
|
try {
|
|
507
737
|
return await azureDownloadToBoundedBytes(
|
|
508
|
-
await
|
|
738
|
+
await requestContainerClient.getBlobClient(file.objectKey).download(range.start, length),
|
|
509
739
|
file.objectKey,
|
|
510
740
|
length,
|
|
511
741
|
);
|
|
@@ -514,9 +744,39 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
514
744
|
throw error;
|
|
515
745
|
}
|
|
516
746
|
},
|
|
747
|
+
async headObject(key) {
|
|
748
|
+
try {
|
|
749
|
+
const properties = await requestContainerClient.getBlobClient(key).getProperties();
|
|
750
|
+
return objectHead({
|
|
751
|
+
contentLength: properties.contentLength,
|
|
752
|
+
contentType: properties.contentType,
|
|
753
|
+
metadata: properties.metadata,
|
|
754
|
+
versionToken: properties.etag,
|
|
755
|
+
});
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (isAzureNotFound(error)) return null;
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
async getObjectRange(args) {
|
|
762
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
763
|
+
try {
|
|
764
|
+
const download = await requestContainerClient
|
|
765
|
+
.getBlobClient(args.key)
|
|
766
|
+
.download(args.start, length, { conditions: { ifMatch: args.expectedVersionToken } });
|
|
767
|
+
if (!download.etag || download.etag !== args.expectedVersionToken) return null;
|
|
768
|
+
return {
|
|
769
|
+
bytes: await azureDownloadToBoundedBytes(download, args.key, length),
|
|
770
|
+
versionToken: download.etag,
|
|
771
|
+
};
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (isAzureNotFound(error) || isAzureVersionMismatch(error)) return null;
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
},
|
|
517
777
|
async getObjectBytes(key) {
|
|
518
778
|
try {
|
|
519
|
-
const download = await
|
|
779
|
+
const download = await requestContainerClient.getBlobClient(key).download();
|
|
520
780
|
const bytes = await azureDownloadToBytes(download);
|
|
521
781
|
return { bytes, ...(download.contentType ? { contentType: download.contentType } : {}) };
|
|
522
782
|
} catch (error) {
|
|
@@ -528,7 +788,7 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
528
788
|
},
|
|
529
789
|
async deleteObject(key) {
|
|
530
790
|
// deleteIfExists keeps the delete idempotent (a missing blob is a no-op).
|
|
531
|
-
await
|
|
791
|
+
await requestContainerClient.getBlockBlobClient(key).deleteIfExists();
|
|
532
792
|
},
|
|
533
793
|
};
|
|
534
794
|
}
|
|
@@ -541,6 +801,14 @@ function isAzureNotFound(error: unknown): boolean {
|
|
|
541
801
|
);
|
|
542
802
|
}
|
|
543
803
|
|
|
804
|
+
function isAzureVersionMismatch(error: unknown): boolean {
|
|
805
|
+
return (
|
|
806
|
+
Boolean(error) &&
|
|
807
|
+
typeof error === "object" &&
|
|
808
|
+
(error as { statusCode?: unknown }).statusCode === 412
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
544
812
|
function azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredential {
|
|
545
813
|
if (settings.objectStorageAzureConnectionString) {
|
|
546
814
|
const parsed = parseConnectionString(settings.objectStorageAzureConnectionString);
|
|
@@ -704,6 +972,25 @@ function assertFileByteRange(
|
|
|
704
972
|
return length;
|
|
705
973
|
}
|
|
706
974
|
|
|
975
|
+
function assertRawObjectByteRange(key: string, start: number, endInclusive: number): number {
|
|
976
|
+
if (
|
|
977
|
+
typeof key !== "string" ||
|
|
978
|
+
key.length < 1 ||
|
|
979
|
+
key.length > 2048 ||
|
|
980
|
+
!Number.isSafeInteger(start) ||
|
|
981
|
+
!Number.isSafeInteger(endInclusive) ||
|
|
982
|
+
start < 0 ||
|
|
983
|
+
endInclusive < start
|
|
984
|
+
) {
|
|
985
|
+
throw new RangeError("Invalid raw object byte range");
|
|
986
|
+
}
|
|
987
|
+
const length = endInclusive - start + 1;
|
|
988
|
+
if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
989
|
+
throw new RangeError("Raw object byte range exceeds the bounded page limit");
|
|
990
|
+
}
|
|
991
|
+
return length;
|
|
992
|
+
}
|
|
993
|
+
|
|
707
994
|
function exactRangeBytes(bytes: Uint8Array, objectKey: string, expectedBytes: number): Uint8Array {
|
|
708
995
|
if (bytes.byteLength !== expectedBytes) {
|
|
709
996
|
throw new Error(
|
|
@@ -717,11 +1004,13 @@ function objectHead(input: {
|
|
|
717
1004
|
contentLength?: number | undefined;
|
|
718
1005
|
contentType?: string | undefined;
|
|
719
1006
|
metadata?: Record<string, string> | undefined;
|
|
1007
|
+
versionToken?: string | undefined;
|
|
720
1008
|
}): ObjectHead {
|
|
721
1009
|
return {
|
|
722
1010
|
...(input.contentLength !== undefined ? { ContentLength: input.contentLength } : {}),
|
|
723
1011
|
...(input.contentType !== undefined ? { ContentType: input.contentType } : {}),
|
|
724
1012
|
...(input.metadata !== undefined ? { Metadata: input.metadata } : {}),
|
|
1013
|
+
...(input.versionToken !== undefined ? { VersionToken: input.versionToken } : {}),
|
|
725
1014
|
};
|
|
726
1015
|
}
|
|
727
1016
|
|