@mountra/mountra-sdk 0.1.0 → 0.3.0
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/CHANGELOG.md +40 -0
- package/README.md +120 -14
- package/dist/index.cjs +1643 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +384 -2
- package/dist/index.d.ts +384 -2
- package/dist/index.js +1636 -15
- package/dist/index.js.map +1 -1
- package/package.json +10 -8
package/dist/index.cjs
CHANGED
|
@@ -21,13 +21,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES: () => DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
24
|
+
DownloadHttpError: () => DownloadHttpError,
|
|
25
|
+
InMemoryDownloadCacheStore: () => InMemoryDownloadCacheStore,
|
|
26
|
+
InMemoryDownloadCheckpointStore: () => InMemoryDownloadCheckpointStore,
|
|
24
27
|
InMemoryJumboChunkCheckpointStore: () => InMemoryJumboChunkCheckpointStore,
|
|
28
|
+
IndexedDBDownloadCacheStore: () => IndexedDBDownloadCacheStore,
|
|
29
|
+
IndexedDBDownloadCheckpointStore: () => IndexedDBDownloadCheckpointStore,
|
|
25
30
|
JUMBO_CHUNK_THRESHOLD_BYTES: () => JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
26
31
|
LocalStorageJumboChunkCheckpointStore: () => LocalStorageJumboChunkCheckpointStore,
|
|
27
32
|
MAX_S3_MULTIPART_PARTS: () => MAX_S3_MULTIPART_PARTS,
|
|
28
33
|
MIN_S3_MULTIPART_PART_SIZE_BYTES: () => MIN_S3_MULTIPART_PART_SIZE_BYTES,
|
|
29
34
|
MountraApiError: () => MountraApiError,
|
|
30
35
|
MountraClient: () => MountraClient,
|
|
36
|
+
MountraDownloader: () => MountraDownloader,
|
|
37
|
+
MountraFile: () => MountraFile,
|
|
31
38
|
MountraSDK: () => MountraClient,
|
|
32
39
|
S3MultipartUploader: () => S3MultipartUploader,
|
|
33
40
|
createMountraClient: () => createMountraClient,
|
|
@@ -37,6 +44,1546 @@ __export(src_exports, {
|
|
|
37
44
|
});
|
|
38
45
|
module.exports = __toCommonJS(src_exports);
|
|
39
46
|
|
|
47
|
+
// src/file.ts
|
|
48
|
+
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
49
|
+
if (!locator || typeof locator !== "object") {
|
|
50
|
+
throw new TypeError("Mountra file locator is required");
|
|
51
|
+
}
|
|
52
|
+
const hasPath = locator.path !== void 0;
|
|
53
|
+
const hasIno = locator.ino !== void 0;
|
|
54
|
+
if (hasPath === hasIno) {
|
|
55
|
+
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
56
|
+
}
|
|
57
|
+
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
58
|
+
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
59
|
+
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
60
|
+
}
|
|
61
|
+
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
62
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
63
|
+
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
64
|
+
}
|
|
65
|
+
if (hasPath) {
|
|
66
|
+
const path = normalizePath(locator.path);
|
|
67
|
+
return {
|
|
68
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
69
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
70
|
+
path
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
75
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
76
|
+
ino: normalizeIno(locator.ino)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
var MountraFile = class {
|
|
80
|
+
constructor(client, locator) {
|
|
81
|
+
this.client = client;
|
|
82
|
+
this.locator = Object.freeze({ ...locator });
|
|
83
|
+
}
|
|
84
|
+
client;
|
|
85
|
+
locator;
|
|
86
|
+
get workspaceCuid() {
|
|
87
|
+
return this.locator.workspaceCuid;
|
|
88
|
+
}
|
|
89
|
+
get workspaceId() {
|
|
90
|
+
return this.locator.workspaceId;
|
|
91
|
+
}
|
|
92
|
+
/** `wsid` is the short alias used by the file-object API. */
|
|
93
|
+
get wsid() {
|
|
94
|
+
return this.locator.workspaceId;
|
|
95
|
+
}
|
|
96
|
+
get path() {
|
|
97
|
+
return "path" in this.locator ? this.locator.path : void 0;
|
|
98
|
+
}
|
|
99
|
+
get ino() {
|
|
100
|
+
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
101
|
+
}
|
|
102
|
+
export(options = {}) {
|
|
103
|
+
return this.client.exportFile({ ...options, ...this.locator });
|
|
104
|
+
}
|
|
105
|
+
download(options = {}) {
|
|
106
|
+
return this.client.downloadFile({ ...options, ...this.locator });
|
|
107
|
+
}
|
|
108
|
+
async resume(options = {}) {
|
|
109
|
+
const { downloadKey, ...resumeOptions } = options;
|
|
110
|
+
const checkpoints = await this.client.listResumableDownloads();
|
|
111
|
+
const candidates = checkpoints.filter(
|
|
112
|
+
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
113
|
+
);
|
|
114
|
+
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
115
|
+
if (!checkpoint) {
|
|
116
|
+
if (downloadKey) {
|
|
117
|
+
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
118
|
+
}
|
|
119
|
+
throw new Error("No resumable download was found for this Mountra file");
|
|
120
|
+
}
|
|
121
|
+
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function matchesLocator(request, locator) {
|
|
125
|
+
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if ("path" in locator) {
|
|
132
|
+
if (request.path === void 0 || request.ino !== void 0) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return normalizePath(request.path) === locator.path;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
142
|
+
}
|
|
143
|
+
function normalizePath(value) {
|
|
144
|
+
if (!value || !value.trim()) {
|
|
145
|
+
throw new TypeError("Mountra file path cannot be empty");
|
|
146
|
+
}
|
|
147
|
+
const raw = value.trim();
|
|
148
|
+
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
149
|
+
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
150
|
+
const stack = [];
|
|
151
|
+
for (const segment of segments) {
|
|
152
|
+
if (segment === "..") {
|
|
153
|
+
stack.pop();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
stack.push(segment);
|
|
157
|
+
}
|
|
158
|
+
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
159
|
+
}
|
|
160
|
+
function normalizeIno(value) {
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
163
|
+
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
if (!/^\d+$/.test(value)) {
|
|
168
|
+
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/cache.ts
|
|
174
|
+
var DEFAULT_MAX_BYTES = 512 * 1024 * 1024;
|
|
175
|
+
var DEFAULT_MAX_CHUNK_BYTES = 128 * 1024 * 1024;
|
|
176
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
177
|
+
function copyFile(file) {
|
|
178
|
+
return { ...file, chunks: file.chunks.map((chunk) => ({ ...chunk })) };
|
|
179
|
+
}
|
|
180
|
+
function memoryKey(workspace, key) {
|
|
181
|
+
return JSON.stringify([workspace, key]);
|
|
182
|
+
}
|
|
183
|
+
var InMemoryDownloadCacheStore = class {
|
|
184
|
+
files = /* @__PURE__ */ new Map();
|
|
185
|
+
/** Map iteration order is the LRU order: the first entry is the least recently used. */
|
|
186
|
+
chunks = /* @__PURE__ */ new Map();
|
|
187
|
+
bytes = 0;
|
|
188
|
+
async getFile(workspace, fileKey) {
|
|
189
|
+
const entry = this.files.get(memoryKey(workspace, fileKey));
|
|
190
|
+
return entry ? copyFile(entry.file) : null;
|
|
191
|
+
}
|
|
192
|
+
async putFile(workspace, fileKey, file) {
|
|
193
|
+
this.files.set(memoryKey(workspace, fileKey), { workspace, file: copyFile(file) });
|
|
194
|
+
}
|
|
195
|
+
async removeFile(workspace, fileKey) {
|
|
196
|
+
this.files.delete(memoryKey(workspace, fileKey));
|
|
197
|
+
}
|
|
198
|
+
async getChunk(workspace, storageKey) {
|
|
199
|
+
const key = memoryKey(workspace, storageKey);
|
|
200
|
+
const entry = this.chunks.get(key);
|
|
201
|
+
if (!entry) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
this.chunks.delete(key);
|
|
205
|
+
this.chunks.set(key, entry);
|
|
206
|
+
return entry.data;
|
|
207
|
+
}
|
|
208
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
209
|
+
const key = memoryKey(workspace, storageKey);
|
|
210
|
+
this.removeChunk(key);
|
|
211
|
+
if (data.size > maxBytes) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.chunks.set(key, { workspace, data });
|
|
215
|
+
this.bytes += data.size;
|
|
216
|
+
for (const candidate of this.chunks.keys()) {
|
|
217
|
+
if (this.bytes <= maxBytes) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
this.removeChunk(candidate);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async clear(workspace) {
|
|
224
|
+
for (const [key, entry] of this.chunks) {
|
|
225
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
226
|
+
this.removeChunk(key);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [key, entry] of this.files) {
|
|
230
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
231
|
+
this.files.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async usage(workspace) {
|
|
236
|
+
const usage = { bytes: 0, chunks: 0 };
|
|
237
|
+
for (const entry of this.chunks.values()) {
|
|
238
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
239
|
+
usage.bytes += entry.data.size;
|
|
240
|
+
usage.chunks += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return usage;
|
|
244
|
+
}
|
|
245
|
+
removeChunk(key) {
|
|
246
|
+
const entry = this.chunks.get(key);
|
|
247
|
+
if (entry) {
|
|
248
|
+
this.chunks.delete(key);
|
|
249
|
+
this.bytes -= entry.data.size;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var CACHE_DATABASE_NAME = "mountra-sdk-download-cache";
|
|
254
|
+
var CACHE_DATABASE_VERSION = 1;
|
|
255
|
+
var FILES_STORE = "files";
|
|
256
|
+
var CHUNKS_STORE = "chunks";
|
|
257
|
+
var CHUNK_DATA_STORE = "chunk-data";
|
|
258
|
+
var META_STORE = "meta";
|
|
259
|
+
var LRU_INDEX = "lastUsed";
|
|
260
|
+
var USAGE_KEY = "bytes";
|
|
261
|
+
var CLOCK_KEY = "clock";
|
|
262
|
+
function getIndexedDb() {
|
|
263
|
+
return globalThis.indexedDB ?? null;
|
|
264
|
+
}
|
|
265
|
+
function workspaceRange(workspace) {
|
|
266
|
+
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
267
|
+
}
|
|
268
|
+
function sameKey(left, right) {
|
|
269
|
+
return Array.isArray(left) && left[0] === right[0] && left[1] === right[1];
|
|
270
|
+
}
|
|
271
|
+
async function nextClock(meta) {
|
|
272
|
+
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
273
|
+
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
274
|
+
return clock;
|
|
275
|
+
}
|
|
276
|
+
function requestResult(request) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
request.onsuccess = () => resolve(request.result);
|
|
279
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async function runTransaction(database, storeNames, mode, body) {
|
|
283
|
+
const transaction = database.transaction(storeNames, mode);
|
|
284
|
+
const done = new Promise((resolve, reject) => {
|
|
285
|
+
transaction.oncomplete = () => resolve();
|
|
286
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
287
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
288
|
+
});
|
|
289
|
+
done.catch(() => void 0);
|
|
290
|
+
let result;
|
|
291
|
+
try {
|
|
292
|
+
result = await body(transaction);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
try {
|
|
295
|
+
transaction.abort();
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
await done;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
var IndexedDBDownloadCacheStore = class {
|
|
304
|
+
databaseName;
|
|
305
|
+
fallback = new InMemoryDownloadCacheStore();
|
|
306
|
+
databasePromise;
|
|
307
|
+
constructor(options = {}) {
|
|
308
|
+
this.databaseName = options.databaseName ?? CACHE_DATABASE_NAME;
|
|
309
|
+
}
|
|
310
|
+
async getFile(workspace, fileKey) {
|
|
311
|
+
const database = await this.openDatabase();
|
|
312
|
+
if (!database) {
|
|
313
|
+
return this.fallback.getFile(workspace, fileKey);
|
|
314
|
+
}
|
|
315
|
+
return runTransaction(database, [FILES_STORE], "readonly", async (transaction) => {
|
|
316
|
+
const file = await requestResult(
|
|
317
|
+
transaction.objectStore(FILES_STORE).get([workspace, fileKey])
|
|
318
|
+
);
|
|
319
|
+
return file ? copyFile(file) : null;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async putFile(workspace, fileKey, file) {
|
|
323
|
+
const database = await this.openDatabase();
|
|
324
|
+
if (!database) {
|
|
325
|
+
return this.fallback.putFile(workspace, fileKey, file);
|
|
326
|
+
}
|
|
327
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
328
|
+
await requestResult(transaction.objectStore(FILES_STORE).put(copyFile(file), [workspace, fileKey]));
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async removeFile(workspace, fileKey) {
|
|
332
|
+
const database = await this.openDatabase();
|
|
333
|
+
if (!database) {
|
|
334
|
+
return this.fallback.removeFile(workspace, fileKey);
|
|
335
|
+
}
|
|
336
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
337
|
+
await requestResult(transaction.objectStore(FILES_STORE).delete([workspace, fileKey]));
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async getChunk(workspace, storageKey) {
|
|
341
|
+
const database = await this.openDatabase();
|
|
342
|
+
if (!database) {
|
|
343
|
+
return this.fallback.getChunk(workspace, storageKey);
|
|
344
|
+
}
|
|
345
|
+
const key = [workspace, storageKey];
|
|
346
|
+
return runTransaction(
|
|
347
|
+
database,
|
|
348
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
349
|
+
"readwrite",
|
|
350
|
+
async (transaction) => {
|
|
351
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
352
|
+
const record = await requestResult(chunks.get(key));
|
|
353
|
+
if (!record) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const data = await requestResult(
|
|
357
|
+
transaction.objectStore(CHUNK_DATA_STORE).get(key)
|
|
358
|
+
);
|
|
359
|
+
const meta = transaction.objectStore(META_STORE);
|
|
360
|
+
if (!data) {
|
|
361
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
362
|
+
await requestResult(chunks.delete(key));
|
|
363
|
+
await requestResult(meta.put(Math.max(bytes - record.size, 0), USAGE_KEY));
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
await requestResult(chunks.put({ size: record.size, lastUsed: await nextClock(meta) }, key));
|
|
367
|
+
return data;
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
372
|
+
const database = await this.openDatabase();
|
|
373
|
+
if (!database) {
|
|
374
|
+
return this.fallback.putChunk(workspace, storageKey, data, maxBytes);
|
|
375
|
+
}
|
|
376
|
+
const key = [workspace, storageKey];
|
|
377
|
+
await runTransaction(
|
|
378
|
+
database,
|
|
379
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
380
|
+
"readwrite",
|
|
381
|
+
async (transaction) => {
|
|
382
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
383
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
384
|
+
const meta = transaction.objectStore(META_STORE);
|
|
385
|
+
let bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
386
|
+
const existing = await requestResult(chunks.get(key));
|
|
387
|
+
if (existing) {
|
|
388
|
+
bytes -= existing.size;
|
|
389
|
+
await requestResult(chunks.delete(key));
|
|
390
|
+
await requestResult(chunkData.delete(key));
|
|
391
|
+
}
|
|
392
|
+
if (data.size <= maxBytes) {
|
|
393
|
+
await requestResult(chunks.put({ size: data.size, lastUsed: await nextClock(meta) }, key));
|
|
394
|
+
await requestResult(chunkData.put(data, key));
|
|
395
|
+
bytes += data.size;
|
|
396
|
+
}
|
|
397
|
+
if (bytes > maxBytes) {
|
|
398
|
+
const cursorRequest = chunks.index(LRU_INDEX).openCursor();
|
|
399
|
+
let cursor = await requestResult(cursorRequest);
|
|
400
|
+
while (cursor && bytes > maxBytes) {
|
|
401
|
+
if (!sameKey(cursor.primaryKey, key)) {
|
|
402
|
+
bytes -= cursor.value.size;
|
|
403
|
+
await requestResult(chunkData.delete(cursor.primaryKey));
|
|
404
|
+
await requestResult(cursor.delete());
|
|
405
|
+
}
|
|
406
|
+
cursor.continue();
|
|
407
|
+
cursor = await requestResult(cursorRequest);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
await requestResult(meta.put(Math.max(bytes, 0), USAGE_KEY));
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
async clear(workspace) {
|
|
415
|
+
const database = await this.openDatabase();
|
|
416
|
+
if (!database) {
|
|
417
|
+
return this.fallback.clear(workspace);
|
|
418
|
+
}
|
|
419
|
+
await runTransaction(
|
|
420
|
+
database,
|
|
421
|
+
[FILES_STORE, CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
422
|
+
"readwrite",
|
|
423
|
+
async (transaction) => {
|
|
424
|
+
const files = transaction.objectStore(FILES_STORE);
|
|
425
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
426
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
427
|
+
const meta = transaction.objectStore(META_STORE);
|
|
428
|
+
if (workspace === void 0) {
|
|
429
|
+
await requestResult(files.clear());
|
|
430
|
+
await requestResult(chunks.clear());
|
|
431
|
+
await requestResult(chunkData.clear());
|
|
432
|
+
await requestResult(meta.put(0, USAGE_KEY));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const range = workspaceRange(workspace);
|
|
436
|
+
const removed = await requestResult(chunks.getAll(range));
|
|
437
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
438
|
+
await requestResult(files.delete(range));
|
|
439
|
+
await requestResult(chunks.delete(range));
|
|
440
|
+
await requestResult(chunkData.delete(range));
|
|
441
|
+
await requestResult(
|
|
442
|
+
meta.put(Math.max(bytes - removed.reduce((total, record) => total + record.size, 0), 0), USAGE_KEY)
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
async usage(workspace) {
|
|
448
|
+
const database = await this.openDatabase();
|
|
449
|
+
if (!database) {
|
|
450
|
+
return this.fallback.usage(workspace);
|
|
451
|
+
}
|
|
452
|
+
return runTransaction(database, [CHUNKS_STORE], "readonly", async (transaction) => {
|
|
453
|
+
const records = await requestResult(
|
|
454
|
+
transaction.objectStore(CHUNKS_STORE).getAll(workspace === void 0 ? void 0 : workspaceRange(workspace))
|
|
455
|
+
);
|
|
456
|
+
return {
|
|
457
|
+
bytes: records.reduce((total, record) => total + record.size, 0),
|
|
458
|
+
chunks: records.length
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async openDatabase() {
|
|
463
|
+
if (!this.databasePromise) {
|
|
464
|
+
const indexedDb = getIndexedDb();
|
|
465
|
+
if (!indexedDb) {
|
|
466
|
+
this.databasePromise = Promise.resolve(null);
|
|
467
|
+
} else {
|
|
468
|
+
this.databasePromise = new Promise((resolve) => {
|
|
469
|
+
let request;
|
|
470
|
+
try {
|
|
471
|
+
request = indexedDb.open(this.databaseName, CACHE_DATABASE_VERSION);
|
|
472
|
+
} catch {
|
|
473
|
+
resolve(null);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
request.onupgradeneeded = () => {
|
|
477
|
+
const database = request.result;
|
|
478
|
+
if (!database.objectStoreNames.contains(FILES_STORE)) {
|
|
479
|
+
database.createObjectStore(FILES_STORE);
|
|
480
|
+
}
|
|
481
|
+
if (!database.objectStoreNames.contains(CHUNKS_STORE)) {
|
|
482
|
+
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
483
|
+
}
|
|
484
|
+
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
485
|
+
database.createObjectStore(CHUNK_DATA_STORE);
|
|
486
|
+
}
|
|
487
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
488
|
+
database.createObjectStore(META_STORE);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
request.onsuccess = () => {
|
|
492
|
+
const database = request.result;
|
|
493
|
+
database.onversionchange = () => {
|
|
494
|
+
database.close();
|
|
495
|
+
this.databasePromise = void 0;
|
|
496
|
+
};
|
|
497
|
+
resolve(database);
|
|
498
|
+
};
|
|
499
|
+
request.onerror = () => resolve(null);
|
|
500
|
+
request.onblocked = () => resolve(null);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return this.databasePromise;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function isImmutableStorageKey(storageKey) {
|
|
508
|
+
return /(?:^|\/)blobs\/sha256\/[0-9a-f]{64}$/.test(storageKey);
|
|
509
|
+
}
|
|
510
|
+
function workspaceCacheKey(baseUrl, selector) {
|
|
511
|
+
const workspaceCuid = selector.workspaceCuid?.trim();
|
|
512
|
+
const workspaceId = selector.workspaceId !== void 0 ? String(selector.workspaceId).trim() : "";
|
|
513
|
+
const workspace = workspaceCuid || workspaceId;
|
|
514
|
+
return workspace ? JSON.stringify([baseUrl, workspace]) : null;
|
|
515
|
+
}
|
|
516
|
+
function fileCacheKey(target) {
|
|
517
|
+
try {
|
|
518
|
+
if (target.ino !== void 0) {
|
|
519
|
+
return `ino:${BigInt(target.ino).toString()}`;
|
|
520
|
+
}
|
|
521
|
+
if (target.path !== void 0) {
|
|
522
|
+
return `path:${normalizePath(target.path)}`;
|
|
523
|
+
}
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function isValidLayout(file) {
|
|
529
|
+
let offset = 0;
|
|
530
|
+
for (const [index, chunk] of file.chunks.entries()) {
|
|
531
|
+
if (chunk.index !== index || chunk.offset !== offset || chunk.size < 0) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
offset += chunk.size;
|
|
535
|
+
}
|
|
536
|
+
return file.chunks.length > 0 && offset === file.size;
|
|
537
|
+
}
|
|
538
|
+
var DownloadCache = class {
|
|
539
|
+
maxBytes;
|
|
540
|
+
maxChunkBytes;
|
|
541
|
+
ttlMs;
|
|
542
|
+
store;
|
|
543
|
+
constructor(options) {
|
|
544
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
545
|
+
if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes <= 0) {
|
|
546
|
+
throw new RangeError("downloadCache.maxBytes must be a positive safe integer");
|
|
547
|
+
}
|
|
548
|
+
const maxChunkBytes = options.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
549
|
+
if (!Number.isSafeInteger(maxChunkBytes) || maxChunkBytes <= 0) {
|
|
550
|
+
throw new RangeError("downloadCache.maxChunkBytes must be a positive safe integer");
|
|
551
|
+
}
|
|
552
|
+
this.maxChunkBytes = Math.min(maxChunkBytes, this.maxBytes);
|
|
553
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
554
|
+
if (Number.isNaN(this.ttlMs) || this.ttlMs < 0) {
|
|
555
|
+
throw new RangeError("downloadCache.ttlMs must be a non-negative number");
|
|
556
|
+
}
|
|
557
|
+
this.store = options.store ?? new IndexedDBDownloadCacheStore();
|
|
558
|
+
}
|
|
559
|
+
/** Return a file that has not expired together with all of its chunk bytes. */
|
|
560
|
+
async loadFile(workspace, fileKey) {
|
|
561
|
+
try {
|
|
562
|
+
const file = await this.store.getFile(workspace, fileKey);
|
|
563
|
+
if (!file) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
if (file.expiresAt <= Date.now() || !isValidLayout(file)) {
|
|
567
|
+
await this.store.removeFile(workspace, fileKey);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
571
|
+
for (const chunk of file.chunks) {
|
|
572
|
+
const data = await this.readChunk(workspace, chunk);
|
|
573
|
+
if (!data) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
chunks.set(chunk.index, data);
|
|
577
|
+
}
|
|
578
|
+
return { file, chunks };
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async readChunk(workspace, chunk) {
|
|
584
|
+
if (chunk.size === 0) {
|
|
585
|
+
return new Blob([]);
|
|
586
|
+
}
|
|
587
|
+
if (!isImmutableStorageKey(chunk.storageKey)) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const data = await this.store.getChunk(workspace, chunk.storageKey);
|
|
592
|
+
return data && data.size === chunk.size ? data : null;
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
canStoreChunk(chunk) {
|
|
598
|
+
return isImmutableStorageKey(chunk.storageKey) && chunk.size <= this.maxChunkBytes;
|
|
599
|
+
}
|
|
600
|
+
/** Returns whether the chunk is now cached. */
|
|
601
|
+
async storeChunk(workspace, chunk, data) {
|
|
602
|
+
if (chunk.size === 0) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
if (!this.canStoreChunk(chunk) || data.size !== chunk.size) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
await this.store.putChunk(workspace, chunk.storageKey, data, this.maxBytes);
|
|
610
|
+
return true;
|
|
611
|
+
} catch {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async storeFile(workspace, fileKey, file) {
|
|
616
|
+
if (this.ttlMs <= 0) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const cachedAt = Date.now();
|
|
620
|
+
try {
|
|
621
|
+
await this.store.putFile(workspace, fileKey, {
|
|
622
|
+
path: file.path,
|
|
623
|
+
size: file.size,
|
|
624
|
+
...file.contentType ? { contentType: file.contentType } : {},
|
|
625
|
+
chunks: file.chunks.map(({ index, offset, size, storageKey }) => ({
|
|
626
|
+
index,
|
|
627
|
+
offset,
|
|
628
|
+
size,
|
|
629
|
+
storageKey
|
|
630
|
+
})),
|
|
631
|
+
cachedAt,
|
|
632
|
+
expiresAt: cachedAt + this.ttlMs
|
|
633
|
+
});
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async invalidateFiles(workspace, fileKeys) {
|
|
638
|
+
await Promise.all(
|
|
639
|
+
fileKeys.map((fileKey) => this.store.removeFile(workspace, fileKey).catch(() => void 0))
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
clear(workspace) {
|
|
643
|
+
return this.store.clear(workspace);
|
|
644
|
+
}
|
|
645
|
+
usage(workspace) {
|
|
646
|
+
return this.store.usage(workspace);
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/download.ts
|
|
651
|
+
function copyCheckpoint(checkpoint) {
|
|
652
|
+
return {
|
|
653
|
+
...checkpoint,
|
|
654
|
+
request: { ...checkpoint.request },
|
|
655
|
+
chunks: checkpoint.chunks.map((chunk) => ({ ...chunk }))
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
var InMemoryDownloadCheckpointStore = class {
|
|
659
|
+
records = /* @__PURE__ */ new Map();
|
|
660
|
+
async load(key) {
|
|
661
|
+
const value = this.records.get(key);
|
|
662
|
+
return value ? copyCheckpoint(value) : null;
|
|
663
|
+
}
|
|
664
|
+
async save(key, checkpoint) {
|
|
665
|
+
this.records.set(key, copyCheckpoint(checkpoint));
|
|
666
|
+
}
|
|
667
|
+
async remove(key) {
|
|
668
|
+
this.records.delete(key);
|
|
669
|
+
}
|
|
670
|
+
async list() {
|
|
671
|
+
return [...this.records.values()].map(copyCheckpoint);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
var DOWNLOAD_DATABASE_NAME = "mountra-sdk";
|
|
675
|
+
var DOWNLOAD_DATABASE_VERSION = 1;
|
|
676
|
+
var DOWNLOAD_STORE_NAME = "download-checkpoints";
|
|
677
|
+
function getIndexedDb2() {
|
|
678
|
+
return globalThis.indexedDB ?? null;
|
|
679
|
+
}
|
|
680
|
+
var IndexedDBDownloadCheckpointStore = class {
|
|
681
|
+
fallback = new InMemoryDownloadCheckpointStore();
|
|
682
|
+
databasePromise;
|
|
683
|
+
async load(key) {
|
|
684
|
+
const database = await this.openDatabase();
|
|
685
|
+
if (!database) {
|
|
686
|
+
return this.fallback.load(key);
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const value = await this.request(
|
|
690
|
+
database,
|
|
691
|
+
"readonly",
|
|
692
|
+
(store) => store.get(key)
|
|
693
|
+
);
|
|
694
|
+
return value ? copyCheckpoint(value) : null;
|
|
695
|
+
} catch {
|
|
696
|
+
return this.fallback.load(key);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async save(key, checkpoint) {
|
|
700
|
+
const value = copyCheckpoint(checkpoint);
|
|
701
|
+
const database = await this.openDatabase();
|
|
702
|
+
if (!database) {
|
|
703
|
+
await this.fallback.save(key, value);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await this.request(database, "readwrite", (store) => store.put(value, key));
|
|
708
|
+
} catch {
|
|
709
|
+
await this.fallback.save(key, value);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async remove(key) {
|
|
713
|
+
const database = await this.openDatabase();
|
|
714
|
+
if (!database) {
|
|
715
|
+
await this.fallback.remove(key);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await this.request(database, "readwrite", (store) => store.delete(key));
|
|
720
|
+
} catch {
|
|
721
|
+
await this.fallback.remove(key);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async list() {
|
|
725
|
+
const database = await this.openDatabase();
|
|
726
|
+
if (!database) {
|
|
727
|
+
return this.fallback.list();
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const values = await this.request(
|
|
731
|
+
database,
|
|
732
|
+
"readonly",
|
|
733
|
+
(store) => store.getAll()
|
|
734
|
+
);
|
|
735
|
+
return values.map(copyCheckpoint);
|
|
736
|
+
} catch {
|
|
737
|
+
return this.fallback.list();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async openDatabase() {
|
|
741
|
+
if (!this.databasePromise) {
|
|
742
|
+
const indexedDb = getIndexedDb2();
|
|
743
|
+
if (!indexedDb) {
|
|
744
|
+
this.databasePromise = Promise.resolve(null);
|
|
745
|
+
} else {
|
|
746
|
+
this.databasePromise = new Promise((resolve) => {
|
|
747
|
+
let request;
|
|
748
|
+
try {
|
|
749
|
+
request = indexedDb.open(DOWNLOAD_DATABASE_NAME, DOWNLOAD_DATABASE_VERSION);
|
|
750
|
+
} catch {
|
|
751
|
+
resolve(null);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
request.onupgradeneeded = () => {
|
|
755
|
+
if (!request.result.objectStoreNames.contains(DOWNLOAD_STORE_NAME)) {
|
|
756
|
+
request.result.createObjectStore(DOWNLOAD_STORE_NAME);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
request.onsuccess = () => resolve(request.result);
|
|
760
|
+
request.onerror = () => resolve(null);
|
|
761
|
+
request.onblocked = () => resolve(null);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return this.databasePromise;
|
|
766
|
+
}
|
|
767
|
+
request(database, mode, createRequest) {
|
|
768
|
+
return new Promise((resolve, reject) => {
|
|
769
|
+
let transaction;
|
|
770
|
+
let request;
|
|
771
|
+
try {
|
|
772
|
+
transaction = database.transaction(DOWNLOAD_STORE_NAME, mode);
|
|
773
|
+
request = createRequest(transaction.objectStore(DOWNLOAD_STORE_NAME));
|
|
774
|
+
} catch (error) {
|
|
775
|
+
reject(error);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
let result;
|
|
779
|
+
request.onsuccess = () => {
|
|
780
|
+
result = request.result;
|
|
781
|
+
};
|
|
782
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
783
|
+
transaction.oncomplete = () => resolve(result);
|
|
784
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
785
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var DownloadHttpError = class extends Error {
|
|
790
|
+
status;
|
|
791
|
+
constructor(status, message) {
|
|
792
|
+
super(message);
|
|
793
|
+
this.name = "DownloadHttpError";
|
|
794
|
+
this.status = status;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
function getShowSaveFilePicker() {
|
|
798
|
+
return globalThis.showSaveFilePicker ?? null;
|
|
799
|
+
}
|
|
800
|
+
function isRecord(value) {
|
|
801
|
+
return typeof value === "object" && value !== null;
|
|
802
|
+
}
|
|
803
|
+
function readSafeNumber(value, field) {
|
|
804
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
805
|
+
throw new TypeError(`Mountra export response field ${field} is invalid`);
|
|
806
|
+
}
|
|
807
|
+
return value;
|
|
808
|
+
}
|
|
809
|
+
function readString(value, field) {
|
|
810
|
+
if (typeof value !== "string" || !value) {
|
|
811
|
+
throw new TypeError(`Mountra export response field ${field} is invalid`);
|
|
812
|
+
}
|
|
813
|
+
return value;
|
|
814
|
+
}
|
|
815
|
+
function ensureExportFileResponse(value) {
|
|
816
|
+
if (!isRecord(value)) {
|
|
817
|
+
throw new TypeError("Mountra export response is invalid");
|
|
818
|
+
}
|
|
819
|
+
const response = {
|
|
820
|
+
path: readString(value.path, "path"),
|
|
821
|
+
storageKey: readString(value.storage_key, "storage_key"),
|
|
822
|
+
url: readString(value.url, "url"),
|
|
823
|
+
timeout: readSafeNumber(value.timeout, "timeout"),
|
|
824
|
+
size: readSafeNumber(value.size, "size")
|
|
825
|
+
};
|
|
826
|
+
try {
|
|
827
|
+
new URL(response.url);
|
|
828
|
+
} catch {
|
|
829
|
+
throw new TypeError("Mountra export response field url is invalid");
|
|
830
|
+
}
|
|
831
|
+
if (typeof value.content_type === "string" && value.content_type) {
|
|
832
|
+
response.contentType = value.content_type;
|
|
833
|
+
}
|
|
834
|
+
if (typeof value.chunk_method === "string") {
|
|
835
|
+
response.chunkMethod = value.chunk_method;
|
|
836
|
+
}
|
|
837
|
+
if (typeof value.is_fast_path === "boolean") {
|
|
838
|
+
response.isFastPath = value.is_fast_path;
|
|
839
|
+
}
|
|
840
|
+
if (Array.isArray(value.chunks)) {
|
|
841
|
+
response.chunks = value.chunks.map((rawChunk, position) => {
|
|
842
|
+
if (!isRecord(rawChunk)) {
|
|
843
|
+
throw new TypeError(`Mountra export response chunk ${position} is invalid`);
|
|
844
|
+
}
|
|
845
|
+
const chunk = {
|
|
846
|
+
index: readSafeNumber(rawChunk.index, `chunks[${position}].index`),
|
|
847
|
+
offset: readSafeNumber(rawChunk.offset, `chunks[${position}].offset`),
|
|
848
|
+
size: readSafeNumber(rawChunk.size, `chunks[${position}].size`),
|
|
849
|
+
storageKey: readString(rawChunk.storage_key, `chunks[${position}].storage_key`),
|
|
850
|
+
url: readString(rawChunk.url, `chunks[${position}].url`)
|
|
851
|
+
};
|
|
852
|
+
try {
|
|
853
|
+
new URL(chunk.url);
|
|
854
|
+
} catch {
|
|
855
|
+
throw new TypeError(`Mountra export response chunk ${position} URL is invalid`);
|
|
856
|
+
}
|
|
857
|
+
return chunk;
|
|
858
|
+
});
|
|
859
|
+
response.chunks.sort((left, right) => left.index - right.index);
|
|
860
|
+
response.chunks.forEach((chunk, index) => {
|
|
861
|
+
if (chunk.index !== index || chunk.offset + chunk.size > response.size || index > 0 && response.chunks[index - 1].offset + response.chunks[index - 1].size !== chunk.offset) {
|
|
862
|
+
throw new TypeError("Mountra export response has an invalid chunk layout");
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
const finalChunk = response.chunks[response.chunks.length - 1];
|
|
866
|
+
if (finalChunk && finalChunk.offset + finalChunk.size !== response.size) {
|
|
867
|
+
throw new TypeError("Mountra export response chunks do not cover the complete file");
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return response;
|
|
871
|
+
}
|
|
872
|
+
function validateRetryOption(value, name) {
|
|
873
|
+
if (value === void 0) {
|
|
874
|
+
return void 0;
|
|
875
|
+
}
|
|
876
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
877
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
878
|
+
}
|
|
879
|
+
return value;
|
|
880
|
+
}
|
|
881
|
+
function throwIfAborted(signal) {
|
|
882
|
+
if (signal?.aborted) {
|
|
883
|
+
throw new DOMException("The download was aborted", "AbortError");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function isAbortError(error, signal) {
|
|
887
|
+
return signal?.aborted === true || error instanceof DOMException && error.name === "AbortError";
|
|
888
|
+
}
|
|
889
|
+
function isRetryableError(error) {
|
|
890
|
+
if (error instanceof DownloadHttpError) {
|
|
891
|
+
return [408, 425, 429, 500, 502, 503, 504].includes(error.status);
|
|
892
|
+
}
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
function waitForRetry(delayMs, signal) {
|
|
896
|
+
if (delayMs <= 0) {
|
|
897
|
+
throwIfAborted(signal);
|
|
898
|
+
return Promise.resolve();
|
|
899
|
+
}
|
|
900
|
+
return new Promise((resolve, reject) => {
|
|
901
|
+
let timer;
|
|
902
|
+
const onAbort = () => {
|
|
903
|
+
if (timer !== void 0) {
|
|
904
|
+
clearTimeout(timer);
|
|
905
|
+
}
|
|
906
|
+
signal?.removeEventListener("abort", onAbort);
|
|
907
|
+
reject(new DOMException("The download was aborted", "AbortError"));
|
|
908
|
+
};
|
|
909
|
+
timer = setTimeout(() => {
|
|
910
|
+
signal?.removeEventListener("abort", onAbort);
|
|
911
|
+
resolve();
|
|
912
|
+
}, delayMs);
|
|
913
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
914
|
+
if (signal?.aborted) {
|
|
915
|
+
onAbort();
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
var SpeedWindow = class {
|
|
920
|
+
samples = [];
|
|
921
|
+
record(bytes, at) {
|
|
922
|
+
this.samples.push({ at, bytes });
|
|
923
|
+
const cutoff = at - 1e3;
|
|
924
|
+
while (this.samples.length > 1 && this.samples[0].at < cutoff) {
|
|
925
|
+
this.samples.shift();
|
|
926
|
+
}
|
|
927
|
+
const first = this.samples[0];
|
|
928
|
+
if (!first || first.bytes === bytes) {
|
|
929
|
+
return 0;
|
|
930
|
+
}
|
|
931
|
+
const seconds = Math.max((at - first.at) / 1e3, 1e-3);
|
|
932
|
+
return (bytes - first.bytes) / seconds;
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
var BlobDownloadSink = class {
|
|
936
|
+
parts = [];
|
|
937
|
+
/** Parts arrive in file order, so the position is implied. */
|
|
938
|
+
async write(bytes, _position) {
|
|
939
|
+
this.parts.push(bytes instanceof Blob ? bytes : bytes.slice());
|
|
940
|
+
}
|
|
941
|
+
async flush() {
|
|
942
|
+
}
|
|
943
|
+
async finalize() {
|
|
944
|
+
}
|
|
945
|
+
blob(contentType) {
|
|
946
|
+
const buffers = this.parts.map((part) => {
|
|
947
|
+
if (part instanceof Blob) {
|
|
948
|
+
return part;
|
|
949
|
+
}
|
|
950
|
+
const buffer = new ArrayBuffer(part.byteLength);
|
|
951
|
+
new Uint8Array(buffer).set(part);
|
|
952
|
+
return buffer;
|
|
953
|
+
});
|
|
954
|
+
return new Blob(buffers, contentType ? { type: contentType } : void 0);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
var FileSystemDownloadSink = class {
|
|
958
|
+
constructor(handle) {
|
|
959
|
+
this.handle = handle;
|
|
960
|
+
}
|
|
961
|
+
handle;
|
|
962
|
+
writable = null;
|
|
963
|
+
position = 0;
|
|
964
|
+
openedOnce = false;
|
|
965
|
+
async write(bytes, position) {
|
|
966
|
+
if (!this.writable) {
|
|
967
|
+
const keepExistingData = this.openedOnce || position > 0;
|
|
968
|
+
this.writable = await this.handle.createWritable({ keepExistingData });
|
|
969
|
+
if (position > 0) {
|
|
970
|
+
await this.writable.seek(position);
|
|
971
|
+
}
|
|
972
|
+
this.position = position;
|
|
973
|
+
this.openedOnce = true;
|
|
974
|
+
}
|
|
975
|
+
if (this.position !== position) {
|
|
976
|
+
throw new Error("File System Access writer position does not match the download checkpoint");
|
|
977
|
+
}
|
|
978
|
+
await this.writable.write(bytes);
|
|
979
|
+
this.position += bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
980
|
+
}
|
|
981
|
+
async flush() {
|
|
982
|
+
if (this.writable) {
|
|
983
|
+
const writable = this.writable;
|
|
984
|
+
await writable.close();
|
|
985
|
+
this.writable = null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
async finalize(size) {
|
|
989
|
+
if (!this.writable) {
|
|
990
|
+
this.writable = await this.handle.createWritable({ keepExistingData: this.openedOnce });
|
|
991
|
+
this.openedOnce = true;
|
|
992
|
+
await this.writable.seek(size);
|
|
993
|
+
}
|
|
994
|
+
await this.writable.truncate(size);
|
|
995
|
+
const writable = this.writable;
|
|
996
|
+
this.writable = null;
|
|
997
|
+
await writable.close();
|
|
998
|
+
}
|
|
999
|
+
async abort() {
|
|
1000
|
+
if (!this.writable) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const writable = this.writable;
|
|
1004
|
+
this.writable = null;
|
|
1005
|
+
if (writable.abort) {
|
|
1006
|
+
await writable.abort();
|
|
1007
|
+
} else {
|
|
1008
|
+
await writable.close();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
function downloadRequestFromOptions(options) {
|
|
1013
|
+
if (options.path === void 0 && options.ino === void 0) {
|
|
1014
|
+
throw new TypeError("download target requires path or ino");
|
|
1015
|
+
}
|
|
1016
|
+
if (options.path !== void 0 && !options.path.trim()) {
|
|
1017
|
+
throw new TypeError("download path cannot be empty");
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
...options.workspaceCuid !== void 0 ? { workspaceCuid: options.workspaceCuid } : {},
|
|
1021
|
+
...options.workspaceId !== void 0 ? { workspaceId: options.workspaceId } : {},
|
|
1022
|
+
...options.path !== void 0 ? { path: options.path } : {},
|
|
1023
|
+
...options.ino !== void 0 ? { ino: options.ino } : {},
|
|
1024
|
+
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
1025
|
+
...options.contentDisposition !== void 0 ? { contentDisposition: options.contentDisposition } : {},
|
|
1026
|
+
...options.downloadFilename !== void 0 ? { downloadFilename: options.downloadFilename } : {}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function requestBodyFromCheckpoint(request) {
|
|
1030
|
+
return {
|
|
1031
|
+
...request.workspaceCuid !== void 0 ? { workspace_cuid: request.workspaceCuid } : {},
|
|
1032
|
+
...request.workspaceId !== void 0 ? { workspace_id: request.workspaceId } : {},
|
|
1033
|
+
...request.path !== void 0 ? { path: request.path } : {},
|
|
1034
|
+
...request.ino !== void 0 ? { ino: request.ino } : {},
|
|
1035
|
+
...request.timeout !== void 0 ? { timeout: request.timeout } : {},
|
|
1036
|
+
...request.contentDisposition !== void 0 ? { content_disposition: request.contentDisposition } : {},
|
|
1037
|
+
...request.downloadFilename !== void 0 ? { download_filename: request.downloadFilename } : {}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function requestKey(request, baseUrl) {
|
|
1041
|
+
return encodeURIComponent(
|
|
1042
|
+
JSON.stringify({
|
|
1043
|
+
baseUrl,
|
|
1044
|
+
workspaceCuid: request.workspaceCuid ?? null,
|
|
1045
|
+
workspaceId: request.workspaceId ?? null,
|
|
1046
|
+
path: request.path ?? null,
|
|
1047
|
+
ino: request.ino ?? null
|
|
1048
|
+
})
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
function buildDownloadChunks(exported, useClientAssembly) {
|
|
1052
|
+
if (useClientAssembly && exported.chunks && exported.chunks.length > 1) {
|
|
1053
|
+
return exported.chunks;
|
|
1054
|
+
}
|
|
1055
|
+
return [
|
|
1056
|
+
{
|
|
1057
|
+
index: 0,
|
|
1058
|
+
offset: 0,
|
|
1059
|
+
size: exported.size,
|
|
1060
|
+
storageKey: exported.storageKey,
|
|
1061
|
+
url: exported.url
|
|
1062
|
+
}
|
|
1063
|
+
];
|
|
1064
|
+
}
|
|
1065
|
+
function buildDownloadKey(plan, baseUrl, request) {
|
|
1066
|
+
return encodeURIComponent(
|
|
1067
|
+
JSON.stringify({
|
|
1068
|
+
request: requestKey(request, baseUrl),
|
|
1069
|
+
path: plan.path,
|
|
1070
|
+
size: plan.size,
|
|
1071
|
+
chunks: plan.chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
1072
|
+
})
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
function checkpointMatches(checkpoint, plan) {
|
|
1076
|
+
if (checkpoint.version !== 1 || checkpoint.path !== plan.path || checkpoint.size !== plan.size) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
if (checkpoint.chunks.length !== plan.chunks.length) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
return checkpoint.chunks.every((saved, index) => {
|
|
1083
|
+
const current = plan.chunks[index];
|
|
1084
|
+
return Boolean(
|
|
1085
|
+
current && saved.index === current.index && saved.offset === current.offset && saved.size === current.size && saved.storageKey === current.storageKey && saved.downloadedBytes >= 0 && saved.downloadedBytes <= saved.size
|
|
1086
|
+
);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
function createCheckpoint(key, baseUrl, request, plan, fileHandle) {
|
|
1090
|
+
return {
|
|
1091
|
+
version: 1,
|
|
1092
|
+
key,
|
|
1093
|
+
requestKey: requestKey(request, baseUrl),
|
|
1094
|
+
request: { ...request },
|
|
1095
|
+
path: plan.path,
|
|
1096
|
+
size: plan.size,
|
|
1097
|
+
chunks: plan.chunks.map((chunk) => ({
|
|
1098
|
+
index: chunk.index,
|
|
1099
|
+
offset: chunk.offset,
|
|
1100
|
+
size: chunk.size,
|
|
1101
|
+
storageKey: chunk.storageKey,
|
|
1102
|
+
downloadedBytes: 0,
|
|
1103
|
+
completed: chunk.size === 0
|
|
1104
|
+
})),
|
|
1105
|
+
...fileHandle ? { fileHandle } : {},
|
|
1106
|
+
updatedAt: Date.now()
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function totalDownloadedBytes(checkpoint) {
|
|
1110
|
+
return checkpoint.chunks.reduce((total, chunk) => total + chunk.downloadedBytes, 0);
|
|
1111
|
+
}
|
|
1112
|
+
function completedChunkCount(checkpoint) {
|
|
1113
|
+
return checkpoint.chunks.filter((chunk) => chunk.completed).length;
|
|
1114
|
+
}
|
|
1115
|
+
function defaultFilename(path, requested) {
|
|
1116
|
+
const candidate = requested?.trim() || path.split("/").filter(Boolean).pop() || "download";
|
|
1117
|
+
return candidate.replace(/[\\/\r\n]/g, "_") || "download";
|
|
1118
|
+
}
|
|
1119
|
+
function triggerBrowserDownload(blob, filename) {
|
|
1120
|
+
const documentValue = globalThis.document;
|
|
1121
|
+
const urlApi = globalThis.URL;
|
|
1122
|
+
if (!documentValue || !urlApi?.createObjectURL) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const url = urlApi.createObjectURL(blob);
|
|
1126
|
+
const anchor = documentValue.createElement("a");
|
|
1127
|
+
anchor.href = url;
|
|
1128
|
+
anchor.download = filename;
|
|
1129
|
+
anchor.style.display = "none";
|
|
1130
|
+
documentValue.body?.appendChild(anchor);
|
|
1131
|
+
anchor.click();
|
|
1132
|
+
anchor.remove();
|
|
1133
|
+
setTimeout(() => urlApi.revokeObjectURL(url), 0);
|
|
1134
|
+
}
|
|
1135
|
+
var MountraDownloader = class {
|
|
1136
|
+
transport;
|
|
1137
|
+
constructor(transport) {
|
|
1138
|
+
this.transport = transport;
|
|
1139
|
+
}
|
|
1140
|
+
async exportFile(options) {
|
|
1141
|
+
return this.exportRequest(this.resolveRequest(options), options.signal);
|
|
1142
|
+
}
|
|
1143
|
+
async downloadFile(options) {
|
|
1144
|
+
const preferFileSystemAccess = options.preferFileSystemAccess !== false;
|
|
1145
|
+
const fileHandle = options.fileHandle ?? (preferFileSystemAccess && getShowSaveFilePicker() ? await this.pickSaveFile(options.downloadFilename ?? options.path?.split("/").filter(Boolean).pop()) : void 0);
|
|
1146
|
+
const request = this.resolveRequest(options);
|
|
1147
|
+
const plan = await this.loadCachedPlan(request, options.cache) ?? this.planExport(
|
|
1148
|
+
await this.exportRequest(request, options.signal),
|
|
1149
|
+
request,
|
|
1150
|
+
options.cache,
|
|
1151
|
+
fileHandle
|
|
1152
|
+
);
|
|
1153
|
+
return this.downloadResolved(plan, request, options, fileHandle, false);
|
|
1154
|
+
}
|
|
1155
|
+
async downloadExport(exported, options = { path: exported.path }) {
|
|
1156
|
+
let fileHandle = options.fileHandle;
|
|
1157
|
+
if (!fileHandle && options.preferFileSystemAccess !== false && getShowSaveFilePicker()) {
|
|
1158
|
+
fileHandle = await this.pickSaveFile(
|
|
1159
|
+
options.downloadFilename ?? defaultFilename(exported.path),
|
|
1160
|
+
exported.contentType
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
const request = this.resolveRequest(
|
|
1164
|
+
options.path === void 0 && options.ino === void 0 ? { ...options, path: exported.path } : options
|
|
1165
|
+
);
|
|
1166
|
+
return this.downloadResolved(
|
|
1167
|
+
this.planExport(exported, request, options.cache, fileHandle),
|
|
1168
|
+
request,
|
|
1169
|
+
options,
|
|
1170
|
+
fileHandle,
|
|
1171
|
+
false
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
async resumeDownload(downloadKey, options = {}) {
|
|
1175
|
+
if (!downloadKey.trim()) {
|
|
1176
|
+
throw new TypeError("downloadKey is required");
|
|
1177
|
+
}
|
|
1178
|
+
const checkpoint = await this.transport.checkpointStore.load(downloadKey);
|
|
1179
|
+
if (!checkpoint || !checkpoint.fileHandle) {
|
|
1180
|
+
throw new Error("No resumable File System Access download was found");
|
|
1181
|
+
}
|
|
1182
|
+
await this.ensureWritePermission(checkpoint.fileHandle);
|
|
1183
|
+
const exported = await this.exportFile({
|
|
1184
|
+
...checkpoint.request,
|
|
1185
|
+
signal: options.signal
|
|
1186
|
+
});
|
|
1187
|
+
return this.downloadResolved(
|
|
1188
|
+
this.planExport(exported, checkpoint.request, void 0, checkpoint.fileHandle),
|
|
1189
|
+
checkpoint.request,
|
|
1190
|
+
options,
|
|
1191
|
+
checkpoint.fileHandle,
|
|
1192
|
+
true
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
listResumableDownloads() {
|
|
1196
|
+
return this.transport.checkpointStore.list();
|
|
1197
|
+
}
|
|
1198
|
+
clearDownloadCheckpoint(downloadKey) {
|
|
1199
|
+
return this.transport.checkpointStore.remove(downloadKey);
|
|
1200
|
+
}
|
|
1201
|
+
/** Drop the cached layout of a file that was just written, so the next download sees the new content. */
|
|
1202
|
+
async invalidateCachedFile(selector, target) {
|
|
1203
|
+
await this.cachedFileScope(selector, target)?.invalidate();
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Cache a file this client just uploaded, so downloading it needs no
|
|
1207
|
+
* network. Falls back to invalidation when any chunk cannot be cached.
|
|
1208
|
+
*/
|
|
1209
|
+
async cacheUploadedFile(selector, file, mode = "default") {
|
|
1210
|
+
const scope = this.cachedFileScope(selector, file);
|
|
1211
|
+
if (!scope) {
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
await scope.invalidate();
|
|
1215
|
+
if (mode === "no-store") {
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
let complete = true;
|
|
1219
|
+
for (const chunk of file.chunks) {
|
|
1220
|
+
complete = await scope.cache.storeChunk(scope.workspace, chunk, chunk.data) && complete;
|
|
1221
|
+
}
|
|
1222
|
+
if (complete) {
|
|
1223
|
+
for (const fileKey of scope.fileKeys) {
|
|
1224
|
+
await scope.cache.storeFile(scope.workspace, fileKey, file);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
cachedFileScope(selector, target) {
|
|
1229
|
+
const cache = this.transport.cache;
|
|
1230
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1231
|
+
if (!cache || workspace === null) {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
const fileKeys = [
|
|
1235
|
+
target.path !== void 0 ? fileCacheKey({ path: target.path }) : null,
|
|
1236
|
+
target.ino !== void 0 ? fileCacheKey({ ino: target.ino }) : null
|
|
1237
|
+
].filter((fileKey) => fileKey !== null);
|
|
1238
|
+
return {
|
|
1239
|
+
cache,
|
|
1240
|
+
workspace,
|
|
1241
|
+
fileKeys,
|
|
1242
|
+
invalidate: () => cache.invalidateFiles(workspace, fileKeys)
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
async clearCache(selector) {
|
|
1246
|
+
const cache = this.transport.cache;
|
|
1247
|
+
if (!cache) {
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
await cache.clear(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1251
|
+
}
|
|
1252
|
+
async cacheUsage(selector) {
|
|
1253
|
+
const cache = this.transport.cache;
|
|
1254
|
+
if (!cache) {
|
|
1255
|
+
return { bytes: 0, chunks: 0 };
|
|
1256
|
+
}
|
|
1257
|
+
return cache.usage(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1258
|
+
}
|
|
1259
|
+
requireCacheWorkspace(selector) {
|
|
1260
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1261
|
+
if (workspace === null) {
|
|
1262
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1263
|
+
}
|
|
1264
|
+
return workspace;
|
|
1265
|
+
}
|
|
1266
|
+
/** Fall back to the client workspace only when the request names none, as uploads do. */
|
|
1267
|
+
resolveRequest(options) {
|
|
1268
|
+
const hasWorkspace = options.workspaceCuid !== void 0 || options.workspaceId !== void 0;
|
|
1269
|
+
return downloadRequestFromOptions(
|
|
1270
|
+
hasWorkspace ? options : {
|
|
1271
|
+
...options,
|
|
1272
|
+
workspaceCuid: this.transport.clientSelector.workspaceCuid,
|
|
1273
|
+
workspaceId: this.transport.clientSelector.workspaceId
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
async exportRequest(request, signal) {
|
|
1278
|
+
return ensureExportFileResponse(
|
|
1279
|
+
await this.transport.requestJson("/file/export", requestBodyFromCheckpoint(request), signal)
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
cacheScope(request, mode = "default") {
|
|
1283
|
+
const cache = this.transport.cache;
|
|
1284
|
+
if (!cache || mode === "no-store") {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, request);
|
|
1288
|
+
const fileKey = fileCacheKey(request);
|
|
1289
|
+
return workspace !== null && fileKey !== null ? { cache, workspace, fileKey } : null;
|
|
1290
|
+
}
|
|
1291
|
+
/** A plan that needs no network at all, when the cached layout is fresh and every chunk is cached. */
|
|
1292
|
+
async loadCachedPlan(request, mode) {
|
|
1293
|
+
const scope = this.cacheScope(request, mode);
|
|
1294
|
+
if (!scope || mode === "no-cache") {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
const cached = await scope.cache.loadFile(scope.workspace, scope.fileKey);
|
|
1298
|
+
if (!cached) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
path: cached.file.path,
|
|
1303
|
+
size: cached.file.size,
|
|
1304
|
+
...cached.file.contentType ? { contentType: cached.file.contentType } : {},
|
|
1305
|
+
chunks: cached.file.chunks,
|
|
1306
|
+
cachedChunks: cached.chunks
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
planExport(exported, request, mode, fileHandle) {
|
|
1310
|
+
const useClientAssembly = (exported.chunks?.length ?? 0) > 1 && (Boolean(fileHandle) || this.cacheScope(request, mode) !== null);
|
|
1311
|
+
return {
|
|
1312
|
+
path: exported.path,
|
|
1313
|
+
size: exported.size,
|
|
1314
|
+
...exported.contentType ? { contentType: exported.contentType } : {},
|
|
1315
|
+
chunks: buildDownloadChunks(exported, useClientAssembly),
|
|
1316
|
+
exported
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
async pickSaveFile(requestedFilename, contentType) {
|
|
1320
|
+
const picker = getShowSaveFilePicker();
|
|
1321
|
+
if (!picker) {
|
|
1322
|
+
throw new Error("File System Access API is not available");
|
|
1323
|
+
}
|
|
1324
|
+
const types = contentType ? [{ description: contentType, accept: { [contentType]: ["." + (requestedFilename?.split(".").pop() || "bin")] } }] : void 0;
|
|
1325
|
+
return picker({
|
|
1326
|
+
suggestedName: requestedFilename?.trim() || "download",
|
|
1327
|
+
...types ? { types } : {}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
async ensureWritePermission(handle) {
|
|
1331
|
+
if (!handle.queryPermission && !handle.requestPermission) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const descriptor = { mode: "readwrite" };
|
|
1335
|
+
const current = await handle.queryPermission?.(descriptor);
|
|
1336
|
+
if (current === "granted") {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const requested = await handle.requestPermission?.(descriptor);
|
|
1340
|
+
if (requested !== void 0 && requested !== "granted") {
|
|
1341
|
+
throw new DOMException("Write permission was not granted", "NotAllowedError");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async downloadResolved(plan, request, options, fileHandle, forceResume) {
|
|
1345
|
+
const chunks = plan.chunks;
|
|
1346
|
+
const key = buildDownloadKey(plan, this.transport.baseUrl, request);
|
|
1347
|
+
const existing = await this.transport.checkpointStore.load(key);
|
|
1348
|
+
const resumeRequested = !("resume" in options) || options.resume !== false;
|
|
1349
|
+
let checkpoint = existing && checkpointMatches(existing, plan) && (forceResume || resumeRequested) && (forceResume || fileHandle && await this.isSameFileHandle(existing.fileHandle, fileHandle)) ? existing : createCheckpoint(key, this.transport.baseUrl, request, plan, fileHandle);
|
|
1350
|
+
const cacheScope = this.cacheScope(request, "cache" in options ? options.cache : void 0);
|
|
1351
|
+
const cachedChunkIndexes = /* @__PURE__ */ new Set();
|
|
1352
|
+
let cachedBytes = 0;
|
|
1353
|
+
if (fileHandle) {
|
|
1354
|
+
checkpoint.fileHandle = fileHandle;
|
|
1355
|
+
}
|
|
1356
|
+
const resumed = totalDownloadedBytes(checkpoint) > 0;
|
|
1357
|
+
const maxRetries = validateRetryOption(options.maxRetries, "maxRetries") ?? 3;
|
|
1358
|
+
const retryDelayMs = validateRetryOption(options.retryDelayMs, "retryDelayMs") ?? 500;
|
|
1359
|
+
const sink = fileHandle ? new FileSystemDownloadSink(fileHandle) : new BlobDownloadSink();
|
|
1360
|
+
const speedWindow = new SpeedWindow();
|
|
1361
|
+
const startedAt = Date.now();
|
|
1362
|
+
let retryCount = 0;
|
|
1363
|
+
let lastCheckpointAt = 0;
|
|
1364
|
+
const emit = (phase, currentChunkIndex) => {
|
|
1365
|
+
const downloadedBytes = totalDownloadedBytes(checkpoint);
|
|
1366
|
+
const speedBytesPerSecond = speedWindow.record(downloadedBytes, Date.now());
|
|
1367
|
+
const remainingBytes = Math.max(plan.size - downloadedBytes, 0);
|
|
1368
|
+
options.onProgress?.({
|
|
1369
|
+
phase,
|
|
1370
|
+
downloadedBytes,
|
|
1371
|
+
totalBytes: plan.size,
|
|
1372
|
+
downloadedChunks: completedChunkCount(checkpoint),
|
|
1373
|
+
totalChunks: checkpoint.chunks.length,
|
|
1374
|
+
...currentChunkIndex !== void 0 ? { currentChunkIndex } : {},
|
|
1375
|
+
speedBytesPerSecond,
|
|
1376
|
+
...speedBytesPerSecond > 0 && remainingBytes > 0 ? { estimatedRemainingSeconds: remainingBytes / speedBytesPerSecond } : {},
|
|
1377
|
+
elapsedMs: Date.now() - startedAt,
|
|
1378
|
+
retryCount
|
|
1379
|
+
});
|
|
1380
|
+
};
|
|
1381
|
+
const persist = async (force) => {
|
|
1382
|
+
const now = Date.now();
|
|
1383
|
+
if (!force && now - lastCheckpointAt < 1e3) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
await sink.flush();
|
|
1387
|
+
checkpoint.updatedAt = now;
|
|
1388
|
+
await this.transport.checkpointStore.save(key, checkpoint);
|
|
1389
|
+
lastCheckpointAt = now;
|
|
1390
|
+
};
|
|
1391
|
+
await this.transport.checkpointStore.save(key, checkpoint);
|
|
1392
|
+
try {
|
|
1393
|
+
for (const chunk of chunks) {
|
|
1394
|
+
const savedChunk = checkpoint.chunks[chunk.index];
|
|
1395
|
+
if (!savedChunk) {
|
|
1396
|
+
throw new Error(`Missing download checkpoint for chunk ${chunk.index}`);
|
|
1397
|
+
}
|
|
1398
|
+
if (savedChunk.completed) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
const cached = plan.cachedChunks?.get(chunk.index) ?? (cacheScope ? await cacheScope.cache.readChunk(cacheScope.workspace, chunk) : null);
|
|
1402
|
+
if (cached) {
|
|
1403
|
+
throwIfAborted(options.signal);
|
|
1404
|
+
const remaining = cached.slice(savedChunk.downloadedBytes);
|
|
1405
|
+
await sink.write(remaining, chunk.offset + savedChunk.downloadedBytes);
|
|
1406
|
+
cachedBytes += remaining.size;
|
|
1407
|
+
savedChunk.downloadedBytes = chunk.size;
|
|
1408
|
+
savedChunk.completed = true;
|
|
1409
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1410
|
+
emit("downloading", chunk.index);
|
|
1411
|
+
await persist(false);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
const url = chunk.url;
|
|
1415
|
+
if (!url) {
|
|
1416
|
+
throw new Error(`Chunk ${chunk.index} is no longer in the download cache`);
|
|
1417
|
+
}
|
|
1418
|
+
const capture = cacheScope && savedChunk.downloadedBytes === 0 && cacheScope.cache.canStoreChunk(chunk) ? [] : null;
|
|
1419
|
+
let attempt = 0;
|
|
1420
|
+
while (true) {
|
|
1421
|
+
throwIfAborted(options.signal);
|
|
1422
|
+
const rangeStart = savedChunk.downloadedBytes;
|
|
1423
|
+
try {
|
|
1424
|
+
const response = await this.transport.fetchImpl(url, {
|
|
1425
|
+
method: "GET",
|
|
1426
|
+
...rangeStart > 0 ? { headers: { Range: `bytes=${rangeStart}-` } } : {},
|
|
1427
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1428
|
+
});
|
|
1429
|
+
if (!response.ok) {
|
|
1430
|
+
throw new DownloadHttpError(
|
|
1431
|
+
response.status,
|
|
1432
|
+
`Download for chunk ${chunk.index} failed with HTTP ${response.status}`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
await this.consumeResponse(
|
|
1436
|
+
response,
|
|
1437
|
+
chunk,
|
|
1438
|
+
savedChunk,
|
|
1439
|
+
sink,
|
|
1440
|
+
plan.size,
|
|
1441
|
+
emit,
|
|
1442
|
+
persist,
|
|
1443
|
+
capture,
|
|
1444
|
+
options.signal
|
|
1445
|
+
);
|
|
1446
|
+
if (savedChunk.downloadedBytes !== savedChunk.size) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`Download for chunk ${chunk.index} ended at ${savedChunk.downloadedBytes} of ${savedChunk.size} bytes`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
savedChunk.completed = true;
|
|
1452
|
+
await persist(true);
|
|
1453
|
+
if (cacheScope && capture && await cacheScope.cache.storeChunk(cacheScope.workspace, chunk, new Blob(capture))) {
|
|
1454
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1455
|
+
}
|
|
1456
|
+
emit("downloading", chunk.index);
|
|
1457
|
+
break;
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
await persist(true);
|
|
1460
|
+
if (isAbortError(error, options.signal) || !isRetryableError(error) || attempt >= maxRetries) {
|
|
1461
|
+
throw error;
|
|
1462
|
+
}
|
|
1463
|
+
attempt += 1;
|
|
1464
|
+
retryCount += 1;
|
|
1465
|
+
emit("retrying", chunk.index);
|
|
1466
|
+
await waitForRetry(Math.min(retryDelayMs * 2 ** (attempt - 1), 3e4), options.signal);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (totalDownloadedBytes(checkpoint) !== plan.size) {
|
|
1471
|
+
throw new Error("Download completed with an unexpected byte count");
|
|
1472
|
+
}
|
|
1473
|
+
if (fileHandle) {
|
|
1474
|
+
await sink.finalize(plan.size);
|
|
1475
|
+
} else {
|
|
1476
|
+
await sink.finalize();
|
|
1477
|
+
}
|
|
1478
|
+
await this.transport.checkpointStore.remove(key);
|
|
1479
|
+
if (cacheScope && plan.exported && chunks.every((chunk) => chunk.size === 0 || cachedChunkIndexes.has(chunk.index))) {
|
|
1480
|
+
await cacheScope.cache.storeFile(cacheScope.workspace, cacheScope.fileKey, plan);
|
|
1481
|
+
}
|
|
1482
|
+
emit("completed");
|
|
1483
|
+
const blob = sink instanceof BlobDownloadSink ? sink.blob(plan.contentType) : void 0;
|
|
1484
|
+
if (blob) {
|
|
1485
|
+
const filename = "downloadFilename" in options ? options.downloadFilename : void 0;
|
|
1486
|
+
triggerBrowserDownload(blob, defaultFilename(plan.path, filename));
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
...plan.exported ? { export: plan.exported } : {},
|
|
1490
|
+
downloadKey: key,
|
|
1491
|
+
downloadedBytes: plan.size,
|
|
1492
|
+
cachedBytes,
|
|
1493
|
+
resumed,
|
|
1494
|
+
...fileHandle ? { fileHandle } : {},
|
|
1495
|
+
...blob ? { blob } : {}
|
|
1496
|
+
};
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
await persist(true);
|
|
1499
|
+
throw error;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
1503
|
+
const sourceOffset = savedChunk.downloadedBytes;
|
|
1504
|
+
let bytesToSkip = response.status === 200 && sourceOffset > 0 ? sourceOffset : 0;
|
|
1505
|
+
const reader = response.body?.getReader();
|
|
1506
|
+
if (!reader) {
|
|
1507
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
1508
|
+
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
while (true) {
|
|
1513
|
+
throwIfAborted(signal);
|
|
1514
|
+
const next = await reader.read();
|
|
1515
|
+
if (next.done) {
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
const bytes = next.value;
|
|
1519
|
+
if (bytesToSkip > 0) {
|
|
1520
|
+
const skipped = Math.min(bytesToSkip, bytes.byteLength);
|
|
1521
|
+
bytesToSkip -= skipped;
|
|
1522
|
+
if (skipped === bytes.byteLength) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
await this.consumeBytes(
|
|
1526
|
+
bytes.subarray(skipped),
|
|
1527
|
+
0,
|
|
1528
|
+
chunk,
|
|
1529
|
+
savedChunk,
|
|
1530
|
+
sink,
|
|
1531
|
+
totalSize,
|
|
1532
|
+
emit,
|
|
1533
|
+
persist,
|
|
1534
|
+
capture,
|
|
1535
|
+
signal
|
|
1536
|
+
);
|
|
1537
|
+
} else {
|
|
1538
|
+
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
} finally {
|
|
1542
|
+
reader.releaseLock();
|
|
1543
|
+
}
|
|
1544
|
+
if (bytesToSkip > 0) {
|
|
1545
|
+
throw new Error(`Download response did not contain the saved offset for chunk ${chunk.index}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
1549
|
+
const remaining = chunk.size - savedChunk.downloadedBytes;
|
|
1550
|
+
const writableBytes = bytes.byteLength - offset;
|
|
1551
|
+
if (writableBytes > remaining) {
|
|
1552
|
+
throw new Error(`Download response for chunk ${chunk.index} is larger than expected`);
|
|
1553
|
+
}
|
|
1554
|
+
if (writableBytes === 0) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
throwIfAborted(signal);
|
|
1558
|
+
const payload = bytes.subarray(offset);
|
|
1559
|
+
const position = chunk.offset + savedChunk.downloadedBytes;
|
|
1560
|
+
await sink.write(payload, position);
|
|
1561
|
+
capture?.push(payload.slice());
|
|
1562
|
+
savedChunk.downloadedBytes += payload.byteLength;
|
|
1563
|
+
emit("downloading", chunk.index);
|
|
1564
|
+
await persist(false);
|
|
1565
|
+
if (savedChunk.downloadedBytes > totalSize) {
|
|
1566
|
+
throw new Error("Download byte count exceeded the exported file size");
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async isSameFileHandle(savedHandle, selectedHandle) {
|
|
1570
|
+
if (!savedHandle) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
if (savedHandle === selectedHandle) {
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
if (savedHandle.isSameEntry) {
|
|
1577
|
+
try {
|
|
1578
|
+
return await savedHandle.isSameEntry(selectedHandle);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
|
|
40
1587
|
// src/upload.ts
|
|
41
1588
|
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
42
1589
|
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
@@ -56,7 +1603,7 @@ var MountraApiError = class extends Error {
|
|
|
56
1603
|
};
|
|
57
1604
|
var memoryCheckpointRecords = /* @__PURE__ */ new Map();
|
|
58
1605
|
function parseJumboCheckpoint(value) {
|
|
59
|
-
if (!
|
|
1606
|
+
if (!isRecord2(value) || value.version !== 1 || typeof value.storageKey !== "string") {
|
|
60
1607
|
return null;
|
|
61
1608
|
}
|
|
62
1609
|
if (typeof value.chunkIndex !== "number" || typeof value.chunkSize !== "number" || typeof value.chunkSha256 !== "string" || typeof value.partSize !== "number" || typeof value.uploadId !== "string" || !Array.isArray(value.parts)) {
|
|
@@ -64,7 +1611,7 @@ function parseJumboCheckpoint(value) {
|
|
|
64
1611
|
}
|
|
65
1612
|
const parts = [];
|
|
66
1613
|
for (const part of value.parts) {
|
|
67
|
-
if (!
|
|
1614
|
+
if (!isRecord2(part)) {
|
|
68
1615
|
return null;
|
|
69
1616
|
}
|
|
70
1617
|
if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
|
|
@@ -237,17 +1784,17 @@ function getWorkspaceBody(clientSelector, requestSelector) {
|
|
|
237
1784
|
...workspaceId !== void 0 ? { workspace_id: workspaceId } : {}
|
|
238
1785
|
};
|
|
239
1786
|
}
|
|
240
|
-
function
|
|
1787
|
+
function throwIfAborted2(signal) {
|
|
241
1788
|
if (!signal?.aborted) {
|
|
242
1789
|
return;
|
|
243
1790
|
}
|
|
244
1791
|
throw new DOMException("The upload was aborted", "AbortError");
|
|
245
1792
|
}
|
|
246
|
-
function
|
|
1793
|
+
function isRecord2(value) {
|
|
247
1794
|
return typeof value === "object" && value !== null;
|
|
248
1795
|
}
|
|
249
1796
|
function parseApiErrorBody(body) {
|
|
250
|
-
if (!
|
|
1797
|
+
if (!isRecord2(body)) {
|
|
251
1798
|
return {};
|
|
252
1799
|
}
|
|
253
1800
|
return {
|
|
@@ -256,7 +1803,7 @@ function parseApiErrorBody(body) {
|
|
|
256
1803
|
};
|
|
257
1804
|
}
|
|
258
1805
|
function ensurePrepareResponse(value) {
|
|
259
|
-
if (!
|
|
1806
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
260
1807
|
throw new TypeError("Mountra prepare response is invalid");
|
|
261
1808
|
}
|
|
262
1809
|
if (!Array.isArray(value.upload_targets)) {
|
|
@@ -265,13 +1812,13 @@ function ensurePrepareResponse(value) {
|
|
|
265
1812
|
return value;
|
|
266
1813
|
}
|
|
267
1814
|
function ensureFinishResponse(value) {
|
|
268
|
-
if (!
|
|
1815
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
269
1816
|
throw new TypeError("Mountra finish response is invalid");
|
|
270
1817
|
}
|
|
271
1818
|
return value;
|
|
272
1819
|
}
|
|
273
1820
|
function ensureUploadStatusResponse(value) {
|
|
274
|
-
if (!
|
|
1821
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
275
1822
|
throw new TypeError("Mountra upload status response is invalid");
|
|
276
1823
|
}
|
|
277
1824
|
if (!Array.isArray(value.chunks)) {
|
|
@@ -289,8 +1836,7 @@ function splitFileIntoChunks(file, chunkCount = 1) {
|
|
|
289
1836
|
}
|
|
290
1837
|
const chunkSize = Math.ceil(file.size / chunkCount);
|
|
291
1838
|
const chunks = [];
|
|
292
|
-
for (let index = 0;
|
|
293
|
-
const offset = index * chunkSize;
|
|
1839
|
+
for (let index = 0, offset = 0; offset < file.size; index += 1, offset += chunkSize) {
|
|
294
1840
|
const end = Math.min(offset + chunkSize, file.size);
|
|
295
1841
|
chunks.push({
|
|
296
1842
|
index,
|
|
@@ -322,6 +1868,7 @@ var MountraClient = class {
|
|
|
322
1868
|
jumboChunkUploader;
|
|
323
1869
|
jumboChunkCheckpointStore;
|
|
324
1870
|
jumboPartSize;
|
|
1871
|
+
downloader;
|
|
325
1872
|
constructor(options) {
|
|
326
1873
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
327
1874
|
this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
|
|
@@ -340,6 +1887,46 @@ var MountraClient = class {
|
|
|
340
1887
|
throw new Error("fetch is not available; provide fetch in MountraClientOptions");
|
|
341
1888
|
}
|
|
342
1889
|
this.fetchImpl = fetchImpl;
|
|
1890
|
+
this.downloader = new MountraDownloader({
|
|
1891
|
+
baseUrl: this.baseUrl,
|
|
1892
|
+
clientSelector: this.clientSelector,
|
|
1893
|
+
fetchImpl: this.fetchImpl,
|
|
1894
|
+
checkpointStore: options.downloadCheckpointStore ?? new IndexedDBDownloadCheckpointStore(),
|
|
1895
|
+
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1896
|
+
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
async exportFile(options) {
|
|
1900
|
+
return this.downloader.exportFile(options);
|
|
1901
|
+
}
|
|
1902
|
+
async downloadFile(options) {
|
|
1903
|
+
return this.downloader.downloadFile(options);
|
|
1904
|
+
}
|
|
1905
|
+
async downloadExport(exported, options) {
|
|
1906
|
+
return this.downloader.downloadExport(exported, options);
|
|
1907
|
+
}
|
|
1908
|
+
async resumeDownload(downloadKey, options) {
|
|
1909
|
+
return this.downloader.resumeDownload(downloadKey, options);
|
|
1910
|
+
}
|
|
1911
|
+
listResumableDownloads() {
|
|
1912
|
+
return this.downloader.listResumableDownloads();
|
|
1913
|
+
}
|
|
1914
|
+
clearDownloadCheckpoint(downloadKey) {
|
|
1915
|
+
return this.downloader.clearDownloadCheckpoint(downloadKey);
|
|
1916
|
+
}
|
|
1917
|
+
/** Clear the download cache of one workspace, or of every workspace when none is given. */
|
|
1918
|
+
clearDownloadCache(selector) {
|
|
1919
|
+
return this.downloader.clearCache(selector);
|
|
1920
|
+
}
|
|
1921
|
+
getDownloadCacheUsage(selector) {
|
|
1922
|
+
return this.downloader.cacheUsage(selector);
|
|
1923
|
+
}
|
|
1924
|
+
/** Create a stable resource object for one file in this or another workspace. */
|
|
1925
|
+
file(locator) {
|
|
1926
|
+
return new MountraFile(
|
|
1927
|
+
this,
|
|
1928
|
+
normalizeMountraFileLocator(locator, this.clientSelector)
|
|
1929
|
+
);
|
|
343
1930
|
}
|
|
344
1931
|
async prepareUpload(file, options) {
|
|
345
1932
|
const chunks = splitFileIntoChunks(file, options.chunkCount ?? 1);
|
|
@@ -347,7 +1934,7 @@ var MountraClient = class {
|
|
|
347
1934
|
const contentHash = await sha256Hex(file);
|
|
348
1935
|
const chunkMethod = chunks.length === 1 ? "none" : "fix";
|
|
349
1936
|
const contentType = options.contentType ?? ("type" in file ? file.type : void 0);
|
|
350
|
-
|
|
1937
|
+
throwIfAborted2(options.signal);
|
|
351
1938
|
const response = ensurePrepareResponse(
|
|
352
1939
|
await this.requestJson("/upload/prepare", {
|
|
353
1940
|
...getWorkspaceBody(this.clientSelector, options),
|
|
@@ -450,7 +2037,7 @@ var MountraClient = class {
|
|
|
450
2037
|
if (!chunk || target.chunk_index !== position) {
|
|
451
2038
|
throw new Error("Mountra upload targets must contain contiguous chunk indexes");
|
|
452
2039
|
}
|
|
453
|
-
|
|
2040
|
+
throwIfAborted2(options.signal);
|
|
454
2041
|
if (isJumboChunk(chunk.size)) {
|
|
455
2042
|
const checkpointKey = this.buildJumboCheckpointKey(context, target, options);
|
|
456
2043
|
jumboCheckpointKeys.add(checkpointKey);
|
|
@@ -493,7 +2080,7 @@ var MountraClient = class {
|
|
|
493
2080
|
totalChunks: orderedTargets.length
|
|
494
2081
|
});
|
|
495
2082
|
}
|
|
496
|
-
|
|
2083
|
+
throwIfAborted2(options.signal);
|
|
497
2084
|
options.onProgress?.({
|
|
498
2085
|
phase: "finishing",
|
|
499
2086
|
uploadedBytes: file.size,
|
|
@@ -514,6 +2101,26 @@ var MountraClient = class {
|
|
|
514
2101
|
if (finished.finish_status !== "completed") {
|
|
515
2102
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
516
2103
|
}
|
|
2104
|
+
await this.downloader.cacheUploadedFile(
|
|
2105
|
+
this.cacheWorkspace(options),
|
|
2106
|
+
{
|
|
2107
|
+
path: finished.path,
|
|
2108
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {},
|
|
2109
|
+
size: context.fileSize,
|
|
2110
|
+
...context.contentType ? { contentType: context.contentType } : {},
|
|
2111
|
+
chunks: orderedTargets.map((target) => {
|
|
2112
|
+
const chunk = chunks[target.chunk_index];
|
|
2113
|
+
return {
|
|
2114
|
+
index: chunk.index,
|
|
2115
|
+
offset: chunk.offset,
|
|
2116
|
+
size: chunk.size,
|
|
2117
|
+
storageKey: target.storage_key,
|
|
2118
|
+
data: chunk.blob
|
|
2119
|
+
};
|
|
2120
|
+
})
|
|
2121
|
+
},
|
|
2122
|
+
options.cache
|
|
2123
|
+
);
|
|
517
2124
|
await Promise.all(
|
|
518
2125
|
[...jumboCheckpointKeys].map(
|
|
519
2126
|
(checkpointKey) => this.jumboChunkCheckpointStore.remove(checkpointKey)
|
|
@@ -597,7 +2204,7 @@ var MountraClient = class {
|
|
|
597
2204
|
let uploadedBytes = 0;
|
|
598
2205
|
let uploadedParts = 0;
|
|
599
2206
|
for (let partNumber = 1; partNumber <= totalParts; partNumber += 1) {
|
|
600
|
-
|
|
2207
|
+
throwIfAborted2(options.signal);
|
|
601
2208
|
const start = (partNumber - 1) * this.jumboPartSize;
|
|
602
2209
|
const partBlob = chunk.blob.slice(start, Math.min(start + this.jumboPartSize, chunk.size));
|
|
603
2210
|
const partHash = await sha256Hex(partBlob);
|
|
@@ -689,12 +2296,26 @@ var MountraClient = class {
|
|
|
689
2296
|
if (!uploadId.trim()) {
|
|
690
2297
|
throw new TypeError("uploadId is required");
|
|
691
2298
|
}
|
|
692
|
-
|
|
2299
|
+
const finished = ensureFinishResponse(
|
|
693
2300
|
await this.requestJson("/upload/finish", {
|
|
694
2301
|
...getWorkspaceBody(this.clientSelector, selector),
|
|
695
2302
|
upload_id: uploadId
|
|
696
2303
|
})
|
|
697
2304
|
);
|
|
2305
|
+
if (finished.finish_status === "completed") {
|
|
2306
|
+
await this.downloader.invalidateCachedFile(this.cacheWorkspace(selector), {
|
|
2307
|
+
path: finished.path,
|
|
2308
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {}
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
return finished;
|
|
2312
|
+
}
|
|
2313
|
+
/** Same workspace resolution as `getWorkspaceBody`, so the cache key matches what Mountra used. */
|
|
2314
|
+
cacheWorkspace(selector) {
|
|
2315
|
+
return {
|
|
2316
|
+
workspaceCuid: selector.workspaceCuid ?? this.clientSelector.workspaceCuid,
|
|
2317
|
+
workspaceId: selector.workspaceId ?? this.clientSelector.workspaceId
|
|
2318
|
+
};
|
|
698
2319
|
}
|
|
699
2320
|
endpoint(path) {
|
|
700
2321
|
return `${this.baseUrl}${this.apiPrefix}${path}`;
|
|
@@ -734,13 +2355,20 @@ function createMountraClient(options) {
|
|
|
734
2355
|
// Annotate the CommonJS export names for ESM import in node:
|
|
735
2356
|
0 && (module.exports = {
|
|
736
2357
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
2358
|
+
DownloadHttpError,
|
|
2359
|
+
InMemoryDownloadCacheStore,
|
|
2360
|
+
InMemoryDownloadCheckpointStore,
|
|
737
2361
|
InMemoryJumboChunkCheckpointStore,
|
|
2362
|
+
IndexedDBDownloadCacheStore,
|
|
2363
|
+
IndexedDBDownloadCheckpointStore,
|
|
738
2364
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
739
2365
|
LocalStorageJumboChunkCheckpointStore,
|
|
740
2366
|
MAX_S3_MULTIPART_PARTS,
|
|
741
2367
|
MIN_S3_MULTIPART_PART_SIZE_BYTES,
|
|
742
2368
|
MountraApiError,
|
|
743
2369
|
MountraClient,
|
|
2370
|
+
MountraDownloader,
|
|
2371
|
+
MountraFile,
|
|
744
2372
|
MountraSDK,
|
|
745
2373
|
S3MultipartUploader,
|
|
746
2374
|
createMountraClient,
|