@opengeni/storage 0.2.8 → 0.2.9
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 +9 -0
- package/dist/index.js +100 -50
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
- package/src/index.ts +166 -66
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,15 @@ type ObjectStorage = {
|
|
|
52
52
|
body: Uint8Array;
|
|
53
53
|
sha256?: string | null;
|
|
54
54
|
}) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
57
|
+
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
58
|
+
* 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2
|
|
59
|
+
* capture GC (dossier §10.1 keep-latest-10) to reap after-image blobs that no
|
|
60
|
+
* surviving revision references. Best-effort at the call site — a failed
|
|
61
|
+
* delete leaves an orphan blob, never corrupts a live capture.
|
|
62
|
+
*/
|
|
63
|
+
deleteObject: (key: string) => Promise<void>;
|
|
55
64
|
};
|
|
56
65
|
declare function createObjectStorage(settings: Settings): ObjectStorage | null;
|
|
57
66
|
declare function bytesToDataUrl(bytes: Uint8Array, contentType: string): string;
|
package/dist/index.js
CHANGED
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
StorageSharedKeyCredential
|
|
7
7
|
} from "@azure/storage-blob";
|
|
8
8
|
import {
|
|
9
|
+
DeleteObjectCommand,
|
|
9
10
|
GetObjectCommand,
|
|
10
11
|
HeadObjectCommand,
|
|
11
12
|
PutObjectCommand,
|
|
12
13
|
S3Client
|
|
13
14
|
} from "@aws-sdk/client-s3";
|
|
14
15
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
Storage as GcsClient
|
|
18
|
+
} from "@google-cloud/storage";
|
|
16
19
|
var MAX_SINGLE_PUT_SIZE_BYTES = 5e9;
|
|
17
20
|
var UPLOAD_URL_TTL_SECONDS = 15 * 60;
|
|
18
21
|
var DOWNLOAD_URL_TTL_SECONDS = 5 * 60;
|
|
@@ -34,10 +37,12 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
34
37
|
forcePathStyle: settings.objectStorageForcePathStyle,
|
|
35
38
|
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
36
39
|
...settings.objectStorageEndpoint ? { endpoint: settings.objectStorageEndpoint } : {},
|
|
37
|
-
...settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey ? {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
...settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey ? {
|
|
41
|
+
credentials: {
|
|
42
|
+
accessKeyId: settings.objectStorageAccessKeyId,
|
|
43
|
+
secretAccessKey: settings.objectStorageSecretAccessKey
|
|
44
|
+
}
|
|
45
|
+
} : {}
|
|
41
46
|
};
|
|
42
47
|
const client = new S3Client(clientConfig);
|
|
43
48
|
return {
|
|
@@ -64,27 +69,35 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
64
69
|
async createGetUrl(args) {
|
|
65
70
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
66
71
|
return {
|
|
67
|
-
url: await getSignedUrl(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
url: await getSignedUrl(
|
|
73
|
+
client,
|
|
74
|
+
new GetObjectCommand({
|
|
75
|
+
Bucket: settings.objectStorageBucket,
|
|
76
|
+
Key: args.key
|
|
77
|
+
}),
|
|
78
|
+
{ expiresIn }
|
|
79
|
+
),
|
|
71
80
|
expiresAt: new Date(Date.now() + expiresIn * 1e3)
|
|
72
81
|
};
|
|
73
82
|
},
|
|
74
83
|
async putObject(args) {
|
|
75
|
-
await client.send(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
await client.send(
|
|
85
|
+
new PutObjectCommand({
|
|
86
|
+
Bucket: settings.objectStorageBucket,
|
|
87
|
+
Key: args.key,
|
|
88
|
+
ContentType: args.contentType,
|
|
89
|
+
Body: args.body,
|
|
90
|
+
Metadata: args.sha256 ? { sha256: args.sha256 } : void 0
|
|
91
|
+
})
|
|
92
|
+
);
|
|
82
93
|
},
|
|
83
94
|
async headFile(file) {
|
|
84
|
-
const head = await client.send(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
const head = await client.send(
|
|
96
|
+
new HeadObjectCommand({
|
|
97
|
+
Bucket: file.bucket,
|
|
98
|
+
Key: file.objectKey
|
|
99
|
+
})
|
|
100
|
+
);
|
|
88
101
|
return objectHead({
|
|
89
102
|
contentLength: head.ContentLength,
|
|
90
103
|
contentType: head.ContentType,
|
|
@@ -92,18 +105,22 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
92
105
|
});
|
|
93
106
|
},
|
|
94
107
|
async getFileBytes(file) {
|
|
95
|
-
const result = await client.send(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
const result = await client.send(
|
|
109
|
+
new GetObjectCommand({
|
|
110
|
+
Bucket: file.bucket,
|
|
111
|
+
Key: file.objectKey
|
|
112
|
+
})
|
|
113
|
+
);
|
|
99
114
|
return await s3BodyToBytes(result.Body, file.objectKey);
|
|
100
115
|
},
|
|
101
116
|
async getObjectBytes(key) {
|
|
102
117
|
try {
|
|
103
|
-
const result = await client.send(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
const result = await client.send(
|
|
119
|
+
new GetObjectCommand({
|
|
120
|
+
Bucket: settings.objectStorageBucket,
|
|
121
|
+
Key: key
|
|
122
|
+
})
|
|
123
|
+
);
|
|
107
124
|
const bytes = await s3BodyToBytes(result.Body, key);
|
|
108
125
|
return { bytes, ...result.ContentType ? { contentType: result.ContentType } : {} };
|
|
109
126
|
} catch (error) {
|
|
@@ -112,6 +129,14 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
112
129
|
}
|
|
113
130
|
throw error;
|
|
114
131
|
}
|
|
132
|
+
},
|
|
133
|
+
async deleteObject(key) {
|
|
134
|
+
await client.send(
|
|
135
|
+
new DeleteObjectCommand({
|
|
136
|
+
Bucket: settings.objectStorageBucket,
|
|
137
|
+
Key: key
|
|
138
|
+
})
|
|
139
|
+
);
|
|
115
140
|
}
|
|
116
141
|
};
|
|
117
142
|
}
|
|
@@ -207,6 +232,9 @@ function createGcsObjectStorage(settings) {
|
|
|
207
232
|
}
|
|
208
233
|
throw error;
|
|
209
234
|
}
|
|
235
|
+
},
|
|
236
|
+
async deleteObject(key) {
|
|
237
|
+
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
210
238
|
}
|
|
211
239
|
};
|
|
212
240
|
}
|
|
@@ -225,13 +253,16 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
225
253
|
const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;
|
|
226
254
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3);
|
|
227
255
|
const blobClient = containerClient.getBlockBlobClient(args.key);
|
|
228
|
-
const sas = generateBlobSASQueryParameters(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
const sas = generateBlobSASQueryParameters(
|
|
257
|
+
{
|
|
258
|
+
containerName: settings.objectStorageBucket,
|
|
259
|
+
blobName: args.key,
|
|
260
|
+
permissions: BlobSASPermissions.parse("cw"),
|
|
261
|
+
expiresOn: expiresAt,
|
|
262
|
+
contentType: args.contentType
|
|
263
|
+
},
|
|
264
|
+
sharedKey
|
|
265
|
+
).toString();
|
|
235
266
|
return {
|
|
236
267
|
url: `${blobClient.url}?${sas}`,
|
|
237
268
|
requiredHeaders: {
|
|
@@ -246,12 +277,15 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
246
277
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
247
278
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3);
|
|
248
279
|
const blobClient = containerClient.getBlobClient(args.key);
|
|
249
|
-
const sas = generateBlobSASQueryParameters(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
280
|
+
const sas = generateBlobSASQueryParameters(
|
|
281
|
+
{
|
|
282
|
+
containerName: settings.objectStorageBucket,
|
|
283
|
+
blobName: args.key,
|
|
284
|
+
permissions: BlobSASPermissions.parse("r"),
|
|
285
|
+
expiresOn: expiresAt
|
|
286
|
+
},
|
|
287
|
+
sharedKey
|
|
288
|
+
).toString();
|
|
255
289
|
return {
|
|
256
290
|
url: `${blobClient.url}?${sas}`,
|
|
257
291
|
expiresAt
|
|
@@ -266,10 +300,14 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
266
300
|
});
|
|
267
301
|
},
|
|
268
302
|
async headFile(file) {
|
|
269
|
-
return azureHeadToObjectHead(
|
|
303
|
+
return azureHeadToObjectHead(
|
|
304
|
+
await containerClient.getBlobClient(file.objectKey).getProperties()
|
|
305
|
+
);
|
|
270
306
|
},
|
|
271
307
|
async getFileBytes(file) {
|
|
272
|
-
return await azureDownloadToBytes(
|
|
308
|
+
return await azureDownloadToBytes(
|
|
309
|
+
await containerClient.getBlobClient(file.objectKey).download()
|
|
310
|
+
);
|
|
273
311
|
},
|
|
274
312
|
async getObjectBytes(key) {
|
|
275
313
|
try {
|
|
@@ -282,6 +320,9 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
282
320
|
}
|
|
283
321
|
throw error;
|
|
284
322
|
}
|
|
323
|
+
},
|
|
324
|
+
async deleteObject(key) {
|
|
325
|
+
await containerClient.getBlockBlobClient(key).deleteIfExists();
|
|
285
326
|
}
|
|
286
327
|
};
|
|
287
328
|
}
|
|
@@ -294,12 +335,17 @@ function azureSharedKeyCredential(settings) {
|
|
|
294
335
|
if (parsed.AccountName && parsed.AccountKey) {
|
|
295
336
|
return new StorageSharedKeyCredential(parsed.AccountName, parsed.AccountKey);
|
|
296
337
|
}
|
|
297
|
-
throw new Error(
|
|
338
|
+
throw new Error(
|
|
339
|
+
"Azure Blob connection string must include AccountName and AccountKey to create presigned URLs"
|
|
340
|
+
);
|
|
298
341
|
}
|
|
299
342
|
if (!settings.objectStorageAzureAccountName || !settings.objectStorageAzureAccountKey) {
|
|
300
343
|
throw new Error("Azure Blob storage requires account name and account key");
|
|
301
344
|
}
|
|
302
|
-
return new StorageSharedKeyCredential(
|
|
345
|
+
return new StorageSharedKeyCredential(
|
|
346
|
+
settings.objectStorageAzureAccountName,
|
|
347
|
+
settings.objectStorageAzureAccountKey
|
|
348
|
+
);
|
|
303
349
|
}
|
|
304
350
|
function azureBlobServiceUrl(settings) {
|
|
305
351
|
if (settings.objectStorageAzureEndpoint) {
|
|
@@ -311,10 +357,12 @@ function azureBlobServiceUrl(settings) {
|
|
|
311
357
|
return `https://${settings.objectStorageAzureAccountName}.blob.core.windows.net`;
|
|
312
358
|
}
|
|
313
359
|
function parseConnectionString(value) {
|
|
314
|
-
return Object.fromEntries(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
360
|
+
return Object.fromEntries(
|
|
361
|
+
value.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
362
|
+
const index = part.indexOf("=");
|
|
363
|
+
return index === -1 ? [part, ""] : [part.slice(0, index), part.slice(index + 1)];
|
|
364
|
+
})
|
|
365
|
+
);
|
|
318
366
|
}
|
|
319
367
|
function gcsClientOptions(settings) {
|
|
320
368
|
const options = {
|
|
@@ -349,7 +397,9 @@ function stringMetadata(value) {
|
|
|
349
397
|
return void 0;
|
|
350
398
|
}
|
|
351
399
|
return Object.fromEntries(
|
|
352
|
-
Object.entries(value).filter(
|
|
400
|
+
Object.entries(value).filter(
|
|
401
|
+
(entry) => typeof entry[1] === "string"
|
|
402
|
+
)
|
|
353
403
|
);
|
|
354
404
|
}
|
|
355
405
|
function azureHeadToObjectHead(head) {
|
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 BlockBlobClient,\n generateBlobSASQueryParameters,\n StorageSharedKeyCredential,\n type BlobDownloadResponseParsed,\n type BlobGetPropertiesResponse,\n} from \"@azure/storage-blob\";\nimport {\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 { Storage as GcsClient, type GetSignedUrlConfig, type StorageOptions } 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: { key: string; contentType: string; sha256?: string | null; expiresInSeconds?: number }) => Promise<{ url: string; requiredHeaders: Record<string, string>; expiresAt: Date }>;\n createGetUrl: (args: { key: string; expiresInSeconds?: number }) => 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: { key: string; contentType: string; body: Uint8Array; sha256?: string | null }) => 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 (settings.objectStorageBackend === \"s3-compatible\" && (!settings.objectStorageEndpoint || !settings.objectStorageAccessKeyId || !settings.objectStorageSecretAccessKey)) {\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 ? { credentials: {\n accessKeyId: settings.objectStorageAccessKeyId,\n secretAccessKey: settings.objectStorageSecretAccessKey,\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(client, new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: args.key,\n }), { expiresIn }),\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(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 async headFile(file) {\n const head = await client.send(new HeadObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\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(new GetObjectCommand({\n Bucket: file.bucket,\n Key: file.objectKey,\n }));\n return await s3BodyToBytes(result.Body, file.objectKey);\n },\n async getObjectBytes(key) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: settings.objectStorageBucket,\n Key: key,\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 };\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 = \"$metadata\" in error ? (error as { $metadata?: { httpStatusCode?: number } }).$metadata : 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 };\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 containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"cw\"),\n expiresOn: expiresAt,\n contentType: args.contentType,\n }, sharedKey).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 containerName: settings.objectStorageBucket,\n blobName: args.key,\n permissions: BlobSASPermissions.parse(\"r\"),\n expiresOn: expiresAt,\n }, sharedKey).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(await containerClient.getBlobClient(file.objectKey).getProperties());\n },\n async getFileBytes(file) {\n return await azureDownloadToBytes(await containerClient.getBlobClient(file.objectKey).download());\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 };\n}\n\nfunction isAzureNotFound(error: unknown): boolean {\n return Boolean(error) && typeof error === \"object\" && (error as { statusCode?: unknown }).statusCode === 404;\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(\"Azure Blob connection string must include AccountName and AccountKey to create presigned URLs\");\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(settings.objectStorageAzureAccountName, settings.objectStorageAzureAccountKey);\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(value.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\nfunction gcsClientOptions(settings: Settings): StorageOptions {\n const options: StorageOptions = {\n ...(settings.objectStorageGcsProjectId ? { projectId: settings.objectStorageGcsProjectId } : {}),\n ...(settings.objectStorageGcsKeyFilename ? { keyFilename: settings.objectStorageGcsKeyFilename } : {}),\n ...(settings.objectStorageGcsApiEndpoint ? { apiEndpoint: settings.objectStorageGcsApiEndpoint } : {}),\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(value: Record<string, string | number | boolean | null> | undefined): Record<string, string> | undefined {\n if (!value) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(value)\n .filter((entry): entry is [string, string] => typeof entry[1] === \"string\"),\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<Uint8Array | Buffer | string>) {\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 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":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/storage",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.2.9",
|
|
5
4
|
"license": "Apache-2.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
8
|
+
"directory": "packages/storage"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
6
15
|
"main": "./dist/index.js",
|
|
7
16
|
"module": "./dist/index.js",
|
|
8
17
|
"types": "./dist/index.d.ts",
|
|
@@ -12,17 +21,13 @@
|
|
|
12
21
|
"import": "./dist/index.js"
|
|
13
22
|
}
|
|
14
23
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"src"
|
|
18
|
-
],
|
|
19
24
|
"publishConfig": {
|
|
20
25
|
"access": "public",
|
|
21
26
|
"provenance": true
|
|
22
27
|
},
|
|
23
28
|
"scripts": {
|
|
24
29
|
"build": "tsup",
|
|
25
|
-
"typecheck": "
|
|
30
|
+
"typecheck": "tsgo --noEmit",
|
|
26
31
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
27
32
|
},
|
|
28
33
|
"dependencies": {
|
|
@@ -30,12 +35,7 @@
|
|
|
30
35
|
"@aws-sdk/s3-request-presigner": "^3.1044.0",
|
|
31
36
|
"@azure/storage-blob": "^12.29.1",
|
|
32
37
|
"@google-cloud/storage": "^7.19.0",
|
|
33
|
-
"@opengeni/config": "^0.
|
|
34
|
-
"@opengeni/contracts": "^0.
|
|
35
|
-
},
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
39
|
-
"directory": "packages/storage"
|
|
38
|
+
"@opengeni/config": "^0.5.0",
|
|
39
|
+
"@opengeni/contracts": "^0.10.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,13 @@ import type { FileAsset } from "@opengeni/contracts";
|
|
|
3
3
|
import {
|
|
4
4
|
BlobSASPermissions,
|
|
5
5
|
BlobServiceClient,
|
|
6
|
-
BlockBlobClient,
|
|
7
6
|
generateBlobSASQueryParameters,
|
|
8
7
|
StorageSharedKeyCredential,
|
|
9
8
|
type BlobDownloadResponseParsed,
|
|
10
9
|
type BlobGetPropertiesResponse,
|
|
11
10
|
} from "@azure/storage-blob";
|
|
12
11
|
import {
|
|
12
|
+
DeleteObjectCommand,
|
|
13
13
|
GetObjectCommand,
|
|
14
14
|
HeadObjectCommand,
|
|
15
15
|
PutObjectCommand,
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
type S3ClientConfig,
|
|
18
18
|
} from "@aws-sdk/client-s3";
|
|
19
19
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
Storage as GcsClient,
|
|
22
|
+
type GetSignedUrlConfig,
|
|
23
|
+
type StorageOptions,
|
|
24
|
+
} from "@google-cloud/storage";
|
|
21
25
|
|
|
22
26
|
export const MAX_SINGLE_PUT_SIZE_BYTES = 5_000_000_000;
|
|
23
27
|
export const UPLOAD_URL_TTL_SECONDS = 15 * 60;
|
|
@@ -33,8 +37,16 @@ export type ObjectStorage = {
|
|
|
33
37
|
bucket: string;
|
|
34
38
|
backend: "s3-compatible" | "aws-s3" | "azure-blob" | "gcs";
|
|
35
39
|
maxSinglePutSizeBytes: number;
|
|
36
|
-
createPutUrl: (args: {
|
|
37
|
-
|
|
40
|
+
createPutUrl: (args: {
|
|
41
|
+
key: string;
|
|
42
|
+
contentType: string;
|
|
43
|
+
sha256?: string | null;
|
|
44
|
+
expiresInSeconds?: number;
|
|
45
|
+
}) => Promise<{ url: string; requiredHeaders: Record<string, string>; expiresAt: Date }>;
|
|
46
|
+
createGetUrl: (args: {
|
|
47
|
+
key: string;
|
|
48
|
+
expiresInSeconds?: number;
|
|
49
|
+
}) => Promise<{ url: string; expiresAt: Date }>;
|
|
38
50
|
headFile: (file: FileAsset) => Promise<ObjectHead>;
|
|
39
51
|
getFileBytes: (file: FileAsset) => Promise<Uint8Array>;
|
|
40
52
|
/** Fetch an object by raw storage key (not a tracked FileAsset). Returns null on 404/missing. */
|
|
@@ -48,7 +60,21 @@ export type ObjectStorage = {
|
|
|
48
60
|
* public `objectStorageEndpoint` with no in-cluster route) would otherwise 401.
|
|
49
61
|
* Browser uploads keep using `createPutUrl`; this is the trusted-server twin.
|
|
50
62
|
*/
|
|
51
|
-
putObject: (args: {
|
|
63
|
+
putObject: (args: {
|
|
64
|
+
key: string;
|
|
65
|
+
contentType: string;
|
|
66
|
+
body: Uint8Array;
|
|
67
|
+
sha256?: string | null;
|
|
68
|
+
}) => Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* SERVER-SIDE authenticated delete of a single object by raw storage key.
|
|
71
|
+
* Idempotent: a missing key is a no-op (S3/GCS/Azure delete-by-key does not
|
|
72
|
+
* 404 on absent objects, or the 404 is swallowed). Used by the Workbench v2
|
|
73
|
+
* capture GC (dossier §10.1 keep-latest-10) to reap after-image blobs that no
|
|
74
|
+
* surviving revision references. Best-effort at the call site — a failed
|
|
75
|
+
* delete leaves an orphan blob, never corrupts a live capture.
|
|
76
|
+
*/
|
|
77
|
+
deleteObject: (key: string) => Promise<void>;
|
|
52
78
|
};
|
|
53
79
|
|
|
54
80
|
export function createObjectStorage(settings: Settings): ObjectStorage | null {
|
|
@@ -62,7 +88,12 @@ export function createObjectStorage(settings: Settings): ObjectStorage | null {
|
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | null {
|
|
65
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
settings.objectStorageBackend === "s3-compatible" &&
|
|
93
|
+
(!settings.objectStorageEndpoint ||
|
|
94
|
+
!settings.objectStorageAccessKeyId ||
|
|
95
|
+
!settings.objectStorageSecretAccessKey)
|
|
96
|
+
) {
|
|
66
97
|
return null;
|
|
67
98
|
}
|
|
68
99
|
const clientConfig: S3ClientConfig = {
|
|
@@ -70,10 +101,14 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
70
101
|
forcePathStyle: settings.objectStorageForcePathStyle,
|
|
71
102
|
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
72
103
|
...(settings.objectStorageEndpoint ? { endpoint: settings.objectStorageEndpoint } : {}),
|
|
73
|
-
...(settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
...(settings.objectStorageAccessKeyId && settings.objectStorageSecretAccessKey
|
|
105
|
+
? {
|
|
106
|
+
credentials: {
|
|
107
|
+
accessKeyId: settings.objectStorageAccessKeyId,
|
|
108
|
+
secretAccessKey: settings.objectStorageSecretAccessKey,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
77
112
|
};
|
|
78
113
|
const client = new S3Client(clientConfig);
|
|
79
114
|
return {
|
|
@@ -100,10 +135,14 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
100
135
|
async createGetUrl(args) {
|
|
101
136
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
102
137
|
return {
|
|
103
|
-
url: await getSignedUrl(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
url: await getSignedUrl(
|
|
139
|
+
client,
|
|
140
|
+
new GetObjectCommand({
|
|
141
|
+
Bucket: settings.objectStorageBucket,
|
|
142
|
+
Key: args.key,
|
|
143
|
+
}),
|
|
144
|
+
{ expiresIn },
|
|
145
|
+
),
|
|
107
146
|
expiresAt: new Date(Date.now() + expiresIn * 1000),
|
|
108
147
|
};
|
|
109
148
|
},
|
|
@@ -112,19 +151,23 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
112
151
|
// A presigned URL buys nothing here — the worker already holds the creds — and
|
|
113
152
|
// on a split public/internal endpoint topology the presigned URL points at the
|
|
114
153
|
// PUBLIC host (no MinIO route → 401). This sends bytes straight to the backend.
|
|
115
|
-
await client.send(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
await client.send(
|
|
155
|
+
new PutObjectCommand({
|
|
156
|
+
Bucket: settings.objectStorageBucket,
|
|
157
|
+
Key: args.key,
|
|
158
|
+
ContentType: args.contentType,
|
|
159
|
+
Body: args.body,
|
|
160
|
+
Metadata: args.sha256 ? { sha256: args.sha256 } : undefined,
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
122
163
|
},
|
|
123
164
|
async headFile(file) {
|
|
124
|
-
const head = await client.send(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
const head = await client.send(
|
|
166
|
+
new HeadObjectCommand({
|
|
167
|
+
Bucket: file.bucket,
|
|
168
|
+
Key: file.objectKey,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
128
171
|
return objectHead({
|
|
129
172
|
contentLength: head.ContentLength,
|
|
130
173
|
contentType: head.ContentType,
|
|
@@ -132,18 +175,22 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
132
175
|
});
|
|
133
176
|
},
|
|
134
177
|
async getFileBytes(file) {
|
|
135
|
-
const result = await client.send(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
const result = await client.send(
|
|
179
|
+
new GetObjectCommand({
|
|
180
|
+
Bucket: file.bucket,
|
|
181
|
+
Key: file.objectKey,
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
139
184
|
return await s3BodyToBytes(result.Body, file.objectKey);
|
|
140
185
|
},
|
|
141
186
|
async getObjectBytes(key) {
|
|
142
187
|
try {
|
|
143
|
-
const result = await client.send(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
const result = await client.send(
|
|
189
|
+
new GetObjectCommand({
|
|
190
|
+
Bucket: settings.objectStorageBucket,
|
|
191
|
+
Key: key,
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
147
194
|
const bytes = await s3BodyToBytes(result.Body, key);
|
|
148
195
|
return { bytes, ...(result.ContentType ? { contentType: result.ContentType } : {}) };
|
|
149
196
|
} catch (error) {
|
|
@@ -153,6 +200,15 @@ function createS3CompatibleObjectStorage(settings: Settings): ObjectStorage | nu
|
|
|
153
200
|
throw error;
|
|
154
201
|
}
|
|
155
202
|
},
|
|
203
|
+
async deleteObject(key) {
|
|
204
|
+
// S3 DeleteObject is idempotent — deleting an absent key returns 204.
|
|
205
|
+
await client.send(
|
|
206
|
+
new DeleteObjectCommand({
|
|
207
|
+
Bucket: settings.objectStorageBucket,
|
|
208
|
+
Key: key,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
},
|
|
156
212
|
};
|
|
157
213
|
}
|
|
158
214
|
|
|
@@ -179,7 +235,10 @@ function isS3NotFound(error: unknown): boolean {
|
|
|
179
235
|
if (name === "NoSuchKey" || name === "NotFound") {
|
|
180
236
|
return true;
|
|
181
237
|
}
|
|
182
|
-
const metadata =
|
|
238
|
+
const metadata =
|
|
239
|
+
"$metadata" in error
|
|
240
|
+
? (error as { $metadata?: { httpStatusCode?: number } }).$metadata
|
|
241
|
+
: undefined;
|
|
183
242
|
return metadata?.httpStatusCode === 404;
|
|
184
243
|
}
|
|
185
244
|
|
|
@@ -252,6 +311,10 @@ function createGcsObjectStorage(settings: Settings): ObjectStorage {
|
|
|
252
311
|
throw error;
|
|
253
312
|
}
|
|
254
313
|
},
|
|
314
|
+
async deleteObject(key) {
|
|
315
|
+
// ignoreNotFound keeps the delete idempotent (a missing blob is a no-op).
|
|
316
|
+
await bucket.file(key).delete({ ignoreNotFound: true });
|
|
317
|
+
},
|
|
255
318
|
};
|
|
256
319
|
}
|
|
257
320
|
|
|
@@ -274,13 +337,16 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
274
337
|
const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;
|
|
275
338
|
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
276
339
|
const blobClient = containerClient.getBlockBlobClient(args.key);
|
|
277
|
-
const sas = generateBlobSASQueryParameters(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
340
|
+
const sas = generateBlobSASQueryParameters(
|
|
341
|
+
{
|
|
342
|
+
containerName: settings.objectStorageBucket,
|
|
343
|
+
blobName: args.key,
|
|
344
|
+
permissions: BlobSASPermissions.parse("cw"),
|
|
345
|
+
expiresOn: expiresAt,
|
|
346
|
+
contentType: args.contentType,
|
|
347
|
+
},
|
|
348
|
+
sharedKey,
|
|
349
|
+
).toString();
|
|
284
350
|
return {
|
|
285
351
|
url: `${blobClient.url}?${sas}`,
|
|
286
352
|
requiredHeaders: {
|
|
@@ -295,12 +361,15 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
295
361
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
296
362
|
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
297
363
|
const blobClient = containerClient.getBlobClient(args.key);
|
|
298
|
-
const sas = generateBlobSASQueryParameters(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
364
|
+
const sas = generateBlobSASQueryParameters(
|
|
365
|
+
{
|
|
366
|
+
containerName: settings.objectStorageBucket,
|
|
367
|
+
blobName: args.key,
|
|
368
|
+
permissions: BlobSASPermissions.parse("r"),
|
|
369
|
+
expiresOn: expiresAt,
|
|
370
|
+
},
|
|
371
|
+
sharedKey,
|
|
372
|
+
).toString();
|
|
304
373
|
return {
|
|
305
374
|
url: `${blobClient.url}?${sas}`,
|
|
306
375
|
expiresAt,
|
|
@@ -316,10 +385,14 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
316
385
|
});
|
|
317
386
|
},
|
|
318
387
|
async headFile(file) {
|
|
319
|
-
return azureHeadToObjectHead(
|
|
388
|
+
return azureHeadToObjectHead(
|
|
389
|
+
await containerClient.getBlobClient(file.objectKey).getProperties(),
|
|
390
|
+
);
|
|
320
391
|
},
|
|
321
392
|
async getFileBytes(file) {
|
|
322
|
-
return await azureDownloadToBytes(
|
|
393
|
+
return await azureDownloadToBytes(
|
|
394
|
+
await containerClient.getBlobClient(file.objectKey).download(),
|
|
395
|
+
);
|
|
323
396
|
},
|
|
324
397
|
async getObjectBytes(key) {
|
|
325
398
|
try {
|
|
@@ -333,11 +406,19 @@ function createAzureBlobObjectStorage(settings: Settings): ObjectStorage | null
|
|
|
333
406
|
throw error;
|
|
334
407
|
}
|
|
335
408
|
},
|
|
409
|
+
async deleteObject(key) {
|
|
410
|
+
// deleteIfExists keeps the delete idempotent (a missing blob is a no-op).
|
|
411
|
+
await containerClient.getBlockBlobClient(key).deleteIfExists();
|
|
412
|
+
},
|
|
336
413
|
};
|
|
337
414
|
}
|
|
338
415
|
|
|
339
416
|
function isAzureNotFound(error: unknown): boolean {
|
|
340
|
-
return
|
|
417
|
+
return (
|
|
418
|
+
Boolean(error) &&
|
|
419
|
+
typeof error === "object" &&
|
|
420
|
+
(error as { statusCode?: unknown }).statusCode === 404
|
|
421
|
+
);
|
|
341
422
|
}
|
|
342
423
|
|
|
343
424
|
function azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredential {
|
|
@@ -346,12 +427,17 @@ function azureSharedKeyCredential(settings: Settings): StorageSharedKeyCredentia
|
|
|
346
427
|
if (parsed.AccountName && parsed.AccountKey) {
|
|
347
428
|
return new StorageSharedKeyCredential(parsed.AccountName, parsed.AccountKey);
|
|
348
429
|
}
|
|
349
|
-
throw new Error(
|
|
430
|
+
throw new Error(
|
|
431
|
+
"Azure Blob connection string must include AccountName and AccountKey to create presigned URLs",
|
|
432
|
+
);
|
|
350
433
|
}
|
|
351
434
|
if (!settings.objectStorageAzureAccountName || !settings.objectStorageAzureAccountKey) {
|
|
352
435
|
throw new Error("Azure Blob storage requires account name and account key");
|
|
353
436
|
}
|
|
354
|
-
return new StorageSharedKeyCredential(
|
|
437
|
+
return new StorageSharedKeyCredential(
|
|
438
|
+
settings.objectStorageAzureAccountName,
|
|
439
|
+
settings.objectStorageAzureAccountKey,
|
|
440
|
+
);
|
|
355
441
|
}
|
|
356
442
|
|
|
357
443
|
function azureBlobServiceUrl(settings: Settings): string {
|
|
@@ -365,20 +451,29 @@ function azureBlobServiceUrl(settings: Settings): string {
|
|
|
365
451
|
}
|
|
366
452
|
|
|
367
453
|
function parseConnectionString(value: string): Record<string, string> {
|
|
368
|
-
return Object.fromEntries(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
454
|
+
return Object.fromEntries(
|
|
455
|
+
value
|
|
456
|
+
.split(";")
|
|
457
|
+
.map((part) => part.trim())
|
|
458
|
+
.filter(Boolean)
|
|
459
|
+
.map((part) => {
|
|
460
|
+
const index = part.indexOf("=");
|
|
461
|
+
return index === -1 ? [part, ""] : [part.slice(0, index), part.slice(index + 1)];
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
375
464
|
}
|
|
376
465
|
|
|
377
466
|
function gcsClientOptions(settings: Settings): StorageOptions {
|
|
378
467
|
const options: StorageOptions = {
|
|
379
|
-
...(settings.objectStorageGcsProjectId
|
|
380
|
-
|
|
381
|
-
|
|
468
|
+
...(settings.objectStorageGcsProjectId
|
|
469
|
+
? { projectId: settings.objectStorageGcsProjectId }
|
|
470
|
+
: {}),
|
|
471
|
+
...(settings.objectStorageGcsKeyFilename
|
|
472
|
+
? { keyFilename: settings.objectStorageGcsKeyFilename }
|
|
473
|
+
: {}),
|
|
474
|
+
...(settings.objectStorageGcsApiEndpoint
|
|
475
|
+
? { apiEndpoint: settings.objectStorageGcsApiEndpoint }
|
|
476
|
+
: {}),
|
|
382
477
|
};
|
|
383
478
|
if (settings.objectStorageGcsCredentialsJson) {
|
|
384
479
|
options.credentials = parseGcsCredentials(settings.objectStorageGcsCredentialsJson);
|
|
@@ -405,13 +500,16 @@ function parseContentLength(value: string | number | undefined): number | undefi
|
|
|
405
500
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
406
501
|
}
|
|
407
502
|
|
|
408
|
-
function stringMetadata(
|
|
503
|
+
function stringMetadata(
|
|
504
|
+
value: Record<string, string | number | boolean | null> | undefined,
|
|
505
|
+
): Record<string, string> | undefined {
|
|
409
506
|
if (!value) {
|
|
410
507
|
return undefined;
|
|
411
508
|
}
|
|
412
509
|
return Object.fromEntries(
|
|
413
|
-
Object.entries(value)
|
|
414
|
-
|
|
510
|
+
Object.entries(value).filter(
|
|
511
|
+
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
512
|
+
),
|
|
415
513
|
);
|
|
416
514
|
}
|
|
417
515
|
|
|
@@ -428,7 +526,9 @@ async function azureDownloadToBytes(download: BlobDownloadResponseParsed): Promi
|
|
|
428
526
|
throw new Error("Azure Blob download response did not include a readable body");
|
|
429
527
|
}
|
|
430
528
|
const chunks: Uint8Array[] = [];
|
|
431
|
-
for await (const chunk of download.readableStreamBody as AsyncIterable<
|
|
529
|
+
for await (const chunk of download.readableStreamBody as AsyncIterable<
|
|
530
|
+
Uint8Array | Buffer | string
|
|
531
|
+
>) {
|
|
432
532
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
433
533
|
}
|
|
434
534
|
return Buffer.concat(chunks);
|