@opengeni/storage 0.2.10 → 0.2.22
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/index.d.ts +11 -1
- package/dist/index.js +134 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +170 -2
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,17 @@ type ObjectStorage = {
|
|
|
31
31
|
expiresAt: Date;
|
|
32
32
|
}>;
|
|
33
33
|
headFile: (file: FileAsset) => Promise<ObjectHead>;
|
|
34
|
+
/** Check provider existence without downloading object bytes. */
|
|
35
|
+
fileExists: (file: FileAsset) => Promise<boolean>;
|
|
34
36
|
getFileBytes: (file: FileAsset) => Promise<Uint8Array>;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch exactly one inclusive byte range without materializing the complete
|
|
39
|
+
* object. Returns null when the provider reports that the object is missing.
|
|
40
|
+
*/
|
|
41
|
+
getFileRange: (file: FileAsset, range: {
|
|
42
|
+
start: number;
|
|
43
|
+
end: number;
|
|
44
|
+
}) => Promise<Uint8Array | null>;
|
|
35
45
|
/** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */
|
|
36
46
|
getObjectBytes: (key: string) => Promise<{
|
|
37
47
|
bytes: Uint8Array;
|
|
@@ -56,7 +66,7 @@ type ObjectStorage = {
|
|
|
56
66
|
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
57
67
|
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
58
68
|
* 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2
|
|
59
|
-
* capture GC (
|
|
69
|
+
* capture GC (keep the latest 10) to reap after-image blobs that no
|
|
60
70
|
* surviving revision references. Best-effort at the call site — a failed
|
|
61
71
|
* delete leaves an orphan blob, never corrupts a live capture.
|
|
62
72
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { RETAINED_OUTPUT_MAX_PAGE_BYTES } from "@opengeni/contracts";
|
|
2
3
|
import {
|
|
3
4
|
BlobSASPermissions,
|
|
4
5
|
BlobServiceClient,
|
|
@@ -104,6 +105,20 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
104
105
|
metadata: head.Metadata
|
|
105
106
|
});
|
|
106
107
|
},
|
|
108
|
+
async fileExists(file) {
|
|
109
|
+
try {
|
|
110
|
+
await client.send(
|
|
111
|
+
new HeadObjectCommand({
|
|
112
|
+
Bucket: file.bucket,
|
|
113
|
+
Key: file.objectKey
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
return true;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (isS3NotFound(error)) return false;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
107
122
|
async getFileBytes(file) {
|
|
108
123
|
const result = await client.send(
|
|
109
124
|
new GetObjectCommand({
|
|
@@ -113,6 +128,22 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
113
128
|
);
|
|
114
129
|
return await s3BodyToBytes(result.Body, file.objectKey);
|
|
115
130
|
},
|
|
131
|
+
async getFileRange(file, range) {
|
|
132
|
+
const length = assertFileByteRange(file, range);
|
|
133
|
+
try {
|
|
134
|
+
const result = await client.send(
|
|
135
|
+
new GetObjectCommand({
|
|
136
|
+
Bucket: file.bucket,
|
|
137
|
+
Key: file.objectKey,
|
|
138
|
+
Range: `bytes=${range.start}-${range.end}`
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
return await s3BodyToBoundedBytes(result.Body, file.objectKey, length);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (isS3NotFound(error)) return null;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
116
147
|
async getObjectBytes(key) {
|
|
117
148
|
try {
|
|
118
149
|
const result = await client.send(
|
|
@@ -154,6 +185,27 @@ async function s3BodyToBytes(body, objectKey) {
|
|
|
154
185
|
}
|
|
155
186
|
return Buffer.concat(chunks);
|
|
156
187
|
}
|
|
188
|
+
async function s3BodyToBoundedBytes(body, objectKey, expectedBytes) {
|
|
189
|
+
if (!body) {
|
|
190
|
+
throw new Error(`Object body is empty: ${objectKey}`);
|
|
191
|
+
}
|
|
192
|
+
const chunks = [];
|
|
193
|
+
let bytes = 0;
|
|
194
|
+
for await (const chunk of body) {
|
|
195
|
+
const normalized = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
196
|
+
bytes += normalized.byteLength;
|
|
197
|
+
if (bytes > expectedBytes) {
|
|
198
|
+
throw new Error(`Object range exceeded requested length: ${objectKey}`);
|
|
199
|
+
}
|
|
200
|
+
chunks.push(normalized);
|
|
201
|
+
}
|
|
202
|
+
if (bytes !== expectedBytes) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return Buffer.concat(chunks, bytes);
|
|
208
|
+
}
|
|
157
209
|
function isS3NotFound(error) {
|
|
158
210
|
if (!error || typeof error !== "object") {
|
|
159
211
|
return false;
|
|
@@ -218,10 +270,29 @@ function createGcsObjectStorage(settings) {
|
|
|
218
270
|
metadata: stringMetadata(metadata.metadata)
|
|
219
271
|
});
|
|
220
272
|
},
|
|
273
|
+
async fileExists(file) {
|
|
274
|
+
try {
|
|
275
|
+
await bucket.file(file.objectKey).getMetadata();
|
|
276
|
+
return true;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (isGcsNotFound(error)) return false;
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
221
282
|
async getFileBytes(file) {
|
|
222
283
|
const [bytes] = await bucket.file(file.objectKey).download();
|
|
223
284
|
return bytes;
|
|
224
285
|
},
|
|
286
|
+
async getFileRange(file, range) {
|
|
287
|
+
const length = assertFileByteRange(file, range);
|
|
288
|
+
try {
|
|
289
|
+
const [bytes] = await bucket.file(file.objectKey).download({ start: range.start, end: range.end });
|
|
290
|
+
return exactRangeBytes(bytes, file.objectKey, length);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (isGcsNotFound(error)) return null;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
},
|
|
225
296
|
async getObjectBytes(key) {
|
|
226
297
|
try {
|
|
227
298
|
const [bytes] = await bucket.file(key).download();
|
|
@@ -304,11 +375,33 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
304
375
|
await containerClient.getBlobClient(file.objectKey).getProperties()
|
|
305
376
|
);
|
|
306
377
|
},
|
|
378
|
+
async fileExists(file) {
|
|
379
|
+
try {
|
|
380
|
+
await containerClient.getBlobClient(file.objectKey).getProperties();
|
|
381
|
+
return true;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (isAzureNotFound(error)) return false;
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
307
387
|
async getFileBytes(file) {
|
|
308
388
|
return await azureDownloadToBytes(
|
|
309
389
|
await containerClient.getBlobClient(file.objectKey).download()
|
|
310
390
|
);
|
|
311
391
|
},
|
|
392
|
+
async getFileRange(file, range) {
|
|
393
|
+
const length = assertFileByteRange(file, range);
|
|
394
|
+
try {
|
|
395
|
+
return await azureDownloadToBoundedBytes(
|
|
396
|
+
await containerClient.getBlobClient(file.objectKey).download(range.start, length),
|
|
397
|
+
file.objectKey,
|
|
398
|
+
length
|
|
399
|
+
);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (isAzureNotFound(error)) return null;
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
312
405
|
async getObjectBytes(key) {
|
|
313
406
|
try {
|
|
314
407
|
const download = await containerClient.getBlobClient(key).download();
|
|
@@ -419,6 +512,47 @@ async function azureDownloadToBytes(download) {
|
|
|
419
512
|
}
|
|
420
513
|
return Buffer.concat(chunks);
|
|
421
514
|
}
|
|
515
|
+
async function azureDownloadToBoundedBytes(download, objectKey, expectedBytes) {
|
|
516
|
+
if (!download.readableStreamBody) {
|
|
517
|
+
throw new Error("Azure Blob download response did not include a readable body");
|
|
518
|
+
}
|
|
519
|
+
const chunks = [];
|
|
520
|
+
let bytes = 0;
|
|
521
|
+
for await (const chunk of download.readableStreamBody) {
|
|
522
|
+
const normalized = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
523
|
+
bytes += normalized.byteLength;
|
|
524
|
+
if (bytes > expectedBytes) {
|
|
525
|
+
throw new Error(`Object range exceeded requested length: ${objectKey}`);
|
|
526
|
+
}
|
|
527
|
+
chunks.push(normalized);
|
|
528
|
+
}
|
|
529
|
+
if (bytes !== expectedBytes) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return Buffer.concat(chunks, bytes);
|
|
535
|
+
}
|
|
536
|
+
function assertFileByteRange(file, range) {
|
|
537
|
+
if (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end < range.start || range.end >= file.sizeBytes) {
|
|
538
|
+
throw new RangeError(`Invalid object byte range for ${file.objectKey}`);
|
|
539
|
+
}
|
|
540
|
+
const length = range.end - range.start + 1;
|
|
541
|
+
if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
542
|
+
throw new RangeError(
|
|
543
|
+
`Object byte range exceeds ${RETAINED_OUTPUT_MAX_PAGE_BYTES} bytes for ${file.objectKey}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return length;
|
|
547
|
+
}
|
|
548
|
+
function exactRangeBytes(bytes, objectKey, expectedBytes) {
|
|
549
|
+
if (bytes.byteLength !== expectedBytes) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes.byteLength}`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
return bytes;
|
|
555
|
+
}
|
|
422
556
|
function objectHead(input) {
|
|
423
557
|
return {
|
|
424
558
|
...input.contentLength !== void 0 ? { ContentLength: input.contentLength } : {},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport type { FileAsset } from \"@opengeni/contracts\";\nimport {\n BlobSASPermissions,\n BlobServiceClient,\n generateBlobSASQueryParameters,\n StorageSharedKeyCredential,\n type BlobDownloadResponseParsed,\n type BlobGetPropertiesResponse,\n} from \"@azure/storage-blob\";\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n PutObjectCommand,\n S3Client,\n type S3ClientConfig,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport {\n Storage as GcsClient,\n type GetSignedUrlConfig,\n type StorageOptions,\n} from \"@google-cloud/storage\";\n\nexport const MAX_SINGLE_PUT_SIZE_BYTES = 5_000_000_000;\nexport const UPLOAD_URL_TTL_SECONDS = 15 * 60;\nexport const DOWNLOAD_URL_TTL_SECONDS = 5 * 60;\n\nexport type ObjectHead = {\n ContentLength?: number;\n ContentType?: string;\n Metadata?: Record<string, string>;\n};\n\nexport type ObjectStorage = {\n bucket: string;\n backend: \"s3-compatible\" | \"aws-s3\" | \"azure-blob\" | \"gcs\";\n maxSinglePutSizeBytes: number;\n createPutUrl: (args: {\n key: string;\n contentType: string;\n sha256?: string | null;\n expiresInSeconds?: number;\n }) => Promise<{ url: string; requiredHeaders: Record<string, string>; expiresAt: Date }>;\n createGetUrl: (args: {\n key: string;\n expiresInSeconds?: number;\n }) => Promise<{ url: string; expiresAt: Date }>;\n headFile: (file: FileAsset) => Promise<ObjectHead>;\n getFileBytes: (file: FileAsset) => Promise<Uint8Array>;\n /** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */\n getObjectBytes: (key: string) => Promise<{ bytes: Uint8Array; contentType?: string } | null>;\n /**\n * SERVER-SIDE authenticated direct PUT (no presign + browser fetch). For an\n * in-process upload from a trusted holder of the storage credentials (e.g. the\n * worker writing a recording), this sends the bytes straight to the storage\n * backend over the configured endpoint with the in-process SDK client — bypassing\n * the presigned-URL round-trip, which on split public/internal topologies (a\n * public `objectStorageEndpoint` with no in-cluster route) would otherwise 401.\n * Browser uploads keep using `createPutUrl`; this is the trusted-server twin.\n */\n putObject: (args: {\n key: string;\n contentType: string;\n body: Uint8Array;\n sha256?: string | null;\n }) => Promise<void>;\n /**\n * SERVER-SIDE authenticated delete of a single object by raw storage key.\n * Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not\n * 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2\n * capture GC (dossier §10.1 keep-latest-10) to reap after-image blobs that no\n * surviving revision references. Best-effort at the call site — a failed\n * delete leaves an orphan blob, never corrupts a live capture.\n */\n deleteObject: (key: string) => Promise<void>;\n};\n\nexport function createObjectStorage(settings: Settings): ObjectStorage | null {\n if (settings.objectStorageBackend === \"azure-blob\") {\n return createAzureBlobObjectStorage(settings);\n }\n if (settings.objectStorageBackend === \"gcs\") {\n return createGcsObjectStorage(settings);\n }\n return createS3CompatibleObjectStorage(settings);\n}\n\nfunction createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | null {\n if (\n settings.objectStorageBackend === \"s3-compatible\" &&\n (!settings.objectStorageEndpoint ||\n !settings.objectStorageAccessKeyId ||\n !settings.objectStorageSecretAccessKey)\n ) {\n return null;\n }\n const clientConfig: S3ClientConfig = {\n region: settings.objectStorageRegion,\n forcePathStyle: settings.objectStorageForcePathStyle,\n requestChecksumCalculation: \"WHEN_REQUIRED\",\n ...(settings.objectStorageEndpoint ? { endpoint: settings.objectStorageEndpoint } : {}),\n ...(settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey\n ? {\n credentials: {\n accessKeyId: settings.objectStorageAccessKeyId,\n secretAccessKey: settings.objectStorageSecretAccessKey,\n },\n }\n : {}),\n };\n const client = new S3Client(clientConfig);\n return {\n bucket: settings.objectStorageBucket,\n backend: settings.objectStorageBackend === \"aws-s3\" ? \"aws-s3\" : \"s3-compatible\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const requiredHeaders: Record<string, string> = {\n \"content-type\": args.contentType,\n };\n const command = new PutObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n ContentType: args.contentType,\n Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,\n });\n return {\n url: await getSignedUrl(client, command, { expiresIn }),\n requiredHeaders,\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n return {\n url: await getSignedUrl(\n client,\n new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n }),\n { expiresIn },\n ),\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n };\n },\n async putObject(args) {\n // Authenticated in-process PUT against the configured (in-cluster) endpoint.\n // A presigned URL buys nothing here — the worker already holds the creds — and\n // on a split public/internal endpoint topology the presigned URL points at the\n // PUBLIC host (no MinIO route → 401). This sends bytes straight to the backend.\n await client.send(\n new PutObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n ContentType: args.contentType,\n Body: args.body,\n Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,\n }),\n );\n },\n async headFile(file) {\n const head = await client.send(\n new HeadObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }),\n );\n return objectHead({\n contentLength: head.ContentLength,\n contentType: head.ContentType,\n metadata: head.Metadata,\n });\n },\n async getFileBytes(file) {\n const result = await client.send(\n new GetObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }),\n );\n return await s3BodyToBytes(result.Body, file.objectKey);\n },\n async getObjectBytes(key) {\n try {\n const result = await client.send(\n new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: key,\n }),\n );\n const bytes = await s3BodyToBytes(result.Body, key);\n return { bytes, ...(result.ContentType ? { contentType: result.ContentType } : {}) };\n } catch (error) {\n if (isS3NotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // S3 DeleteObject is idempotent — deleting an absent key returns 204.\n await client.send(\n new DeleteObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: key,\n }),\n );\n },\n };\n}\n\nasync function s3BodyToBytes(body: unknown, objectKey: string): Promise<Uint8Array> {\n if (!body) {\n throw new Error(`Object body is empty: ${objectKey}`);\n }\n const withTransform = body as { transformToByteArray?: () => Promise<Uint8Array> };\n if (typeof withTransform.transformToByteArray === \"function\") {\n return await withTransform.transformToByteArray();\n }\n const chunks: Uint8Array[] = [];\n for await (const chunk of body as AsyncIterable<Uint8Array | Buffer | string>) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n return Buffer.concat(chunks);\n}\n\nfunction isS3NotFound(error: unknown): boolean {\n if (!error || typeof error !== \"object\") {\n return false;\n }\n const name = \"name\" in error ? (error as { name?: unknown }).name : undefined;\n if (name === \"NoSuchKey\" || name === \"NotFound\") {\n return true;\n }\n const metadata =\n \"$metadata\" in error\n ? (error as { $metadata?: { httpStatusCode?: number } }).$metadata\n : undefined;\n return metadata?.httpStatusCode === 404;\n}\n\nfunction createGcsObjectStorage(settings: Settings): ObjectStorage {\n const client = new GcsClient(gcsClientOptions(settings));\n const bucket = client.bucket(settings.objectStorageBucket);\n return {\n bucket: settings.objectStorageBucket,\n backend: \"gcs\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const config: GetSignedUrlConfig = {\n version: \"v4\",\n action: \"write\",\n expires: expiresAt,\n contentType: args.contentType,\n };\n if (args.sha256) {\n config.extensionHeaders = { \"x-goog-meta-sha256\": args.sha256 };\n }\n const [url] = await bucket.file(args.key).getSignedUrl(config);\n return {\n url,\n requiredHeaders: {\n \"content-type\": args.contentType,\n ...(args.sha256 ? { \"x-goog-meta-sha256\": args.sha256 } : {}),\n },\n expiresAt,\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const [url] = await bucket.file(args.key).getSignedUrl({\n version: \"v4\",\n action: \"read\",\n expires: expiresAt,\n });\n return { url, expiresAt };\n },\n async putObject(args) {\n // Authenticated in-process PUT via the GCS SDK (the server holds the creds).\n await bucket.file(args.key).save(Buffer.from(args.body), {\n contentType: args.contentType,\n ...(args.sha256 ? { metadata: { metadata: { sha256: args.sha256 } } } : {}),\n });\n },\n async headFile(file) {\n const [metadata] = await bucket.file(file.objectKey).getMetadata();\n return objectHead({\n contentLength: parseContentLength(metadata.size),\n contentType: metadata.contentType,\n metadata: stringMetadata(metadata.metadata),\n });\n },\n async getFileBytes(file) {\n const [bytes] = await bucket.file(file.objectKey).download();\n return bytes;\n },\n async getObjectBytes(key) {\n try {\n const [bytes] = await bucket.file(key).download();\n return { bytes };\n } catch (error) {\n if (isGcsNotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // ignoreNotFound keeps the delete idempotent (a missing blob is a no-op).\n await bucket.file(key).delete({ ignoreNotFound: true });\n },\n };\n}\n\nfunction isGcsNotFound(error: unknown): boolean {\n return Boolean(error) && typeof error === \"object\" && (error as { code?: unknown }).code === 404;\n}\n\nfunction createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null {\n const sharedKey = azureSharedKeyCredential(settings);\n const serviceClient = settings.objectStorageAzureConnectionString\n ? BlobServiceClient.fromConnectionString(settings.objectStorageAzureConnectionString)\n : new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey);\n const containerClient = serviceClient.getContainerClient(settings.objectStorageBucket);\n\n return {\n bucket: settings.objectStorageBucket,\n backend: \"azure-blob\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const blobClient = containerClient.getBlockBlobClient(args.key);\n const sas = generateBlobSASQueryParameters(\n {\n containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"cw\"),\n expiresOn: expiresAt,\n contentType: args.contentType,\n },\n sharedKey,\n ).toString();\n return {\n url: `${blobClient.url}?${sas}`,\n requiredHeaders: {\n \"content-type\": args.contentType,\n \"x-ms-blob-type\": \"BlockBlob\",\n ...(args.sha256 ? { \"x-ms-meta-sha256\": args.sha256 } : {}),\n },\n expiresAt,\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const blobClient = containerClient.getBlobClient(args.key);\n const sas = generateBlobSASQueryParameters(\n {\n containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"r\"),\n expiresOn: expiresAt,\n },\n sharedKey,\n ).toString();\n return {\n url: `${blobClient.url}?${sas}`,\n expiresAt,\n };\n },\n async putObject(args) {\n // Authenticated in-process upload via the shared-key Azure client (no SAS).\n const blobClient = containerClient.getBlockBlobClient(args.key);\n const body = Buffer.from(args.body);\n await blobClient.upload(body, body.byteLength, {\n blobHTTPHeaders: { blobContentType: args.contentType },\n ...(args.sha256 ? { metadata: { sha256: args.sha256 } } : {}),\n });\n },\n async headFile(file) {\n return azureHeadToObjectHead(\n await containerClient.getBlobClient(file.objectKey).getProperties(),\n );\n },\n async getFileBytes(file) {\n return await azureDownloadToBytes(\n await containerClient.getBlobClient(file.objectKey).download(),\n );\n },\n async getObjectBytes(key) {\n try {\n const download = await containerClient.getBlobClient(key).download();\n const bytes = await azureDownloadToBytes(download);\n return { bytes, ...(download.contentType ? { contentType: download.contentType } : {}) };\n } catch (error) {\n if (isAzureNotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // deleteIfExists keeps the delete idempotent (a missing blob is a no-op).\n await containerClient.getBlockBlobClient(key).deleteIfExists();\n },\n };\n}\n\nfunction isAzureNotFound(error: unknown): boolean {\n return (\n Boolean(error) &&\n typeof error === \"object\" &&\n (error as { statusCode?: unknown }).statusCode === 404\n );\n}\n\nfunction azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredential {\n if (settings.objectStorageAzureConnectionString) {\n const parsed = parseConnectionString(settings.objectStorageAzureConnectionString);\n if (parsed.AccountName && parsed.AccountKey) {\n return new StorageSharedKeyCredential(parsed.AccountName, parsed.AccountKey);\n }\n throw new Error(\n \"Azure Blob connection string must include AccountName and AccountKey to create presigned URLs\",\n );\n }\n if (!settings.objectStorageAzureAccountName || !settings.objectStorageAzureAccountKey) {\n throw new Error(\"Azure Blob storage requires account name and account key\");\n }\n return new StorageSharedKeyCredential(\n settings.objectStorageAzureAccountName,\n settings.objectStorageAzureAccountKey,\n );\n}\n\nfunction azureBlobServiceUrl(settings: Settings): string {\n if (settings.objectStorageAzureEndpoint) {\n return settings.objectStorageAzureEndpoint.replace(/\\/+$/, \"\");\n }\n if (!settings.objectStorageAzureAccountName) {\n throw new Error(\"Azure Blob storage requires account name\");\n }\n return `https://${settings.objectStorageAzureAccountName}.blob.core.windows.net`;\n}\n\nfunction parseConnectionString(value: string): Record<string, string> {\n return Object.fromEntries(\n value\n .split(\";\")\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const index = part.indexOf(\"=\");\n return index === -1 ? [part, \"\"] : [part.slice(0, index), part.slice(index + 1)];\n }),\n );\n}\n\nfunction gcsClientOptions(settings: Settings): StorageOptions {\n const options: StorageOptions = {\n ...(settings.objectStorageGcsProjectId\n ? { projectId: settings.objectStorageGcsProjectId }\n : {}),\n ...(settings.objectStorageGcsKeyFilename\n ? { keyFilename: settings.objectStorageGcsKeyFilename }\n : {}),\n ...(settings.objectStorageGcsApiEndpoint\n ? { apiEndpoint: settings.objectStorageGcsApiEndpoint }\n : {}),\n };\n if (settings.objectStorageGcsCredentialsJson) {\n options.credentials = parseGcsCredentials(settings.objectStorageGcsCredentialsJson);\n }\n return options;\n}\n\nfunction parseGcsCredentials(raw: string): Record<string, string> {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\"GCS credentials JSON must be an object\");\n }\n return parsed as Record<string, string>;\n}\n\nfunction parseContentLength(value: string | number | undefined): number | undefined {\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value !== \"string\" || value.length === 0) {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction stringMetadata(\n value: Record<string, string | number | boolean | null> | undefined,\n): Record<string, string> | undefined {\n if (!value) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(value).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n );\n}\n\nfunction azureHeadToObjectHead(head: BlobGetPropertiesResponse): ObjectHead {\n return objectHead({\n contentLength: head.contentLength,\n contentType: head.contentType,\n metadata: head.metadata,\n });\n}\n\nasync function azureDownloadToBytes(download: BlobDownloadResponseParsed): Promise<Uint8Array> {\n if (!download.readableStreamBody) {\n throw new Error(\"Azure Blob download response did not include a readable body\");\n }\n const chunks: Uint8Array[] = [];\n for await (const chunk of download.readableStreamBody as AsyncIterable<\n Uint8Array | Buffer | string\n >) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n return Buffer.concat(chunks);\n}\n\nfunction objectHead(input: {\n contentLength?: number | undefined;\n contentType?: string | undefined;\n metadata?: Record<string, string> | undefined;\n}): ObjectHead {\n return {\n ...(input.contentLength !== undefined ? { ContentLength: input.contentLength } : {}),\n ...(input.contentType !== undefined ? { ContentType: input.contentType } : {}),\n ...(input.metadata !== undefined ? { Metadata: input.metadata } : {}),\n };\n}\n\nexport function bytesToDataUrl(bytes: Uint8Array, contentType: string): string {\n return `data:${contentType};base64,${Buffer.from(bytes).toString(\"base64\")}`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport { RETAINED_OUTPUT_MAX_PAGE_BYTES, type FileAsset } from \"@opengeni/contracts\";\nimport {\n BlobSASPermissions,\n BlobServiceClient,\n generateBlobSASQueryParameters,\n StorageSharedKeyCredential,\n type BlobDownloadResponseParsed,\n type BlobGetPropertiesResponse,\n} from \"@azure/storage-blob\";\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n PutObjectCommand,\n S3Client,\n type S3ClientConfig,\n} from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport {\n Storage as GcsClient,\n type GetSignedUrlConfig,\n type StorageOptions,\n} from \"@google-cloud/storage\";\n\nexport const MAX_SINGLE_PUT_SIZE_BYTES = 5_000_000_000;\nexport const UPLOAD_URL_TTL_SECONDS = 15 * 60;\nexport const DOWNLOAD_URL_TTL_SECONDS = 5 * 60;\n\nexport type ObjectHead = {\n ContentLength?: number;\n ContentType?: string;\n Metadata?: Record<string, string>;\n};\n\nexport type ObjectStorage = {\n bucket: string;\n backend: \"s3-compatible\" | \"aws-s3\" | \"azure-blob\" | \"gcs\";\n maxSinglePutSizeBytes: number;\n createPutUrl: (args: {\n key: string;\n contentType: string;\n sha256?: string | null;\n expiresInSeconds?: number;\n }) => Promise<{ url: string; requiredHeaders: Record<string, string>; expiresAt: Date }>;\n createGetUrl: (args: {\n key: string;\n expiresInSeconds?: number;\n }) => Promise<{ url: string; expiresAt: Date }>;\n headFile: (file: FileAsset) => Promise<ObjectHead>;\n /** Check provider existence without downloading object bytes. */\n fileExists: (file: FileAsset) => Promise<boolean>;\n getFileBytes: (file: FileAsset) => Promise<Uint8Array>;\n /**\n * Fetch exactly one inclusive byte range without materializing the complete\n * object. Returns null when the provider reports that the object is missing.\n */\n getFileRange: (\n file: FileAsset,\n range: { start: number; end: number },\n ) => Promise<Uint8Array | null>;\n /** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */\n getObjectBytes: (key: string) => Promise<{ bytes: Uint8Array; contentType?: string } | null>;\n /**\n * SERVER-SIDE authenticated direct PUT (no presign + browser fetch). For an\n * in-process upload from a trusted holder of the storage credentials (e.g. the\n * worker writing a recording), this sends the bytes straight to the storage\n * backend over the configured endpoint with the in-process SDK client — bypassing\n * the presigned-URL round-trip, which on split public/internal topologies (a\n * public `objectStorageEndpoint` with no in-cluster route) would otherwise 401.\n * Browser uploads keep using `createPutUrl`; this is the trusted-server twin.\n */\n putObject: (args: {\n key: string;\n contentType: string;\n body: Uint8Array;\n sha256?: string | null;\n }) => Promise<void>;\n /**\n * SERVER-SIDE authenticated delete of a single object by raw storage key.\n * Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not\n * 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2\n * capture GC (keep the latest 10) to reap after-image blobs that no\n * surviving revision references. Best-effort at the call site — a failed\n * delete leaves an orphan blob, never corrupts a live capture.\n */\n deleteObject: (key: string) => Promise<void>;\n};\n\nexport function createObjectStorage(settings: Settings): ObjectStorage | null {\n if (settings.objectStorageBackend === \"azure-blob\") {\n return createAzureBlobObjectStorage(settings);\n }\n if (settings.objectStorageBackend === \"gcs\") {\n return createGcsObjectStorage(settings);\n }\n return createS3CompatibleObjectStorage(settings);\n}\n\nfunction createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | null {\n if (\n settings.objectStorageBackend === \"s3-compatible\" &&\n (!settings.objectStorageEndpoint ||\n !settings.objectStorageAccessKeyId ||\n !settings.objectStorageSecretAccessKey)\n ) {\n return null;\n }\n const clientConfig: S3ClientConfig = {\n region: settings.objectStorageRegion,\n forcePathStyle: settings.objectStorageForcePathStyle,\n requestChecksumCalculation: \"WHEN_REQUIRED\",\n ...(settings.objectStorageEndpoint ? { endpoint: settings.objectStorageEndpoint } : {}),\n ...(settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey\n ? {\n credentials: {\n accessKeyId: settings.objectStorageAccessKeyId,\n secretAccessKey: settings.objectStorageSecretAccessKey,\n },\n }\n : {}),\n };\n const client = new S3Client(clientConfig);\n return {\n bucket: settings.objectStorageBucket,\n backend: settings.objectStorageBackend === \"aws-s3\" ? \"aws-s3\" : \"s3-compatible\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const requiredHeaders: Record<string, string> = {\n \"content-type\": args.contentType,\n };\n const command = new PutObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n ContentType: args.contentType,\n Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,\n });\n return {\n url: await getSignedUrl(client, command, { expiresIn }),\n requiredHeaders,\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n return {\n url: await getSignedUrl(\n client,\n new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n }),\n { expiresIn },\n ),\n expiresAt: new Date(Date.now() + expiresIn * 1000),\n };\n },\n async putObject(args) {\n // Authenticated in-process PUT against the configured (in-cluster) endpoint.\n // A presigned URL buys nothing here — the worker already holds the creds — and\n // on a split public/internal endpoint topology the presigned URL points at the\n // PUBLIC host (no MinIO route → 401). This sends bytes straight to the backend.\n await client.send(\n new PutObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n ContentType: args.contentType,\n Body: args.body,\n Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,\n }),\n );\n },\n async headFile(file) {\n const head = await client.send(\n new HeadObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }),\n );\n return objectHead({\n contentLength: head.ContentLength,\n contentType: head.ContentType,\n metadata: head.Metadata,\n });\n },\n async fileExists(file) {\n try {\n await client.send(\n new HeadObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }),\n );\n return true;\n } catch (error) {\n if (isS3NotFound(error)) return false;\n throw error;\n }\n },\n async getFileBytes(file) {\n const result = await client.send(\n new GetObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }),\n );\n return await s3BodyToBytes(result.Body, file.objectKey);\n },\n async getFileRange(file, range) {\n const length = assertFileByteRange(file, range);\n try {\n const result = await client.send(\n new GetObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n Range: `bytes=${range.start}-${range.end}`,\n }),\n );\n return await s3BodyToBoundedBytes(result.Body, file.objectKey, length);\n } catch (error) {\n if (isS3NotFound(error)) return null;\n throw error;\n }\n },\n async getObjectBytes(key) {\n try {\n const result = await client.send(\n new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: key,\n }),\n );\n const bytes = await s3BodyToBytes(result.Body, key);\n return { bytes, ...(result.ContentType ? { contentType: result.ContentType } : {}) };\n } catch (error) {\n if (isS3NotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // S3 DeleteObject is idempotent — deleting an absent key returns 204.\n await client.send(\n new DeleteObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: key,\n }),\n );\n },\n };\n}\n\nasync function s3BodyToBytes(body: unknown, objectKey: string): Promise<Uint8Array> {\n if (!body) {\n throw new Error(`Object body is empty: ${objectKey}`);\n }\n const withTransform = body as { transformToByteArray?: () => Promise<Uint8Array> };\n if (typeof withTransform.transformToByteArray === \"function\") {\n return await withTransform.transformToByteArray();\n }\n const chunks: Uint8Array[] = [];\n for await (const chunk of body as AsyncIterable<Uint8Array | Buffer | string>) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n return Buffer.concat(chunks);\n}\n\nasync function s3BodyToBoundedBytes(\n body: unknown,\n objectKey: string,\n expectedBytes: number,\n): Promise<Uint8Array> {\n if (!body) {\n throw new Error(`Object body is empty: ${objectKey}`);\n }\n const chunks: Uint8Array[] = [];\n let bytes = 0;\n for await (const chunk of body as AsyncIterable<Uint8Array | Buffer | string>) {\n const normalized = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n bytes += normalized.byteLength;\n if (bytes > expectedBytes) {\n throw new Error(`Object range exceeded requested length: ${objectKey}`);\n }\n chunks.push(normalized);\n }\n if (bytes !== expectedBytes) {\n throw new Error(\n `Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`,\n );\n }\n return Buffer.concat(chunks, bytes);\n}\n\nfunction isS3NotFound(error: unknown): boolean {\n if (!error || typeof error !== \"object\") {\n return false;\n }\n const name = \"name\" in error ? (error as { name?: unknown }).name : undefined;\n if (name === \"NoSuchKey\" || name === \"NotFound\") {\n return true;\n }\n const metadata =\n \"$metadata\" in error\n ? (error as { $metadata?: { httpStatusCode?: number } }).$metadata\n : undefined;\n return metadata?.httpStatusCode === 404;\n}\n\nfunction createGcsObjectStorage(settings: Settings): ObjectStorage {\n const client = new GcsClient(gcsClientOptions(settings));\n const bucket = client.bucket(settings.objectStorageBucket);\n return {\n bucket: settings.objectStorageBucket,\n backend: \"gcs\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const config: GetSignedUrlConfig = {\n version: \"v4\",\n action: \"write\",\n expires: expiresAt,\n contentType: args.contentType,\n };\n if (args.sha256) {\n config.extensionHeaders = { \"x-goog-meta-sha256\": args.sha256 };\n }\n const [url] = await bucket.file(args.key).getSignedUrl(config);\n return {\n url,\n requiredHeaders: {\n \"content-type\": args.contentType,\n ...(args.sha256 ? { \"x-goog-meta-sha256\": args.sha256 } : {}),\n },\n expiresAt,\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const [url] = await bucket.file(args.key).getSignedUrl({\n version: \"v4\",\n action: \"read\",\n expires: expiresAt,\n });\n return { url, expiresAt };\n },\n async putObject(args) {\n // Authenticated in-process PUT via the GCS SDK (the server holds the creds).\n await bucket.file(args.key).save(Buffer.from(args.body), {\n contentType: args.contentType,\n ...(args.sha256 ? { metadata: { metadata: { sha256: args.sha256 } } } : {}),\n });\n },\n async headFile(file) {\n const [metadata] = await bucket.file(file.objectKey).getMetadata();\n return objectHead({\n contentLength: parseContentLength(metadata.size),\n contentType: metadata.contentType,\n metadata: stringMetadata(metadata.metadata),\n });\n },\n async fileExists(file) {\n try {\n await bucket.file(file.objectKey).getMetadata();\n return true;\n } catch (error) {\n if (isGcsNotFound(error)) return false;\n throw error;\n }\n },\n async getFileBytes(file) {\n const [bytes] = await bucket.file(file.objectKey).download();\n return bytes;\n },\n async getFileRange(file, range) {\n const length = assertFileByteRange(file, range);\n try {\n const [bytes] = await bucket\n .file(file.objectKey)\n .download({ start: range.start, end: range.end });\n return exactRangeBytes(bytes, file.objectKey, length);\n } catch (error) {\n if (isGcsNotFound(error)) return null;\n throw error;\n }\n },\n async getObjectBytes(key) {\n try {\n const [bytes] = await bucket.file(key).download();\n return { bytes };\n } catch (error) {\n if (isGcsNotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // ignoreNotFound keeps the delete idempotent (a missing blob is a no-op).\n await bucket.file(key).delete({ ignoreNotFound: true });\n },\n };\n}\n\nfunction isGcsNotFound(error: unknown): boolean {\n return Boolean(error) && typeof error === \"object\" && (error as { code?: unknown }).code === 404;\n}\n\nfunction createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null {\n const sharedKey = azureSharedKeyCredential(settings);\n const serviceClient = settings.objectStorageAzureConnectionString\n ? BlobServiceClient.fromConnectionString(settings.objectStorageAzureConnectionString)\n : new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey);\n const containerClient = serviceClient.getContainerClient(settings.objectStorageBucket);\n\n return {\n bucket: settings.objectStorageBucket,\n backend: \"azure-blob\",\n maxSinglePutSizeBytes: MAX_SINGLE_PUT_SIZE_BYTES,\n async createPutUrl(args) {\n const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const blobClient = containerClient.getBlockBlobClient(args.key);\n const sas = generateBlobSASQueryParameters(\n {\n containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"cw\"),\n expiresOn: expiresAt,\n contentType: args.contentType,\n },\n sharedKey,\n ).toString();\n return {\n url: `${blobClient.url}?${sas}`,\n requiredHeaders: {\n \"content-type\": args.contentType,\n \"x-ms-blob-type\": \"BlockBlob\",\n ...(args.sha256 ? { \"x-ms-meta-sha256\": args.sha256 } : {}),\n },\n expiresAt,\n };\n },\n async createGetUrl(args) {\n const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;\n const expiresAt = new Date(Date.now() + expiresIn * 1000);\n const blobClient = containerClient.getBlobClient(args.key);\n const sas = generateBlobSASQueryParameters(\n {\n containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"r\"),\n expiresOn: expiresAt,\n },\n sharedKey,\n ).toString();\n return {\n url: `${blobClient.url}?${sas}`,\n expiresAt,\n };\n },\n async putObject(args) {\n // Authenticated in-process upload via the shared-key Azure client (no SAS).\n const blobClient = containerClient.getBlockBlobClient(args.key);\n const body = Buffer.from(args.body);\n await blobClient.upload(body, body.byteLength, {\n blobHTTPHeaders: { blobContentType: args.contentType },\n ...(args.sha256 ? { metadata: { sha256: args.sha256 } } : {}),\n });\n },\n async headFile(file) {\n return azureHeadToObjectHead(\n await containerClient.getBlobClient(file.objectKey).getProperties(),\n );\n },\n async fileExists(file) {\n try {\n await containerClient.getBlobClient(file.objectKey).getProperties();\n return true;\n } catch (error) {\n if (isAzureNotFound(error)) return false;\n throw error;\n }\n },\n async getFileBytes(file) {\n return await azureDownloadToBytes(\n await containerClient.getBlobClient(file.objectKey).download(),\n );\n },\n async getFileRange(file, range) {\n const length = assertFileByteRange(file, range);\n try {\n return await azureDownloadToBoundedBytes(\n await containerClient.getBlobClient(file.objectKey).download(range.start, length),\n file.objectKey,\n length,\n );\n } catch (error) {\n if (isAzureNotFound(error)) return null;\n throw error;\n }\n },\n async getObjectBytes(key) {\n try {\n const download = await containerClient.getBlobClient(key).download();\n const bytes = await azureDownloadToBytes(download);\n return { bytes, ...(download.contentType ? { contentType: download.contentType } : {}) };\n } catch (error) {\n if (isAzureNotFound(error)) {\n return null;\n }\n throw error;\n }\n },\n async deleteObject(key) {\n // deleteIfExists keeps the delete idempotent (a missing blob is a no-op).\n await containerClient.getBlockBlobClient(key).deleteIfExists();\n },\n };\n}\n\nfunction isAzureNotFound(error: unknown): boolean {\n return (\n Boolean(error) &&\n typeof error === \"object\" &&\n (error as { statusCode?: unknown }).statusCode === 404\n );\n}\n\nfunction azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredential {\n if (settings.objectStorageAzureConnectionString) {\n const parsed = parseConnectionString(settings.objectStorageAzureConnectionString);\n if (parsed.AccountName && parsed.AccountKey) {\n return new StorageSharedKeyCredential(parsed.AccountName, parsed.AccountKey);\n }\n throw new Error(\n \"Azure Blob connection string must include AccountName and AccountKey to create presigned URLs\",\n );\n }\n if (!settings.objectStorageAzureAccountName || !settings.objectStorageAzureAccountKey) {\n throw new Error(\"Azure Blob storage requires account name and account key\");\n }\n return new StorageSharedKeyCredential(\n settings.objectStorageAzureAccountName,\n settings.objectStorageAzureAccountKey,\n );\n}\n\nfunction azureBlobServiceUrl(settings: Settings): string {\n if (settings.objectStorageAzureEndpoint) {\n return settings.objectStorageAzureEndpoint.replace(/\\/+$/, \"\");\n }\n if (!settings.objectStorageAzureAccountName) {\n throw new Error(\"Azure Blob storage requires account name\");\n }\n return `https://${settings.objectStorageAzureAccountName}.blob.core.windows.net`;\n}\n\nfunction parseConnectionString(value: string): Record<string, string> {\n return Object.fromEntries(\n value\n .split(\";\")\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const index = part.indexOf(\"=\");\n return index === -1 ? [part, \"\"] : [part.slice(0, index), part.slice(index + 1)];\n }),\n );\n}\n\nfunction gcsClientOptions(settings: Settings): StorageOptions {\n const options: StorageOptions = {\n ...(settings.objectStorageGcsProjectId\n ? { projectId: settings.objectStorageGcsProjectId }\n : {}),\n ...(settings.objectStorageGcsKeyFilename\n ? { keyFilename: settings.objectStorageGcsKeyFilename }\n : {}),\n ...(settings.objectStorageGcsApiEndpoint\n ? { apiEndpoint: settings.objectStorageGcsApiEndpoint }\n : {}),\n };\n if (settings.objectStorageGcsCredentialsJson) {\n options.credentials = parseGcsCredentials(settings.objectStorageGcsCredentialsJson);\n }\n return options;\n}\n\nfunction parseGcsCredentials(raw: string): Record<string, string> {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\"GCS credentials JSON must be an object\");\n }\n return parsed as Record<string, string>;\n}\n\nfunction parseContentLength(value: string | number | undefined): number | undefined {\n if (typeof value === \"number\") {\n return value;\n }\n if (typeof value !== \"string\" || value.length === 0) {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\nfunction stringMetadata(\n value: Record<string, string | number | boolean | null> | undefined,\n): Record<string, string> | undefined {\n if (!value) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(value).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n );\n}\n\nfunction azureHeadToObjectHead(head: BlobGetPropertiesResponse): ObjectHead {\n return objectHead({\n contentLength: head.contentLength,\n contentType: head.contentType,\n metadata: head.metadata,\n });\n}\n\nasync function azureDownloadToBytes(download: BlobDownloadResponseParsed): Promise<Uint8Array> {\n if (!download.readableStreamBody) {\n throw new Error(\"Azure Blob download response did not include a readable body\");\n }\n const chunks: Uint8Array[] = [];\n for await (const chunk of download.readableStreamBody as AsyncIterable<\n Uint8Array | Buffer | string\n >) {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n }\n return Buffer.concat(chunks);\n}\n\nasync function azureDownloadToBoundedBytes(\n download: BlobDownloadResponseParsed,\n objectKey: string,\n expectedBytes: number,\n): Promise<Uint8Array> {\n if (!download.readableStreamBody) {\n throw new Error(\"Azure Blob download response did not include a readable body\");\n }\n const chunks: Uint8Array[] = [];\n let bytes = 0;\n for await (const chunk of download.readableStreamBody as AsyncIterable<\n Uint8Array | Buffer | string\n >) {\n const normalized = typeof chunk === \"string\" ? Buffer.from(chunk) : chunk;\n bytes += normalized.byteLength;\n if (bytes > expectedBytes) {\n throw new Error(`Object range exceeded requested length: ${objectKey}`);\n }\n chunks.push(normalized);\n }\n if (bytes !== expectedBytes) {\n throw new Error(\n `Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`,\n );\n }\n return Buffer.concat(chunks, bytes);\n}\n\nfunction assertFileByteRange(\n file: Pick<FileAsset, \"sizeBytes\" | \"objectKey\">,\n range: { start: number; end: number },\n): number {\n if (\n !Number.isSafeInteger(range.start) ||\n !Number.isSafeInteger(range.end) ||\n range.start < 0 ||\n range.end < range.start ||\n range.end >= file.sizeBytes\n ) {\n throw new RangeError(`Invalid object byte range for ${file.objectKey}`);\n }\n const length = range.end - range.start + 1;\n if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {\n throw new RangeError(\n `Object byte range exceeds ${RETAINED_OUTPUT_MAX_PAGE_BYTES} bytes for ${file.objectKey}`,\n );\n }\n return length;\n}\n\nfunction exactRangeBytes(bytes: Uint8Array, objectKey: string, expectedBytes: number): Uint8Array {\n if (bytes.byteLength !== expectedBytes) {\n throw new Error(\n `Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes.byteLength}`,\n );\n }\n return bytes;\n}\n\nfunction objectHead(input: {\n contentLength?: number | undefined;\n contentType?: string | undefined;\n metadata?: Record<string, string> | undefined;\n}): ObjectHead {\n return {\n ...(input.contentLength !== undefined ? { ContentLength: input.contentLength } : {}),\n ...(input.contentType !== undefined ? { ContentType: input.contentType } : {}),\n ...(input.metadata !== undefined ? { Metadata: input.metadata } : {}),\n };\n}\n\nexport function bytesToDataUrl(bytes: Uint8Array, contentType: string): string {\n return `data:${contentType};base64,${Buffer.from(bytes).toString(\"base64\")}`;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/storage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@aws-sdk/s3-request-presigner": "^3.1044.0",
|
|
36
36
|
"@azure/storage-blob": "^12.29.1",
|
|
37
37
|
"@google-cloud/storage": "^7.19.0",
|
|
38
|
-
"@opengeni/config": "^0.
|
|
39
|
-
"@opengeni/contracts": "^0.
|
|
38
|
+
"@opengeni/config": "^0.6.9",
|
|
39
|
+
"@opengeni/contracts": "^0.18.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
|
-
import type
|
|
2
|
+
import { RETAINED_OUTPUT_MAX_PAGE_BYTES, type FileAsset } from "@opengeni/contracts";
|
|
3
3
|
import {
|
|
4
4
|
BlobSASPermissions,
|
|
5
5
|
BlobServiceClient,
|
|
@@ -48,7 +48,17 @@ export type ObjectStorage = {
|
|
|
48
48
|
expiresInSeconds?: number;
|
|
49
49
|
}) => Promise<{ url: string; expiresAt: Date }>;
|
|
50
50
|
headFile: (file: FileAsset) => Promise<ObjectHead>;
|
|
51
|
+
/** Check provider existence without downloading object bytes. */
|
|
52
|
+
fileExists: (file: FileAsset) => Promise<boolean>;
|
|
51
53
|
getFileBytes: (file: FileAsset) => Promise<Uint8Array>;
|
|
54
|
+
/**
|
|
55
|
+
* Fetch exactly one inclusive byte range without materializing the complete
|
|
56
|
+
* object. Returns null when the provider reports that the object is missing.
|
|
57
|
+
*/
|
|
58
|
+
getFileRange: (
|
|
59
|
+
file: FileAsset,
|
|
60
|
+
range: { start: number; end: number },
|
|
61
|
+
) => Promise<Uint8Array | null>;
|
|
52
62
|
/** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */
|
|
53
63
|
getObjectBytes: (key: string) => Promise<{ bytes: Uint8Array; contentType?: string } | null>;
|
|
54
64
|
/**
|
|
@@ -70,7 +80,7 @@ export type ObjectStorage = {
|
|
|
70
80
|
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
71
81
|
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
72
82
|
* 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2
|
|
73
|
-
* capture GC (
|
|
83
|
+
* capture GC (keep the latest 10) to reap after-image blobs that no
|
|
74
84
|
* surviving revision references. Best-effort at the call site — a failed
|
|
75
85
|
* delete leaves an orphan blob, never corrupts a live capture.
|
|
76
86
|
*/
|
|
@@ -174,6 +184,20 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
174
184
|
metadata: head.Metadata,
|
|
175
185
|
});
|
|
176
186
|
},
|
|
187
|
+
async fileExists(file) {
|
|
188
|
+
try {
|
|
189
|
+
await client.send(
|
|
190
|
+
new HeadObjectCommand({
|
|
191
|
+
Bucket: file.bucket,
|
|
192
|
+
Key: file.objectKey,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
return true;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isS3NotFound(error)) return false;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
177
201
|
async getFileBytes(file) {
|
|
178
202
|
const result = await client.send(
|
|
179
203
|
new GetObjectCommand({
|
|
@@ -183,6 +207,22 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
183
207
|
);
|
|
184
208
|
return await s3BodyToBytes(result.Body, file.objectKey);
|
|
185
209
|
},
|
|
210
|
+
async getFileRange(file, range) {
|
|
211
|
+
const length = assertFileByteRange(file, range);
|
|
212
|
+
try {
|
|
213
|
+
const result = await client.send(
|
|
214
|
+
new GetObjectCommand({
|
|
215
|
+
Bucket: file.bucket,
|
|
216
|
+
Key: file.objectKey,
|
|
217
|
+
Range: `bytes=${range.start}-${range.end}`,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
return await s3BodyToBoundedBytes(result.Body, file.objectKey, length);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (isS3NotFound(error)) return null;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
186
226
|
async getObjectBytes(key) {
|
|
187
227
|
try {
|
|
188
228
|
const result = await client.send(
|
|
@@ -227,6 +267,32 @@ async function s3BodyToBytes(body: unknown, objectKey: string): Promise<Uint8Arr
|
|
|
227
267
|
return Buffer.concat(chunks);
|
|
228
268
|
}
|
|
229
269
|
|
|
270
|
+
async function s3BodyToBoundedBytes(
|
|
271
|
+
body: unknown,
|
|
272
|
+
objectKey: string,
|
|
273
|
+
expectedBytes: number,
|
|
274
|
+
): Promise<Uint8Array> {
|
|
275
|
+
if (!body) {
|
|
276
|
+
throw new Error(`Object body is empty: ${objectKey}`);
|
|
277
|
+
}
|
|
278
|
+
const chunks: Uint8Array[] = [];
|
|
279
|
+
let bytes = 0;
|
|
280
|
+
for await (const chunk of body as AsyncIterable<Uint8Array | Buffer | string>) {
|
|
281
|
+
const normalized = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
282
|
+
bytes += normalized.byteLength;
|
|
283
|
+
if (bytes > expectedBytes) {
|
|
284
|
+
throw new Error(`Object range exceeded requested length: ${objectKey}`);
|
|
285
|
+
}
|
|
286
|
+
chunks.push(normalized);
|
|
287
|
+
}
|
|
288
|
+
if (bytes !== expectedBytes) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return Buffer.concat(chunks, bytes);
|
|
294
|
+
}
|
|
295
|
+
|
|
230
296
|
function isS3NotFound(error: unknown): boolean {
|
|
231
297
|
if (!error || typeof error !== "object") {
|
|
232
298
|
return false;
|
|
@@ -296,10 +362,31 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
296
362
|
metadata: stringMetadata(metadata.metadata),
|
|
297
363
|
});
|
|
298
364
|
},
|
|
365
|
+
async fileExists(file) {
|
|
366
|
+
try {
|
|
367
|
+
await bucket.file(file.objectKey).getMetadata();
|
|
368
|
+
return true;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (isGcsNotFound(error)) return false;
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
},
|
|
299
374
|
async getFileBytes(file) {
|
|
300
375
|
const [bytes] = await bucket.file(file.objectKey).download();
|
|
301
376
|
return bytes;
|
|
302
377
|
},
|
|
378
|
+
async getFileRange(file, range) {
|
|
379
|
+
const length = assertFileByteRange(file, range);
|
|
380
|
+
try {
|
|
381
|
+
const [bytes] = await bucket
|
|
382
|
+
.file(file.objectKey)
|
|
383
|
+
.download({ start: range.start, end: range.end });
|
|
384
|
+
return exactRangeBytes(bytes, file.objectKey, length);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (isGcsNotFound(error)) return null;
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
},
|
|
303
390
|
async getObjectBytes(key) {
|
|
304
391
|
try {
|
|
305
392
|
const [bytes] = await bucket.file(key).download();
|
|
@@ -389,11 +476,33 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
389
476
|
await containerClient.getBlobClient(file.objectKey).getProperties(),
|
|
390
477
|
);
|
|
391
478
|
},
|
|
479
|
+
async fileExists(file) {
|
|
480
|
+
try {
|
|
481
|
+
await containerClient.getBlobClient(file.objectKey).getProperties();
|
|
482
|
+
return true;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (isAzureNotFound(error)) return false;
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
},
|
|
392
488
|
async getFileBytes(file) {
|
|
393
489
|
return await azureDownloadToBytes(
|
|
394
490
|
await containerClient.getBlobClient(file.objectKey).download(),
|
|
395
491
|
);
|
|
396
492
|
},
|
|
493
|
+
async getFileRange(file, range) {
|
|
494
|
+
const length = assertFileByteRange(file, range);
|
|
495
|
+
try {
|
|
496
|
+
return await azureDownloadToBoundedBytes(
|
|
497
|
+
await containerClient.getBlobClient(file.objectKey).download(range.start, length),
|
|
498
|
+
file.objectKey,
|
|
499
|
+
length,
|
|
500
|
+
);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if (isAzureNotFound(error)) return null;
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
},
|
|
397
506
|
async getObjectBytes(key) {
|
|
398
507
|
try {
|
|
399
508
|
const download = await containerClient.getBlobClient(key).download();
|
|
@@ -534,6 +643,65 @@ async function azureDownloadToBytes(download: BlobDownloadResponseParsed): Promi
|
|
|
534
643
|
return Buffer.concat(chunks);
|
|
535
644
|
}
|
|
536
645
|
|
|
646
|
+
async function azureDownloadToBoundedBytes(
|
|
647
|
+
download: BlobDownloadResponseParsed,
|
|
648
|
+
objectKey: string,
|
|
649
|
+
expectedBytes: number,
|
|
650
|
+
): Promise<Uint8Array> {
|
|
651
|
+
if (!download.readableStreamBody) {
|
|
652
|
+
throw new Error("Azure Blob download response did not include a readable body");
|
|
653
|
+
}
|
|
654
|
+
const chunks: Uint8Array[] = [];
|
|
655
|
+
let bytes = 0;
|
|
656
|
+
for await (const chunk of download.readableStreamBody as AsyncIterable<
|
|
657
|
+
Uint8Array | Buffer | string
|
|
658
|
+
>) {
|
|
659
|
+
const normalized = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
660
|
+
bytes += normalized.byteLength;
|
|
661
|
+
if (bytes > expectedBytes) {
|
|
662
|
+
throw new Error(`Object range exceeded requested length: ${objectKey}`);
|
|
663
|
+
}
|
|
664
|
+
chunks.push(normalized);
|
|
665
|
+
}
|
|
666
|
+
if (bytes !== expectedBytes) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes}`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return Buffer.concat(chunks, bytes);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function assertFileByteRange(
|
|
675
|
+
file: Pick<FileAsset, "sizeBytes" | "objectKey">,
|
|
676
|
+
range: { start: number; end: number },
|
|
677
|
+
): number {
|
|
678
|
+
if (
|
|
679
|
+
!Number.isSafeInteger(range.start) ||
|
|
680
|
+
!Number.isSafeInteger(range.end) ||
|
|
681
|
+
range.start < 0 ||
|
|
682
|
+
range.end < range.start ||
|
|
683
|
+
range.end >= file.sizeBytes
|
|
684
|
+
) {
|
|
685
|
+
throw new RangeError(`Invalid object byte range for ${file.objectKey}`);
|
|
686
|
+
}
|
|
687
|
+
const length = range.end - range.start + 1;
|
|
688
|
+
if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
689
|
+
throw new RangeError(
|
|
690
|
+
`Object byte range exceeds ${RETAINED_OUTPUT_MAX_PAGE_BYTES} bytes for ${file.objectKey}`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return length;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function exactRangeBytes(bytes: Uint8Array, objectKey: string, expectedBytes: number): Uint8Array {
|
|
697
|
+
if (bytes.byteLength !== expectedBytes) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Object range length mismatch for ${objectKey}: expected ${expectedBytes}, received ${bytes.byteLength}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return bytes;
|
|
703
|
+
}
|
|
704
|
+
|
|
537
705
|
function objectHead(input: {
|
|
538
706
|
contentLength?: number | undefined;
|
|
539
707
|
contentType?: string | undefined;
|