deeplake 0.3.31 → 0.3.33
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/browser/browser/storage.d.ts +2 -0
- package/dist/browser/browser/storage.d.ts.map +1 -1
- package/dist/browser/browser/storage.js +30 -1
- package/dist/browser/browser/storage.js.map +1 -1
- package/dist/browser/shared/wasm-common.d.ts +7 -1
- package/dist/browser/shared/wasm-common.d.ts.map +1 -1
- package/dist/browser/shared/wasm-common.js +49 -11
- package/dist/browser/shared/wasm-common.js.map +1 -1
- package/dist/node/node/storage.d.ts +4 -4
- package/dist/node/node/storage.d.ts.map +1 -1
- package/dist/node/node/storage.js +1013 -91
- package/dist/node/node/storage.js.map +1 -1
- package/dist/node/shared/wasm-common.d.ts +7 -1
- package/dist/node/shared/wasm-common.d.ts.map +1 -1
- package/dist/node/shared/wasm-common.js +49 -11
- package/dist/node/shared/wasm-common.js.map +1 -1
- package/package.json +1 -1
- package/wasm/browser/deeplake_browser.js +1 -1
- package/wasm/browser/deeplake_browser.wasm +0 -0
- package/wasm/node/deeplake_node.js +1 -1
- package/wasm/node/deeplake_node.wasm +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Node.js
|
|
3
|
+
* Node.js storage client factory for the WASM deeplake engine.
|
|
4
4
|
*
|
|
5
5
|
* The C++ WASM code calls js_storage::client_factory()(type, config, mode?)
|
|
6
6
|
* to create a JS client object with methods like download, exists, list, etc.
|
|
7
|
-
* This module provides the factory that creates those client objects
|
|
8
|
-
*
|
|
7
|
+
* This module provides the factory that creates those client objects for
|
|
8
|
+
* S3 (AWS SDK v3), GCS (REST API), and Azure Blob Storage (REST API).
|
|
9
9
|
*
|
|
10
|
-
* Credential rotation:
|
|
10
|
+
* Credential rotation: credentials are refreshed automatically before
|
|
11
11
|
* they expire (5 minutes before expiry, matching the native C++ behavior).
|
|
12
12
|
* The C++ side passes refresh metadata (orgId, dsName, token) through the
|
|
13
13
|
* config object so the JS side can call the backend API for fresh creds.
|
|
@@ -204,86 +204,42 @@ function releaseStorageClients() {
|
|
|
204
204
|
}
|
|
205
205
|
function createStorageFactory(wasmModule) {
|
|
206
206
|
return function clientFactory(type, config, mode) {
|
|
207
|
-
if (type !== 'aws') {
|
|
208
|
-
// GCS and Azure storage types are supported in the browser visualizer
|
|
209
|
-
// (via GCSGetter/AzureGetter) but not yet in the Node.js SDK.
|
|
210
|
-
// The C++ side may request these types when datasets are backed by
|
|
211
|
-
// non-S3 storage. For now, throw a clear error.
|
|
212
|
-
throw new Error(`Unsupported storage type: ${type}. The Node.js SDK currently supports "aws" only. ` +
|
|
213
|
-
`GCS and Azure require additional dependencies (@azure/storage-blob, etc.).`);
|
|
214
|
-
}
|
|
215
|
-
const credentials = config?.credentials;
|
|
216
|
-
const region = config?.region ||
|
|
217
|
-
process.env.AWS_REGION ||
|
|
218
|
-
process.env.AWS_DEFAULT_REGION ||
|
|
219
|
-
'us-east-1';
|
|
220
|
-
const endpoint = config?.endpoint ?? undefined;
|
|
221
|
-
// Resolve credentials: prefer config from C++, fall back to process.env.
|
|
222
|
-
// In WASM, std::getenv() doesn't read Node.js process.env, so the C++
|
|
223
|
-
// credential chain can't pick up env vars — we must do it here.
|
|
224
|
-
let resolvedCreds;
|
|
225
|
-
if (credentials?.accessKeyId && credentials?.secretAccessKey) {
|
|
226
|
-
resolvedCreds = {
|
|
227
|
-
accessKeyId: credentials.accessKeyId,
|
|
228
|
-
secretAccessKey: credentials.secretAccessKey,
|
|
229
|
-
sessionToken: credentials.sessionToken,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
else if (process.env.AWS_ACCESS_KEY_ID &&
|
|
233
|
-
process.env.AWS_SECRET_ACCESS_KEY) {
|
|
234
|
-
resolvedCreds = {
|
|
235
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
236
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
237
|
-
sessionToken: process.env.AWS_SESSION_TOKEN,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
// Build refresh metadata from C++ params (org_id, ds_name, token)
|
|
241
|
-
const refresh = config?.refresh;
|
|
242
|
-
let refreshMeta = null;
|
|
243
|
-
if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
|
|
244
|
-
refreshMeta = {
|
|
245
|
-
orgId: refresh.orgId,
|
|
246
|
-
dsName: refresh.dsName,
|
|
247
|
-
credsKey: refresh.credsKey,
|
|
248
|
-
token: refresh.token,
|
|
249
|
-
apiEndpoint: resolveApiEndpoint(wasmModule),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const s3Config = {
|
|
253
|
-
region,
|
|
254
|
-
credentials: resolvedCreds,
|
|
255
|
-
// Match C++ native client: 8s request/connect timeouts (s3_utils.hpp:313-314)
|
|
256
|
-
requestHandler: new (require('@smithy/node-http-handler').NodeHttpHandler)({
|
|
257
|
-
requestTimeout: 8_000,
|
|
258
|
-
connectionTimeout: 8_000,
|
|
259
|
-
}),
|
|
260
|
-
// Match C++ native client: custom retry (s3_retry_strategy.hpp)
|
|
261
|
-
// 200ms interval, ~6s max → roughly 30 attempts
|
|
262
|
-
maxAttempts: 30,
|
|
263
|
-
// Disable IMDS lookups to avoid 5-10s hangs when no instance role
|
|
264
|
-
// is available (matches C++ shouldDisableIMDS=true)
|
|
265
|
-
disableHostPrefix: false,
|
|
266
|
-
followRegionRedirects: true,
|
|
267
|
-
};
|
|
268
|
-
if (endpoint) {
|
|
269
|
-
s3Config.endpoint = endpoint;
|
|
270
|
-
// Match C++ native client: use path-style addressing for custom
|
|
271
|
-
// endpoints (s3_utils.hpp:329 — use_virtual_addressing=false)
|
|
272
|
-
s3Config.forcePathStyle = true;
|
|
273
|
-
}
|
|
274
207
|
const dbg = !!process.env.DEEPLAKE_STORAGE_DEBUG;
|
|
275
|
-
if (dbg) {
|
|
276
|
-
console.log(`[storage] clientFactory type=${type} mode=${mode ?? 'r'} region=${region} creds=${resolvedCreds ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
|
|
277
|
-
}
|
|
278
|
-
const rotatingClient = new RotatingS3Client(s3Config, region, endpoint, refreshMeta, dbg);
|
|
279
208
|
let result;
|
|
280
|
-
|
|
281
|
-
|
|
209
|
+
let destroyable = null;
|
|
210
|
+
if (type === 'aws') {
|
|
211
|
+
const built = buildS3Client(config, wasmModule, dbg);
|
|
212
|
+
destroyable = built.rotatingClient;
|
|
213
|
+
if (mode === 'w') {
|
|
214
|
+
result = createS3Writer(built.rotatingClient, wasmModule);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
result = createS3Getter(built.rotatingClient, wasmModule);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (type === 'gcs') {
|
|
221
|
+
const rotatingCreds = buildGcsCredentials(config, wasmModule, dbg);
|
|
222
|
+
if (mode === 'w') {
|
|
223
|
+
result = createGcsWriter(rotatingCreds, wasmModule);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
result = createGcsGetter(rotatingCreds, wasmModule);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (type === 'azure') {
|
|
230
|
+
const rotatingCreds = buildAzureCredentials(config, wasmModule, dbg);
|
|
231
|
+
if (mode === 'w') {
|
|
232
|
+
result = createAzureWriter(rotatingCreds, wasmModule);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
result = createAzureGetter(rotatingCreds, wasmModule);
|
|
236
|
+
}
|
|
282
237
|
}
|
|
283
238
|
else {
|
|
284
|
-
|
|
239
|
+
throw new Error(`Unsupported storage type: ${type}. Supported types: "aws", "gcs", "azure".`);
|
|
285
240
|
}
|
|
286
241
|
if (dbg) {
|
|
242
|
+
console.log(`[storage] clientFactory type=${type} mode=${mode ?? 'r'}`);
|
|
287
243
|
// Wrap in Proxy to trace ALL method accesses and calls
|
|
288
244
|
const label = mode === 'w' ? 'writer' : 'reader';
|
|
289
245
|
result = new Proxy(result, {
|
|
@@ -291,13 +247,13 @@ function createStorageFactory(wasmModule) {
|
|
|
291
247
|
const val = target[prop];
|
|
292
248
|
if (typeof val === 'function') {
|
|
293
249
|
return function (...args) {
|
|
294
|
-
console.log(`[storage:${label}:${prop}] called with ${args.length} args`);
|
|
250
|
+
console.log(`[storage:${type}:${label}:${prop}] called with ${args.length} args`);
|
|
295
251
|
try {
|
|
296
252
|
const r = val.apply(target, args);
|
|
297
253
|
return r;
|
|
298
254
|
}
|
|
299
255
|
catch (e) {
|
|
300
|
-
console.log(`[storage:${label}:${prop}] threw: ${e.message}`);
|
|
256
|
+
console.log(`[storage:${type}:${label}:${prop}] threw: ${e.message}`);
|
|
301
257
|
throw e;
|
|
302
258
|
}
|
|
303
259
|
};
|
|
@@ -313,14 +269,76 @@ function createStorageFactory(wasmModule) {
|
|
|
313
269
|
// when the C++ dataset handle is freed.
|
|
314
270
|
result._destroy = () => {
|
|
315
271
|
_liveObjects.delete(result);
|
|
316
|
-
if (
|
|
317
|
-
|
|
272
|
+
if (destroyable && typeof destroyable.destroy === 'function') {
|
|
273
|
+
destroyable.destroy();
|
|
318
274
|
}
|
|
319
275
|
};
|
|
320
276
|
return result;
|
|
321
277
|
};
|
|
322
278
|
}
|
|
323
|
-
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// S3 client builder
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
function buildS3Client(config, wasmModule, dbg) {
|
|
283
|
+
const credentials = config?.credentials;
|
|
284
|
+
const region = config?.region ||
|
|
285
|
+
process.env.AWS_REGION ||
|
|
286
|
+
process.env.AWS_DEFAULT_REGION ||
|
|
287
|
+
'us-east-1';
|
|
288
|
+
const endpoint = config?.endpoint ?? undefined;
|
|
289
|
+
// Resolve credentials: prefer config from C++, fall back to process.env.
|
|
290
|
+
let resolvedCreds;
|
|
291
|
+
if (credentials?.accessKeyId && credentials?.secretAccessKey) {
|
|
292
|
+
resolvedCreds = {
|
|
293
|
+
accessKeyId: credentials.accessKeyId,
|
|
294
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
295
|
+
sessionToken: credentials.sessionToken,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
else if (process.env.AWS_ACCESS_KEY_ID &&
|
|
299
|
+
process.env.AWS_SECRET_ACCESS_KEY) {
|
|
300
|
+
resolvedCreds = {
|
|
301
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
302
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
303
|
+
sessionToken: process.env.AWS_SESSION_TOKEN,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const refresh = config?.refresh;
|
|
307
|
+
let refreshMeta = null;
|
|
308
|
+
if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
|
|
309
|
+
refreshMeta = {
|
|
310
|
+
orgId: refresh.orgId,
|
|
311
|
+
dsName: refresh.dsName,
|
|
312
|
+
credsKey: refresh.credsKey,
|
|
313
|
+
token: refresh.token,
|
|
314
|
+
apiEndpoint: resolveApiEndpoint(wasmModule),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const s3Config = {
|
|
318
|
+
region,
|
|
319
|
+
credentials: resolvedCreds,
|
|
320
|
+
requestHandler: new (require('@smithy/node-http-handler').NodeHttpHandler)({
|
|
321
|
+
requestTimeout: 8_000,
|
|
322
|
+
connectionTimeout: 8_000,
|
|
323
|
+
}),
|
|
324
|
+
maxAttempts: 30,
|
|
325
|
+
disableHostPrefix: false,
|
|
326
|
+
followRegionRedirects: true,
|
|
327
|
+
};
|
|
328
|
+
if (endpoint) {
|
|
329
|
+
s3Config.endpoint = endpoint;
|
|
330
|
+
s3Config.forcePathStyle = true;
|
|
331
|
+
}
|
|
332
|
+
if (dbg) {
|
|
333
|
+
console.log(`[storage:s3] region=${region} creds=${resolvedCreds ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
|
|
334
|
+
}
|
|
335
|
+
const rotatingClient = new RotatingS3Client(s3Config, region, endpoint, refreshMeta, dbg);
|
|
336
|
+
return { rotatingClient };
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// S3 helpers
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
function removeS3Scheme(key) {
|
|
324
342
|
if (key.startsWith('s3://')) {
|
|
325
343
|
return key.substring(5);
|
|
326
344
|
}
|
|
@@ -335,11 +353,11 @@ function splitBucketPrefix(root) {
|
|
|
335
353
|
const epos = root.lastIndexOf('/');
|
|
336
354
|
return [root.substring(0, pos), root.substring(pos + 1, epos)];
|
|
337
355
|
}
|
|
338
|
-
function
|
|
356
|
+
function createS3Getter(rotatingClient, wasmModule) {
|
|
339
357
|
const debug = !!process.env.DEEPLAKE_STORAGE_DEBUG;
|
|
340
358
|
const getter = {
|
|
341
359
|
generateGetPresignedUrl(key, completion) {
|
|
342
|
-
key =
|
|
360
|
+
key = removeS3Scheme(key);
|
|
343
361
|
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
344
362
|
const [bucket, objKey] = splitBucketKey(key);
|
|
345
363
|
rotatingClient.getClient().then((client) => (0, s3_request_presigner_1.getSignedUrl)(client, new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: objKey }), {
|
|
@@ -355,7 +373,7 @@ function createGetter(rotatingClient, wasmModule) {
|
|
|
355
373
|
}));
|
|
356
374
|
},
|
|
357
375
|
exists(key, completion) {
|
|
358
|
-
key =
|
|
376
|
+
key = removeS3Scheme(key);
|
|
359
377
|
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
360
378
|
const [bucket, objKey] = splitBucketKey(key);
|
|
361
379
|
if (debug) {
|
|
@@ -373,7 +391,7 @@ function createGetter(rotatingClient, wasmModule) {
|
|
|
373
391
|
}));
|
|
374
392
|
},
|
|
375
393
|
download(key, range, index, completion) {
|
|
376
|
-
key =
|
|
394
|
+
key = removeS3Scheme(key);
|
|
377
395
|
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
378
396
|
const [bucket, objKey] = splitBucketKey(key);
|
|
379
397
|
if (debug) {
|
|
@@ -529,11 +547,11 @@ function createGetter(rotatingClient, wasmModule) {
|
|
|
529
547
|
};
|
|
530
548
|
return getter;
|
|
531
549
|
}
|
|
532
|
-
function
|
|
550
|
+
function createS3Writer(rotatingClient, wasmModule) {
|
|
533
551
|
const debug = !!process.env.DEEPLAKE_STORAGE_DEBUG;
|
|
534
552
|
const writer = {
|
|
535
553
|
saveWithCompletion(key, content, completion) {
|
|
536
|
-
key =
|
|
554
|
+
key = removeS3Scheme(key);
|
|
537
555
|
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
538
556
|
const [bucket, objKey] = splitBucketKey(key);
|
|
539
557
|
if (debug) {
|
|
@@ -544,11 +562,13 @@ function createWriter(rotatingClient, wasmModule) {
|
|
|
544
562
|
rotatingClient.getClient().then((client) => client
|
|
545
563
|
.send(new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: objKey, Body: body }))
|
|
546
564
|
.then((result) => {
|
|
565
|
+
const etag = result.ETag ? result.ETag.replace(/"/g, '') : '';
|
|
547
566
|
if (debug) {
|
|
548
|
-
console.log(`[storage:write:done] key=${objKey} code=${result.$metadata.httpStatusCode}`);
|
|
567
|
+
console.log(`[storage:write:done] key=${objKey} code=${result.$metadata.httpStatusCode} etag=${etag}`);
|
|
549
568
|
}
|
|
550
569
|
c1.call({
|
|
551
570
|
responseCode: result.$metadata.httpStatusCode ?? 200,
|
|
571
|
+
etag,
|
|
552
572
|
});
|
|
553
573
|
c1.delete();
|
|
554
574
|
})
|
|
@@ -563,8 +583,85 @@ function createWriter(rotatingClient, wasmModule) {
|
|
|
563
583
|
c1.delete();
|
|
564
584
|
}));
|
|
565
585
|
},
|
|
586
|
+
/**
|
|
587
|
+
* Conditional write: only succeeds if the object's current ETag matches.
|
|
588
|
+
* Used by CAS counter and commit operations for optimistic concurrency.
|
|
589
|
+
* Returns 412 (Precondition Failed) if ETag doesn't match.
|
|
590
|
+
*/
|
|
591
|
+
saveConditional(key, content, ifMatchEtag, completion) {
|
|
592
|
+
key = removeS3Scheme(key);
|
|
593
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
594
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
595
|
+
const body = typeof content === 'string' ? Buffer.from(content) : content;
|
|
596
|
+
const quotedEtag = ifMatchEtag.startsWith('"') ? ifMatchEtag : `"${ifMatchEtag}"`;
|
|
597
|
+
if (debug) {
|
|
598
|
+
const size = typeof content === 'string' ? content.length : content.byteLength;
|
|
599
|
+
console.log(`[storage:write_if_match] bucket=${bucket} key=${objKey} size=${size} ifMatch=${quotedEtag}`);
|
|
600
|
+
}
|
|
601
|
+
rotatingClient.getClient().then((client) => client
|
|
602
|
+
.send(new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: objKey, Body: body, IfMatch: quotedEtag }))
|
|
603
|
+
.then((result) => {
|
|
604
|
+
const etag = result.ETag ? result.ETag.replace(/"/g, '') : '';
|
|
605
|
+
if (debug) {
|
|
606
|
+
console.log(`[storage:write_if_match:done] key=${objKey} code=${result.$metadata.httpStatusCode} etag=${etag}`);
|
|
607
|
+
}
|
|
608
|
+
c1.call({
|
|
609
|
+
responseCode: result.$metadata.httpStatusCode ?? 200,
|
|
610
|
+
etag,
|
|
611
|
+
});
|
|
612
|
+
c1.delete();
|
|
613
|
+
})
|
|
614
|
+
.catch((err) => {
|
|
615
|
+
const code = err.$metadata?.httpStatusCode ?? 1000;
|
|
616
|
+
const name = err.name ?? err.Code ?? '';
|
|
617
|
+
const msg = name ? `${name}: ${err.message ?? 'unknown_error'}` : (err.message ?? 'unknown_error');
|
|
618
|
+
if (debug) {
|
|
619
|
+
console.log(`[storage:write_if_match:error] key=${objKey} code=${code} err=${msg}`);
|
|
620
|
+
}
|
|
621
|
+
c1.call({ responseCode: code, message: msg });
|
|
622
|
+
c1.delete();
|
|
623
|
+
}));
|
|
624
|
+
},
|
|
625
|
+
/**
|
|
626
|
+
* Exclusive write: only succeeds if the object does NOT exist.
|
|
627
|
+
* Used to prevent concurrent creation of the same file.
|
|
628
|
+
* Returns 412 (Precondition Failed) if the object already exists.
|
|
629
|
+
*/
|
|
630
|
+
saveExclusive(key, content, completion) {
|
|
631
|
+
key = removeS3Scheme(key);
|
|
632
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
633
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
634
|
+
const body = typeof content === 'string' ? Buffer.from(content) : content;
|
|
635
|
+
if (debug) {
|
|
636
|
+
const size = typeof content === 'string' ? content.length : content.byteLength;
|
|
637
|
+
console.log(`[storage:write_exclusive] bucket=${bucket} key=${objKey} size=${size}`);
|
|
638
|
+
}
|
|
639
|
+
rotatingClient.getClient().then((client) => client
|
|
640
|
+
.send(new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: objKey, Body: body, IfNoneMatch: '*' }))
|
|
641
|
+
.then((result) => {
|
|
642
|
+
const etag = result.ETag ? result.ETag.replace(/"/g, '') : '';
|
|
643
|
+
if (debug) {
|
|
644
|
+
console.log(`[storage:write_exclusive:done] key=${objKey} code=${result.$metadata.httpStatusCode} etag=${etag}`);
|
|
645
|
+
}
|
|
646
|
+
c1.call({
|
|
647
|
+
responseCode: result.$metadata.httpStatusCode ?? 200,
|
|
648
|
+
etag,
|
|
649
|
+
});
|
|
650
|
+
c1.delete();
|
|
651
|
+
})
|
|
652
|
+
.catch((err) => {
|
|
653
|
+
const code = err.$metadata?.httpStatusCode ?? 1000;
|
|
654
|
+
const name = err.name ?? err.Code ?? '';
|
|
655
|
+
const msg = name ? `${name}: ${err.message ?? 'unknown_error'}` : (err.message ?? 'unknown_error');
|
|
656
|
+
if (debug) {
|
|
657
|
+
console.log(`[storage:write_exclusive:error] key=${objKey} code=${code} err=${msg}`);
|
|
658
|
+
}
|
|
659
|
+
c1.call({ responseCode: code, message: msg });
|
|
660
|
+
c1.delete();
|
|
661
|
+
}));
|
|
662
|
+
},
|
|
566
663
|
remove(key, completion) {
|
|
567
|
-
key =
|
|
664
|
+
key = removeS3Scheme(key);
|
|
568
665
|
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
569
666
|
const [bucket, objKey] = splitBucketKey(key);
|
|
570
667
|
rotatingClient.getClient().then((client) => client
|
|
@@ -589,4 +686,829 @@ function createWriter(rotatingClient, wasmModule) {
|
|
|
589
686
|
};
|
|
590
687
|
return writer;
|
|
591
688
|
}
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
// Rotating credentials for GCS and Azure
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
/**
|
|
693
|
+
* Generic rotating credential holder. Works like RotatingS3Client but
|
|
694
|
+
* stores plain credential objects instead of SDK client instances.
|
|
695
|
+
*/
|
|
696
|
+
class RotatingCredentials {
|
|
697
|
+
creds;
|
|
698
|
+
refreshMeta;
|
|
699
|
+
applyCreds;
|
|
700
|
+
createdAt;
|
|
701
|
+
lastFailureAt = 0;
|
|
702
|
+
refreshInProgress = null;
|
|
703
|
+
debug;
|
|
704
|
+
constructor(initial, refreshMeta, applyCreds, debug) {
|
|
705
|
+
this.creds = initial;
|
|
706
|
+
this.refreshMeta = refreshMeta;
|
|
707
|
+
this.applyCreds = applyCreds;
|
|
708
|
+
this.createdAt = Date.now();
|
|
709
|
+
this.debug = debug;
|
|
710
|
+
}
|
|
711
|
+
async get() {
|
|
712
|
+
if (!this.refreshMeta)
|
|
713
|
+
return this.creds;
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
const age = now - this.createdAt;
|
|
716
|
+
if (age <= DEFAULT_CRED_LIFETIME_MS - REFRESH_BUFFER_MS)
|
|
717
|
+
return this.creds;
|
|
718
|
+
if (this.lastFailureAt && now - this.lastFailureAt < REFRESH_FAILURE_COOLDOWN_MS) {
|
|
719
|
+
return this.creds;
|
|
720
|
+
}
|
|
721
|
+
if (this.refreshInProgress) {
|
|
722
|
+
await this.refreshInProgress;
|
|
723
|
+
return this.creds;
|
|
724
|
+
}
|
|
725
|
+
this.refreshInProgress = this.doRefresh();
|
|
726
|
+
try {
|
|
727
|
+
await this.refreshInProgress;
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
this.refreshInProgress = null;
|
|
731
|
+
}
|
|
732
|
+
return this.creds;
|
|
733
|
+
}
|
|
734
|
+
async doRefresh() {
|
|
735
|
+
const meta = this.refreshMeta;
|
|
736
|
+
if (this.debug) {
|
|
737
|
+
console.log(`[storage] refreshing credentials for ds=${meta.dsName ?? ''} credsKey=${meta.credsKey ?? ''}`);
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const resp = await fetchCredentials(meta);
|
|
741
|
+
this.creds = this.applyCreds(resp.creds);
|
|
742
|
+
this.createdAt = Date.now();
|
|
743
|
+
this.lastFailureAt = 0;
|
|
744
|
+
if (this.debug) {
|
|
745
|
+
console.log(`[storage] credential refresh succeeded (repo_type=${resp.repoType})`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch (e) {
|
|
749
|
+
this.lastFailureAt = Date.now();
|
|
750
|
+
console.warn(`[storage] credential refresh failed: ${e.message ?? e}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function buildRefreshMeta(config, wasmModule) {
|
|
755
|
+
const refresh = config?.refresh;
|
|
756
|
+
if (refresh?.orgId && refresh?.token && (refresh?.dsName || refresh?.credsKey)) {
|
|
757
|
+
return {
|
|
758
|
+
orgId: refresh.orgId,
|
|
759
|
+
dsName: refresh.dsName,
|
|
760
|
+
credsKey: refresh.credsKey,
|
|
761
|
+
token: refresh.token,
|
|
762
|
+
apiEndpoint: resolveApiEndpoint(wasmModule),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
// GCS (Google Cloud Storage) — fetch-based, no extra dependencies
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
/** Convert string or Uint8Array to a value accepted by Node.js fetch as body. */
|
|
771
|
+
function toFetchBody(content) {
|
|
772
|
+
if (typeof content === 'string')
|
|
773
|
+
return Buffer.from(content);
|
|
774
|
+
return Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
|
|
775
|
+
}
|
|
776
|
+
const GCS_API_BASE = 'https://storage.googleapis.com';
|
|
777
|
+
function buildGcsCredentials(config, wasmModule, dbg) {
|
|
778
|
+
const initial = { accessToken: config?.accessToken ?? '' };
|
|
779
|
+
const refreshMeta = buildRefreshMeta(config, wasmModule);
|
|
780
|
+
if (dbg) {
|
|
781
|
+
console.log(`[storage:gcs] token=${initial.accessToken ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
|
|
782
|
+
}
|
|
783
|
+
return new RotatingCredentials(initial, refreshMeta, (raw) => ({
|
|
784
|
+
accessToken: raw.gcs_oauth_token ?? raw.oauth_token ?? raw.access_token ?? '',
|
|
785
|
+
}), dbg);
|
|
786
|
+
}
|
|
787
|
+
function gcsHeaders(creds) {
|
|
788
|
+
const h = {};
|
|
789
|
+
if (creds.accessToken) {
|
|
790
|
+
h['Authorization'] = `Bearer ${creds.accessToken}`;
|
|
791
|
+
}
|
|
792
|
+
return h;
|
|
793
|
+
}
|
|
794
|
+
function removeGcsScheme(key) {
|
|
795
|
+
if (key.startsWith('gs://'))
|
|
796
|
+
return key.substring(5);
|
|
797
|
+
if (key.startsWith('gcs://'))
|
|
798
|
+
return key.substring(6);
|
|
799
|
+
return key;
|
|
800
|
+
}
|
|
801
|
+
function createGcsGetter(rotatingCreds, wasmModule) {
|
|
802
|
+
const getter = {
|
|
803
|
+
generateGetPresignedUrl(_key, completion) {
|
|
804
|
+
// GCS presigned URLs require service account private keys, which
|
|
805
|
+
// aren't available here. Return empty like the browser implementation.
|
|
806
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
807
|
+
c1.call('');
|
|
808
|
+
c1.delete();
|
|
809
|
+
},
|
|
810
|
+
exists(key, completion) {
|
|
811
|
+
key = removeGcsScheme(key);
|
|
812
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
813
|
+
rotatingCreds.get().then((creds) => {
|
|
814
|
+
fetch(`${GCS_API_BASE}/${key}`, {
|
|
815
|
+
method: 'HEAD',
|
|
816
|
+
headers: gcsHeaders(creds),
|
|
817
|
+
})
|
|
818
|
+
.then((resp) => {
|
|
819
|
+
if (resp.ok) {
|
|
820
|
+
c1.call(Number(resp.headers.get('content-length') ?? 0));
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
c1.call(-1);
|
|
824
|
+
}
|
|
825
|
+
c1.delete();
|
|
826
|
+
})
|
|
827
|
+
.catch(() => {
|
|
828
|
+
c1.call(-1);
|
|
829
|
+
c1.delete();
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
},
|
|
833
|
+
download(key, range, index, completion) {
|
|
834
|
+
key = removeGcsScheme(key);
|
|
835
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
836
|
+
rotatingCreds.get().then((creds) => {
|
|
837
|
+
const headers = gcsHeaders(creds);
|
|
838
|
+
if (range)
|
|
839
|
+
headers['Range'] = range;
|
|
840
|
+
const controller = new AbortController();
|
|
841
|
+
wasmModule.storeJsObject(index, { abort() { controller.abort(); } });
|
|
842
|
+
fetch(`${GCS_API_BASE}/${key}`, { headers, signal: controller.signal })
|
|
843
|
+
.then(async (resp) => {
|
|
844
|
+
if (!resp.ok) {
|
|
845
|
+
c1.call({ responseCode: resp.status, message: resp.statusText || 'download_failed' });
|
|
846
|
+
c1.delete();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
850
|
+
const view = wasmModule.provideResponseBuffer(index, buf.length);
|
|
851
|
+
view.set(buf);
|
|
852
|
+
c1.call({ responseCode: 200 });
|
|
853
|
+
c1.delete();
|
|
854
|
+
})
|
|
855
|
+
.catch((err) => {
|
|
856
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
857
|
+
c1.delete();
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
},
|
|
861
|
+
list(root, key, startAfter, completion) {
|
|
862
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
863
|
+
const [bucket, prefix] = splitBucketKey(root);
|
|
864
|
+
const finalPrefix = (prefix.endsWith('/') ? prefix : `${prefix}/`) + key;
|
|
865
|
+
const results = [];
|
|
866
|
+
rotatingCreds.get().then((creds) => {
|
|
867
|
+
const impl = (pageToken) => {
|
|
868
|
+
let url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o?prefix=${encodeURIComponent(finalPrefix)}`;
|
|
869
|
+
if (startAfter)
|
|
870
|
+
url += `&startOffset=${encodeURIComponent(startAfter)}`;
|
|
871
|
+
if (pageToken)
|
|
872
|
+
url += `&pageToken=${encodeURIComponent(pageToken)}`;
|
|
873
|
+
fetch(url, { headers: gcsHeaders(creds) })
|
|
874
|
+
.then((resp) => {
|
|
875
|
+
if (!resp.ok) {
|
|
876
|
+
c1.call({ responseCode: resp.status, message: 'list_request_failed' });
|
|
877
|
+
c1.delete();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
return resp.json();
|
|
881
|
+
})
|
|
882
|
+
.then((json) => {
|
|
883
|
+
if (!json)
|
|
884
|
+
return; // error already handled
|
|
885
|
+
for (const item of json.items ?? []) {
|
|
886
|
+
results.push({
|
|
887
|
+
path: item.name.substring(prefix.length + 1),
|
|
888
|
+
size: Number(item.size),
|
|
889
|
+
lastModified: new Date(item.updated),
|
|
890
|
+
etag: item.etag ?? '',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (json.nextPageToken) {
|
|
894
|
+
impl(json.nextPageToken);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
c1.call({ responseCode: 200, data: results });
|
|
898
|
+
c1.delete();
|
|
899
|
+
}
|
|
900
|
+
})
|
|
901
|
+
.catch((err) => {
|
|
902
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'list_request_failed' });
|
|
903
|
+
c1.delete();
|
|
904
|
+
});
|
|
905
|
+
};
|
|
906
|
+
impl();
|
|
907
|
+
});
|
|
908
|
+
},
|
|
909
|
+
listDirs(root, prefix, completion) {
|
|
910
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
911
|
+
const [bucket, rootPrefix] = splitBucketPrefix(root);
|
|
912
|
+
const finalPrefix = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + prefix;
|
|
913
|
+
rotatingCreds.get().then((creds) => {
|
|
914
|
+
const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o?prefix=${encodeURIComponent(finalPrefix)}&delimiter=/`;
|
|
915
|
+
fetch(url, { headers: gcsHeaders(creds) })
|
|
916
|
+
.then((resp) => {
|
|
917
|
+
if (!resp.ok) {
|
|
918
|
+
c1.call({ responseCode: resp.status, message: 'list_dirs_failed' });
|
|
919
|
+
c1.delete();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
return resp.json();
|
|
923
|
+
})
|
|
924
|
+
.then((json) => {
|
|
925
|
+
if (!json)
|
|
926
|
+
return;
|
|
927
|
+
const dirs = (json.prefixes ?? []);
|
|
928
|
+
c1.call({ responseCode: 200, data: dirs });
|
|
929
|
+
c1.delete();
|
|
930
|
+
})
|
|
931
|
+
.catch((err) => {
|
|
932
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'list_dirs_failed' });
|
|
933
|
+
c1.delete();
|
|
934
|
+
});
|
|
935
|
+
});
|
|
936
|
+
},
|
|
937
|
+
metadata(root, key, completion) {
|
|
938
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
939
|
+
const [bucket, prefix] = splitBucketPrefix(root);
|
|
940
|
+
const objName = (prefix.endsWith('/') ? prefix : `${prefix}/`) + key;
|
|
941
|
+
rotatingCreds.get().then((creds) => {
|
|
942
|
+
const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objName)}`;
|
|
943
|
+
fetch(url, { headers: gcsHeaders(creds) })
|
|
944
|
+
.then((resp) => {
|
|
945
|
+
if (!resp.ok) {
|
|
946
|
+
c1.call({ responseCode: resp.status, message: 'metadata_request_failed' });
|
|
947
|
+
c1.delete();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
return resp.json();
|
|
951
|
+
})
|
|
952
|
+
.then((json) => {
|
|
953
|
+
if (!json)
|
|
954
|
+
return;
|
|
955
|
+
c1.call({
|
|
956
|
+
responseCode: 200,
|
|
957
|
+
data: {
|
|
958
|
+
path: json.name.substring(prefix.length + 1),
|
|
959
|
+
size: Number(json.size),
|
|
960
|
+
lastModified: new Date(json.updated),
|
|
961
|
+
etag: json.etag ?? '',
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
c1.delete();
|
|
965
|
+
})
|
|
966
|
+
.catch((err) => {
|
|
967
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'metadata_request_failed' });
|
|
968
|
+
c1.delete();
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
},
|
|
972
|
+
copyGetter() {
|
|
973
|
+
return getter;
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
return getter;
|
|
977
|
+
}
|
|
978
|
+
function createGcsWriter(rotatingCreds, _wasmModule) {
|
|
979
|
+
const writer = {
|
|
980
|
+
saveWithCompletion(key, content, completion) {
|
|
981
|
+
key = removeGcsScheme(key);
|
|
982
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
983
|
+
const body = toFetchBody(content);
|
|
984
|
+
rotatingCreds.get().then((creds) => {
|
|
985
|
+
// Upload via GCS JSON API resumable or simple upload
|
|
986
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
987
|
+
const url = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}`;
|
|
988
|
+
fetch(url, {
|
|
989
|
+
method: 'POST',
|
|
990
|
+
headers: { ...gcsHeaders(creds), 'Content-Type': 'application/octet-stream' },
|
|
991
|
+
body,
|
|
992
|
+
})
|
|
993
|
+
.then(async (resp) => {
|
|
994
|
+
if (!resp.ok) {
|
|
995
|
+
const text = await resp.text().catch(() => '');
|
|
996
|
+
c1.call({ responseCode: resp.status, message: text || 'upload_failed' });
|
|
997
|
+
c1.delete();
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const json = await resp.json().catch(() => ({}));
|
|
1001
|
+
c1.call({ responseCode: 200, etag: json.etag ?? '' });
|
|
1002
|
+
c1.delete();
|
|
1003
|
+
})
|
|
1004
|
+
.catch((err) => {
|
|
1005
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1006
|
+
c1.delete();
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
},
|
|
1010
|
+
saveConditional(key, content, ifMatchEtag, completion) {
|
|
1011
|
+
key = removeGcsScheme(key);
|
|
1012
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1013
|
+
const body = toFetchBody(content);
|
|
1014
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
1015
|
+
const normalizedEtag = ifMatchEtag.replace(/"/g, '');
|
|
1016
|
+
rotatingCreds.get().then((creds) => {
|
|
1017
|
+
// First, fetch the object's metadata to get its generation number.
|
|
1018
|
+
// GCS CAS requires ifGenerationMatch=<generation>, not HTTP If-Match.
|
|
1019
|
+
const metaUrl = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objKey)}`;
|
|
1020
|
+
fetch(metaUrl, { headers: gcsHeaders(creds) })
|
|
1021
|
+
.then((resp) => {
|
|
1022
|
+
if (!resp.ok) {
|
|
1023
|
+
// Object doesn't exist or other error — CAS fails
|
|
1024
|
+
c1.call({ responseCode: resp.status === 404 ? 412 : resp.status, message: 'conditional_write_failed' });
|
|
1025
|
+
c1.delete();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
return resp.json();
|
|
1029
|
+
})
|
|
1030
|
+
.then((meta) => {
|
|
1031
|
+
if (!meta)
|
|
1032
|
+
return; // error already handled
|
|
1033
|
+
const currentEtag = (meta.etag ?? '').replace(/"/g, '');
|
|
1034
|
+
if (currentEtag !== normalizedEtag) {
|
|
1035
|
+
// ETag mismatch — another writer changed the object
|
|
1036
|
+
c1.call({ responseCode: 412, message: 'conditional_write_failed' });
|
|
1037
|
+
c1.delete();
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
// ETag matches — upload with ifGenerationMatch to guarantee atomicity
|
|
1041
|
+
const uploadUrl = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}&ifGenerationMatch=${meta.generation}`;
|
|
1042
|
+
fetch(uploadUrl, {
|
|
1043
|
+
method: 'POST',
|
|
1044
|
+
headers: {
|
|
1045
|
+
...gcsHeaders(creds),
|
|
1046
|
+
'Content-Type': 'application/octet-stream',
|
|
1047
|
+
},
|
|
1048
|
+
body,
|
|
1049
|
+
})
|
|
1050
|
+
.then(async (resp) => {
|
|
1051
|
+
if (!resp.ok) {
|
|
1052
|
+
c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'conditional_write_failed' });
|
|
1053
|
+
c1.delete();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const json = await resp.json().catch(() => ({}));
|
|
1057
|
+
c1.call({ responseCode: 200, etag: json.etag ?? '' });
|
|
1058
|
+
c1.delete();
|
|
1059
|
+
})
|
|
1060
|
+
.catch((err) => {
|
|
1061
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1062
|
+
c1.delete();
|
|
1063
|
+
});
|
|
1064
|
+
})
|
|
1065
|
+
.catch((err) => {
|
|
1066
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1067
|
+
c1.delete();
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
},
|
|
1071
|
+
saveExclusive(key, content, completion) {
|
|
1072
|
+
key = removeGcsScheme(key);
|
|
1073
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1074
|
+
const body = toFetchBody(content);
|
|
1075
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
1076
|
+
rotatingCreds.get().then((creds) => {
|
|
1077
|
+
// ifGenerationMatch=0 means "only if the object does not exist"
|
|
1078
|
+
const url = `${GCS_API_BASE}/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(objKey)}&ifGenerationMatch=0`;
|
|
1079
|
+
fetch(url, {
|
|
1080
|
+
method: 'POST',
|
|
1081
|
+
headers: { ...gcsHeaders(creds), 'Content-Type': 'application/octet-stream' },
|
|
1082
|
+
body,
|
|
1083
|
+
})
|
|
1084
|
+
.then(async (resp) => {
|
|
1085
|
+
if (!resp.ok) {
|
|
1086
|
+
c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'exclusive_write_failed' });
|
|
1087
|
+
c1.delete();
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const json = await resp.json().catch(() => ({}));
|
|
1091
|
+
c1.call({ responseCode: 200, etag: json.etag ?? '' });
|
|
1092
|
+
c1.delete();
|
|
1093
|
+
})
|
|
1094
|
+
.catch((err) => {
|
|
1095
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1096
|
+
c1.delete();
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
},
|
|
1100
|
+
remove(key, completion) {
|
|
1101
|
+
key = removeGcsScheme(key);
|
|
1102
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1103
|
+
const [bucket, objKey] = splitBucketKey(key);
|
|
1104
|
+
rotatingCreds.get().then((creds) => {
|
|
1105
|
+
const url = `${GCS_API_BASE}/storage/v1/b/${bucket}/o/${encodeURIComponent(objKey)}`;
|
|
1106
|
+
fetch(url, {
|
|
1107
|
+
method: 'DELETE',
|
|
1108
|
+
headers: gcsHeaders(creds),
|
|
1109
|
+
})
|
|
1110
|
+
.then((resp) => {
|
|
1111
|
+
c1.call({ responseCode: resp.ok ? 200 : resp.status });
|
|
1112
|
+
c1.delete();
|
|
1113
|
+
})
|
|
1114
|
+
.catch((err) => {
|
|
1115
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1116
|
+
c1.delete();
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
1120
|
+
copyWriter() {
|
|
1121
|
+
return writer;
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1124
|
+
return writer;
|
|
1125
|
+
}
|
|
1126
|
+
function buildAzureCredentials(config, wasmModule, dbg) {
|
|
1127
|
+
const initial = {
|
|
1128
|
+
accountName: config?.accountName ?? '',
|
|
1129
|
+
sasToken: config?.blobSasToken ?? '',
|
|
1130
|
+
containerName: config?.containerName ?? '',
|
|
1131
|
+
};
|
|
1132
|
+
const refreshMeta = buildRefreshMeta(config, wasmModule);
|
|
1133
|
+
if (dbg) {
|
|
1134
|
+
console.log(`[storage:azure] account=${initial.accountName} container=${initial.containerName} sas=${initial.sasToken ? 'yes' : 'no'} refresh=${refreshMeta ? 'yes' : 'no'}`);
|
|
1135
|
+
}
|
|
1136
|
+
return new RotatingCredentials(initial, refreshMeta, (raw) => ({
|
|
1137
|
+
accountName: raw.account_name ?? initial.accountName,
|
|
1138
|
+
sasToken: raw.sas_token ?? '',
|
|
1139
|
+
containerName: raw.container_name ?? initial.containerName,
|
|
1140
|
+
}), dbg);
|
|
1141
|
+
}
|
|
1142
|
+
function azureBlobUrl(creds, blobPath) {
|
|
1143
|
+
const base = `https://${creds.accountName}.blob.core.windows.net/${creds.containerName}/${blobPath}`;
|
|
1144
|
+
return creds.sasToken ? `${base}?${creds.sasToken}` : base;
|
|
1145
|
+
}
|
|
1146
|
+
function azureListUrl(creds, params) {
|
|
1147
|
+
const base = `https://${creds.accountName}.blob.core.windows.net/${creds.containerName}`;
|
|
1148
|
+
const sep = creds.sasToken ? `?${creds.sasToken}&` : '?';
|
|
1149
|
+
return `${base}${sep}${params}`;
|
|
1150
|
+
}
|
|
1151
|
+
function removeAzureScheme(key) {
|
|
1152
|
+
if (key.startsWith('azure://'))
|
|
1153
|
+
return key.substring(8);
|
|
1154
|
+
if (key.startsWith('az://'))
|
|
1155
|
+
return key.substring(5);
|
|
1156
|
+
return key;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Parse an Azure blob path. The C++ side sends paths as:
|
|
1160
|
+
* accountName/containerName/blob/path
|
|
1161
|
+
* We need to extract just the blob path portion since accountName
|
|
1162
|
+
* and containerName come from the credentials.
|
|
1163
|
+
*/
|
|
1164
|
+
function azureBlobPath(key, creds) {
|
|
1165
|
+
const prefix = `${creds.accountName}/${creds.containerName}/`;
|
|
1166
|
+
if (key.startsWith(prefix)) {
|
|
1167
|
+
return key.substring(prefix.length);
|
|
1168
|
+
}
|
|
1169
|
+
// May already be just the blob path
|
|
1170
|
+
return key;
|
|
1171
|
+
}
|
|
1172
|
+
/** Parse a minimal XML list-blobs response */
|
|
1173
|
+
function parseAzureListBlobsXml(xml) {
|
|
1174
|
+
const blobs = [];
|
|
1175
|
+
const blobRegex = /<Blob>[\s\S]*?<\/Blob>/g;
|
|
1176
|
+
let match;
|
|
1177
|
+
while ((match = blobRegex.exec(xml)) !== null) {
|
|
1178
|
+
const blob = match[0];
|
|
1179
|
+
const name = blob.match(/<Name>([\s\S]*?)<\/Name>/)?.[1] ?? '';
|
|
1180
|
+
const size = Number(blob.match(/<Content-Length>([\s\S]*?)<\/Content-Length>/)?.[1] ?? 0);
|
|
1181
|
+
const lastMod = blob.match(/<Last-Modified>([\s\S]*?)<\/Last-Modified>/)?.[1] ?? '';
|
|
1182
|
+
const etag = (blob.match(/<Etag>([\s\S]*?)<\/Etag>/)?.[1] ?? '').replace(/"/g, '');
|
|
1183
|
+
blobs.push({ name, size, lastModified: new Date(lastMod), etag });
|
|
1184
|
+
}
|
|
1185
|
+
const nextMarker = xml.match(/<NextMarker>([\s\S]*?)<\/NextMarker>/)?.[1] || undefined;
|
|
1186
|
+
return { blobs, nextMarker };
|
|
1187
|
+
}
|
|
1188
|
+
/** Parse directory prefixes from Azure list-blobs XML with delimiter */
|
|
1189
|
+
function parseAzureBlobPrefixes(xml) {
|
|
1190
|
+
const prefixes = [];
|
|
1191
|
+
const prefixRegex = /<BlobPrefix><Name>([\s\S]*?)<\/Name><\/BlobPrefix>/g;
|
|
1192
|
+
let match;
|
|
1193
|
+
while ((match = prefixRegex.exec(xml)) !== null) {
|
|
1194
|
+
prefixes.push(match[1]);
|
|
1195
|
+
}
|
|
1196
|
+
return prefixes;
|
|
1197
|
+
}
|
|
1198
|
+
function createAzureGetter(rotatingCreds, wasmModule) {
|
|
1199
|
+
const getter = {
|
|
1200
|
+
generateGetPresignedUrl(key, completion) {
|
|
1201
|
+
// With SAS token, the URL is already "presigned"
|
|
1202
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1203
|
+
rotatingCreds.get().then((creds) => {
|
|
1204
|
+
key = removeAzureScheme(key);
|
|
1205
|
+
const blobP = azureBlobPath(key, creds);
|
|
1206
|
+
c1.call(azureBlobUrl(creds, blobP));
|
|
1207
|
+
c1.delete();
|
|
1208
|
+
});
|
|
1209
|
+
},
|
|
1210
|
+
exists(key, completion) {
|
|
1211
|
+
key = removeAzureScheme(key);
|
|
1212
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1213
|
+
rotatingCreds.get().then((creds) => {
|
|
1214
|
+
const blobP = azureBlobPath(key, creds);
|
|
1215
|
+
fetch(azureBlobUrl(creds, blobP), { method: 'HEAD' })
|
|
1216
|
+
.then((resp) => {
|
|
1217
|
+
if (resp.ok) {
|
|
1218
|
+
c1.call(Number(resp.headers.get('content-length') ?? 0));
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
c1.call(-1);
|
|
1222
|
+
}
|
|
1223
|
+
c1.delete();
|
|
1224
|
+
})
|
|
1225
|
+
.catch(() => {
|
|
1226
|
+
c1.call(-1);
|
|
1227
|
+
c1.delete();
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
},
|
|
1231
|
+
download(key, range, index, completion) {
|
|
1232
|
+
key = removeAzureScheme(key);
|
|
1233
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1234
|
+
rotatingCreds.get().then((creds) => {
|
|
1235
|
+
const blobP = azureBlobPath(key, creds);
|
|
1236
|
+
const headers = {};
|
|
1237
|
+
if (range)
|
|
1238
|
+
headers['Range'] = range;
|
|
1239
|
+
// Azure requires this header for range requests
|
|
1240
|
+
if (range)
|
|
1241
|
+
headers['x-ms-range'] = range;
|
|
1242
|
+
const controller = new AbortController();
|
|
1243
|
+
wasmModule.storeJsObject(index, { abort() { controller.abort(); } });
|
|
1244
|
+
fetch(azureBlobUrl(creds, blobP), { headers, signal: controller.signal })
|
|
1245
|
+
.then(async (resp) => {
|
|
1246
|
+
if (!resp.ok) {
|
|
1247
|
+
c1.call({ responseCode: resp.status, message: resp.statusText || 'download_failed' });
|
|
1248
|
+
c1.delete();
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
1252
|
+
const view = wasmModule.provideResponseBuffer(index, buf.length);
|
|
1253
|
+
view.set(buf);
|
|
1254
|
+
c1.call({ responseCode: 200 });
|
|
1255
|
+
c1.delete();
|
|
1256
|
+
})
|
|
1257
|
+
.catch((err) => {
|
|
1258
|
+
if (err.name === 'AbortError') {
|
|
1259
|
+
c1.call({ responseCode: 1000, message: 'aborted' });
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1263
|
+
}
|
|
1264
|
+
c1.delete();
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
},
|
|
1268
|
+
list(root, key, _startAfter, completion) {
|
|
1269
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1270
|
+
const results = [];
|
|
1271
|
+
rotatingCreds.get().then((creds) => {
|
|
1272
|
+
// root comes as accountName/containerName/prefix/
|
|
1273
|
+
// We need to extract the prefix portion
|
|
1274
|
+
const rootClean = removeAzureScheme(root);
|
|
1275
|
+
const prefixPart = azureBlobPath(rootClean, creds);
|
|
1276
|
+
const finalPrefix = (prefixPart.endsWith('/') ? prefixPart : `${prefixPart}/`) + key;
|
|
1277
|
+
const impl = (marker) => {
|
|
1278
|
+
let params = `restype=container&comp=list&prefix=${encodeURIComponent(finalPrefix)}`;
|
|
1279
|
+
if (marker)
|
|
1280
|
+
params += `&marker=${encodeURIComponent(marker)}`;
|
|
1281
|
+
fetch(azureListUrl(creds, params))
|
|
1282
|
+
.then((resp) => {
|
|
1283
|
+
if (!resp.ok) {
|
|
1284
|
+
c1.call({ responseCode: resp.status, message: 'list_request_failed' });
|
|
1285
|
+
c1.delete();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
return resp.text();
|
|
1289
|
+
})
|
|
1290
|
+
.then((xml) => {
|
|
1291
|
+
if (!xml)
|
|
1292
|
+
return;
|
|
1293
|
+
const parsed = parseAzureListBlobsXml(xml);
|
|
1294
|
+
for (const blob of parsed.blobs) {
|
|
1295
|
+
results.push({
|
|
1296
|
+
path: blob.name.substring(prefixPart.length + 1),
|
|
1297
|
+
size: blob.size,
|
|
1298
|
+
lastModified: blob.lastModified,
|
|
1299
|
+
etag: blob.etag,
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
if (parsed.nextMarker) {
|
|
1303
|
+
impl(parsed.nextMarker);
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
c1.call({ responseCode: 200, data: results });
|
|
1307
|
+
c1.delete();
|
|
1308
|
+
}
|
|
1309
|
+
})
|
|
1310
|
+
.catch((err) => {
|
|
1311
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'list_request_failed' });
|
|
1312
|
+
c1.delete();
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1315
|
+
impl();
|
|
1316
|
+
});
|
|
1317
|
+
},
|
|
1318
|
+
listDirs(root, prefix, completion) {
|
|
1319
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1320
|
+
rotatingCreds.get().then((creds) => {
|
|
1321
|
+
const rootClean = removeAzureScheme(root);
|
|
1322
|
+
const rootPrefix = azureBlobPath(rootClean, creds);
|
|
1323
|
+
const finalPrefix = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + prefix;
|
|
1324
|
+
let params = `restype=container&comp=list&prefix=${encodeURIComponent(finalPrefix)}&delimiter=/`;
|
|
1325
|
+
fetch(azureListUrl(creds, params))
|
|
1326
|
+
.then((resp) => {
|
|
1327
|
+
if (!resp.ok) {
|
|
1328
|
+
c1.call({ responseCode: resp.status, message: 'list_dirs_failed' });
|
|
1329
|
+
c1.delete();
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
return resp.text();
|
|
1333
|
+
})
|
|
1334
|
+
.then((xml) => {
|
|
1335
|
+
if (!xml)
|
|
1336
|
+
return;
|
|
1337
|
+
const dirs = parseAzureBlobPrefixes(xml);
|
|
1338
|
+
c1.call({ responseCode: 200, data: dirs });
|
|
1339
|
+
c1.delete();
|
|
1340
|
+
})
|
|
1341
|
+
.catch((err) => {
|
|
1342
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'list_dirs_failed' });
|
|
1343
|
+
c1.delete();
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
},
|
|
1347
|
+
metadata(root, key, completion) {
|
|
1348
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1349
|
+
rotatingCreds.get().then((creds) => {
|
|
1350
|
+
const rootClean = removeAzureScheme(root);
|
|
1351
|
+
const rootPrefix = azureBlobPath(rootClean, creds);
|
|
1352
|
+
const blobName = (rootPrefix.endsWith('/') ? rootPrefix : `${rootPrefix}/`) + key;
|
|
1353
|
+
// Use a list call with exact prefix to get metadata
|
|
1354
|
+
let params = `restype=container&comp=list&prefix=${encodeURIComponent(blobName)}`;
|
|
1355
|
+
fetch(azureListUrl(creds, params))
|
|
1356
|
+
.then((resp) => {
|
|
1357
|
+
if (!resp.ok) {
|
|
1358
|
+
c1.call({ responseCode: resp.status, message: 'metadata_request_failed' });
|
|
1359
|
+
c1.delete();
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
return resp.text();
|
|
1363
|
+
})
|
|
1364
|
+
.then((xml) => {
|
|
1365
|
+
if (!xml)
|
|
1366
|
+
return;
|
|
1367
|
+
const parsed = parseAzureListBlobsXml(xml);
|
|
1368
|
+
if (parsed.blobs.length === 0) {
|
|
1369
|
+
c1.call({ responseCode: 404, message: 'resource_not_found' });
|
|
1370
|
+
c1.delete();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const blob = parsed.blobs[0];
|
|
1374
|
+
c1.call({
|
|
1375
|
+
responseCode: 200,
|
|
1376
|
+
data: {
|
|
1377
|
+
path: blob.name.substring(rootPrefix.length + 1),
|
|
1378
|
+
size: blob.size,
|
|
1379
|
+
lastModified: blob.lastModified,
|
|
1380
|
+
etag: blob.etag,
|
|
1381
|
+
},
|
|
1382
|
+
});
|
|
1383
|
+
c1.delete();
|
|
1384
|
+
})
|
|
1385
|
+
.catch((err) => {
|
|
1386
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'metadata_request_failed' });
|
|
1387
|
+
c1.delete();
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
},
|
|
1391
|
+
copyGetter() {
|
|
1392
|
+
return getter;
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
return getter;
|
|
1396
|
+
}
|
|
1397
|
+
function createAzureWriter(rotatingCreds, _wasmModule) {
|
|
1398
|
+
const writer = {
|
|
1399
|
+
saveWithCompletion(key, content, completion) {
|
|
1400
|
+
key = removeAzureScheme(key);
|
|
1401
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1402
|
+
const body = toFetchBody(content);
|
|
1403
|
+
rotatingCreds.get().then((creds) => {
|
|
1404
|
+
const blobP = azureBlobPath(key, creds);
|
|
1405
|
+
fetch(azureBlobUrl(creds, blobP), {
|
|
1406
|
+
method: 'PUT',
|
|
1407
|
+
headers: {
|
|
1408
|
+
'x-ms-blob-type': 'BlockBlob',
|
|
1409
|
+
'Content-Type': 'application/octet-stream',
|
|
1410
|
+
},
|
|
1411
|
+
body,
|
|
1412
|
+
})
|
|
1413
|
+
.then((resp) => {
|
|
1414
|
+
const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
|
|
1415
|
+
if (resp.ok) {
|
|
1416
|
+
c1.call({ responseCode: resp.status, etag });
|
|
1417
|
+
}
|
|
1418
|
+
else {
|
|
1419
|
+
c1.call({ responseCode: resp.status, message: 'upload_failed' });
|
|
1420
|
+
}
|
|
1421
|
+
c1.delete();
|
|
1422
|
+
})
|
|
1423
|
+
.catch((err) => {
|
|
1424
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1425
|
+
c1.delete();
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
},
|
|
1429
|
+
saveConditional(key, content, ifMatchEtag, completion) {
|
|
1430
|
+
key = removeAzureScheme(key);
|
|
1431
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1432
|
+
const body = toFetchBody(content);
|
|
1433
|
+
const quotedEtag = ifMatchEtag.startsWith('"') ? ifMatchEtag : `"${ifMatchEtag}"`;
|
|
1434
|
+
rotatingCreds.get().then((creds) => {
|
|
1435
|
+
const blobP = azureBlobPath(key, creds);
|
|
1436
|
+
fetch(azureBlobUrl(creds, blobP), {
|
|
1437
|
+
method: 'PUT',
|
|
1438
|
+
headers: {
|
|
1439
|
+
'x-ms-blob-type': 'BlockBlob',
|
|
1440
|
+
'Content-Type': 'application/octet-stream',
|
|
1441
|
+
'If-Match': quotedEtag,
|
|
1442
|
+
},
|
|
1443
|
+
body,
|
|
1444
|
+
})
|
|
1445
|
+
.then((resp) => {
|
|
1446
|
+
const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
|
|
1447
|
+
if (resp.ok) {
|
|
1448
|
+
c1.call({ responseCode: resp.status, etag });
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'conditional_write_failed' });
|
|
1452
|
+
}
|
|
1453
|
+
c1.delete();
|
|
1454
|
+
})
|
|
1455
|
+
.catch((err) => {
|
|
1456
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1457
|
+
c1.delete();
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
},
|
|
1461
|
+
saveExclusive(key, content, completion) {
|
|
1462
|
+
key = removeAzureScheme(key);
|
|
1463
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1464
|
+
const body = toFetchBody(content);
|
|
1465
|
+
rotatingCreds.get().then((creds) => {
|
|
1466
|
+
const blobP = azureBlobPath(key, creds);
|
|
1467
|
+
fetch(azureBlobUrl(creds, blobP), {
|
|
1468
|
+
method: 'PUT',
|
|
1469
|
+
headers: {
|
|
1470
|
+
'x-ms-blob-type': 'BlockBlob',
|
|
1471
|
+
'Content-Type': 'application/octet-stream',
|
|
1472
|
+
'If-None-Match': '*',
|
|
1473
|
+
},
|
|
1474
|
+
body,
|
|
1475
|
+
})
|
|
1476
|
+
.then((resp) => {
|
|
1477
|
+
const etag = (resp.headers.get('etag') ?? '').replace(/"/g, '');
|
|
1478
|
+
if (resp.ok) {
|
|
1479
|
+
c1.call({ responseCode: resp.status, etag });
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
c1.call({ responseCode: resp.status === 412 ? 412 : resp.status, message: 'exclusive_write_failed' });
|
|
1483
|
+
}
|
|
1484
|
+
c1.delete();
|
|
1485
|
+
})
|
|
1486
|
+
.catch((err) => {
|
|
1487
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1488
|
+
c1.delete();
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
},
|
|
1492
|
+
remove(key, completion) {
|
|
1493
|
+
key = removeAzureScheme(key);
|
|
1494
|
+
const c1 = (0, inflight_tracker_1.trackClone)(completion);
|
|
1495
|
+
rotatingCreds.get().then((creds) => {
|
|
1496
|
+
const blobP = azureBlobPath(key, creds);
|
|
1497
|
+
fetch(azureBlobUrl(creds, blobP), { method: 'DELETE' })
|
|
1498
|
+
.then((resp) => {
|
|
1499
|
+
c1.call({ responseCode: resp.ok ? 200 : resp.status });
|
|
1500
|
+
c1.delete();
|
|
1501
|
+
})
|
|
1502
|
+
.catch((err) => {
|
|
1503
|
+
c1.call({ responseCode: 1000, message: err.message ?? 'unknown_error' });
|
|
1504
|
+
c1.delete();
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
},
|
|
1508
|
+
copyWriter() {
|
|
1509
|
+
return writer;
|
|
1510
|
+
},
|
|
1511
|
+
};
|
|
1512
|
+
return writer;
|
|
1513
|
+
}
|
|
592
1514
|
//# sourceMappingURL=storage.js.map
|