@opengeni/storage 0.2.68 → 0.2.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bounded-object-read.d.ts +76 -0
- package/dist/bounded-object-write.d.ts +53 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +815 -13
- package/dist/index.js.map +1 -1
- package/dist/object-storage-bounded.d.ts +21 -0
- package/package.json +3 -3
- package/src/bounded-object-read.ts +323 -0
- package/src/bounded-object-write.ts +269 -0
- package/src/index.ts +300 -11
- package/src/object-storage-bounded.ts +297 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { RETAINED_OUTPUT_MAX_PAGE_BYTES } from "@opengeni/contracts";
|
|
3
|
+
import { Readable } from "stream";
|
|
4
|
+
import { pipeline } from "stream/promises";
|
|
3
5
|
import {
|
|
4
6
|
BlobSASPermissions,
|
|
5
7
|
BlobServiceClient,
|
|
@@ -17,9 +19,576 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
|
17
19
|
import {
|
|
18
20
|
Storage as GcsClient
|
|
19
21
|
} from "@google-cloud/storage";
|
|
22
|
+
|
|
23
|
+
// src/bounded-object-read.ts
|
|
24
|
+
var DEFAULT_BOUNDED_OBJECT_CHUNK_BYTES = 1024 * 1024;
|
|
25
|
+
var MAX_BOUNDED_OBJECT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
26
|
+
var BoundedObjectReadError = class extends Error {
|
|
27
|
+
code;
|
|
28
|
+
constructor(code) {
|
|
29
|
+
super(messageForCode(code));
|
|
30
|
+
this.name = "BoundedObjectReadError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function createBoundedObjectReadPort(backend) {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
async open(input) {
|
|
37
|
+
validateReference(input.opaqueReference);
|
|
38
|
+
assertPositiveSafeInteger(input.maxBytes);
|
|
39
|
+
if (input.expectedByteSize !== void 0) {
|
|
40
|
+
assertNonnegativeSafeInteger(input.expectedByteSize);
|
|
41
|
+
}
|
|
42
|
+
throwIfAborted(input.signal);
|
|
43
|
+
const description = await scrubBackendFailure(
|
|
44
|
+
async () => backend.describe({
|
|
45
|
+
opaqueReference: input.opaqueReference,
|
|
46
|
+
...input.signal ? { signal: input.signal } : {}
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
throwIfAborted(input.signal);
|
|
50
|
+
if (!description) throw new BoundedObjectReadError("object_missing");
|
|
51
|
+
validateDescription(description);
|
|
52
|
+
if (description.byteSize > input.maxBytes) {
|
|
53
|
+
throw new BoundedObjectReadError("size_limit");
|
|
54
|
+
}
|
|
55
|
+
if (input.expectedByteSize !== void 0 && description.byteSize !== input.expectedByteSize) {
|
|
56
|
+
throw new BoundedObjectReadError("truncated");
|
|
57
|
+
}
|
|
58
|
+
return new VersionPinnedBoundedObjectRead(backend, input.opaqueReference, description);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
var VersionPinnedBoundedObjectRead = class {
|
|
63
|
+
byteSize;
|
|
64
|
+
contentType;
|
|
65
|
+
#backend;
|
|
66
|
+
#opaqueReference;
|
|
67
|
+
#versionToken;
|
|
68
|
+
#claimed = false;
|
|
69
|
+
#closed = false;
|
|
70
|
+
constructor(backend, opaqueReference, description) {
|
|
71
|
+
this.#backend = backend;
|
|
72
|
+
this.#opaqueReference = opaqueReference;
|
|
73
|
+
this.#versionToken = description.versionToken;
|
|
74
|
+
this.byteSize = description.byteSize;
|
|
75
|
+
this.contentType = description.contentType;
|
|
76
|
+
}
|
|
77
|
+
chunks(input = {}) {
|
|
78
|
+
if (this.#closed || this.#claimed) {
|
|
79
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
80
|
+
}
|
|
81
|
+
const chunkBytes = input.chunkBytes ?? DEFAULT_BOUNDED_OBJECT_CHUNK_BYTES;
|
|
82
|
+
assertPositiveSafeInteger(chunkBytes);
|
|
83
|
+
if (chunkBytes > MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
|
|
84
|
+
throw new BoundedObjectReadError("size_limit");
|
|
85
|
+
}
|
|
86
|
+
this.#claimed = true;
|
|
87
|
+
return this.#streamChunks(chunkBytes, input.signal);
|
|
88
|
+
}
|
|
89
|
+
async *#streamChunks(chunkBytes, signal) {
|
|
90
|
+
let offset = 0;
|
|
91
|
+
let completed = false;
|
|
92
|
+
try {
|
|
93
|
+
while (offset < this.byteSize) {
|
|
94
|
+
throwIfAborted(signal);
|
|
95
|
+
if (this.#closed) {
|
|
96
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
97
|
+
}
|
|
98
|
+
const endInclusive = Math.min(this.byteSize - 1, offset + chunkBytes - 1);
|
|
99
|
+
const range = await scrubBackendFailure(
|
|
100
|
+
async () => this.#backend.readRange({
|
|
101
|
+
opaqueReference: this.#opaqueReference,
|
|
102
|
+
start: offset,
|
|
103
|
+
endInclusive,
|
|
104
|
+
expectedVersionToken: this.#versionToken,
|
|
105
|
+
...signal ? { signal } : {}
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
throwIfAborted(signal);
|
|
109
|
+
if (!range) {
|
|
110
|
+
throw new BoundedObjectReadError("object_changed");
|
|
111
|
+
}
|
|
112
|
+
if (range.versionToken !== this.#versionToken) {
|
|
113
|
+
throw new BoundedObjectReadError("object_changed");
|
|
114
|
+
}
|
|
115
|
+
const expectedLength = endInclusive - offset + 1;
|
|
116
|
+
if (range.bytes.byteLength !== expectedLength) {
|
|
117
|
+
throw new BoundedObjectReadError("truncated");
|
|
118
|
+
}
|
|
119
|
+
offset += range.bytes.byteLength;
|
|
120
|
+
yield range.bytes.slice();
|
|
121
|
+
}
|
|
122
|
+
throwIfAborted(signal);
|
|
123
|
+
completed = true;
|
|
124
|
+
} finally {
|
|
125
|
+
if (!completed) await this.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async assertUnchanged(signal) {
|
|
129
|
+
if (this.#closed) {
|
|
130
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
131
|
+
}
|
|
132
|
+
throwIfAborted(signal);
|
|
133
|
+
const current = await scrubBackendFailure(
|
|
134
|
+
async () => this.#backend.describe({
|
|
135
|
+
opaqueReference: this.#opaqueReference,
|
|
136
|
+
...signal ? { signal } : {}
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
throwIfAborted(signal);
|
|
140
|
+
if (!current || current.versionToken !== this.#versionToken || current.byteSize !== this.byteSize) {
|
|
141
|
+
throw new BoundedObjectReadError("object_changed");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async close() {
|
|
145
|
+
if (this.#closed) return;
|
|
146
|
+
this.#closed = true;
|
|
147
|
+
if (!this.#backend.close) return;
|
|
148
|
+
try {
|
|
149
|
+
await this.#backend.close({ opaqueReference: this.#opaqueReference });
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
async function scrubBackendFailure(operation) {
|
|
155
|
+
try {
|
|
156
|
+
return await operation();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof BoundedObjectReadError) throw error;
|
|
159
|
+
throw new BoundedObjectReadError("backend_failure");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function validateReference(value) {
|
|
163
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 2048 || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
164
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function validateDescription(value) {
|
|
168
|
+
assertNonnegativeSafeInteger(value.byteSize);
|
|
169
|
+
if (value.immutableReference !== true) {
|
|
170
|
+
throw new BoundedObjectReadError("backend_failure");
|
|
171
|
+
}
|
|
172
|
+
if (typeof value.versionToken !== "string" || value.versionToken.length < 1 || value.versionToken.length > 2048) {
|
|
173
|
+
throw new BoundedObjectReadError("backend_failure");
|
|
174
|
+
}
|
|
175
|
+
if (value.contentType !== void 0 && (value.contentType.length < 1 || value.contentType.length > 256)) {
|
|
176
|
+
throw new BoundedObjectReadError("backend_failure");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function assertPositiveSafeInteger(value) {
|
|
180
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
181
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function assertNonnegativeSafeInteger(value) {
|
|
185
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
186
|
+
throw new BoundedObjectReadError("invalid_request");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function throwIfAborted(signal) {
|
|
190
|
+
if (signal?.aborted) throw new BoundedObjectReadError("aborted");
|
|
191
|
+
}
|
|
192
|
+
function messageForCode(code) {
|
|
193
|
+
switch (code) {
|
|
194
|
+
case "aborted":
|
|
195
|
+
return "Bounded object read was cancelled";
|
|
196
|
+
case "backend_failure":
|
|
197
|
+
return "Bounded object read failed";
|
|
198
|
+
case "invalid_request":
|
|
199
|
+
return "Bounded object read request is invalid";
|
|
200
|
+
case "object_changed":
|
|
201
|
+
return "Bounded object changed during verification";
|
|
202
|
+
case "object_missing":
|
|
203
|
+
return "Bounded object is unavailable";
|
|
204
|
+
case "size_limit":
|
|
205
|
+
return "Bounded object exceeds the configured limit";
|
|
206
|
+
case "truncated":
|
|
207
|
+
return "Bounded object length does not match its immutable metadata";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/bounded-object-write.ts
|
|
212
|
+
import { createHash } from "crypto";
|
|
213
|
+
var BoundedObjectWriteError = class extends Error {
|
|
214
|
+
code;
|
|
215
|
+
constructor(code) {
|
|
216
|
+
super(writeErrorMessage(code));
|
|
217
|
+
this.name = "BoundedObjectWriteError";
|
|
218
|
+
this.code = code;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function createBoundedImmutableObjectWritePort(input) {
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
async write(request) {
|
|
224
|
+
validateWriteRequest(request);
|
|
225
|
+
throwIfAborted2(request.signal);
|
|
226
|
+
let session;
|
|
227
|
+
let committed = false;
|
|
228
|
+
try {
|
|
229
|
+
session = await scrubWriteBackend(
|
|
230
|
+
async () => input.backend.begin({
|
|
231
|
+
contentType: request.contentType,
|
|
232
|
+
...request.signal ? { signal: request.signal } : {}
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
const digest = createHash("sha256");
|
|
236
|
+
let byteSize = 0;
|
|
237
|
+
for await (const sourceChunk of request.chunks) {
|
|
238
|
+
throwIfAborted2(request.signal);
|
|
239
|
+
if (!(sourceChunk instanceof Uint8Array) || sourceChunk.byteLength === 0) {
|
|
240
|
+
throw new BoundedObjectWriteError("truncated");
|
|
241
|
+
}
|
|
242
|
+
if (sourceChunk.byteLength > MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
|
|
243
|
+
throw new BoundedObjectWriteError("size_limit");
|
|
244
|
+
}
|
|
245
|
+
byteSize += sourceChunk.byteLength;
|
|
246
|
+
if (byteSize > request.maxBytes) {
|
|
247
|
+
throw new BoundedObjectWriteError("size_limit");
|
|
248
|
+
}
|
|
249
|
+
const chunk = sourceChunk.slice();
|
|
250
|
+
digest.update(chunk);
|
|
251
|
+
await scrubWriteBackend(async () => session.write(chunk, request.signal));
|
|
252
|
+
}
|
|
253
|
+
throwIfAborted2(request.signal);
|
|
254
|
+
if (request.expectedByteSize !== void 0 && byteSize !== request.expectedByteSize) {
|
|
255
|
+
throw new BoundedObjectWriteError("truncated");
|
|
256
|
+
}
|
|
257
|
+
const contentHash = `sha256:${digest.digest("hex")}`;
|
|
258
|
+
if (request.expectedContentHash !== void 0 && contentHash !== request.expectedContentHash) {
|
|
259
|
+
throw new BoundedObjectWriteError("content_hash_mismatch");
|
|
260
|
+
}
|
|
261
|
+
const promoted = await scrubWriteBackend(
|
|
262
|
+
async () => session.commit({
|
|
263
|
+
byteSize,
|
|
264
|
+
contentHash,
|
|
265
|
+
contentType: request.contentType,
|
|
266
|
+
...request.signal ? { signal: request.signal } : {}
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
validateOpaqueReference(promoted.opaqueReference);
|
|
270
|
+
committed = true;
|
|
271
|
+
throwIfAborted2(request.signal);
|
|
272
|
+
const reader = await input.readback.open({
|
|
273
|
+
opaqueReference: promoted.opaqueReference,
|
|
274
|
+
maxBytes: request.maxBytes,
|
|
275
|
+
expectedByteSize: byteSize,
|
|
276
|
+
...request.signal ? { signal: request.signal } : {}
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
if (reader.contentType !== void 0 && reader.contentType !== request.contentType) {
|
|
280
|
+
throw new BoundedObjectWriteError("readback_mismatch");
|
|
281
|
+
}
|
|
282
|
+
const readbackHash = createHash("sha256");
|
|
283
|
+
let readbackBytes = 0;
|
|
284
|
+
for await (const chunk of reader.chunks({
|
|
285
|
+
...request.signal ? { signal: request.signal } : {}
|
|
286
|
+
})) {
|
|
287
|
+
readbackBytes += chunk.byteLength;
|
|
288
|
+
if (readbackBytes > byteSize) {
|
|
289
|
+
throw new BoundedObjectWriteError("readback_mismatch");
|
|
290
|
+
}
|
|
291
|
+
readbackHash.update(chunk);
|
|
292
|
+
}
|
|
293
|
+
const readbackContentHash = `sha256:${readbackHash.digest("hex")}`;
|
|
294
|
+
if (readbackBytes !== byteSize || readbackContentHash !== contentHash) {
|
|
295
|
+
throw new BoundedObjectWriteError("readback_mismatch");
|
|
296
|
+
}
|
|
297
|
+
await reader.assertUnchanged(request.signal);
|
|
298
|
+
throwIfAborted2(request.signal);
|
|
299
|
+
} finally {
|
|
300
|
+
await reader.close();
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
opaqueReference: promoted.opaqueReference,
|
|
304
|
+
byteSize,
|
|
305
|
+
contentHash,
|
|
306
|
+
contentType: request.contentType
|
|
307
|
+
});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw mapWriteFailure(error, request.signal);
|
|
310
|
+
} finally {
|
|
311
|
+
if (session && !committed) {
|
|
312
|
+
try {
|
|
313
|
+
await session.abort();
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function validateWriteRequest(input) {
|
|
322
|
+
if (!Number.isSafeInteger(input.maxBytes) || input.maxBytes <= 0) {
|
|
323
|
+
throw new BoundedObjectWriteError("invalid_request");
|
|
324
|
+
}
|
|
325
|
+
if (input.expectedByteSize !== void 0 && (!Number.isSafeInteger(input.expectedByteSize) || input.expectedByteSize < 0)) {
|
|
326
|
+
throw new BoundedObjectWriteError("invalid_request");
|
|
327
|
+
}
|
|
328
|
+
if (typeof input.contentType !== "string" || input.contentType.length < 1 || input.contentType.length > 256 || /[\u0000-\u001f\u007f]/.test(input.contentType)) {
|
|
329
|
+
throw new BoundedObjectWriteError("invalid_request");
|
|
330
|
+
}
|
|
331
|
+
if (input.expectedContentHash !== void 0 && !/^sha256:[0-9a-f]{64}$/.test(input.expectedContentHash)) {
|
|
332
|
+
throw new BoundedObjectWriteError("invalid_request");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function validateOpaqueReference(value) {
|
|
336
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 2048 || value.trim() !== value || /[\u0000-\u001f\u007f]/.test(value)) {
|
|
337
|
+
throw new BoundedObjectWriteError("backend_failure");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function scrubWriteBackend(operation) {
|
|
341
|
+
try {
|
|
342
|
+
return await operation();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error instanceof BoundedObjectWriteError) throw error;
|
|
345
|
+
throw new BoundedObjectWriteError("backend_failure");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function mapWriteFailure(error, signal) {
|
|
349
|
+
if (error instanceof BoundedObjectWriteError) return error;
|
|
350
|
+
if (signal?.aborted) return new BoundedObjectWriteError("aborted");
|
|
351
|
+
return new BoundedObjectWriteError("backend_failure");
|
|
352
|
+
}
|
|
353
|
+
function throwIfAborted2(signal) {
|
|
354
|
+
if (signal?.aborted) throw new BoundedObjectWriteError("aborted");
|
|
355
|
+
}
|
|
356
|
+
function writeErrorMessage(code) {
|
|
357
|
+
switch (code) {
|
|
358
|
+
case "aborted":
|
|
359
|
+
return "Bounded object write was cancelled";
|
|
360
|
+
case "backend_failure":
|
|
361
|
+
return "Bounded object write failed";
|
|
362
|
+
case "content_hash_mismatch":
|
|
363
|
+
return "Bounded object digest does not match expected canonical bytes";
|
|
364
|
+
case "invalid_request":
|
|
365
|
+
return "Bounded object write request is invalid";
|
|
366
|
+
case "readback_mismatch":
|
|
367
|
+
return "Immutable object read-back verification failed";
|
|
368
|
+
case "size_limit":
|
|
369
|
+
return "Bounded object write exceeds the configured limit";
|
|
370
|
+
case "truncated":
|
|
371
|
+
return "Bounded object write length does not match expected canonical bytes";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/object-storage-bounded.ts
|
|
376
|
+
import { createHash as createHash2 } from "crypto";
|
|
377
|
+
import { chmod, mkdtemp, open, rm } from "fs/promises";
|
|
378
|
+
import { tmpdir } from "os";
|
|
379
|
+
import { join } from "path";
|
|
380
|
+
var SNAPSHOT_KEY_PREFIX = "editable-artifacts/snapshots/sha256/";
|
|
381
|
+
function createObjectStorageBoundedPorts(storage, options = {}) {
|
|
382
|
+
if (!storage.headObject || !storage.getObjectRange || !storage.putObjectStreamIfAbsent) {
|
|
383
|
+
throw new Error("Object storage lacks streaming immutable create/versioned range primitives");
|
|
384
|
+
}
|
|
385
|
+
const keyPrefix = validateKeyPrefix(options.keyPrefix ?? SNAPSHOT_KEY_PREFIX);
|
|
386
|
+
const referenceHash = (reference) => hashForReference(reference, keyPrefix);
|
|
387
|
+
const read = createBoundedObjectReadPort(versionedBackend(storage, referenceHash));
|
|
388
|
+
const write = createBoundedImmutableObjectWritePort({
|
|
389
|
+
backend: immutableWriteBackend(storage, keyPrefix),
|
|
390
|
+
readback: read
|
|
391
|
+
});
|
|
392
|
+
return Object.freeze({ read, write });
|
|
393
|
+
}
|
|
394
|
+
function versionedBackend(storage, hashForReferenceValue) {
|
|
395
|
+
const headObject = storage.headObject;
|
|
396
|
+
const getObjectRange = storage.getObjectRange;
|
|
397
|
+
return Object.freeze({
|
|
398
|
+
async describe(input) {
|
|
399
|
+
const expectedHash = hashForReferenceValue(input.opaqueReference);
|
|
400
|
+
throwIfAborted3(input.signal);
|
|
401
|
+
const head = await headObject(input.opaqueReference);
|
|
402
|
+
throwIfAborted3(input.signal);
|
|
403
|
+
if (!head) return null;
|
|
404
|
+
if (!Number.isSafeInteger(head.ContentLength) || head.ContentLength < 0 || typeof head.VersionToken !== "string" || head.VersionToken.length < 1 || head.VersionToken.length > 2048 || head.Metadata?.sha256 !== expectedHash) {
|
|
405
|
+
throw new Error("Immutable object metadata is invalid");
|
|
406
|
+
}
|
|
407
|
+
return Object.freeze({
|
|
408
|
+
byteSize: head.ContentLength,
|
|
409
|
+
versionToken: head.VersionToken,
|
|
410
|
+
immutableReference: true,
|
|
411
|
+
...head.ContentType ? { contentType: head.ContentType } : {}
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
async readRange(input) {
|
|
415
|
+
hashForReferenceValue(input.opaqueReference);
|
|
416
|
+
throwIfAborted3(input.signal);
|
|
417
|
+
const result = await getObjectRange({
|
|
418
|
+
key: input.opaqueReference,
|
|
419
|
+
start: input.start,
|
|
420
|
+
endInclusive: input.endInclusive,
|
|
421
|
+
expectedVersionToken: input.expectedVersionToken
|
|
422
|
+
});
|
|
423
|
+
throwIfAborted3(input.signal);
|
|
424
|
+
if (!result) return null;
|
|
425
|
+
return Object.freeze({
|
|
426
|
+
bytes: result.bytes.slice(),
|
|
427
|
+
versionToken: result.versionToken
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function immutableWriteBackend(storage, keyPrefix) {
|
|
433
|
+
return Object.freeze({
|
|
434
|
+
async begin(input) {
|
|
435
|
+
validateContentType(input.contentType);
|
|
436
|
+
throwIfAborted3(input.signal);
|
|
437
|
+
const stagingDirectory = await mkdtemp(join(tmpdir(), "opengeni-artifact-write-"));
|
|
438
|
+
const stagingPath = join(stagingDirectory, "payload");
|
|
439
|
+
let handle = null;
|
|
440
|
+
try {
|
|
441
|
+
await chmod(stagingDirectory, 448);
|
|
442
|
+
throwIfAborted3(input.signal);
|
|
443
|
+
handle = await open(stagingPath, "wx", 384);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
let byteSize = 0;
|
|
449
|
+
let closed = false;
|
|
450
|
+
let cleaned = false;
|
|
451
|
+
const digest = createHash2("sha256");
|
|
452
|
+
const cleanup = async () => {
|
|
453
|
+
if (cleaned) return;
|
|
454
|
+
cleaned = true;
|
|
455
|
+
if (handle) {
|
|
456
|
+
const closing = handle;
|
|
457
|
+
handle = null;
|
|
458
|
+
await closing.close().catch(() => void 0);
|
|
459
|
+
}
|
|
460
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
461
|
+
};
|
|
462
|
+
return {
|
|
463
|
+
async write(chunk, signal) {
|
|
464
|
+
if (closed) throw new Error("Immutable write session is closed");
|
|
465
|
+
throwIfAborted3(signal);
|
|
466
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength === 0) {
|
|
467
|
+
throw new Error("Immutable write chunk is invalid");
|
|
468
|
+
}
|
|
469
|
+
await writeAll(handle, chunk, byteSize);
|
|
470
|
+
digest.update(chunk);
|
|
471
|
+
byteSize += chunk.byteLength;
|
|
472
|
+
},
|
|
473
|
+
async commit(commit) {
|
|
474
|
+
if (closed) throw new Error("Immutable write session is closed");
|
|
475
|
+
closed = true;
|
|
476
|
+
try {
|
|
477
|
+
throwIfAborted3(commit.signal);
|
|
478
|
+
if (commit.contentType !== input.contentType || commit.byteSize !== byteSize) {
|
|
479
|
+
throw new Error("Immutable write commit metadata changed");
|
|
480
|
+
}
|
|
481
|
+
const contentHash = `sha256:${digest.digest("hex")}`;
|
|
482
|
+
if (contentHash !== commit.contentHash) {
|
|
483
|
+
throw new Error("Immutable write digest changed");
|
|
484
|
+
}
|
|
485
|
+
const opaqueReference = `${keyPrefix}${contentHash.slice("sha256:".length)}`;
|
|
486
|
+
const closing = handle;
|
|
487
|
+
handle = null;
|
|
488
|
+
await closing.close();
|
|
489
|
+
const existing = await storage.headObject(opaqueReference);
|
|
490
|
+
if (existing) {
|
|
491
|
+
assertStoredObject(existing, byteSize, contentHash, input.contentType);
|
|
492
|
+
} else {
|
|
493
|
+
await storage.putObjectStreamIfAbsent({
|
|
494
|
+
key: opaqueReference,
|
|
495
|
+
contentType: input.contentType,
|
|
496
|
+
chunks: fileChunks(stagingPath, byteSize, commit.signal),
|
|
497
|
+
byteSize,
|
|
498
|
+
sha256: contentHash,
|
|
499
|
+
...commit.signal ? { signal: commit.signal } : {}
|
|
500
|
+
});
|
|
501
|
+
const stored = await storage.headObject(opaqueReference);
|
|
502
|
+
if (!stored) throw new Error("Immutable object was not visible after write");
|
|
503
|
+
assertStoredObject(stored, byteSize, contentHash, input.contentType);
|
|
504
|
+
}
|
|
505
|
+
throwIfAborted3(commit.signal);
|
|
506
|
+
return Object.freeze({ opaqueReference });
|
|
507
|
+
} finally {
|
|
508
|
+
await cleanup();
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
async abort() {
|
|
512
|
+
closed = true;
|
|
513
|
+
byteSize = 0;
|
|
514
|
+
await cleanup();
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function assertStoredObject(head, byteSize, contentHash, contentType) {
|
|
521
|
+
if (!head || head.ContentLength !== byteSize || head.Metadata?.sha256 !== contentHash || head.ContentType !== contentType || typeof head.VersionToken !== "string" || head.VersionToken.length < 1) {
|
|
522
|
+
throw new Error("Immutable content-addressed object conflicts with stored metadata");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function hashForReference(reference, keyPrefix) {
|
|
526
|
+
const match = new RegExp(`^${escapeRegExp(keyPrefix)}([0-9a-f]{64})$`, "u").exec(reference);
|
|
527
|
+
if (!match) throw new Error("Immutable snapshot reference is malformed");
|
|
528
|
+
return `sha256:${match[1]}`;
|
|
529
|
+
}
|
|
530
|
+
function validateKeyPrefix(value) {
|
|
531
|
+
if (typeof value !== "string" || !/^editable-artifacts\/[a-z0-9/-]+\/sha256\/$/u.test(value) || value.includes("//") || value.includes("..")) {
|
|
532
|
+
throw new Error("Immutable object key prefix is invalid");
|
|
533
|
+
}
|
|
534
|
+
return value;
|
|
535
|
+
}
|
|
536
|
+
function escapeRegExp(value) {
|
|
537
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
538
|
+
}
|
|
539
|
+
function validateContentType(value) {
|
|
540
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 256 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
541
|
+
throw new Error("Immutable object content type is invalid");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function* fileChunks(path, expectedByteSize, signal) {
|
|
545
|
+
const handle = await open(path, "r");
|
|
546
|
+
try {
|
|
547
|
+
const buffer = new Uint8Array(1024 * 1024);
|
|
548
|
+
let offset = 0;
|
|
549
|
+
while (offset < expectedByteSize) {
|
|
550
|
+
throwIfAborted3(signal);
|
|
551
|
+
const length = Math.min(buffer.byteLength, expectedByteSize - offset);
|
|
552
|
+
let filled = 0;
|
|
553
|
+
while (filled < length) {
|
|
554
|
+
const { bytesRead } = await handle.read(buffer, filled, length - filled, offset + filled);
|
|
555
|
+
if (bytesRead <= 0) throw new Error("Immutable staging file was truncated");
|
|
556
|
+
filled += bytesRead;
|
|
557
|
+
}
|
|
558
|
+
offset += filled;
|
|
559
|
+
yield buffer.slice(0, filled);
|
|
560
|
+
}
|
|
561
|
+
const extra = new Uint8Array(1);
|
|
562
|
+
if ((await handle.read(extra, 0, 1, expectedByteSize)).bytesRead !== 0) {
|
|
563
|
+
throw new Error("Immutable staging file exceeded its committed size");
|
|
564
|
+
}
|
|
565
|
+
} finally {
|
|
566
|
+
await handle.close();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function writeAll(handle, bytes, position) {
|
|
570
|
+
let offset = 0;
|
|
571
|
+
while (offset < bytes.byteLength) {
|
|
572
|
+
const { bytesWritten } = await handle.write(
|
|
573
|
+
bytes,
|
|
574
|
+
offset,
|
|
575
|
+
bytes.byteLength - offset,
|
|
576
|
+
position + offset
|
|
577
|
+
);
|
|
578
|
+
if (bytesWritten <= 0) throw new Error("Immutable staging write was truncated");
|
|
579
|
+
offset += bytesWritten;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function throwIfAborted3(signal) {
|
|
583
|
+
if (signal?.aborted) throw new Error("Object storage operation was cancelled");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/index.ts
|
|
20
587
|
var MAX_SINGLE_PUT_SIZE_BYTES = 5e9;
|
|
21
588
|
var UPLOAD_URL_TTL_SECONDS = 15 * 60;
|
|
22
589
|
var DOWNLOAD_URL_TTL_SECONDS = 5 * 60;
|
|
590
|
+
var INTERNAL_STREAM_BUFFER_BYTES = 1024 * 1024;
|
|
591
|
+
var INTERNAL_STREAM_CONCURRENCY = 2;
|
|
23
592
|
function createObjectStorage(settings) {
|
|
24
593
|
if (settings.objectStorageBackend === "azure-blob") {
|
|
25
594
|
return createAzureBlobObjectStorage(settings);
|
|
@@ -98,6 +667,44 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
98
667
|
})
|
|
99
668
|
);
|
|
100
669
|
},
|
|
670
|
+
async putObjectIfAbsent(args) {
|
|
671
|
+
try {
|
|
672
|
+
await requestClient.send(
|
|
673
|
+
new PutObjectCommand({
|
|
674
|
+
Bucket: settings.objectStorageBucket,
|
|
675
|
+
Key: args.key,
|
|
676
|
+
ContentType: args.contentType,
|
|
677
|
+
Body: args.body,
|
|
678
|
+
Metadata: { sha256: args.sha256 },
|
|
679
|
+
IfNoneMatch: "*"
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
return true;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (isS3VersionMismatch(error)) return false;
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
async putObjectStreamIfAbsent(args) {
|
|
689
|
+
try {
|
|
690
|
+
await requestClient.send(
|
|
691
|
+
new PutObjectCommand({
|
|
692
|
+
Bucket: settings.objectStorageBucket,
|
|
693
|
+
Key: args.key,
|
|
694
|
+
ContentType: args.contentType,
|
|
695
|
+
ContentLength: args.byteSize,
|
|
696
|
+
Body: Readable.from(args.chunks),
|
|
697
|
+
Metadata: { sha256: args.sha256 },
|
|
698
|
+
IfNoneMatch: "*"
|
|
699
|
+
}),
|
|
700
|
+
args.signal ? { abortSignal: args.signal } : void 0
|
|
701
|
+
);
|
|
702
|
+
return true;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (isS3VersionMismatch(error)) return false;
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
},
|
|
101
708
|
async headFile(file) {
|
|
102
709
|
const head = await requestClient.send(
|
|
103
710
|
new HeadObjectCommand({
|
|
@@ -150,6 +757,43 @@ function createS3CompatibleObjectStorage(settings) {
|
|
|
150
757
|
throw error;
|
|
151
758
|
}
|
|
152
759
|
},
|
|
760
|
+
async headObject(key) {
|
|
761
|
+
try {
|
|
762
|
+
const head = await requestClient.send(
|
|
763
|
+
new HeadObjectCommand({ Bucket: settings.objectStorageBucket, Key: key })
|
|
764
|
+
);
|
|
765
|
+
return objectHead({
|
|
766
|
+
contentLength: head.ContentLength,
|
|
767
|
+
contentType: head.ContentType,
|
|
768
|
+
metadata: head.Metadata,
|
|
769
|
+
versionToken: head.ETag
|
|
770
|
+
});
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (isS3NotFound(error)) return null;
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
async getObjectRange(args) {
|
|
777
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
778
|
+
try {
|
|
779
|
+
const result = await requestClient.send(
|
|
780
|
+
new GetObjectCommand({
|
|
781
|
+
Bucket: settings.objectStorageBucket,
|
|
782
|
+
Key: args.key,
|
|
783
|
+
Range: `bytes=${args.start}-${args.endInclusive}`,
|
|
784
|
+
IfMatch: args.expectedVersionToken
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
if (!result.ETag || result.ETag !== args.expectedVersionToken) return null;
|
|
788
|
+
return {
|
|
789
|
+
bytes: await s3BodyToBoundedBytes(result.Body, args.key, length),
|
|
790
|
+
versionToken: result.ETag
|
|
791
|
+
};
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (isS3NotFound(error) || isS3VersionMismatch(error)) return null;
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
},
|
|
153
797
|
async getObjectBytes(key) {
|
|
154
798
|
try {
|
|
155
799
|
const result = await requestClient.send(
|
|
@@ -223,6 +867,11 @@ function isS3NotFound(error) {
|
|
|
223
867
|
const metadata = "$metadata" in error ? error.$metadata : void 0;
|
|
224
868
|
return metadata?.httpStatusCode === 404;
|
|
225
869
|
}
|
|
870
|
+
function isS3VersionMismatch(error) {
|
|
871
|
+
if (!error || typeof error !== "object") return false;
|
|
872
|
+
const metadata = "$metadata" in error ? error.$metadata : void 0;
|
|
873
|
+
return metadata?.httpStatusCode === 412;
|
|
874
|
+
}
|
|
226
875
|
function createGcsObjectStorage(settings) {
|
|
227
876
|
const client = new GcsClient(gcsClientOptions(settings));
|
|
228
877
|
const bucket = client.bucket(settings.objectStorageBucket);
|
|
@@ -268,6 +917,39 @@ function createGcsObjectStorage(settings) {
|
|
|
268
917
|
...args.sha256 ? { metadata: { metadata: { sha256: args.sha256 } } } : {}
|
|
269
918
|
});
|
|
270
919
|
},
|
|
920
|
+
async putObjectIfAbsent(args) {
|
|
921
|
+
try {
|
|
922
|
+
await bucket.file(args.key).save(Buffer.from(args.body), {
|
|
923
|
+
contentType: args.contentType,
|
|
924
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
925
|
+
preconditionOpts: { ifGenerationMatch: 0 }
|
|
926
|
+
});
|
|
927
|
+
return true;
|
|
928
|
+
} catch (error) {
|
|
929
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
},
|
|
933
|
+
async putObjectStreamIfAbsent(args) {
|
|
934
|
+
const destination = bucket.file(args.key).createWriteStream({
|
|
935
|
+
resumable: false,
|
|
936
|
+
contentType: args.contentType,
|
|
937
|
+
highWaterMark: INTERNAL_STREAM_BUFFER_BYTES,
|
|
938
|
+
metadata: { metadata: { sha256: args.sha256 } },
|
|
939
|
+
preconditionOpts: { ifGenerationMatch: 0 }
|
|
940
|
+
});
|
|
941
|
+
try {
|
|
942
|
+
if (args.signal) {
|
|
943
|
+
await pipeline(Readable.from(args.chunks), destination, { signal: args.signal });
|
|
944
|
+
} else {
|
|
945
|
+
await pipeline(Readable.from(args.chunks), destination);
|
|
946
|
+
}
|
|
947
|
+
return true;
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (isGcsVersionMismatch(error)) return false;
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
},
|
|
271
953
|
async headFile(file) {
|
|
272
954
|
const [metadata] = await bucket.file(file.objectKey).getMetadata();
|
|
273
955
|
return objectHead({
|
|
@@ -299,6 +981,33 @@ function createGcsObjectStorage(settings) {
|
|
|
299
981
|
throw error;
|
|
300
982
|
}
|
|
301
983
|
},
|
|
984
|
+
async headObject(key) {
|
|
985
|
+
try {
|
|
986
|
+
const [metadata] = await bucket.file(key).getMetadata();
|
|
987
|
+
return objectHead({
|
|
988
|
+
contentLength: parseContentLength(metadata.size),
|
|
989
|
+
contentType: metadata.contentType,
|
|
990
|
+
metadata: stringMetadata(metadata.metadata),
|
|
991
|
+
versionToken: metadata.generation === void 0 ? void 0 : String(metadata.generation)
|
|
992
|
+
});
|
|
993
|
+
} catch (error) {
|
|
994
|
+
if (isGcsNotFound(error)) return null;
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
async getObjectRange(args) {
|
|
999
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
1000
|
+
try {
|
|
1001
|
+
const [bytes] = await bucket.file(args.key, { generation: args.expectedVersionToken }).download({ start: args.start, end: args.endInclusive });
|
|
1002
|
+
return {
|
|
1003
|
+
bytes: exactRangeBytes(bytes, args.key, length),
|
|
1004
|
+
versionToken: args.expectedVersionToken
|
|
1005
|
+
};
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
if (isGcsNotFound(error) || isGcsVersionMismatch(error)) return null;
|
|
1008
|
+
throw error;
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
302
1011
|
async getObjectBytes(key) {
|
|
303
1012
|
try {
|
|
304
1013
|
const [bytes] = await bucket.file(key).download();
|
|
@@ -318,10 +1027,19 @@ function createGcsObjectStorage(settings) {
|
|
|
318
1027
|
function isGcsNotFound(error) {
|
|
319
1028
|
return Boolean(error) && typeof error === "object" && error.code === 404;
|
|
320
1029
|
}
|
|
1030
|
+
function isGcsVersionMismatch(error) {
|
|
1031
|
+
return Boolean(error) && typeof error === "object" && error.code === 412;
|
|
1032
|
+
}
|
|
321
1033
|
function createAzureBlobObjectStorage(settings) {
|
|
322
1034
|
const sharedKey = azureSharedKeyCredential(settings);
|
|
323
|
-
const
|
|
324
|
-
const
|
|
1035
|
+
const requestServiceClient = settings.objectStorageAzureConnectionString ? BlobServiceClient.fromConnectionString(settings.objectStorageAzureConnectionString) : new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey);
|
|
1036
|
+
const presignServiceClient = settings.objectStorageAzureEndpoint ? new BlobServiceClient(azureBlobServiceUrl(settings), sharedKey) : requestServiceClient;
|
|
1037
|
+
const requestContainerClient = requestServiceClient.getContainerClient(
|
|
1038
|
+
settings.objectStorageBucket
|
|
1039
|
+
);
|
|
1040
|
+
const presignContainerClient = presignServiceClient.getContainerClient(
|
|
1041
|
+
settings.objectStorageBucket
|
|
1042
|
+
);
|
|
325
1043
|
return {
|
|
326
1044
|
bucket: settings.objectStorageBucket,
|
|
327
1045
|
backend: "azure-blob",
|
|
@@ -329,7 +1047,7 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
329
1047
|
async createPutUrl(args) {
|
|
330
1048
|
const expiresIn = args.expiresInSeconds ?? UPLOAD_URL_TTL_SECONDS;
|
|
331
1049
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3);
|
|
332
|
-
const blobClient =
|
|
1050
|
+
const blobClient = presignContainerClient.getBlockBlobClient(args.key);
|
|
333
1051
|
const sas = generateBlobSASQueryParameters(
|
|
334
1052
|
{
|
|
335
1053
|
containerName: settings.objectStorageBucket,
|
|
@@ -353,7 +1071,7 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
353
1071
|
async createGetUrl(args) {
|
|
354
1072
|
const expiresIn = args.expiresInSeconds ?? DOWNLOAD_URL_TTL_SECONDS;
|
|
355
1073
|
const expiresAt = new Date(Date.now() + expiresIn * 1e3);
|
|
356
|
-
const blobClient =
|
|
1074
|
+
const blobClient = presignContainerClient.getBlobClient(args.key);
|
|
357
1075
|
const sas = generateBlobSASQueryParameters(
|
|
358
1076
|
{
|
|
359
1077
|
containerName: settings.objectStorageBucket,
|
|
@@ -369,21 +1087,56 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
369
1087
|
};
|
|
370
1088
|
},
|
|
371
1089
|
async putObject(args) {
|
|
372
|
-
const blobClient =
|
|
1090
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
373
1091
|
const body = Buffer.from(args.body);
|
|
374
1092
|
await blobClient.upload(body, body.byteLength, {
|
|
375
1093
|
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
376
1094
|
...args.sha256 ? { metadata: { sha256: args.sha256 } } : {}
|
|
377
1095
|
});
|
|
378
1096
|
},
|
|
1097
|
+
async putObjectIfAbsent(args) {
|
|
1098
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
1099
|
+
const body = Buffer.from(args.body);
|
|
1100
|
+
try {
|
|
1101
|
+
await blobClient.upload(body, body.byteLength, {
|
|
1102
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
1103
|
+
metadata: { sha256: args.sha256 },
|
|
1104
|
+
conditions: { ifNoneMatch: "*" }
|
|
1105
|
+
});
|
|
1106
|
+
return true;
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
async putObjectStreamIfAbsent(args) {
|
|
1113
|
+
const blobClient = requestContainerClient.getBlockBlobClient(args.key);
|
|
1114
|
+
try {
|
|
1115
|
+
await blobClient.uploadStream(
|
|
1116
|
+
Readable.from(args.chunks),
|
|
1117
|
+
INTERNAL_STREAM_BUFFER_BYTES,
|
|
1118
|
+
INTERNAL_STREAM_CONCURRENCY,
|
|
1119
|
+
{
|
|
1120
|
+
blobHTTPHeaders: { blobContentType: args.contentType },
|
|
1121
|
+
metadata: { sha256: args.sha256 },
|
|
1122
|
+
conditions: { ifNoneMatch: "*" },
|
|
1123
|
+
...args.signal ? { abortSignal: args.signal } : {}
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
return true;
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
if (isAzureVersionMismatch(error)) return false;
|
|
1129
|
+
throw error;
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
379
1132
|
async headFile(file) {
|
|
380
1133
|
return azureHeadToObjectHead(
|
|
381
|
-
await
|
|
1134
|
+
await requestContainerClient.getBlobClient(file.objectKey).getProperties()
|
|
382
1135
|
);
|
|
383
1136
|
},
|
|
384
1137
|
async fileExists(file) {
|
|
385
1138
|
try {
|
|
386
|
-
await
|
|
1139
|
+
await requestContainerClient.getBlobClient(file.objectKey).getProperties();
|
|
387
1140
|
return true;
|
|
388
1141
|
} catch (error) {
|
|
389
1142
|
if (isAzureNotFound(error)) return false;
|
|
@@ -392,14 +1145,14 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
392
1145
|
},
|
|
393
1146
|
async getFileBytes(file) {
|
|
394
1147
|
return await azureDownloadToBytes(
|
|
395
|
-
await
|
|
1148
|
+
await requestContainerClient.getBlobClient(file.objectKey).download()
|
|
396
1149
|
);
|
|
397
1150
|
},
|
|
398
1151
|
async getFileRange(file, range) {
|
|
399
1152
|
const length = assertFileByteRange(file, range);
|
|
400
1153
|
try {
|
|
401
1154
|
return await azureDownloadToBoundedBytes(
|
|
402
|
-
await
|
|
1155
|
+
await requestContainerClient.getBlobClient(file.objectKey).download(range.start, length),
|
|
403
1156
|
file.objectKey,
|
|
404
1157
|
length
|
|
405
1158
|
);
|
|
@@ -408,9 +1161,37 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
408
1161
|
throw error;
|
|
409
1162
|
}
|
|
410
1163
|
},
|
|
1164
|
+
async headObject(key) {
|
|
1165
|
+
try {
|
|
1166
|
+
const properties = await requestContainerClient.getBlobClient(key).getProperties();
|
|
1167
|
+
return objectHead({
|
|
1168
|
+
contentLength: properties.contentLength,
|
|
1169
|
+
contentType: properties.contentType,
|
|
1170
|
+
metadata: properties.metadata,
|
|
1171
|
+
versionToken: properties.etag
|
|
1172
|
+
});
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (isAzureNotFound(error)) return null;
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
async getObjectRange(args) {
|
|
1179
|
+
const length = assertRawObjectByteRange(args.key, args.start, args.endInclusive);
|
|
1180
|
+
try {
|
|
1181
|
+
const download = await requestContainerClient.getBlobClient(args.key).download(args.start, length, { conditions: { ifMatch: args.expectedVersionToken } });
|
|
1182
|
+
if (!download.etag || download.etag !== args.expectedVersionToken) return null;
|
|
1183
|
+
return {
|
|
1184
|
+
bytes: await azureDownloadToBoundedBytes(download, args.key, length),
|
|
1185
|
+
versionToken: download.etag
|
|
1186
|
+
};
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
if (isAzureNotFound(error) || isAzureVersionMismatch(error)) return null;
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
411
1192
|
async getObjectBytes(key) {
|
|
412
1193
|
try {
|
|
413
|
-
const download = await
|
|
1194
|
+
const download = await requestContainerClient.getBlobClient(key).download();
|
|
414
1195
|
const bytes = await azureDownloadToBytes(download);
|
|
415
1196
|
return { bytes, ...download.contentType ? { contentType: download.contentType } : {} };
|
|
416
1197
|
} catch (error) {
|
|
@@ -421,13 +1202,16 @@ function createAzureBlobObjectStorage(settings) {
|
|
|
421
1202
|
}
|
|
422
1203
|
},
|
|
423
1204
|
async deleteObject(key) {
|
|
424
|
-
await
|
|
1205
|
+
await requestContainerClient.getBlockBlobClient(key).deleteIfExists();
|
|
425
1206
|
}
|
|
426
1207
|
};
|
|
427
1208
|
}
|
|
428
1209
|
function isAzureNotFound(error) {
|
|
429
1210
|
return Boolean(error) && typeof error === "object" && error.statusCode === 404;
|
|
430
1211
|
}
|
|
1212
|
+
function isAzureVersionMismatch(error) {
|
|
1213
|
+
return Boolean(error) && typeof error === "object" && error.statusCode === 412;
|
|
1214
|
+
}
|
|
431
1215
|
function azureSharedKeyCredential(settings) {
|
|
432
1216
|
if (settings.objectStorageAzureConnectionString) {
|
|
433
1217
|
const parsed = parseConnectionString(settings.objectStorageAzureConnectionString);
|
|
@@ -551,6 +1335,16 @@ function assertFileByteRange(file, range) {
|
|
|
551
1335
|
}
|
|
552
1336
|
return length;
|
|
553
1337
|
}
|
|
1338
|
+
function assertRawObjectByteRange(key, start, endInclusive) {
|
|
1339
|
+
if (typeof key !== "string" || key.length < 1 || key.length > 2048 || !Number.isSafeInteger(start) || !Number.isSafeInteger(endInclusive) || start < 0 || endInclusive < start) {
|
|
1340
|
+
throw new RangeError("Invalid raw object byte range");
|
|
1341
|
+
}
|
|
1342
|
+
const length = endInclusive - start + 1;
|
|
1343
|
+
if (length > RETAINED_OUTPUT_MAX_PAGE_BYTES) {
|
|
1344
|
+
throw new RangeError("Raw object byte range exceeds the bounded page limit");
|
|
1345
|
+
}
|
|
1346
|
+
return length;
|
|
1347
|
+
}
|
|
554
1348
|
function exactRangeBytes(bytes, objectKey, expectedBytes) {
|
|
555
1349
|
if (bytes.byteLength !== expectedBytes) {
|
|
556
1350
|
throw new Error(
|
|
@@ -563,17 +1357,25 @@ function objectHead(input) {
|
|
|
563
1357
|
return {
|
|
564
1358
|
...input.contentLength !== void 0 ? { ContentLength: input.contentLength } : {},
|
|
565
1359
|
...input.contentType !== void 0 ? { ContentType: input.contentType } : {},
|
|
566
|
-
...input.metadata !== void 0 ? { Metadata: input.metadata } : {}
|
|
1360
|
+
...input.metadata !== void 0 ? { Metadata: input.metadata } : {},
|
|
1361
|
+
...input.versionToken !== void 0 ? { VersionToken: input.versionToken } : {}
|
|
567
1362
|
};
|
|
568
1363
|
}
|
|
569
1364
|
function bytesToDataUrl(bytes, contentType) {
|
|
570
1365
|
return `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
|
|
571
1366
|
}
|
|
572
1367
|
export {
|
|
1368
|
+
BoundedObjectReadError,
|
|
1369
|
+
BoundedObjectWriteError,
|
|
1370
|
+
DEFAULT_BOUNDED_OBJECT_CHUNK_BYTES,
|
|
573
1371
|
DOWNLOAD_URL_TTL_SECONDS,
|
|
1372
|
+
MAX_BOUNDED_OBJECT_CHUNK_BYTES,
|
|
574
1373
|
MAX_SINGLE_PUT_SIZE_BYTES,
|
|
575
1374
|
UPLOAD_URL_TTL_SECONDS,
|
|
576
1375
|
bytesToDataUrl,
|
|
577
|
-
|
|
1376
|
+
createBoundedImmutableObjectWritePort,
|
|
1377
|
+
createBoundedObjectReadPort,
|
|
1378
|
+
createObjectStorage,
|
|
1379
|
+
createObjectStorageBoundedPorts
|
|
578
1380
|
};
|
|
579
1381
|
//# sourceMappingURL=index.js.map
|