@mountra/mountra-sdk 0.2.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 +21 -0
- package/README.md +60 -14
- package/dist/index.cjs +861 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -3
- package/dist/index.d.ts +175 -3
- package/dist/index.js +859 -186
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -22,8 +22,10 @@ var src_exports = {};
|
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES: () => DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
24
24
|
DownloadHttpError: () => DownloadHttpError,
|
|
25
|
+
InMemoryDownloadCacheStore: () => InMemoryDownloadCacheStore,
|
|
25
26
|
InMemoryDownloadCheckpointStore: () => InMemoryDownloadCheckpointStore,
|
|
26
27
|
InMemoryJumboChunkCheckpointStore: () => InMemoryJumboChunkCheckpointStore,
|
|
28
|
+
IndexedDBDownloadCacheStore: () => IndexedDBDownloadCacheStore,
|
|
27
29
|
IndexedDBDownloadCheckpointStore: () => IndexedDBDownloadCheckpointStore,
|
|
28
30
|
JUMBO_CHUNK_THRESHOLD_BYTES: () => JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
29
31
|
LocalStorageJumboChunkCheckpointStore: () => LocalStorageJumboChunkCheckpointStore,
|
|
@@ -42,6 +44,609 @@ __export(src_exports, {
|
|
|
42
44
|
});
|
|
43
45
|
module.exports = __toCommonJS(src_exports);
|
|
44
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
|
+
|
|
45
650
|
// src/download.ts
|
|
46
651
|
function copyCheckpoint(checkpoint) {
|
|
47
652
|
return {
|
|
@@ -69,7 +674,7 @@ var InMemoryDownloadCheckpointStore = class {
|
|
|
69
674
|
var DOWNLOAD_DATABASE_NAME = "mountra-sdk";
|
|
70
675
|
var DOWNLOAD_DATABASE_VERSION = 1;
|
|
71
676
|
var DOWNLOAD_STORE_NAME = "download-checkpoints";
|
|
72
|
-
function
|
|
677
|
+
function getIndexedDb2() {
|
|
73
678
|
return globalThis.indexedDB ?? null;
|
|
74
679
|
}
|
|
75
680
|
var IndexedDBDownloadCheckpointStore = class {
|
|
@@ -134,7 +739,7 @@ var IndexedDBDownloadCheckpointStore = class {
|
|
|
134
739
|
}
|
|
135
740
|
async openDatabase() {
|
|
136
741
|
if (!this.databasePromise) {
|
|
137
|
-
const indexedDb =
|
|
742
|
+
const indexedDb = getIndexedDb2();
|
|
138
743
|
if (!indexedDb) {
|
|
139
744
|
this.databasePromise = Promise.resolve(null);
|
|
140
745
|
} else {
|
|
@@ -329,8 +934,9 @@ var SpeedWindow = class {
|
|
|
329
934
|
};
|
|
330
935
|
var BlobDownloadSink = class {
|
|
331
936
|
parts = [];
|
|
332
|
-
|
|
333
|
-
|
|
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());
|
|
334
940
|
}
|
|
335
941
|
async flush() {
|
|
336
942
|
}
|
|
@@ -338,6 +944,9 @@ var BlobDownloadSink = class {
|
|
|
338
944
|
}
|
|
339
945
|
blob(contentType) {
|
|
340
946
|
const buffers = this.parts.map((part) => {
|
|
947
|
+
if (part instanceof Blob) {
|
|
948
|
+
return part;
|
|
949
|
+
}
|
|
341
950
|
const buffer = new ArrayBuffer(part.byteLength);
|
|
342
951
|
new Uint8Array(buffer).set(part);
|
|
343
952
|
return buffer;
|
|
@@ -367,7 +976,7 @@ var FileSystemDownloadSink = class {
|
|
|
367
976
|
throw new Error("File System Access writer position does not match the download checkpoint");
|
|
368
977
|
}
|
|
369
978
|
await this.writable.write(bytes);
|
|
370
|
-
this.position += bytes.byteLength;
|
|
979
|
+
this.position += bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
371
980
|
}
|
|
372
981
|
async flush() {
|
|
373
982
|
if (this.writable) {
|
|
@@ -453,39 +1062,39 @@ function buildDownloadChunks(exported, useClientAssembly) {
|
|
|
453
1062
|
}
|
|
454
1063
|
];
|
|
455
1064
|
}
|
|
456
|
-
function buildDownloadKey(
|
|
1065
|
+
function buildDownloadKey(plan, baseUrl, request) {
|
|
457
1066
|
return encodeURIComponent(
|
|
458
1067
|
JSON.stringify({
|
|
459
1068
|
request: requestKey(request, baseUrl),
|
|
460
|
-
path:
|
|
461
|
-
size:
|
|
462
|
-
chunks: chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
1069
|
+
path: plan.path,
|
|
1070
|
+
size: plan.size,
|
|
1071
|
+
chunks: plan.chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
463
1072
|
})
|
|
464
1073
|
);
|
|
465
1074
|
}
|
|
466
|
-
function checkpointMatches(checkpoint,
|
|
467
|
-
if (checkpoint.version !== 1 || checkpoint.path !==
|
|
1075
|
+
function checkpointMatches(checkpoint, plan) {
|
|
1076
|
+
if (checkpoint.version !== 1 || checkpoint.path !== plan.path || checkpoint.size !== plan.size) {
|
|
468
1077
|
return false;
|
|
469
1078
|
}
|
|
470
|
-
if (checkpoint.chunks.length !== chunks.length) {
|
|
1079
|
+
if (checkpoint.chunks.length !== plan.chunks.length) {
|
|
471
1080
|
return false;
|
|
472
1081
|
}
|
|
473
1082
|
return checkpoint.chunks.every((saved, index) => {
|
|
474
|
-
const current = chunks[index];
|
|
1083
|
+
const current = plan.chunks[index];
|
|
475
1084
|
return Boolean(
|
|
476
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
|
|
477
1086
|
);
|
|
478
1087
|
});
|
|
479
1088
|
}
|
|
480
|
-
function createCheckpoint(key, baseUrl, request,
|
|
1089
|
+
function createCheckpoint(key, baseUrl, request, plan, fileHandle) {
|
|
481
1090
|
return {
|
|
482
1091
|
version: 1,
|
|
483
1092
|
key,
|
|
484
1093
|
requestKey: requestKey(request, baseUrl),
|
|
485
1094
|
request: { ...request },
|
|
486
|
-
path:
|
|
487
|
-
size:
|
|
488
|
-
chunks: chunks.map((chunk) => ({
|
|
1095
|
+
path: plan.path,
|
|
1096
|
+
size: plan.size,
|
|
1097
|
+
chunks: plan.chunks.map((chunk) => ({
|
|
489
1098
|
index: chunk.index,
|
|
490
1099
|
offset: chunk.offset,
|
|
491
1100
|
size: chunk.size,
|
|
@@ -503,8 +1112,8 @@ function totalDownloadedBytes(checkpoint) {
|
|
|
503
1112
|
function completedChunkCount(checkpoint) {
|
|
504
1113
|
return checkpoint.chunks.filter((chunk) => chunk.completed).length;
|
|
505
1114
|
}
|
|
506
|
-
function defaultFilename(
|
|
507
|
-
const candidate = requested?.trim() ||
|
|
1115
|
+
function defaultFilename(path, requested) {
|
|
1116
|
+
const candidate = requested?.trim() || path.split("/").filter(Boolean).pop() || "download";
|
|
508
1117
|
return candidate.replace(/[\\/\r\n]/g, "_") || "download";
|
|
509
1118
|
}
|
|
510
1119
|
function triggerBrowserDownload(blob, filename) {
|
|
@@ -529,33 +1138,38 @@ var MountraDownloader = class {
|
|
|
529
1138
|
this.transport = transport;
|
|
530
1139
|
}
|
|
531
1140
|
async exportFile(options) {
|
|
532
|
-
|
|
533
|
-
return ensureExportFileResponse(
|
|
534
|
-
await this.transport.requestJson(
|
|
535
|
-
"/file/export",
|
|
536
|
-
requestBodyFromCheckpoint(request),
|
|
537
|
-
options.signal
|
|
538
|
-
)
|
|
539
|
-
);
|
|
1141
|
+
return this.exportRequest(this.resolveRequest(options), options.signal);
|
|
540
1142
|
}
|
|
541
1143
|
async downloadFile(options) {
|
|
542
1144
|
const preferFileSystemAccess = options.preferFileSystemAccess !== false;
|
|
543
1145
|
const fileHandle = options.fileHandle ?? (preferFileSystemAccess && getShowSaveFilePicker() ? await this.pickSaveFile(options.downloadFilename ?? options.path?.split("/").filter(Boolean).pop()) : void 0);
|
|
544
|
-
const
|
|
545
|
-
|
|
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);
|
|
546
1154
|
}
|
|
547
1155
|
async downloadExport(exported, options = { path: exported.path }) {
|
|
548
1156
|
let fileHandle = options.fileHandle;
|
|
549
1157
|
if (!fileHandle && options.preferFileSystemAccess !== false && getShowSaveFilePicker()) {
|
|
550
1158
|
fileHandle = await this.pickSaveFile(
|
|
551
|
-
options.downloadFilename ?? defaultFilename(exported),
|
|
1159
|
+
options.downloadFilename ?? defaultFilename(exported.path),
|
|
552
1160
|
exported.contentType
|
|
553
1161
|
);
|
|
554
1162
|
}
|
|
555
|
-
const request =
|
|
1163
|
+
const request = this.resolveRequest(
|
|
556
1164
|
options.path === void 0 && options.ino === void 0 ? { ...options, path: exported.path } : options
|
|
557
1165
|
);
|
|
558
|
-
return this.downloadResolved(
|
|
1166
|
+
return this.downloadResolved(
|
|
1167
|
+
this.planExport(exported, request, options.cache, fileHandle),
|
|
1168
|
+
request,
|
|
1169
|
+
options,
|
|
1170
|
+
fileHandle,
|
|
1171
|
+
false
|
|
1172
|
+
);
|
|
559
1173
|
}
|
|
560
1174
|
async resumeDownload(downloadKey, options = {}) {
|
|
561
1175
|
if (!downloadKey.trim()) {
|
|
@@ -571,7 +1185,7 @@ var MountraDownloader = class {
|
|
|
571
1185
|
signal: options.signal
|
|
572
1186
|
});
|
|
573
1187
|
return this.downloadResolved(
|
|
574
|
-
exported,
|
|
1188
|
+
this.planExport(exported, checkpoint.request, void 0, checkpoint.fileHandle),
|
|
575
1189
|
checkpoint.request,
|
|
576
1190
|
options,
|
|
577
1191
|
checkpoint.fileHandle,
|
|
@@ -584,6 +1198,124 @@ var MountraDownloader = class {
|
|
|
584
1198
|
clearDownloadCheckpoint(downloadKey) {
|
|
585
1199
|
return this.transport.checkpointStore.remove(downloadKey);
|
|
586
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
|
+
}
|
|
587
1319
|
async pickSaveFile(requestedFilename, contentType) {
|
|
588
1320
|
const picker = getShowSaveFilePicker();
|
|
589
1321
|
if (!picker) {
|
|
@@ -609,13 +1341,15 @@ var MountraDownloader = class {
|
|
|
609
1341
|
throw new DOMException("Write permission was not granted", "NotAllowedError");
|
|
610
1342
|
}
|
|
611
1343
|
}
|
|
612
|
-
async downloadResolved(
|
|
613
|
-
const
|
|
614
|
-
const
|
|
615
|
-
const key = buildDownloadKey(exported, chunks, this.transport.baseUrl, request);
|
|
1344
|
+
async downloadResolved(plan, request, options, fileHandle, forceResume) {
|
|
1345
|
+
const chunks = plan.chunks;
|
|
1346
|
+
const key = buildDownloadKey(plan, this.transport.baseUrl, request);
|
|
616
1347
|
const existing = await this.transport.checkpointStore.load(key);
|
|
617
1348
|
const resumeRequested = !("resume" in options) || options.resume !== false;
|
|
618
|
-
let checkpoint = existing && checkpointMatches(existing,
|
|
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;
|
|
619
1353
|
if (fileHandle) {
|
|
620
1354
|
checkpoint.fileHandle = fileHandle;
|
|
621
1355
|
}
|
|
@@ -630,11 +1364,11 @@ var MountraDownloader = class {
|
|
|
630
1364
|
const emit = (phase, currentChunkIndex) => {
|
|
631
1365
|
const downloadedBytes = totalDownloadedBytes(checkpoint);
|
|
632
1366
|
const speedBytesPerSecond = speedWindow.record(downloadedBytes, Date.now());
|
|
633
|
-
const remainingBytes = Math.max(
|
|
1367
|
+
const remainingBytes = Math.max(plan.size - downloadedBytes, 0);
|
|
634
1368
|
options.onProgress?.({
|
|
635
1369
|
phase,
|
|
636
1370
|
downloadedBytes,
|
|
637
|
-
totalBytes:
|
|
1371
|
+
totalBytes: plan.size,
|
|
638
1372
|
downloadedChunks: completedChunkCount(checkpoint),
|
|
639
1373
|
totalChunks: checkpoint.chunks.length,
|
|
640
1374
|
...currentChunkIndex !== void 0 ? { currentChunkIndex } : {},
|
|
@@ -664,12 +1398,30 @@ var MountraDownloader = class {
|
|
|
664
1398
|
if (savedChunk.completed) {
|
|
665
1399
|
continue;
|
|
666
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;
|
|
667
1419
|
let attempt = 0;
|
|
668
1420
|
while (true) {
|
|
669
1421
|
throwIfAborted(options.signal);
|
|
670
1422
|
const rangeStart = savedChunk.downloadedBytes;
|
|
671
1423
|
try {
|
|
672
|
-
const response = await this.transport.fetchImpl(
|
|
1424
|
+
const response = await this.transport.fetchImpl(url, {
|
|
673
1425
|
method: "GET",
|
|
674
1426
|
...rangeStart > 0 ? { headers: { Range: `bytes=${rangeStart}-` } } : {},
|
|
675
1427
|
...options.signal ? { signal: options.signal } : {}
|
|
@@ -685,9 +1437,10 @@ var MountraDownloader = class {
|
|
|
685
1437
|
chunk,
|
|
686
1438
|
savedChunk,
|
|
687
1439
|
sink,
|
|
688
|
-
|
|
1440
|
+
plan.size,
|
|
689
1441
|
emit,
|
|
690
1442
|
persist,
|
|
1443
|
+
capture,
|
|
691
1444
|
options.signal
|
|
692
1445
|
);
|
|
693
1446
|
if (savedChunk.downloadedBytes !== savedChunk.size) {
|
|
@@ -697,6 +1450,9 @@ var MountraDownloader = class {
|
|
|
697
1450
|
}
|
|
698
1451
|
savedChunk.completed = true;
|
|
699
1452
|
await persist(true);
|
|
1453
|
+
if (cacheScope && capture && await cacheScope.cache.storeChunk(cacheScope.workspace, chunk, new Blob(capture))) {
|
|
1454
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1455
|
+
}
|
|
700
1456
|
emit("downloading", chunk.index);
|
|
701
1457
|
break;
|
|
702
1458
|
} catch (error) {
|
|
@@ -711,25 +1467,29 @@ var MountraDownloader = class {
|
|
|
711
1467
|
}
|
|
712
1468
|
}
|
|
713
1469
|
}
|
|
714
|
-
if (totalDownloadedBytes(checkpoint) !==
|
|
1470
|
+
if (totalDownloadedBytes(checkpoint) !== plan.size) {
|
|
715
1471
|
throw new Error("Download completed with an unexpected byte count");
|
|
716
1472
|
}
|
|
717
1473
|
if (fileHandle) {
|
|
718
|
-
await sink.finalize(
|
|
1474
|
+
await sink.finalize(plan.size);
|
|
719
1475
|
} else {
|
|
720
1476
|
await sink.finalize();
|
|
721
1477
|
}
|
|
722
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
|
+
}
|
|
723
1482
|
emit("completed");
|
|
724
|
-
const blob = sink instanceof BlobDownloadSink ? sink.blob(
|
|
1483
|
+
const blob = sink instanceof BlobDownloadSink ? sink.blob(plan.contentType) : void 0;
|
|
725
1484
|
if (blob) {
|
|
726
1485
|
const filename = "downloadFilename" in options ? options.downloadFilename : void 0;
|
|
727
|
-
triggerBrowserDownload(blob, defaultFilename(
|
|
1486
|
+
triggerBrowserDownload(blob, defaultFilename(plan.path, filename));
|
|
728
1487
|
}
|
|
729
1488
|
return {
|
|
730
|
-
export: exported,
|
|
1489
|
+
...plan.exported ? { export: plan.exported } : {},
|
|
731
1490
|
downloadKey: key,
|
|
732
|
-
downloadedBytes:
|
|
1491
|
+
downloadedBytes: plan.size,
|
|
1492
|
+
cachedBytes,
|
|
733
1493
|
resumed,
|
|
734
1494
|
...fileHandle ? { fileHandle } : {},
|
|
735
1495
|
...blob ? { blob } : {}
|
|
@@ -739,13 +1499,13 @@ var MountraDownloader = class {
|
|
|
739
1499
|
throw error;
|
|
740
1500
|
}
|
|
741
1501
|
}
|
|
742
|
-
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1502
|
+
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
743
1503
|
const sourceOffset = savedChunk.downloadedBytes;
|
|
744
1504
|
let bytesToSkip = response.status === 200 && sourceOffset > 0 ? sourceOffset : 0;
|
|
745
1505
|
const reader = response.body?.getReader();
|
|
746
1506
|
if (!reader) {
|
|
747
1507
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
748
|
-
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1508
|
+
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
749
1509
|
return;
|
|
750
1510
|
}
|
|
751
1511
|
try {
|
|
@@ -771,10 +1531,11 @@ var MountraDownloader = class {
|
|
|
771
1531
|
totalSize,
|
|
772
1532
|
emit,
|
|
773
1533
|
persist,
|
|
1534
|
+
capture,
|
|
774
1535
|
signal
|
|
775
1536
|
);
|
|
776
1537
|
} else {
|
|
777
|
-
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1538
|
+
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
778
1539
|
}
|
|
779
1540
|
}
|
|
780
1541
|
} finally {
|
|
@@ -784,7 +1545,7 @@ var MountraDownloader = class {
|
|
|
784
1545
|
throw new Error(`Download response did not contain the saved offset for chunk ${chunk.index}`);
|
|
785
1546
|
}
|
|
786
1547
|
}
|
|
787
|
-
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1548
|
+
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
788
1549
|
const remaining = chunk.size - savedChunk.downloadedBytes;
|
|
789
1550
|
const writableBytes = bytes.byteLength - offset;
|
|
790
1551
|
if (writableBytes > remaining) {
|
|
@@ -796,11 +1557,8 @@ var MountraDownloader = class {
|
|
|
796
1557
|
throwIfAborted(signal);
|
|
797
1558
|
const payload = bytes.subarray(offset);
|
|
798
1559
|
const position = chunk.offset + savedChunk.downloadedBytes;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
} else {
|
|
802
|
-
await sink.write(payload);
|
|
803
|
-
}
|
|
1560
|
+
await sink.write(payload, position);
|
|
1561
|
+
capture?.push(payload.slice());
|
|
804
1562
|
savedChunk.downloadedBytes += payload.byteLength;
|
|
805
1563
|
emit("downloading", chunk.index);
|
|
806
1564
|
await persist(false);
|
|
@@ -826,132 +1584,6 @@ var MountraDownloader = class {
|
|
|
826
1584
|
}
|
|
827
1585
|
};
|
|
828
1586
|
|
|
829
|
-
// src/file.ts
|
|
830
|
-
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
831
|
-
if (!locator || typeof locator !== "object") {
|
|
832
|
-
throw new TypeError("Mountra file locator is required");
|
|
833
|
-
}
|
|
834
|
-
const hasPath = locator.path !== void 0;
|
|
835
|
-
const hasIno = locator.ino !== void 0;
|
|
836
|
-
if (hasPath === hasIno) {
|
|
837
|
-
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
838
|
-
}
|
|
839
|
-
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
840
|
-
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
841
|
-
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
842
|
-
}
|
|
843
|
-
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
844
|
-
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
845
|
-
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
846
|
-
}
|
|
847
|
-
if (hasPath) {
|
|
848
|
-
const path = normalizePath(locator.path);
|
|
849
|
-
return {
|
|
850
|
-
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
851
|
-
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
852
|
-
path
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
return {
|
|
856
|
-
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
857
|
-
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
858
|
-
ino: normalizeIno(locator.ino)
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
var MountraFile = class {
|
|
862
|
-
constructor(client, locator) {
|
|
863
|
-
this.client = client;
|
|
864
|
-
this.locator = Object.freeze({ ...locator });
|
|
865
|
-
}
|
|
866
|
-
client;
|
|
867
|
-
locator;
|
|
868
|
-
get workspaceCuid() {
|
|
869
|
-
return this.locator.workspaceCuid;
|
|
870
|
-
}
|
|
871
|
-
get workspaceId() {
|
|
872
|
-
return this.locator.workspaceId;
|
|
873
|
-
}
|
|
874
|
-
/** `wsid` is the short alias used by the file-object API. */
|
|
875
|
-
get wsid() {
|
|
876
|
-
return this.locator.workspaceId;
|
|
877
|
-
}
|
|
878
|
-
get path() {
|
|
879
|
-
return "path" in this.locator ? this.locator.path : void 0;
|
|
880
|
-
}
|
|
881
|
-
get ino() {
|
|
882
|
-
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
883
|
-
}
|
|
884
|
-
export(options = {}) {
|
|
885
|
-
return this.client.exportFile({ ...options, ...this.locator });
|
|
886
|
-
}
|
|
887
|
-
download(options = {}) {
|
|
888
|
-
return this.client.downloadFile({ ...options, ...this.locator });
|
|
889
|
-
}
|
|
890
|
-
async resume(options = {}) {
|
|
891
|
-
const { downloadKey, ...resumeOptions } = options;
|
|
892
|
-
const checkpoints = await this.client.listResumableDownloads();
|
|
893
|
-
const candidates = checkpoints.filter(
|
|
894
|
-
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
895
|
-
);
|
|
896
|
-
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
897
|
-
if (!checkpoint) {
|
|
898
|
-
if (downloadKey) {
|
|
899
|
-
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
900
|
-
}
|
|
901
|
-
throw new Error("No resumable download was found for this Mountra file");
|
|
902
|
-
}
|
|
903
|
-
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
904
|
-
}
|
|
905
|
-
};
|
|
906
|
-
function matchesLocator(request, locator) {
|
|
907
|
-
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
if ("path" in locator) {
|
|
914
|
-
if (request.path === void 0 || request.ino !== void 0) {
|
|
915
|
-
return false;
|
|
916
|
-
}
|
|
917
|
-
try {
|
|
918
|
-
return normalizePath(request.path) === locator.path;
|
|
919
|
-
} catch {
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
924
|
-
}
|
|
925
|
-
function normalizePath(value) {
|
|
926
|
-
if (!value || !value.trim()) {
|
|
927
|
-
throw new TypeError("Mountra file path cannot be empty");
|
|
928
|
-
}
|
|
929
|
-
const raw = value.trim();
|
|
930
|
-
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
931
|
-
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
932
|
-
const stack = [];
|
|
933
|
-
for (const segment of segments) {
|
|
934
|
-
if (segment === "..") {
|
|
935
|
-
stack.pop();
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
938
|
-
stack.push(segment);
|
|
939
|
-
}
|
|
940
|
-
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
941
|
-
}
|
|
942
|
-
function normalizeIno(value) {
|
|
943
|
-
if (typeof value === "number") {
|
|
944
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
945
|
-
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
946
|
-
}
|
|
947
|
-
return value;
|
|
948
|
-
}
|
|
949
|
-
if (!/^\d+$/.test(value)) {
|
|
950
|
-
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
951
|
-
}
|
|
952
|
-
return value;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
1587
|
// src/upload.ts
|
|
956
1588
|
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
957
1589
|
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
@@ -1204,8 +1836,7 @@ function splitFileIntoChunks(file, chunkCount = 1) {
|
|
|
1204
1836
|
}
|
|
1205
1837
|
const chunkSize = Math.ceil(file.size / chunkCount);
|
|
1206
1838
|
const chunks = [];
|
|
1207
|
-
for (let index = 0;
|
|
1208
|
-
const offset = index * chunkSize;
|
|
1839
|
+
for (let index = 0, offset = 0; offset < file.size; index += 1, offset += chunkSize) {
|
|
1209
1840
|
const end = Math.min(offset + chunkSize, file.size);
|
|
1210
1841
|
chunks.push({
|
|
1211
1842
|
index,
|
|
@@ -1261,6 +1892,7 @@ var MountraClient = class {
|
|
|
1261
1892
|
clientSelector: this.clientSelector,
|
|
1262
1893
|
fetchImpl: this.fetchImpl,
|
|
1263
1894
|
checkpointStore: options.downloadCheckpointStore ?? new IndexedDBDownloadCheckpointStore(),
|
|
1895
|
+
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1264
1896
|
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1265
1897
|
});
|
|
1266
1898
|
}
|
|
@@ -1282,6 +1914,13 @@ var MountraClient = class {
|
|
|
1282
1914
|
clearDownloadCheckpoint(downloadKey) {
|
|
1283
1915
|
return this.downloader.clearDownloadCheckpoint(downloadKey);
|
|
1284
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
|
+
}
|
|
1285
1924
|
/** Create a stable resource object for one file in this or another workspace. */
|
|
1286
1925
|
file(locator) {
|
|
1287
1926
|
return new MountraFile(
|
|
@@ -1462,6 +2101,26 @@ var MountraClient = class {
|
|
|
1462
2101
|
if (finished.finish_status !== "completed") {
|
|
1463
2102
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
1464
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
|
+
);
|
|
1465
2124
|
await Promise.all(
|
|
1466
2125
|
[...jumboCheckpointKeys].map(
|
|
1467
2126
|
(checkpointKey) => this.jumboChunkCheckpointStore.remove(checkpointKey)
|
|
@@ -1637,12 +2296,26 @@ var MountraClient = class {
|
|
|
1637
2296
|
if (!uploadId.trim()) {
|
|
1638
2297
|
throw new TypeError("uploadId is required");
|
|
1639
2298
|
}
|
|
1640
|
-
|
|
2299
|
+
const finished = ensureFinishResponse(
|
|
1641
2300
|
await this.requestJson("/upload/finish", {
|
|
1642
2301
|
...getWorkspaceBody(this.clientSelector, selector),
|
|
1643
2302
|
upload_id: uploadId
|
|
1644
2303
|
})
|
|
1645
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
|
+
};
|
|
1646
2319
|
}
|
|
1647
2320
|
endpoint(path) {
|
|
1648
2321
|
return `${this.baseUrl}${this.apiPrefix}${path}`;
|
|
@@ -1683,8 +2356,10 @@ function createMountraClient(options) {
|
|
|
1683
2356
|
0 && (module.exports = {
|
|
1684
2357
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
1685
2358
|
DownloadHttpError,
|
|
2359
|
+
InMemoryDownloadCacheStore,
|
|
1686
2360
|
InMemoryDownloadCheckpointStore,
|
|
1687
2361
|
InMemoryJumboChunkCheckpointStore,
|
|
2362
|
+
IndexedDBDownloadCacheStore,
|
|
1688
2363
|
IndexedDBDownloadCheckpointStore,
|
|
1689
2364
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
1690
2365
|
LocalStorageJumboChunkCheckpointStore,
|