@mountra/mountra-sdk 0.2.0 → 0.4.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 +34 -0
- package/README.md +144 -15
- package/dist/index.cjs +1869 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +408 -13
- package/dist/index.d.ts +408 -13
- package/dist/index.js +1865 -173
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,655 @@
|
|
|
1
|
+
// src/file.ts
|
|
2
|
+
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
3
|
+
if (!locator || typeof locator !== "object") {
|
|
4
|
+
throw new TypeError("Mountra file locator is required");
|
|
5
|
+
}
|
|
6
|
+
const hasPath = locator.path !== void 0;
|
|
7
|
+
const hasIno = locator.ino !== void 0;
|
|
8
|
+
if (hasPath === hasIno) {
|
|
9
|
+
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
10
|
+
}
|
|
11
|
+
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
12
|
+
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
13
|
+
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
14
|
+
}
|
|
15
|
+
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
16
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
17
|
+
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
18
|
+
}
|
|
19
|
+
if (hasPath) {
|
|
20
|
+
const path = normalizePath(locator.path);
|
|
21
|
+
return {
|
|
22
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
23
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
24
|
+
path
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
29
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
30
|
+
ino: normalizeIno(locator.ino)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
var MountraFile = class {
|
|
34
|
+
constructor(client, locator) {
|
|
35
|
+
this.client = client;
|
|
36
|
+
this.locator = Object.freeze({ ...locator });
|
|
37
|
+
}
|
|
38
|
+
client;
|
|
39
|
+
locator;
|
|
40
|
+
get workspaceCuid() {
|
|
41
|
+
return this.locator.workspaceCuid;
|
|
42
|
+
}
|
|
43
|
+
get workspaceId() {
|
|
44
|
+
return this.locator.workspaceId;
|
|
45
|
+
}
|
|
46
|
+
/** `wsid` is the short alias used by the file-object API. */
|
|
47
|
+
get wsid() {
|
|
48
|
+
return this.locator.workspaceId;
|
|
49
|
+
}
|
|
50
|
+
get path() {
|
|
51
|
+
return "path" in this.locator ? this.locator.path : void 0;
|
|
52
|
+
}
|
|
53
|
+
get ino() {
|
|
54
|
+
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
55
|
+
}
|
|
56
|
+
export(options = {}) {
|
|
57
|
+
return this.client.exportFile({ ...options, ...this.locator });
|
|
58
|
+
}
|
|
59
|
+
download(options = {}) {
|
|
60
|
+
return this.client.downloadFile({ ...options, ...this.locator });
|
|
61
|
+
}
|
|
62
|
+
async resume(options = {}) {
|
|
63
|
+
const { downloadKey, ...resumeOptions } = options;
|
|
64
|
+
const checkpoints = await this.client.listResumableDownloads();
|
|
65
|
+
const candidates = checkpoints.filter(
|
|
66
|
+
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
67
|
+
);
|
|
68
|
+
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
69
|
+
if (!checkpoint) {
|
|
70
|
+
if (downloadKey) {
|
|
71
|
+
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
72
|
+
}
|
|
73
|
+
throw new Error("No resumable download was found for this Mountra file");
|
|
74
|
+
}
|
|
75
|
+
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function matchesLocator(request, locator) {
|
|
79
|
+
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if ("path" in locator) {
|
|
86
|
+
if (request.path === void 0 || request.ino !== void 0) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return normalizePath(request.path) === locator.path;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
96
|
+
}
|
|
97
|
+
function isPathWithin(path, ancestor) {
|
|
98
|
+
return ancestor === "/" || path === ancestor || path.startsWith(`${ancestor}/`);
|
|
99
|
+
}
|
|
100
|
+
function normalizePath(value) {
|
|
101
|
+
if (!value || !value.trim()) {
|
|
102
|
+
throw new TypeError("Mountra file path cannot be empty");
|
|
103
|
+
}
|
|
104
|
+
const raw = value.trim();
|
|
105
|
+
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
106
|
+
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
107
|
+
const stack = [];
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
if (segment === "..") {
|
|
110
|
+
stack.pop();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
stack.push(segment);
|
|
114
|
+
}
|
|
115
|
+
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
116
|
+
}
|
|
117
|
+
function normalizeIno(value) {
|
|
118
|
+
if (typeof value === "number") {
|
|
119
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
120
|
+
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
if (!/^\d+$/.test(value)) {
|
|
125
|
+
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/idb.ts
|
|
131
|
+
function getIndexedDb() {
|
|
132
|
+
return globalThis.indexedDB ?? null;
|
|
133
|
+
}
|
|
134
|
+
function requestResult(request) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
request.onsuccess = () => resolve(request.result);
|
|
137
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function runTransaction(database, storeNames, mode, body) {
|
|
141
|
+
const transaction = database.transaction(storeNames, mode);
|
|
142
|
+
const done = new Promise((resolve, reject) => {
|
|
143
|
+
transaction.oncomplete = () => resolve();
|
|
144
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
145
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
146
|
+
});
|
|
147
|
+
done.catch(() => void 0);
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
result = await body(transaction);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
try {
|
|
153
|
+
transaction.abort();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
await done;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function workspaceRange(workspace) {
|
|
162
|
+
return IDBKeyRange.bound([workspace], [workspace, []]);
|
|
163
|
+
}
|
|
164
|
+
var CLOCK_KEY = "clock";
|
|
165
|
+
async function nextClock(meta) {
|
|
166
|
+
const clock = (await requestResult(meta.get(CLOCK_KEY)) ?? 0) + 1;
|
|
167
|
+
await requestResult(meta.put(clock, CLOCK_KEY));
|
|
168
|
+
return clock;
|
|
169
|
+
}
|
|
170
|
+
function openDatabase(name, version, upgrade, onClose) {
|
|
171
|
+
const indexedDb = getIndexedDb();
|
|
172
|
+
if (!indexedDb) {
|
|
173
|
+
return Promise.resolve(null);
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
let request;
|
|
177
|
+
try {
|
|
178
|
+
request = indexedDb.open(name, version);
|
|
179
|
+
} catch {
|
|
180
|
+
resolve(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
request.onupgradeneeded = () => upgrade(request.result);
|
|
184
|
+
request.onsuccess = () => {
|
|
185
|
+
const database = request.result;
|
|
186
|
+
database.onversionchange = () => {
|
|
187
|
+
database.close();
|
|
188
|
+
onClose();
|
|
189
|
+
};
|
|
190
|
+
resolve(database);
|
|
191
|
+
};
|
|
192
|
+
request.onerror = () => resolve(null);
|
|
193
|
+
request.onblocked = () => resolve(null);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/cache.ts
|
|
198
|
+
var DEFAULT_MAX_BYTES = 512 * 1024 * 1024;
|
|
199
|
+
var DEFAULT_MAX_CHUNK_BYTES = 128 * 1024 * 1024;
|
|
200
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
201
|
+
function copyFile(file) {
|
|
202
|
+
return { ...file, chunks: file.chunks.map((chunk) => ({ ...chunk })) };
|
|
203
|
+
}
|
|
204
|
+
function memoryKey(workspace, key) {
|
|
205
|
+
return JSON.stringify([workspace, key]);
|
|
206
|
+
}
|
|
207
|
+
var InMemoryDownloadCacheStore = class {
|
|
208
|
+
files = /* @__PURE__ */ new Map();
|
|
209
|
+
/** Map iteration order is the LRU order: the first entry is the least recently used. */
|
|
210
|
+
chunks = /* @__PURE__ */ new Map();
|
|
211
|
+
bytes = 0;
|
|
212
|
+
async getFile(workspace, fileKey) {
|
|
213
|
+
const entry = this.files.get(memoryKey(workspace, fileKey));
|
|
214
|
+
return entry ? copyFile(entry.file) : null;
|
|
215
|
+
}
|
|
216
|
+
async putFile(workspace, fileKey, file) {
|
|
217
|
+
this.files.set(memoryKey(workspace, fileKey), { workspace, file: copyFile(file) });
|
|
218
|
+
}
|
|
219
|
+
async removeFile(workspace, fileKey) {
|
|
220
|
+
this.files.delete(memoryKey(workspace, fileKey));
|
|
221
|
+
}
|
|
222
|
+
async removeFilesUnder(workspace, path) {
|
|
223
|
+
for (const [key, entry] of this.files) {
|
|
224
|
+
if (entry.workspace === workspace && isPathWithin(entry.file.path, path)) {
|
|
225
|
+
this.files.delete(key);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async getChunk(workspace, storageKey) {
|
|
230
|
+
const key = memoryKey(workspace, storageKey);
|
|
231
|
+
const entry = this.chunks.get(key);
|
|
232
|
+
if (!entry) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
this.chunks.delete(key);
|
|
236
|
+
this.chunks.set(key, entry);
|
|
237
|
+
return entry.data;
|
|
238
|
+
}
|
|
239
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
240
|
+
const key = memoryKey(workspace, storageKey);
|
|
241
|
+
this.removeChunk(key);
|
|
242
|
+
if (data.size > maxBytes) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.chunks.set(key, { workspace, data });
|
|
246
|
+
this.bytes += data.size;
|
|
247
|
+
for (const candidate of this.chunks.keys()) {
|
|
248
|
+
if (this.bytes <= maxBytes) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
this.removeChunk(candidate);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async clear(workspace) {
|
|
255
|
+
for (const [key, entry] of this.chunks) {
|
|
256
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
257
|
+
this.removeChunk(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [key, entry] of this.files) {
|
|
261
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
262
|
+
this.files.delete(key);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async usage(workspace) {
|
|
267
|
+
const usage = { bytes: 0, chunks: 0 };
|
|
268
|
+
for (const entry of this.chunks.values()) {
|
|
269
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
270
|
+
usage.bytes += entry.data.size;
|
|
271
|
+
usage.chunks += 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return usage;
|
|
275
|
+
}
|
|
276
|
+
removeChunk(key) {
|
|
277
|
+
const entry = this.chunks.get(key);
|
|
278
|
+
if (entry) {
|
|
279
|
+
this.chunks.delete(key);
|
|
280
|
+
this.bytes -= entry.data.size;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var CACHE_DATABASE_NAME = "mountra-sdk-download-cache";
|
|
285
|
+
var CACHE_DATABASE_VERSION = 1;
|
|
286
|
+
var FILES_STORE = "files";
|
|
287
|
+
var CHUNKS_STORE = "chunks";
|
|
288
|
+
var CHUNK_DATA_STORE = "chunk-data";
|
|
289
|
+
var META_STORE = "meta";
|
|
290
|
+
var LRU_INDEX = "lastUsed";
|
|
291
|
+
var USAGE_KEY = "bytes";
|
|
292
|
+
function sameKey(left, right) {
|
|
293
|
+
return Array.isArray(left) && left[0] === right[0] && left[1] === right[1];
|
|
294
|
+
}
|
|
295
|
+
var IndexedDBDownloadCacheStore = class {
|
|
296
|
+
databaseName;
|
|
297
|
+
fallback = new InMemoryDownloadCacheStore();
|
|
298
|
+
databasePromise;
|
|
299
|
+
constructor(options = {}) {
|
|
300
|
+
this.databaseName = options.databaseName ?? CACHE_DATABASE_NAME;
|
|
301
|
+
}
|
|
302
|
+
async getFile(workspace, fileKey) {
|
|
303
|
+
const database = await this.openDatabase();
|
|
304
|
+
if (!database) {
|
|
305
|
+
return this.fallback.getFile(workspace, fileKey);
|
|
306
|
+
}
|
|
307
|
+
return runTransaction(database, [FILES_STORE], "readonly", async (transaction) => {
|
|
308
|
+
const file = await requestResult(
|
|
309
|
+
transaction.objectStore(FILES_STORE).get([workspace, fileKey])
|
|
310
|
+
);
|
|
311
|
+
return file ? copyFile(file) : null;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async putFile(workspace, fileKey, file) {
|
|
315
|
+
const database = await this.openDatabase();
|
|
316
|
+
if (!database) {
|
|
317
|
+
return this.fallback.putFile(workspace, fileKey, file);
|
|
318
|
+
}
|
|
319
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
320
|
+
await requestResult(transaction.objectStore(FILES_STORE).put(copyFile(file), [workspace, fileKey]));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async removeFile(workspace, fileKey) {
|
|
324
|
+
const database = await this.openDatabase();
|
|
325
|
+
if (!database) {
|
|
326
|
+
return this.fallback.removeFile(workspace, fileKey);
|
|
327
|
+
}
|
|
328
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
329
|
+
await requestResult(transaction.objectStore(FILES_STORE).delete([workspace, fileKey]));
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async removeFilesUnder(workspace, path) {
|
|
333
|
+
const database = await this.openDatabase();
|
|
334
|
+
if (!database) {
|
|
335
|
+
return this.fallback.removeFilesUnder(workspace, path);
|
|
336
|
+
}
|
|
337
|
+
await runTransaction(database, [FILES_STORE], "readwrite", async (transaction) => {
|
|
338
|
+
const cursorRequest = transaction.objectStore(FILES_STORE).openCursor(workspaceRange(workspace));
|
|
339
|
+
let cursor = await requestResult(cursorRequest);
|
|
340
|
+
while (cursor) {
|
|
341
|
+
if (isPathWithin(cursor.value.path, path)) {
|
|
342
|
+
await requestResult(cursor.delete());
|
|
343
|
+
}
|
|
344
|
+
cursor.continue();
|
|
345
|
+
cursor = await requestResult(cursorRequest);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async getChunk(workspace, storageKey) {
|
|
350
|
+
const database = await this.openDatabase();
|
|
351
|
+
if (!database) {
|
|
352
|
+
return this.fallback.getChunk(workspace, storageKey);
|
|
353
|
+
}
|
|
354
|
+
const key = [workspace, storageKey];
|
|
355
|
+
return runTransaction(
|
|
356
|
+
database,
|
|
357
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
358
|
+
"readwrite",
|
|
359
|
+
async (transaction) => {
|
|
360
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
361
|
+
const record = await requestResult(chunks.get(key));
|
|
362
|
+
if (!record) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const data = await requestResult(
|
|
366
|
+
transaction.objectStore(CHUNK_DATA_STORE).get(key)
|
|
367
|
+
);
|
|
368
|
+
const meta = transaction.objectStore(META_STORE);
|
|
369
|
+
if (!data) {
|
|
370
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
371
|
+
await requestResult(chunks.delete(key));
|
|
372
|
+
await requestResult(meta.put(Math.max(bytes - record.size, 0), USAGE_KEY));
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
await requestResult(chunks.put({ size: record.size, lastUsed: await nextClock(meta) }, key));
|
|
376
|
+
return data;
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
async putChunk(workspace, storageKey, data, maxBytes) {
|
|
381
|
+
const database = await this.openDatabase();
|
|
382
|
+
if (!database) {
|
|
383
|
+
return this.fallback.putChunk(workspace, storageKey, data, maxBytes);
|
|
384
|
+
}
|
|
385
|
+
const key = [workspace, storageKey];
|
|
386
|
+
await runTransaction(
|
|
387
|
+
database,
|
|
388
|
+
[CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
389
|
+
"readwrite",
|
|
390
|
+
async (transaction) => {
|
|
391
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
392
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
393
|
+
const meta = transaction.objectStore(META_STORE);
|
|
394
|
+
let bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
395
|
+
const existing = await requestResult(chunks.get(key));
|
|
396
|
+
if (existing) {
|
|
397
|
+
bytes -= existing.size;
|
|
398
|
+
await requestResult(chunks.delete(key));
|
|
399
|
+
await requestResult(chunkData.delete(key));
|
|
400
|
+
}
|
|
401
|
+
if (data.size <= maxBytes) {
|
|
402
|
+
await requestResult(chunks.put({ size: data.size, lastUsed: await nextClock(meta) }, key));
|
|
403
|
+
await requestResult(chunkData.put(data, key));
|
|
404
|
+
bytes += data.size;
|
|
405
|
+
}
|
|
406
|
+
if (bytes > maxBytes) {
|
|
407
|
+
const cursorRequest = chunks.index(LRU_INDEX).openCursor();
|
|
408
|
+
let cursor = await requestResult(cursorRequest);
|
|
409
|
+
while (cursor && bytes > maxBytes) {
|
|
410
|
+
if (!sameKey(cursor.primaryKey, key)) {
|
|
411
|
+
bytes -= cursor.value.size;
|
|
412
|
+
await requestResult(chunkData.delete(cursor.primaryKey));
|
|
413
|
+
await requestResult(cursor.delete());
|
|
414
|
+
}
|
|
415
|
+
cursor.continue();
|
|
416
|
+
cursor = await requestResult(cursorRequest);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await requestResult(meta.put(Math.max(bytes, 0), USAGE_KEY));
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
async clear(workspace) {
|
|
424
|
+
const database = await this.openDatabase();
|
|
425
|
+
if (!database) {
|
|
426
|
+
return this.fallback.clear(workspace);
|
|
427
|
+
}
|
|
428
|
+
await runTransaction(
|
|
429
|
+
database,
|
|
430
|
+
[FILES_STORE, CHUNKS_STORE, CHUNK_DATA_STORE, META_STORE],
|
|
431
|
+
"readwrite",
|
|
432
|
+
async (transaction) => {
|
|
433
|
+
const files = transaction.objectStore(FILES_STORE);
|
|
434
|
+
const chunks = transaction.objectStore(CHUNKS_STORE);
|
|
435
|
+
const chunkData = transaction.objectStore(CHUNK_DATA_STORE);
|
|
436
|
+
const meta = transaction.objectStore(META_STORE);
|
|
437
|
+
if (workspace === void 0) {
|
|
438
|
+
await requestResult(files.clear());
|
|
439
|
+
await requestResult(chunks.clear());
|
|
440
|
+
await requestResult(chunkData.clear());
|
|
441
|
+
await requestResult(meta.put(0, USAGE_KEY));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const range = workspaceRange(workspace);
|
|
445
|
+
const removed = await requestResult(chunks.getAll(range));
|
|
446
|
+
const bytes = await requestResult(meta.get(USAGE_KEY)) ?? 0;
|
|
447
|
+
await requestResult(files.delete(range));
|
|
448
|
+
await requestResult(chunks.delete(range));
|
|
449
|
+
await requestResult(chunkData.delete(range));
|
|
450
|
+
await requestResult(
|
|
451
|
+
meta.put(Math.max(bytes - removed.reduce((total, record) => total + record.size, 0), 0), USAGE_KEY)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
async usage(workspace) {
|
|
457
|
+
const database = await this.openDatabase();
|
|
458
|
+
if (!database) {
|
|
459
|
+
return this.fallback.usage(workspace);
|
|
460
|
+
}
|
|
461
|
+
return runTransaction(database, [CHUNKS_STORE], "readonly", async (transaction) => {
|
|
462
|
+
const records = await requestResult(
|
|
463
|
+
transaction.objectStore(CHUNKS_STORE).getAll(workspace === void 0 ? void 0 : workspaceRange(workspace))
|
|
464
|
+
);
|
|
465
|
+
return {
|
|
466
|
+
bytes: records.reduce((total, record) => total + record.size, 0),
|
|
467
|
+
chunks: records.length
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async openDatabase() {
|
|
472
|
+
this.databasePromise ??= openDatabase(
|
|
473
|
+
this.databaseName,
|
|
474
|
+
CACHE_DATABASE_VERSION,
|
|
475
|
+
(database) => {
|
|
476
|
+
if (!database.objectStoreNames.contains(FILES_STORE)) {
|
|
477
|
+
database.createObjectStore(FILES_STORE);
|
|
478
|
+
}
|
|
479
|
+
if (!database.objectStoreNames.contains(CHUNKS_STORE)) {
|
|
480
|
+
database.createObjectStore(CHUNKS_STORE).createIndex(LRU_INDEX, "lastUsed");
|
|
481
|
+
}
|
|
482
|
+
if (!database.objectStoreNames.contains(CHUNK_DATA_STORE)) {
|
|
483
|
+
database.createObjectStore(CHUNK_DATA_STORE);
|
|
484
|
+
}
|
|
485
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
486
|
+
database.createObjectStore(META_STORE);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
() => {
|
|
490
|
+
this.databasePromise = void 0;
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
return this.databasePromise;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
function isImmutableStorageKey(storageKey) {
|
|
497
|
+
return /(?:^|\/)blobs\/sha256\/[0-9a-f]{64}$/.test(storageKey);
|
|
498
|
+
}
|
|
499
|
+
function workspaceCacheKey(baseUrl, selector) {
|
|
500
|
+
const workspaceCuid = selector.workspaceCuid?.trim();
|
|
501
|
+
const workspaceId = selector.workspaceId !== void 0 ? String(selector.workspaceId).trim() : "";
|
|
502
|
+
const workspace = workspaceCuid || workspaceId;
|
|
503
|
+
return workspace ? JSON.stringify([baseUrl, workspace]) : null;
|
|
504
|
+
}
|
|
505
|
+
function fileCacheKey(target) {
|
|
506
|
+
try {
|
|
507
|
+
if (target.ino !== void 0) {
|
|
508
|
+
return `ino:${BigInt(target.ino).toString()}`;
|
|
509
|
+
}
|
|
510
|
+
if (target.path !== void 0) {
|
|
511
|
+
return `path:${normalizePath(target.path)}`;
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
function isValidLayout(file) {
|
|
518
|
+
let offset = 0;
|
|
519
|
+
for (const [index, chunk] of file.chunks.entries()) {
|
|
520
|
+
if (chunk.index !== index || chunk.offset !== offset || chunk.size < 0) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
offset += chunk.size;
|
|
524
|
+
}
|
|
525
|
+
return file.chunks.length > 0 && offset === file.size;
|
|
526
|
+
}
|
|
527
|
+
var DownloadCache = class {
|
|
528
|
+
maxBytes;
|
|
529
|
+
maxChunkBytes;
|
|
530
|
+
ttlMs;
|
|
531
|
+
store;
|
|
532
|
+
constructor(options) {
|
|
533
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
534
|
+
if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes <= 0) {
|
|
535
|
+
throw new RangeError("downloadCache.maxBytes must be a positive safe integer");
|
|
536
|
+
}
|
|
537
|
+
const maxChunkBytes = options.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
538
|
+
if (!Number.isSafeInteger(maxChunkBytes) || maxChunkBytes <= 0) {
|
|
539
|
+
throw new RangeError("downloadCache.maxChunkBytes must be a positive safe integer");
|
|
540
|
+
}
|
|
541
|
+
this.maxChunkBytes = Math.min(maxChunkBytes, this.maxBytes);
|
|
542
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
543
|
+
if (Number.isNaN(this.ttlMs) || this.ttlMs < 0) {
|
|
544
|
+
throw new RangeError("downloadCache.ttlMs must be a non-negative number");
|
|
545
|
+
}
|
|
546
|
+
this.store = options.store ?? new IndexedDBDownloadCacheStore();
|
|
547
|
+
}
|
|
548
|
+
/** Return a file that has not expired together with all of its chunk bytes. */
|
|
549
|
+
async loadFile(workspace, fileKey) {
|
|
550
|
+
try {
|
|
551
|
+
const file = await this.store.getFile(workspace, fileKey);
|
|
552
|
+
if (!file) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if (file.expiresAt <= Date.now() || !isValidLayout(file)) {
|
|
556
|
+
await this.store.removeFile(workspace, fileKey);
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const chunks = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const chunk of file.chunks) {
|
|
561
|
+
const data = await this.readChunk(workspace, chunk);
|
|
562
|
+
if (!data) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
chunks.set(chunk.index, data);
|
|
566
|
+
}
|
|
567
|
+
return { file, chunks };
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async readChunk(workspace, chunk) {
|
|
573
|
+
if (chunk.size === 0) {
|
|
574
|
+
return new Blob([]);
|
|
575
|
+
}
|
|
576
|
+
if (!isImmutableStorageKey(chunk.storageKey)) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
const data = await this.store.getChunk(workspace, chunk.storageKey);
|
|
581
|
+
return data && data.size === chunk.size ? data : null;
|
|
582
|
+
} catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
canStoreChunk(chunk) {
|
|
587
|
+
return isImmutableStorageKey(chunk.storageKey) && chunk.size <= this.maxChunkBytes;
|
|
588
|
+
}
|
|
589
|
+
/** Returns whether the chunk is now cached. */
|
|
590
|
+
async storeChunk(workspace, chunk, data) {
|
|
591
|
+
if (chunk.size === 0) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
if (!this.canStoreChunk(chunk) || data.size !== chunk.size) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await this.store.putChunk(workspace, chunk.storageKey, data, this.maxBytes);
|
|
599
|
+
return true;
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async storeFile(workspace, fileKey, file) {
|
|
605
|
+
if (this.ttlMs <= 0) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const cachedAt = Date.now();
|
|
609
|
+
try {
|
|
610
|
+
await this.store.putFile(workspace, fileKey, {
|
|
611
|
+
path: file.path,
|
|
612
|
+
size: file.size,
|
|
613
|
+
...file.contentType ? { contentType: file.contentType } : {},
|
|
614
|
+
chunks: file.chunks.map(({ index, offset, size, storageKey }) => ({
|
|
615
|
+
index,
|
|
616
|
+
offset,
|
|
617
|
+
size,
|
|
618
|
+
storageKey
|
|
619
|
+
})),
|
|
620
|
+
cachedAt,
|
|
621
|
+
expiresAt: cachedAt + this.ttlMs
|
|
622
|
+
});
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async invalidateFiles(workspace, fileKeys) {
|
|
627
|
+
await Promise.all(
|
|
628
|
+
fileKeys.map((fileKey) => this.store.removeFile(workspace, fileKey).catch(() => void 0))
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
/** Drop the files at or below `paths`, which a write changed; their chunks stay cached. */
|
|
632
|
+
async invalidatePaths(workspace, paths) {
|
|
633
|
+
try {
|
|
634
|
+
if (!this.store.removeFilesUnder) {
|
|
635
|
+
await this.store.clear(workspace);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
for (const path of paths) {
|
|
639
|
+
await this.store.removeFilesUnder(workspace, path);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
await this.store.clear(workspace).catch(() => void 0);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
clear(workspace) {
|
|
646
|
+
return this.store.clear(workspace);
|
|
647
|
+
}
|
|
648
|
+
usage(workspace) {
|
|
649
|
+
return this.store.usage(workspace);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
1
653
|
// src/download.ts
|
|
2
654
|
function copyCheckpoint(checkpoint) {
|
|
3
655
|
return {
|
|
@@ -25,7 +677,7 @@ var InMemoryDownloadCheckpointStore = class {
|
|
|
25
677
|
var DOWNLOAD_DATABASE_NAME = "mountra-sdk";
|
|
26
678
|
var DOWNLOAD_DATABASE_VERSION = 1;
|
|
27
679
|
var DOWNLOAD_STORE_NAME = "download-checkpoints";
|
|
28
|
-
function
|
|
680
|
+
function getIndexedDb2() {
|
|
29
681
|
return globalThis.indexedDB ?? null;
|
|
30
682
|
}
|
|
31
683
|
var IndexedDBDownloadCheckpointStore = class {
|
|
@@ -90,7 +742,7 @@ var IndexedDBDownloadCheckpointStore = class {
|
|
|
90
742
|
}
|
|
91
743
|
async openDatabase() {
|
|
92
744
|
if (!this.databasePromise) {
|
|
93
|
-
const indexedDb =
|
|
745
|
+
const indexedDb = getIndexedDb2();
|
|
94
746
|
if (!indexedDb) {
|
|
95
747
|
this.databasePromise = Promise.resolve(null);
|
|
96
748
|
} else {
|
|
@@ -285,8 +937,9 @@ var SpeedWindow = class {
|
|
|
285
937
|
};
|
|
286
938
|
var BlobDownloadSink = class {
|
|
287
939
|
parts = [];
|
|
288
|
-
|
|
289
|
-
|
|
940
|
+
/** Parts arrive in file order, so the position is implied. */
|
|
941
|
+
async write(bytes, _position) {
|
|
942
|
+
this.parts.push(bytes instanceof Blob ? bytes : bytes.slice());
|
|
290
943
|
}
|
|
291
944
|
async flush() {
|
|
292
945
|
}
|
|
@@ -294,6 +947,9 @@ var BlobDownloadSink = class {
|
|
|
294
947
|
}
|
|
295
948
|
blob(contentType) {
|
|
296
949
|
const buffers = this.parts.map((part) => {
|
|
950
|
+
if (part instanceof Blob) {
|
|
951
|
+
return part;
|
|
952
|
+
}
|
|
297
953
|
const buffer = new ArrayBuffer(part.byteLength);
|
|
298
954
|
new Uint8Array(buffer).set(part);
|
|
299
955
|
return buffer;
|
|
@@ -323,7 +979,7 @@ var FileSystemDownloadSink = class {
|
|
|
323
979
|
throw new Error("File System Access writer position does not match the download checkpoint");
|
|
324
980
|
}
|
|
325
981
|
await this.writable.write(bytes);
|
|
326
|
-
this.position += bytes.byteLength;
|
|
982
|
+
this.position += bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
327
983
|
}
|
|
328
984
|
async flush() {
|
|
329
985
|
if (this.writable) {
|
|
@@ -409,39 +1065,39 @@ function buildDownloadChunks(exported, useClientAssembly) {
|
|
|
409
1065
|
}
|
|
410
1066
|
];
|
|
411
1067
|
}
|
|
412
|
-
function buildDownloadKey(
|
|
1068
|
+
function buildDownloadKey(plan, baseUrl, request) {
|
|
413
1069
|
return encodeURIComponent(
|
|
414
1070
|
JSON.stringify({
|
|
415
1071
|
request: requestKey(request, baseUrl),
|
|
416
|
-
path:
|
|
417
|
-
size:
|
|
418
|
-
chunks: chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
1072
|
+
path: plan.path,
|
|
1073
|
+
size: plan.size,
|
|
1074
|
+
chunks: plan.chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
419
1075
|
})
|
|
420
1076
|
);
|
|
421
1077
|
}
|
|
422
|
-
function checkpointMatches(checkpoint,
|
|
423
|
-
if (checkpoint.version !== 1 || checkpoint.path !==
|
|
1078
|
+
function checkpointMatches(checkpoint, plan) {
|
|
1079
|
+
if (checkpoint.version !== 1 || checkpoint.path !== plan.path || checkpoint.size !== plan.size) {
|
|
424
1080
|
return false;
|
|
425
1081
|
}
|
|
426
|
-
if (checkpoint.chunks.length !== chunks.length) {
|
|
1082
|
+
if (checkpoint.chunks.length !== plan.chunks.length) {
|
|
427
1083
|
return false;
|
|
428
1084
|
}
|
|
429
1085
|
return checkpoint.chunks.every((saved, index) => {
|
|
430
|
-
const current = chunks[index];
|
|
1086
|
+
const current = plan.chunks[index];
|
|
431
1087
|
return Boolean(
|
|
432
1088
|
current && saved.index === current.index && saved.offset === current.offset && saved.size === current.size && saved.storageKey === current.storageKey && saved.downloadedBytes >= 0 && saved.downloadedBytes <= saved.size
|
|
433
1089
|
);
|
|
434
1090
|
});
|
|
435
1091
|
}
|
|
436
|
-
function createCheckpoint(key, baseUrl, request,
|
|
1092
|
+
function createCheckpoint(key, baseUrl, request, plan, fileHandle) {
|
|
437
1093
|
return {
|
|
438
1094
|
version: 1,
|
|
439
1095
|
key,
|
|
440
1096
|
requestKey: requestKey(request, baseUrl),
|
|
441
1097
|
request: { ...request },
|
|
442
|
-
path:
|
|
443
|
-
size:
|
|
444
|
-
chunks: chunks.map((chunk) => ({
|
|
1098
|
+
path: plan.path,
|
|
1099
|
+
size: plan.size,
|
|
1100
|
+
chunks: plan.chunks.map((chunk) => ({
|
|
445
1101
|
index: chunk.index,
|
|
446
1102
|
offset: chunk.offset,
|
|
447
1103
|
size: chunk.size,
|
|
@@ -459,8 +1115,8 @@ function totalDownloadedBytes(checkpoint) {
|
|
|
459
1115
|
function completedChunkCount(checkpoint) {
|
|
460
1116
|
return checkpoint.chunks.filter((chunk) => chunk.completed).length;
|
|
461
1117
|
}
|
|
462
|
-
function defaultFilename(
|
|
463
|
-
const candidate = requested?.trim() ||
|
|
1118
|
+
function defaultFilename(path, requested) {
|
|
1119
|
+
const candidate = requested?.trim() || path.split("/").filter(Boolean).pop() || "download";
|
|
464
1120
|
return candidate.replace(/[\\/\r\n]/g, "_") || "download";
|
|
465
1121
|
}
|
|
466
1122
|
function triggerBrowserDownload(blob, filename) {
|
|
@@ -485,33 +1141,38 @@ var MountraDownloader = class {
|
|
|
485
1141
|
this.transport = transport;
|
|
486
1142
|
}
|
|
487
1143
|
async exportFile(options) {
|
|
488
|
-
|
|
489
|
-
return ensureExportFileResponse(
|
|
490
|
-
await this.transport.requestJson(
|
|
491
|
-
"/file/export",
|
|
492
|
-
requestBodyFromCheckpoint(request),
|
|
493
|
-
options.signal
|
|
494
|
-
)
|
|
495
|
-
);
|
|
1144
|
+
return this.exportRequest(this.resolveRequest(options), options.signal);
|
|
496
1145
|
}
|
|
497
1146
|
async downloadFile(options) {
|
|
498
1147
|
const preferFileSystemAccess = options.preferFileSystemAccess !== false;
|
|
499
1148
|
const fileHandle = options.fileHandle ?? (preferFileSystemAccess && getShowSaveFilePicker() ? await this.pickSaveFile(options.downloadFilename ?? options.path?.split("/").filter(Boolean).pop()) : void 0);
|
|
500
|
-
const
|
|
501
|
-
|
|
1149
|
+
const request = this.resolveRequest(options);
|
|
1150
|
+
const plan = await this.loadCachedPlan(request, options.cache) ?? this.planExport(
|
|
1151
|
+
await this.exportRequest(request, options.signal),
|
|
1152
|
+
request,
|
|
1153
|
+
options.cache,
|
|
1154
|
+
fileHandle
|
|
1155
|
+
);
|
|
1156
|
+
return this.downloadResolved(plan, request, options, fileHandle, false);
|
|
502
1157
|
}
|
|
503
1158
|
async downloadExport(exported, options = { path: exported.path }) {
|
|
504
1159
|
let fileHandle = options.fileHandle;
|
|
505
1160
|
if (!fileHandle && options.preferFileSystemAccess !== false && getShowSaveFilePicker()) {
|
|
506
1161
|
fileHandle = await this.pickSaveFile(
|
|
507
|
-
options.downloadFilename ?? defaultFilename(exported),
|
|
1162
|
+
options.downloadFilename ?? defaultFilename(exported.path),
|
|
508
1163
|
exported.contentType
|
|
509
1164
|
);
|
|
510
1165
|
}
|
|
511
|
-
const request =
|
|
1166
|
+
const request = this.resolveRequest(
|
|
512
1167
|
options.path === void 0 && options.ino === void 0 ? { ...options, path: exported.path } : options
|
|
513
1168
|
);
|
|
514
|
-
return this.downloadResolved(
|
|
1169
|
+
return this.downloadResolved(
|
|
1170
|
+
this.planExport(exported, request, options.cache, fileHandle),
|
|
1171
|
+
request,
|
|
1172
|
+
options,
|
|
1173
|
+
fileHandle,
|
|
1174
|
+
false
|
|
1175
|
+
);
|
|
515
1176
|
}
|
|
516
1177
|
async resumeDownload(downloadKey, options = {}) {
|
|
517
1178
|
if (!downloadKey.trim()) {
|
|
@@ -527,7 +1188,7 @@ var MountraDownloader = class {
|
|
|
527
1188
|
signal: options.signal
|
|
528
1189
|
});
|
|
529
1190
|
return this.downloadResolved(
|
|
530
|
-
exported,
|
|
1191
|
+
this.planExport(exported, checkpoint.request, void 0, checkpoint.fileHandle),
|
|
531
1192
|
checkpoint.request,
|
|
532
1193
|
options,
|
|
533
1194
|
checkpoint.fileHandle,
|
|
@@ -540,6 +1201,132 @@ var MountraDownloader = class {
|
|
|
540
1201
|
clearDownloadCheckpoint(downloadKey) {
|
|
541
1202
|
return this.transport.checkpointStore.remove(downloadKey);
|
|
542
1203
|
}
|
|
1204
|
+
/** Drop the cached layout of a file that was just written, so the next download sees the new content. */
|
|
1205
|
+
async invalidateCachedFile(selector, target) {
|
|
1206
|
+
await this.cachedFileScope(selector, target)?.invalidate();
|
|
1207
|
+
}
|
|
1208
|
+
/** Drop the cached files at or below paths that a delete, move, or copy changed. */
|
|
1209
|
+
async invalidateCachedPaths(selector, paths) {
|
|
1210
|
+
const cache = this.transport.cache;
|
|
1211
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1212
|
+
if (cache && workspace !== null) {
|
|
1213
|
+
await cache.invalidatePaths(workspace, paths);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Cache a file this client just uploaded, so downloading it needs no
|
|
1218
|
+
* network. Falls back to invalidation when any chunk cannot be cached.
|
|
1219
|
+
*/
|
|
1220
|
+
async cacheUploadedFile(selector, file, mode = "default") {
|
|
1221
|
+
const scope = this.cachedFileScope(selector, file);
|
|
1222
|
+
if (!scope) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
await scope.invalidate();
|
|
1226
|
+
if (mode === "no-store") {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
let complete = true;
|
|
1230
|
+
for (const chunk of file.chunks) {
|
|
1231
|
+
complete = await scope.cache.storeChunk(scope.workspace, chunk, chunk.data) && complete;
|
|
1232
|
+
}
|
|
1233
|
+
if (complete) {
|
|
1234
|
+
for (const fileKey of scope.fileKeys) {
|
|
1235
|
+
await scope.cache.storeFile(scope.workspace, fileKey, file);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
cachedFileScope(selector, target) {
|
|
1240
|
+
const cache = this.transport.cache;
|
|
1241
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1242
|
+
if (!cache || workspace === null) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
const fileKeys = [
|
|
1246
|
+
target.path !== void 0 ? fileCacheKey({ path: target.path }) : null,
|
|
1247
|
+
target.ino !== void 0 ? fileCacheKey({ ino: target.ino }) : null
|
|
1248
|
+
].filter((fileKey) => fileKey !== null);
|
|
1249
|
+
return {
|
|
1250
|
+
cache,
|
|
1251
|
+
workspace,
|
|
1252
|
+
fileKeys,
|
|
1253
|
+
invalidate: () => cache.invalidateFiles(workspace, fileKeys)
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
async clearCache(selector) {
|
|
1257
|
+
const cache = this.transport.cache;
|
|
1258
|
+
if (!cache) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
await cache.clear(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1262
|
+
}
|
|
1263
|
+
async cacheUsage(selector) {
|
|
1264
|
+
const cache = this.transport.cache;
|
|
1265
|
+
if (!cache) {
|
|
1266
|
+
return { bytes: 0, chunks: 0 };
|
|
1267
|
+
}
|
|
1268
|
+
return cache.usage(selector === void 0 ? void 0 : this.requireCacheWorkspace(selector));
|
|
1269
|
+
}
|
|
1270
|
+
requireCacheWorkspace(selector) {
|
|
1271
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1272
|
+
if (workspace === null) {
|
|
1273
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1274
|
+
}
|
|
1275
|
+
return workspace;
|
|
1276
|
+
}
|
|
1277
|
+
/** Fall back to the client workspace only when the request names none, as uploads do. */
|
|
1278
|
+
resolveRequest(options) {
|
|
1279
|
+
const hasWorkspace = options.workspaceCuid !== void 0 || options.workspaceId !== void 0;
|
|
1280
|
+
return downloadRequestFromOptions(
|
|
1281
|
+
hasWorkspace ? options : {
|
|
1282
|
+
...options,
|
|
1283
|
+
workspaceCuid: this.transport.clientSelector.workspaceCuid,
|
|
1284
|
+
workspaceId: this.transport.clientSelector.workspaceId
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
async exportRequest(request, signal) {
|
|
1289
|
+
return ensureExportFileResponse(
|
|
1290
|
+
await this.transport.requestJson("/file/export", requestBodyFromCheckpoint(request), signal)
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
cacheScope(request, mode = "default") {
|
|
1294
|
+
const cache = this.transport.cache;
|
|
1295
|
+
if (!cache || mode === "no-store") {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, request);
|
|
1299
|
+
const fileKey = fileCacheKey(request);
|
|
1300
|
+
return workspace !== null && fileKey !== null ? { cache, workspace, fileKey } : null;
|
|
1301
|
+
}
|
|
1302
|
+
/** A plan that needs no network at all, when the cached layout is fresh and every chunk is cached. */
|
|
1303
|
+
async loadCachedPlan(request, mode) {
|
|
1304
|
+
const scope = this.cacheScope(request, mode);
|
|
1305
|
+
if (!scope || mode === "no-cache") {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
const cached = await scope.cache.loadFile(scope.workspace, scope.fileKey);
|
|
1309
|
+
if (!cached) {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
path: cached.file.path,
|
|
1314
|
+
size: cached.file.size,
|
|
1315
|
+
...cached.file.contentType ? { contentType: cached.file.contentType } : {},
|
|
1316
|
+
chunks: cached.file.chunks,
|
|
1317
|
+
cachedChunks: cached.chunks
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
planExport(exported, request, mode, fileHandle) {
|
|
1321
|
+
const useClientAssembly = (exported.chunks?.length ?? 0) > 1 && (Boolean(fileHandle) || this.cacheScope(request, mode) !== null);
|
|
1322
|
+
return {
|
|
1323
|
+
path: exported.path,
|
|
1324
|
+
size: exported.size,
|
|
1325
|
+
...exported.contentType ? { contentType: exported.contentType } : {},
|
|
1326
|
+
chunks: buildDownloadChunks(exported, useClientAssembly),
|
|
1327
|
+
exported
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
543
1330
|
async pickSaveFile(requestedFilename, contentType) {
|
|
544
1331
|
const picker = getShowSaveFilePicker();
|
|
545
1332
|
if (!picker) {
|
|
@@ -565,13 +1352,15 @@ var MountraDownloader = class {
|
|
|
565
1352
|
throw new DOMException("Write permission was not granted", "NotAllowedError");
|
|
566
1353
|
}
|
|
567
1354
|
}
|
|
568
|
-
async downloadResolved(
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
const key = buildDownloadKey(exported, chunks, this.transport.baseUrl, request);
|
|
1355
|
+
async downloadResolved(plan, request, options, fileHandle, forceResume) {
|
|
1356
|
+
const chunks = plan.chunks;
|
|
1357
|
+
const key = buildDownloadKey(plan, this.transport.baseUrl, request);
|
|
572
1358
|
const existing = await this.transport.checkpointStore.load(key);
|
|
573
1359
|
const resumeRequested = !("resume" in options) || options.resume !== false;
|
|
574
|
-
let checkpoint = existing && checkpointMatches(existing,
|
|
1360
|
+
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);
|
|
1361
|
+
const cacheScope = this.cacheScope(request, "cache" in options ? options.cache : void 0);
|
|
1362
|
+
const cachedChunkIndexes = /* @__PURE__ */ new Set();
|
|
1363
|
+
let cachedBytes = 0;
|
|
575
1364
|
if (fileHandle) {
|
|
576
1365
|
checkpoint.fileHandle = fileHandle;
|
|
577
1366
|
}
|
|
@@ -586,11 +1375,11 @@ var MountraDownloader = class {
|
|
|
586
1375
|
const emit = (phase, currentChunkIndex) => {
|
|
587
1376
|
const downloadedBytes = totalDownloadedBytes(checkpoint);
|
|
588
1377
|
const speedBytesPerSecond = speedWindow.record(downloadedBytes, Date.now());
|
|
589
|
-
const remainingBytes = Math.max(
|
|
1378
|
+
const remainingBytes = Math.max(plan.size - downloadedBytes, 0);
|
|
590
1379
|
options.onProgress?.({
|
|
591
1380
|
phase,
|
|
592
1381
|
downloadedBytes,
|
|
593
|
-
totalBytes:
|
|
1382
|
+
totalBytes: plan.size,
|
|
594
1383
|
downloadedChunks: completedChunkCount(checkpoint),
|
|
595
1384
|
totalChunks: checkpoint.chunks.length,
|
|
596
1385
|
...currentChunkIndex !== void 0 ? { currentChunkIndex } : {},
|
|
@@ -620,12 +1409,30 @@ var MountraDownloader = class {
|
|
|
620
1409
|
if (savedChunk.completed) {
|
|
621
1410
|
continue;
|
|
622
1411
|
}
|
|
1412
|
+
const cached = plan.cachedChunks?.get(chunk.index) ?? (cacheScope ? await cacheScope.cache.readChunk(cacheScope.workspace, chunk) : null);
|
|
1413
|
+
if (cached) {
|
|
1414
|
+
throwIfAborted(options.signal);
|
|
1415
|
+
const remaining = cached.slice(savedChunk.downloadedBytes);
|
|
1416
|
+
await sink.write(remaining, chunk.offset + savedChunk.downloadedBytes);
|
|
1417
|
+
cachedBytes += remaining.size;
|
|
1418
|
+
savedChunk.downloadedBytes = chunk.size;
|
|
1419
|
+
savedChunk.completed = true;
|
|
1420
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1421
|
+
emit("downloading", chunk.index);
|
|
1422
|
+
await persist(false);
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
const url = chunk.url;
|
|
1426
|
+
if (!url) {
|
|
1427
|
+
throw new Error(`Chunk ${chunk.index} is no longer in the download cache`);
|
|
1428
|
+
}
|
|
1429
|
+
const capture = cacheScope && savedChunk.downloadedBytes === 0 && cacheScope.cache.canStoreChunk(chunk) ? [] : null;
|
|
623
1430
|
let attempt = 0;
|
|
624
1431
|
while (true) {
|
|
625
1432
|
throwIfAborted(options.signal);
|
|
626
1433
|
const rangeStart = savedChunk.downloadedBytes;
|
|
627
1434
|
try {
|
|
628
|
-
const response = await this.transport.fetchImpl(
|
|
1435
|
+
const response = await this.transport.fetchImpl(url, {
|
|
629
1436
|
method: "GET",
|
|
630
1437
|
...rangeStart > 0 ? { headers: { Range: `bytes=${rangeStart}-` } } : {},
|
|
631
1438
|
...options.signal ? { signal: options.signal } : {}
|
|
@@ -641,9 +1448,10 @@ var MountraDownloader = class {
|
|
|
641
1448
|
chunk,
|
|
642
1449
|
savedChunk,
|
|
643
1450
|
sink,
|
|
644
|
-
|
|
1451
|
+
plan.size,
|
|
645
1452
|
emit,
|
|
646
1453
|
persist,
|
|
1454
|
+
capture,
|
|
647
1455
|
options.signal
|
|
648
1456
|
);
|
|
649
1457
|
if (savedChunk.downloadedBytes !== savedChunk.size) {
|
|
@@ -653,6 +1461,9 @@ var MountraDownloader = class {
|
|
|
653
1461
|
}
|
|
654
1462
|
savedChunk.completed = true;
|
|
655
1463
|
await persist(true);
|
|
1464
|
+
if (cacheScope && capture && await cacheScope.cache.storeChunk(cacheScope.workspace, chunk, new Blob(capture))) {
|
|
1465
|
+
cachedChunkIndexes.add(chunk.index);
|
|
1466
|
+
}
|
|
656
1467
|
emit("downloading", chunk.index);
|
|
657
1468
|
break;
|
|
658
1469
|
} catch (error) {
|
|
@@ -667,25 +1478,29 @@ var MountraDownloader = class {
|
|
|
667
1478
|
}
|
|
668
1479
|
}
|
|
669
1480
|
}
|
|
670
|
-
if (totalDownloadedBytes(checkpoint) !==
|
|
1481
|
+
if (totalDownloadedBytes(checkpoint) !== plan.size) {
|
|
671
1482
|
throw new Error("Download completed with an unexpected byte count");
|
|
672
1483
|
}
|
|
673
1484
|
if (fileHandle) {
|
|
674
|
-
await sink.finalize(
|
|
1485
|
+
await sink.finalize(plan.size);
|
|
675
1486
|
} else {
|
|
676
1487
|
await sink.finalize();
|
|
677
1488
|
}
|
|
678
1489
|
await this.transport.checkpointStore.remove(key);
|
|
1490
|
+
if (cacheScope && plan.exported && chunks.every((chunk) => chunk.size === 0 || cachedChunkIndexes.has(chunk.index))) {
|
|
1491
|
+
await cacheScope.cache.storeFile(cacheScope.workspace, cacheScope.fileKey, plan);
|
|
1492
|
+
}
|
|
679
1493
|
emit("completed");
|
|
680
|
-
const blob = sink instanceof BlobDownloadSink ? sink.blob(
|
|
1494
|
+
const blob = sink instanceof BlobDownloadSink ? sink.blob(plan.contentType) : void 0;
|
|
681
1495
|
if (blob) {
|
|
682
1496
|
const filename = "downloadFilename" in options ? options.downloadFilename : void 0;
|
|
683
|
-
triggerBrowserDownload(blob, defaultFilename(
|
|
1497
|
+
triggerBrowserDownload(blob, defaultFilename(plan.path, filename));
|
|
684
1498
|
}
|
|
685
1499
|
return {
|
|
686
|
-
export: exported,
|
|
1500
|
+
...plan.exported ? { export: plan.exported } : {},
|
|
687
1501
|
downloadKey: key,
|
|
688
|
-
downloadedBytes:
|
|
1502
|
+
downloadedBytes: plan.size,
|
|
1503
|
+
cachedBytes,
|
|
689
1504
|
resumed,
|
|
690
1505
|
...fileHandle ? { fileHandle } : {},
|
|
691
1506
|
...blob ? { blob } : {}
|
|
@@ -695,13 +1510,13 @@ var MountraDownloader = class {
|
|
|
695
1510
|
throw error;
|
|
696
1511
|
}
|
|
697
1512
|
}
|
|
698
|
-
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1513
|
+
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
699
1514
|
const sourceOffset = savedChunk.downloadedBytes;
|
|
700
1515
|
let bytesToSkip = response.status === 200 && sourceOffset > 0 ? sourceOffset : 0;
|
|
701
1516
|
const reader = response.body?.getReader();
|
|
702
1517
|
if (!reader) {
|
|
703
1518
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
704
|
-
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1519
|
+
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
705
1520
|
return;
|
|
706
1521
|
}
|
|
707
1522
|
try {
|
|
@@ -727,10 +1542,11 @@ var MountraDownloader = class {
|
|
|
727
1542
|
totalSize,
|
|
728
1543
|
emit,
|
|
729
1544
|
persist,
|
|
1545
|
+
capture,
|
|
730
1546
|
signal
|
|
731
1547
|
);
|
|
732
1548
|
} else {
|
|
733
|
-
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
1549
|
+
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal);
|
|
734
1550
|
}
|
|
735
1551
|
}
|
|
736
1552
|
} finally {
|
|
@@ -740,7 +1556,7 @@ var MountraDownloader = class {
|
|
|
740
1556
|
throw new Error(`Download response did not contain the saved offset for chunk ${chunk.index}`);
|
|
741
1557
|
}
|
|
742
1558
|
}
|
|
743
|
-
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
1559
|
+
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, capture, signal) {
|
|
744
1560
|
const remaining = chunk.size - savedChunk.downloadedBytes;
|
|
745
1561
|
const writableBytes = bytes.byteLength - offset;
|
|
746
1562
|
if (writableBytes > remaining) {
|
|
@@ -752,11 +1568,8 @@ var MountraDownloader = class {
|
|
|
752
1568
|
throwIfAborted(signal);
|
|
753
1569
|
const payload = bytes.subarray(offset);
|
|
754
1570
|
const position = chunk.offset + savedChunk.downloadedBytes;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
} else {
|
|
758
|
-
await sink.write(payload);
|
|
759
|
-
}
|
|
1571
|
+
await sink.write(payload, position);
|
|
1572
|
+
capture?.push(payload.slice());
|
|
760
1573
|
savedChunk.downloadedBytes += payload.byteLength;
|
|
761
1574
|
emit("downloading", chunk.index);
|
|
762
1575
|
await persist(false);
|
|
@@ -778,156 +1591,953 @@ var MountraDownloader = class {
|
|
|
778
1591
|
return false;
|
|
779
1592
|
}
|
|
780
1593
|
}
|
|
781
|
-
return false;
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
|
|
1598
|
+
// src/errors.ts
|
|
1599
|
+
var MountraApiError = class extends Error {
|
|
1600
|
+
status;
|
|
1601
|
+
code;
|
|
1602
|
+
body;
|
|
1603
|
+
constructor(message, input) {
|
|
1604
|
+
super(message);
|
|
1605
|
+
this.name = "MountraApiError";
|
|
1606
|
+
this.status = input.status;
|
|
1607
|
+
this.code = input.code;
|
|
1608
|
+
this.body = input.body;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
// src/metadata.ts
|
|
1613
|
+
function isRecord2(value) {
|
|
1614
|
+
return typeof value === "object" && value !== null;
|
|
1615
|
+
}
|
|
1616
|
+
function invalidField(field) {
|
|
1617
|
+
return new TypeError(`Mountra metadata response field ${field} is invalid`);
|
|
1618
|
+
}
|
|
1619
|
+
function readString2(value, field) {
|
|
1620
|
+
if (typeof value !== "string") {
|
|
1621
|
+
throw invalidField(field);
|
|
1622
|
+
}
|
|
1623
|
+
return value;
|
|
1624
|
+
}
|
|
1625
|
+
function readCount(value, field) {
|
|
1626
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
1627
|
+
throw invalidField(field);
|
|
1628
|
+
}
|
|
1629
|
+
return value;
|
|
1630
|
+
}
|
|
1631
|
+
function readBoolean(value, field) {
|
|
1632
|
+
if (typeof value !== "boolean") {
|
|
1633
|
+
throw invalidField(field);
|
|
1634
|
+
}
|
|
1635
|
+
return value;
|
|
1636
|
+
}
|
|
1637
|
+
function optionalInteger(value) {
|
|
1638
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
function optionalText(value) {
|
|
1641
|
+
return typeof value === "string" && value ? value : void 0;
|
|
1642
|
+
}
|
|
1643
|
+
function withoutUndefined(value) {
|
|
1644
|
+
for (const key of Object.keys(value)) {
|
|
1645
|
+
if (value[key] === void 0) {
|
|
1646
|
+
delete value[key];
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return value;
|
|
1650
|
+
}
|
|
1651
|
+
function readNodeFields(raw, field) {
|
|
1652
|
+
return {
|
|
1653
|
+
name: readString2(raw.name, `${field}.name`),
|
|
1654
|
+
path: normalizePath(readString2(raw.path, `${field}.path`)),
|
|
1655
|
+
ino: readCount(raw.ino, `${field}.ino`),
|
|
1656
|
+
isDirectory: readBoolean(raw.is_directory, `${field}.is_directory`),
|
|
1657
|
+
size: readCount(raw.size, `${field}.size`),
|
|
1658
|
+
mtimeMs: readCount(raw.mtime_ms, `${field}.mtime_ms`),
|
|
1659
|
+
uid: optionalInteger(raw.uid),
|
|
1660
|
+
gid: optionalInteger(raw.gid),
|
|
1661
|
+
uidSubjectId: optionalText(raw.uid_subject_id),
|
|
1662
|
+
uidSubjectType: optionalText(raw.uid_subject_type),
|
|
1663
|
+
gidSubjectId: optionalText(raw.gid_subject_id),
|
|
1664
|
+
gidSubjectType: optionalText(raw.gid_subject_type)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function readEntry(raw, field) {
|
|
1668
|
+
if (!isRecord2(raw)) {
|
|
1669
|
+
throw invalidField(field);
|
|
1670
|
+
}
|
|
1671
|
+
return withoutUndefined({
|
|
1672
|
+
...readNodeFields(raw, field),
|
|
1673
|
+
atimeMs: readCount(raw.atime_ms, `${field}.atime_ms`),
|
|
1674
|
+
uidDisplayName: optionalText(raw.uid_display_name),
|
|
1675
|
+
uidUsername: optionalText(raw.uid_username),
|
|
1676
|
+
gidDisplayName: optionalText(raw.gid_display_name),
|
|
1677
|
+
gidUsername: optionalText(raw.gid_username)
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function readTreeNode(raw, field) {
|
|
1681
|
+
if (!isRecord2(raw) || !Array.isArray(raw.children)) {
|
|
1682
|
+
throw invalidField(field);
|
|
1683
|
+
}
|
|
1684
|
+
return withoutUndefined({
|
|
1685
|
+
...readNodeFields(raw, field),
|
|
1686
|
+
children: raw.children.map((child, index) => readTreeNode(child, `${field}.children[${index}]`))
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
function readListResponse(value) {
|
|
1690
|
+
if (!isRecord2(value) || !Array.isArray(value.entries)) {
|
|
1691
|
+
throw invalidField("entries");
|
|
1692
|
+
}
|
|
1693
|
+
return value.entries.map((entry, index) => readEntry(entry, `entries[${index}]`));
|
|
1694
|
+
}
|
|
1695
|
+
function readOperationResponse(value) {
|
|
1696
|
+
if (!isRecord2(value)) {
|
|
1697
|
+
throw new TypeError("Mountra file operation response is invalid");
|
|
1698
|
+
}
|
|
1699
|
+
return {
|
|
1700
|
+
path: normalizePath(readString2(value.path, "path")),
|
|
1701
|
+
ino: readCount(value.ino, "ino")
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function sameJson(left, right) {
|
|
1705
|
+
if (left === right) {
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
if (!isRecord2(left) || !isRecord2(right) || Array.isArray(left) !== Array.isArray(right)) {
|
|
1709
|
+
return false;
|
|
1710
|
+
}
|
|
1711
|
+
const leftKeys = Object.keys(left);
|
|
1712
|
+
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
1713
|
+
}
|
|
1714
|
+
function sameContent(left, right) {
|
|
1715
|
+
return left.kind === "list" && right.kind === "list" ? sameJson(left.entries, right.entries) : left.kind === "tree" && right.kind === "tree" && sameJson(left.root, right.root);
|
|
1716
|
+
}
|
|
1717
|
+
function workspaceBody(selector) {
|
|
1718
|
+
return {
|
|
1719
|
+
...selector.workspaceCuid !== void 0 ? { workspace_cuid: selector.workspaceCuid } : {},
|
|
1720
|
+
...selector.workspaceId !== void 0 ? { workspace_id: selector.workspaceId } : {}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
var MountraMetadata = class {
|
|
1724
|
+
transport;
|
|
1725
|
+
constructor(transport) {
|
|
1726
|
+
this.transport = transport;
|
|
1727
|
+
}
|
|
1728
|
+
list(options = {}) {
|
|
1729
|
+
return this.read("list", options, (record, source) => ({
|
|
1730
|
+
path: record.path,
|
|
1731
|
+
entries: record.entries,
|
|
1732
|
+
source,
|
|
1733
|
+
fetchedAt: record.fetchedAt
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
tree(options = {}) {
|
|
1737
|
+
return this.read("tree", options, (record, source) => ({
|
|
1738
|
+
path: record.path,
|
|
1739
|
+
root: record.root,
|
|
1740
|
+
source,
|
|
1741
|
+
fetchedAt: record.fetchedAt
|
|
1742
|
+
}));
|
|
1743
|
+
}
|
|
1744
|
+
async mkdir(options) {
|
|
1745
|
+
const selector = this.resolveWorkspace(options);
|
|
1746
|
+
const created = await this.operate(selector, "/file/mkdir", { path: normalizePath(options.path) }, options.signal);
|
|
1747
|
+
await this.afterWrite(selector, {
|
|
1748
|
+
type: "create",
|
|
1749
|
+
path: created.path,
|
|
1750
|
+
ino: created.ino,
|
|
1751
|
+
isDirectory: true,
|
|
1752
|
+
size: 0
|
|
1753
|
+
});
|
|
1754
|
+
return created;
|
|
1755
|
+
}
|
|
1756
|
+
async rename(options) {
|
|
1757
|
+
const selector = this.resolveWorkspace(options);
|
|
1758
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1759
|
+
const renamed = await this.operate(
|
|
1760
|
+
selector,
|
|
1761
|
+
"/file/rename",
|
|
1762
|
+
{ from_path: fromPath, to_name: options.toName },
|
|
1763
|
+
options.signal
|
|
1764
|
+
);
|
|
1765
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: renamed.path, ino: renamed.ino });
|
|
1766
|
+
return renamed;
|
|
1767
|
+
}
|
|
1768
|
+
async move(options) {
|
|
1769
|
+
const selector = this.resolveWorkspace(options);
|
|
1770
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1771
|
+
const moved = await this.operate(
|
|
1772
|
+
selector,
|
|
1773
|
+
"/file/move",
|
|
1774
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1775
|
+
options.signal
|
|
1776
|
+
);
|
|
1777
|
+
await this.afterWrite(selector, { type: "move", fromPath, toPath: moved.path, ino: moved.ino });
|
|
1778
|
+
return moved;
|
|
1779
|
+
}
|
|
1780
|
+
async copy(options) {
|
|
1781
|
+
const selector = this.resolveWorkspace(options);
|
|
1782
|
+
const fromPath = normalizePath(options.fromPath);
|
|
1783
|
+
const copied = await this.operate(
|
|
1784
|
+
selector,
|
|
1785
|
+
"/file/copy",
|
|
1786
|
+
{ from_path: fromPath, to_path: normalizePath(options.toPath) },
|
|
1787
|
+
options.signal
|
|
1788
|
+
);
|
|
1789
|
+
await this.afterWrite(selector, { type: "copy", fromPath, toPath: copied.path, ino: copied.ino });
|
|
1790
|
+
return copied;
|
|
1791
|
+
}
|
|
1792
|
+
async delete(options) {
|
|
1793
|
+
const selector = this.resolveWorkspace(options);
|
|
1794
|
+
const path = normalizePath(options.path);
|
|
1795
|
+
await this.transport.requestJson("/file/delete", { ...workspaceBody(selector), path }, options.signal);
|
|
1796
|
+
await this.afterWrite(selector, { type: "remove", path });
|
|
1797
|
+
}
|
|
1798
|
+
/** Show a file that an upload just finished. The upload updates the download cache itself. */
|
|
1799
|
+
recordUpload(selector, file) {
|
|
1800
|
+
return this.applyChange(selector, {
|
|
1801
|
+
type: "create",
|
|
1802
|
+
path: normalizePath(file.path),
|
|
1803
|
+
...file.ino !== void 0 ? { ino: file.ino } : {},
|
|
1804
|
+
isDirectory: false,
|
|
1805
|
+
size: file.size
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
async clearCache(selector) {
|
|
1809
|
+
const cache = this.transport.cache;
|
|
1810
|
+
if (!cache) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (selector === void 0) {
|
|
1814
|
+
await cache.clear();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1818
|
+
if (workspace === null) {
|
|
1819
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1820
|
+
}
|
|
1821
|
+
await cache.clear(workspace);
|
|
1822
|
+
}
|
|
1823
|
+
async read(kind, options, present) {
|
|
1824
|
+
const selector = this.resolveWorkspace(options);
|
|
1825
|
+
const path = normalizePath(options.path ?? "/");
|
|
1826
|
+
const scope = this.cacheScope(selector);
|
|
1827
|
+
const { signal, onUpdate, onError } = options;
|
|
1828
|
+
const cached = options.mode === "async" && scope ? await scope.cache.get(scope.workspace, kind, path) : null;
|
|
1829
|
+
if (!cached) {
|
|
1830
|
+
return present(await this.fetch(kind, selector, path, signal), "remote");
|
|
1831
|
+
}
|
|
1832
|
+
void this.fetch(kind, selector, path, signal).then(
|
|
1833
|
+
(fresh) => {
|
|
1834
|
+
if (!signal?.aborted && !sameContent(cached, fresh)) {
|
|
1835
|
+
onUpdate?.(present(fresh, "remote"));
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
(error) => {
|
|
1839
|
+
if (!signal?.aborted) {
|
|
1840
|
+
onError?.(error);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
);
|
|
1844
|
+
return present(cached, "cache");
|
|
1845
|
+
}
|
|
1846
|
+
/** Fetch from Mountra, and cache the result before returning it. */
|
|
1847
|
+
async fetch(kind, selector, path, signal) {
|
|
1848
|
+
const scope = this.cacheScope(selector);
|
|
1849
|
+
const ticket = scope?.cache.beginFetch(scope.workspace);
|
|
1850
|
+
let body;
|
|
1851
|
+
try {
|
|
1852
|
+
body = await this.transport.requestJson(`/file/${kind}`, { ...workspaceBody(selector), path }, signal);
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
if (scope && ticket && error instanceof MountraApiError && error.status === 404) {
|
|
1855
|
+
await scope.cache.forgetFetched(scope.workspace, path, ticket);
|
|
1856
|
+
}
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
const fetchedAt = ticket?.startedAt ?? Date.now();
|
|
1860
|
+
let record;
|
|
1861
|
+
if (kind === "list") {
|
|
1862
|
+
record = { kind, path, entries: readListResponse(body), fetchedAt };
|
|
1863
|
+
} else {
|
|
1864
|
+
if (!isRecord2(body)) {
|
|
1865
|
+
throw invalidField("root");
|
|
1866
|
+
}
|
|
1867
|
+
record = { kind, path, root: readTreeNode(body.root, "root"), fetchedAt };
|
|
1868
|
+
}
|
|
1869
|
+
if (scope && ticket) {
|
|
1870
|
+
await scope.cache.storeFetched(scope.workspace, record, ticket);
|
|
1871
|
+
}
|
|
1872
|
+
return record;
|
|
1873
|
+
}
|
|
1874
|
+
async operate(selector, endpoint, body, signal) {
|
|
1875
|
+
return readOperationResponse(
|
|
1876
|
+
await this.transport.requestJson(endpoint, { ...workspaceBody(selector), ...body }, signal)
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Reflect a write in both caches. Any download cached at a path the write
|
|
1881
|
+
* changed is stale: the file is gone, moved, or a new one now lives there.
|
|
1882
|
+
*/
|
|
1883
|
+
async afterWrite(selector, change) {
|
|
1884
|
+
const paths = change.type === "move" ? [change.fromPath, change.toPath] : change.type === "copy" ? [change.toPath] : [change.path];
|
|
1885
|
+
await Promise.all([
|
|
1886
|
+
this.applyChange(selector, change),
|
|
1887
|
+
this.transport.invalidateDownloads(selector, paths)
|
|
1888
|
+
]);
|
|
1889
|
+
}
|
|
1890
|
+
async applyChange(selector, change) {
|
|
1891
|
+
const scope = this.cacheScope(selector);
|
|
1892
|
+
await scope?.cache.applyChange(scope.workspace, change);
|
|
1893
|
+
}
|
|
1894
|
+
/** Fall back to the client workspace only when the call names none, as downloads do. */
|
|
1895
|
+
resolveWorkspace(selector) {
|
|
1896
|
+
const named = selector.workspaceCuid !== void 0 || selector.workspaceId !== void 0;
|
|
1897
|
+
const { workspaceCuid, workspaceId } = named ? selector : this.transport.clientSelector;
|
|
1898
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
1899
|
+
throw new TypeError("workspaceCuid or workspaceId is required");
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
1903
|
+
...workspaceId !== void 0 ? { workspaceId } : {}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
cacheScope(selector) {
|
|
1907
|
+
const cache = this.transport.cache;
|
|
1908
|
+
const workspace = workspaceCacheKey(this.transport.baseUrl, selector);
|
|
1909
|
+
return cache && workspace !== null ? { cache, workspace } : null;
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// src/metadata-cache.ts
|
|
1914
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
1915
|
+
var KINDS = ["list", "tree"];
|
|
1916
|
+
function parentOf(path) {
|
|
1917
|
+
const index = path.lastIndexOf("/");
|
|
1918
|
+
return index <= 0 ? "/" : path.slice(0, index);
|
|
1919
|
+
}
|
|
1920
|
+
function baseName(path) {
|
|
1921
|
+
return path.slice(path.lastIndexOf("/") + 1) || "/";
|
|
1922
|
+
}
|
|
1923
|
+
function selfAndAncestors(path) {
|
|
1924
|
+
const paths = [path];
|
|
1925
|
+
for (let current = path; current !== "/"; ) {
|
|
1926
|
+
current = parentOf(current);
|
|
1927
|
+
paths.push(current);
|
|
1928
|
+
}
|
|
1929
|
+
return paths;
|
|
1930
|
+
}
|
|
1931
|
+
function rebasePath(path, from, to) {
|
|
1932
|
+
return `${to}${path.slice(from.length)}`;
|
|
1933
|
+
}
|
|
1934
|
+
function memoryKey2(workspace, kind, path) {
|
|
1935
|
+
return JSON.stringify([workspace, kind, path]);
|
|
1936
|
+
}
|
|
1937
|
+
var InMemoryMetadataCacheStore = class {
|
|
1938
|
+
/** Map iteration order is the eviction order: the first entry was written least recently. */
|
|
1939
|
+
records = /* @__PURE__ */ new Map();
|
|
1940
|
+
async get(workspace, kind, path) {
|
|
1941
|
+
const entry = this.records.get(memoryKey2(workspace, kind, path));
|
|
1942
|
+
return entry ? structuredClone(entry.record) : null;
|
|
1943
|
+
}
|
|
1944
|
+
async getSubtree(workspace, path, kind) {
|
|
1945
|
+
return [...this.records.values()].filter(
|
|
1946
|
+
(entry) => entry.workspace === workspace && (kind === void 0 || entry.record.kind === kind) && isPathWithin(entry.record.path, path)
|
|
1947
|
+
).map((entry) => structuredClone(entry.record));
|
|
1948
|
+
}
|
|
1949
|
+
async write(workspace, batch, maxEntries) {
|
|
1950
|
+
for (const { kind, path } of batch.remove) {
|
|
1951
|
+
this.records.delete(memoryKey2(workspace, kind, path));
|
|
1952
|
+
}
|
|
1953
|
+
for (const record of batch.put) {
|
|
1954
|
+
const key = memoryKey2(workspace, record.kind, record.path);
|
|
1955
|
+
this.records.delete(key);
|
|
1956
|
+
this.records.set(key, { workspace, record: structuredClone(record) });
|
|
1957
|
+
}
|
|
1958
|
+
for (const key of this.records.keys()) {
|
|
1959
|
+
if (this.records.size <= maxEntries) {
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
this.records.delete(key);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async clear(workspace) {
|
|
1966
|
+
for (const [key, entry] of this.records) {
|
|
1967
|
+
if (workspace === void 0 || entry.workspace === workspace) {
|
|
1968
|
+
this.records.delete(key);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
var METADATA_DATABASE_NAME = "mountra-sdk-metadata-cache";
|
|
1974
|
+
var METADATA_DATABASE_VERSION = 1;
|
|
1975
|
+
var RECORDS_STORE = "records";
|
|
1976
|
+
var META_STORE2 = "meta";
|
|
1977
|
+
var WRITE_ORDER_INDEX = "written";
|
|
1978
|
+
var IndexedDBMetadataCacheStore = class {
|
|
1979
|
+
databaseName;
|
|
1980
|
+
fallback = new InMemoryMetadataCacheStore();
|
|
1981
|
+
databasePromise;
|
|
1982
|
+
constructor(options = {}) {
|
|
1983
|
+
this.databaseName = options.databaseName ?? METADATA_DATABASE_NAME;
|
|
1984
|
+
}
|
|
1985
|
+
async get(workspace, kind, path) {
|
|
1986
|
+
const database = await this.openDatabase();
|
|
1987
|
+
if (!database) {
|
|
1988
|
+
return this.fallback.get(workspace, kind, path);
|
|
1989
|
+
}
|
|
1990
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
1991
|
+
const stored = await requestResult(
|
|
1992
|
+
transaction.objectStore(RECORDS_STORE).get([workspace, kind, path])
|
|
1993
|
+
);
|
|
1994
|
+
return stored?.record ?? null;
|
|
1995
|
+
});
|
|
782
1996
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1997
|
+
async getSubtree(workspace, path, kind) {
|
|
1998
|
+
const database = await this.openDatabase();
|
|
1999
|
+
if (!database) {
|
|
2000
|
+
return this.fallback.getSubtree(workspace, path, kind);
|
|
2001
|
+
}
|
|
2002
|
+
const upper = path === "/" ? "0" : `${path}0`;
|
|
2003
|
+
return runTransaction(database, [RECORDS_STORE], "readonly", async (transaction) => {
|
|
2004
|
+
const records = [];
|
|
2005
|
+
for (const candidate of kind === void 0 ? KINDS : [kind]) {
|
|
2006
|
+
const stored = await requestResult(
|
|
2007
|
+
transaction.objectStore(RECORDS_STORE).getAll(IDBKeyRange.bound([workspace, candidate, path], [workspace, candidate, upper], false, true))
|
|
2008
|
+
);
|
|
2009
|
+
records.push(
|
|
2010
|
+
...stored.map(({ record }) => record).filter((record) => isPathWithin(record.path, path))
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
return records;
|
|
2014
|
+
});
|
|
789
2015
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
2016
|
+
async write(workspace, batch, maxEntries) {
|
|
2017
|
+
const database = await this.openDatabase();
|
|
2018
|
+
if (!database) {
|
|
2019
|
+
return this.fallback.write(workspace, batch, maxEntries);
|
|
2020
|
+
}
|
|
2021
|
+
await runTransaction(database, [RECORDS_STORE, META_STORE2], "readwrite", async (transaction) => {
|
|
2022
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2023
|
+
const meta = transaction.objectStore(META_STORE2);
|
|
2024
|
+
for (const { kind, path } of batch.remove) {
|
|
2025
|
+
await requestResult(records.delete([workspace, kind, path]));
|
|
2026
|
+
}
|
|
2027
|
+
for (const record of batch.put) {
|
|
2028
|
+
const stored = { record, written: await nextClock(meta) };
|
|
2029
|
+
await requestResult(records.put(stored, [workspace, record.kind, record.path]));
|
|
2030
|
+
}
|
|
2031
|
+
let count = await requestResult(records.count());
|
|
2032
|
+
if (count <= maxEntries) {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const cursorRequest = records.index(WRITE_ORDER_INDEX).openCursor();
|
|
2036
|
+
let cursor = await requestResult(cursorRequest);
|
|
2037
|
+
while (cursor && count > maxEntries) {
|
|
2038
|
+
await requestResult(cursor.delete());
|
|
2039
|
+
count -= 1;
|
|
2040
|
+
cursor.continue();
|
|
2041
|
+
cursor = await requestResult(cursorRequest);
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
794
2044
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
2045
|
+
async clear(workspace) {
|
|
2046
|
+
const database = await this.openDatabase();
|
|
2047
|
+
if (!database) {
|
|
2048
|
+
return this.fallback.clear(workspace);
|
|
2049
|
+
}
|
|
2050
|
+
await runTransaction(database, [RECORDS_STORE], "readwrite", async (transaction) => {
|
|
2051
|
+
const records = transaction.objectStore(RECORDS_STORE);
|
|
2052
|
+
await requestResult(
|
|
2053
|
+
workspace === void 0 ? records.clear() : records.delete(workspaceRange(workspace))
|
|
2054
|
+
);
|
|
2055
|
+
});
|
|
798
2056
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
2057
|
+
async openDatabase() {
|
|
2058
|
+
this.databasePromise ??= openDatabase(
|
|
2059
|
+
this.databaseName,
|
|
2060
|
+
METADATA_DATABASE_VERSION,
|
|
2061
|
+
(database) => {
|
|
2062
|
+
if (!database.objectStoreNames.contains(RECORDS_STORE)) {
|
|
2063
|
+
database.createObjectStore(RECORDS_STORE).createIndex(WRITE_ORDER_INDEX, "written");
|
|
2064
|
+
}
|
|
2065
|
+
if (!database.objectStoreNames.contains(META_STORE2)) {
|
|
2066
|
+
database.createObjectStore(META_STORE2);
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
() => {
|
|
2070
|
+
this.databasePromise = void 0;
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
2073
|
+
return this.databasePromise;
|
|
802
2074
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
2075
|
+
};
|
|
2076
|
+
var NODE_FIELDS = [
|
|
2077
|
+
"name",
|
|
2078
|
+
"path",
|
|
2079
|
+
"ino",
|
|
2080
|
+
"isDirectory",
|
|
2081
|
+
"size",
|
|
2082
|
+
"mtimeMs",
|
|
2083
|
+
"uid",
|
|
2084
|
+
"gid",
|
|
2085
|
+
"uidSubjectId",
|
|
2086
|
+
"uidSubjectType",
|
|
2087
|
+
"gidSubjectId",
|
|
2088
|
+
"gidSubjectType"
|
|
2089
|
+
];
|
|
2090
|
+
function pickDefined(source, keys) {
|
|
2091
|
+
const picked = {};
|
|
2092
|
+
for (const key of keys) {
|
|
2093
|
+
if (source[key] !== void 0) {
|
|
2094
|
+
picked[key] = source[key];
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
return picked;
|
|
2098
|
+
}
|
|
2099
|
+
function nodeFromEntry(entry, children) {
|
|
2100
|
+
return { ...pickDefined(entry, NODE_FIELDS), children };
|
|
2101
|
+
}
|
|
2102
|
+
function entryFromNode(node, previous) {
|
|
2103
|
+
return {
|
|
2104
|
+
...pickDefined(node, NODE_FIELDS),
|
|
2105
|
+
atimeMs: previous.atimeMs,
|
|
2106
|
+
...node.uid === previous.uid ? pickDefined(previous, ["uidDisplayName", "uidUsername"]) : {},
|
|
2107
|
+
...node.gid === previous.gid ? pickDefined(previous, ["gidDisplayName", "gidUsername"]) : {}
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function findNode(root, path) {
|
|
2111
|
+
let node = root;
|
|
2112
|
+
while (node && node.path !== path) {
|
|
2113
|
+
node = isPathWithin(path, node.path) ? node.children.find((child) => isPathWithin(path, child.path)) : void 0;
|
|
810
2114
|
}
|
|
2115
|
+
return node;
|
|
2116
|
+
}
|
|
2117
|
+
function rebaseNode(node, from, to) {
|
|
811
2118
|
return {
|
|
812
|
-
...
|
|
813
|
-
...
|
|
814
|
-
|
|
2119
|
+
...node,
|
|
2120
|
+
...node.path === from ? { name: baseName(to) } : {},
|
|
2121
|
+
path: rebasePath(node.path, from, to),
|
|
2122
|
+
children: node.children.map((child) => rebaseNode(child, from, to))
|
|
815
2123
|
};
|
|
816
2124
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
2125
|
+
function compareChildren(left, right) {
|
|
2126
|
+
return Number(right.isDirectory) - Number(left.isDirectory) || left.name.localeCompare(right.name);
|
|
2127
|
+
}
|
|
2128
|
+
function withChild(children, child) {
|
|
2129
|
+
const result = [...children];
|
|
2130
|
+
const existing = result.findIndex((candidate) => candidate.path === child.path);
|
|
2131
|
+
if (existing >= 0) {
|
|
2132
|
+
result[existing] = child;
|
|
2133
|
+
return result;
|
|
2134
|
+
}
|
|
2135
|
+
const position = result.findIndex((candidate) => compareChildren(child, candidate) < 0);
|
|
2136
|
+
result.splice(position < 0 ? result.length : position, 0, child);
|
|
2137
|
+
return result;
|
|
2138
|
+
}
|
|
2139
|
+
function recordKey(kind, path) {
|
|
2140
|
+
return `${kind}:${path}`;
|
|
2141
|
+
}
|
|
2142
|
+
var MetadataDraft = class {
|
|
2143
|
+
records = /* @__PURE__ */ new Map();
|
|
2144
|
+
changed = /* @__PURE__ */ new Set();
|
|
2145
|
+
removed = /* @__PURE__ */ new Map();
|
|
2146
|
+
constructor(records) {
|
|
2147
|
+
for (const record of records) {
|
|
2148
|
+
this.records.set(recordKey(record.kind, record.path), record);
|
|
2149
|
+
}
|
|
821
2150
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return this.locator.workspaceCuid;
|
|
2151
|
+
list(path) {
|
|
2152
|
+
const record = this.records.get(recordKey("list", path));
|
|
2153
|
+
return record?.kind === "list" ? record : void 0;
|
|
826
2154
|
}
|
|
827
|
-
|
|
828
|
-
|
|
2155
|
+
tree(path) {
|
|
2156
|
+
const record = this.records.get(recordKey("tree", path));
|
|
2157
|
+
return record?.kind === "tree" ? record : void 0;
|
|
829
2158
|
}
|
|
830
|
-
/** `
|
|
831
|
-
|
|
832
|
-
return this.
|
|
2159
|
+
/** Loaded trees that hold `path`: those rooted at `path` or at one of its ancestors. */
|
|
2160
|
+
treesContaining(path) {
|
|
2161
|
+
return [...this.records.values()].filter(
|
|
2162
|
+
(record) => record.kind === "tree" && isPathWithin(path, record.path)
|
|
2163
|
+
);
|
|
833
2164
|
}
|
|
834
|
-
|
|
835
|
-
|
|
2165
|
+
/** Loaded records at `path` or below it. */
|
|
2166
|
+
within(path) {
|
|
2167
|
+
return [...this.records.values()].filter((record) => isPathWithin(record.path, path));
|
|
836
2168
|
}
|
|
837
|
-
|
|
838
|
-
|
|
2169
|
+
put(record) {
|
|
2170
|
+
const key = recordKey(record.kind, record.path);
|
|
2171
|
+
this.records.set(key, record);
|
|
2172
|
+
this.removed.delete(key);
|
|
2173
|
+
this.changed.add(key);
|
|
839
2174
|
}
|
|
840
|
-
|
|
841
|
-
|
|
2175
|
+
/** Mark a loaded record that was changed in place. */
|
|
2176
|
+
touch(record) {
|
|
2177
|
+
this.changed.add(recordKey(record.kind, record.path));
|
|
842
2178
|
}
|
|
843
|
-
|
|
844
|
-
|
|
2179
|
+
remove(record) {
|
|
2180
|
+
const key = recordKey(record.kind, record.path);
|
|
2181
|
+
this.records.delete(key);
|
|
2182
|
+
this.changed.delete(key);
|
|
2183
|
+
this.removed.set(key, { kind: record.kind, path: record.path });
|
|
845
2184
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
2185
|
+
batch() {
|
|
2186
|
+
return {
|
|
2187
|
+
put: [...this.changed].map((key) => this.records.get(key)),
|
|
2188
|
+
remove: [...this.removed.values()]
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
findEntry(path) {
|
|
2192
|
+
return this.list(parentOf(path))?.entries.find((entry) => entry.path === path);
|
|
2193
|
+
}
|
|
2194
|
+
/** The node of `path` with its complete subtree, from any loaded tree. */
|
|
2195
|
+
findNode(path) {
|
|
2196
|
+
for (const tree of this.treesContaining(path)) {
|
|
2197
|
+
const node = findNode(tree.root, path);
|
|
2198
|
+
if (node) {
|
|
2199
|
+
return node;
|
|
856
2200
|
}
|
|
857
|
-
throw new Error("No resumable download was found for this Mountra file");
|
|
858
2201
|
}
|
|
859
|
-
return
|
|
2202
|
+
return void 0;
|
|
860
2203
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
2204
|
+
/** Remove `path` from the listing and the trees that show it. */
|
|
2205
|
+
removeEntry(path) {
|
|
2206
|
+
const parentPath = parentOf(path);
|
|
2207
|
+
const list = this.list(parentPath);
|
|
2208
|
+
if (list?.entries.some((entry) => entry.path === path)) {
|
|
2209
|
+
list.entries = list.entries.filter((entry) => entry.path !== path);
|
|
2210
|
+
this.touch(list);
|
|
2211
|
+
}
|
|
2212
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2213
|
+
const parent = findNode(tree.root, parentPath);
|
|
2214
|
+
if (parent?.children.some((child) => child.path === path)) {
|
|
2215
|
+
parent.children = parent.children.filter((child) => child.path !== path);
|
|
2216
|
+
this.touch(tree);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
865
2219
|
}
|
|
866
|
-
|
|
867
|
-
|
|
2220
|
+
/**
|
|
2221
|
+
* Show `path` in its parent's listing and in the trees holding its parent.
|
|
2222
|
+
* A record that cannot show it faithfully, because the entry or the
|
|
2223
|
+
* complete node is unknown, is dropped instead.
|
|
2224
|
+
*/
|
|
2225
|
+
insertEntry(path, entry, node) {
|
|
2226
|
+
const parentPath = parentOf(path);
|
|
2227
|
+
const list = this.list(parentPath);
|
|
2228
|
+
if (list && entry) {
|
|
2229
|
+
list.entries = withChild(list.entries, entry);
|
|
2230
|
+
this.touch(list);
|
|
2231
|
+
} else if (list) {
|
|
2232
|
+
this.remove(list);
|
|
2233
|
+
}
|
|
2234
|
+
for (const tree of this.treesContaining(parentPath)) {
|
|
2235
|
+
const parent = findNode(tree.root, parentPath);
|
|
2236
|
+
if (node && parent?.isDirectory) {
|
|
2237
|
+
parent.children = withChild(parent.children, structuredClone(node));
|
|
2238
|
+
this.touch(tree);
|
|
2239
|
+
} else {
|
|
2240
|
+
this.remove(tree);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
868
2243
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
2244
|
+
removeSubtree(path) {
|
|
2245
|
+
for (const record of this.within(path)) {
|
|
2246
|
+
this.remove(record);
|
|
872
2247
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
2248
|
+
}
|
|
2249
|
+
/** Re-key the records at or below `from` to `to`, rewriting the paths they hold. */
|
|
2250
|
+
moveSubtree(from, to) {
|
|
2251
|
+
for (const record of this.within(from)) {
|
|
2252
|
+
this.remove(record);
|
|
2253
|
+
const path = rebasePath(record.path, from, to);
|
|
2254
|
+
this.put(
|
|
2255
|
+
record.kind === "list" ? {
|
|
2256
|
+
...record,
|
|
2257
|
+
path,
|
|
2258
|
+
entries: record.entries.map((entry) => ({ ...entry, path: rebasePath(entry.path, from, to) }))
|
|
2259
|
+
} : { ...record, path, root: rebaseNode(record.root, from, to) }
|
|
2260
|
+
);
|
|
877
2261
|
}
|
|
878
2262
|
}
|
|
879
|
-
|
|
2263
|
+
};
|
|
2264
|
+
function changeScope(change) {
|
|
2265
|
+
const paths = change.type === "move" || change.type === "copy" ? [change.fromPath, change.toPath] : [change.path];
|
|
2266
|
+
return {
|
|
2267
|
+
lists: paths.map(parentOf),
|
|
2268
|
+
trees: paths.flatMap((path) => selfAndAncestors(parentOf(path))),
|
|
2269
|
+
// A copy keeps its source; any other change makes records below its paths stale or moves them.
|
|
2270
|
+
subtrees: change.type === "copy" ? [change.toPath] : paths
|
|
2271
|
+
};
|
|
880
2272
|
}
|
|
881
|
-
function
|
|
882
|
-
|
|
883
|
-
|
|
2273
|
+
function patchChange(draft, change, now) {
|
|
2274
|
+
switch (change.type) {
|
|
2275
|
+
case "create": {
|
|
2276
|
+
const previousEntry = draft.findEntry(change.path);
|
|
2277
|
+
const previousNode = draft.findNode(change.path);
|
|
2278
|
+
draft.removeSubtree(change.path);
|
|
2279
|
+
if (change.ino === void 0) {
|
|
2280
|
+
draft.insertEntry(change.path, null, null);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
const fields = {
|
|
2284
|
+
name: baseName(change.path),
|
|
2285
|
+
path: change.path,
|
|
2286
|
+
ino: change.ino,
|
|
2287
|
+
isDirectory: change.isDirectory,
|
|
2288
|
+
size: change.size,
|
|
2289
|
+
mtimeMs: now
|
|
2290
|
+
};
|
|
2291
|
+
const sameKind = (previous) => previous?.isDirectory === change.isDirectory;
|
|
2292
|
+
draft.insertEntry(
|
|
2293
|
+
change.path,
|
|
2294
|
+
{ atimeMs: now, ...sameKind(previousEntry) ? previousEntry : {}, ...fields },
|
|
2295
|
+
{ ...sameKind(previousNode) ? previousNode : {}, ...fields, children: [] }
|
|
2296
|
+
);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
case "remove":
|
|
2300
|
+
draft.removeEntry(change.path);
|
|
2301
|
+
draft.removeSubtree(change.path);
|
|
2302
|
+
return;
|
|
2303
|
+
case "move": {
|
|
2304
|
+
const { fromPath, toPath } = change;
|
|
2305
|
+
if (fromPath === toPath) {
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const entry = draft.findEntry(fromPath);
|
|
2309
|
+
const node = draft.findNode(fromPath);
|
|
2310
|
+
draft.removeEntry(fromPath);
|
|
2311
|
+
draft.removeSubtree(toPath);
|
|
2312
|
+
draft.moveSubtree(fromPath, toPath);
|
|
2313
|
+
const fields = { name: baseName(toPath), path: toPath, ino: change.ino };
|
|
2314
|
+
const movedEntry = entry ? { ...entry, ...fields } : null;
|
|
2315
|
+
draft.insertEntry(
|
|
2316
|
+
toPath,
|
|
2317
|
+
movedEntry,
|
|
2318
|
+
node ? { ...rebaseNode(node, fromPath, toPath), ino: change.ino } : movedEntry && !movedEntry.isDirectory ? nodeFromEntry(movedEntry, []) : null
|
|
2319
|
+
);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
case "copy": {
|
|
2323
|
+
const source = draft.findEntry(change.fromPath) ?? draft.findNode(change.fromPath);
|
|
2324
|
+
draft.removeSubtree(change.toPath);
|
|
2325
|
+
if (!source || source.isDirectory) {
|
|
2326
|
+
draft.insertEntry(change.toPath, null, null);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const fields = {
|
|
2330
|
+
name: baseName(change.toPath),
|
|
2331
|
+
path: change.toPath,
|
|
2332
|
+
ino: change.ino,
|
|
2333
|
+
isDirectory: false,
|
|
2334
|
+
size: source.size,
|
|
2335
|
+
mtimeMs: now
|
|
2336
|
+
};
|
|
2337
|
+
draft.insertEntry(change.toPath, { ...fields, atimeMs: now }, { ...fields, children: [] });
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
884
2340
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
if (
|
|
891
|
-
|
|
2341
|
+
}
|
|
2342
|
+
function childrenFromEntries(directory, entries) {
|
|
2343
|
+
const children = [];
|
|
2344
|
+
for (const entry of entries) {
|
|
2345
|
+
const previous = directory.children.find((child) => child.path === entry.path);
|
|
2346
|
+
if (!entry.isDirectory) {
|
|
2347
|
+
children.push(nodeFromEntry(entry, []));
|
|
2348
|
+
} else if (previous?.isDirectory && previous.ino === entry.ino) {
|
|
2349
|
+
children.push(nodeFromEntry(entry, previous.children));
|
|
2350
|
+
} else {
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return children;
|
|
2355
|
+
}
|
|
2356
|
+
function entriesFromNodes(entries, nodes) {
|
|
2357
|
+
const previous = new Map(entries.map((entry) => [entry.path, entry]));
|
|
2358
|
+
const fresh = new Map(nodes.map((node) => [node.path, node]));
|
|
2359
|
+
for (const node of nodes) {
|
|
2360
|
+
const entry = previous.get(node.path);
|
|
2361
|
+
if (!entry || entry.ino !== node.ino || entry.isDirectory !== node.isDirectory) {
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return entries.filter((entry) => fresh.has(entry.path)).map((entry) => entryFromNode(fresh.get(entry.path), entry));
|
|
2366
|
+
}
|
|
2367
|
+
function patchFetchedList(draft, record) {
|
|
2368
|
+
if ((draft.list(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
draft.put(record);
|
|
2372
|
+
for (const tree of draft.treesContaining(record.path)) {
|
|
2373
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
892
2374
|
continue;
|
|
893
2375
|
}
|
|
894
|
-
|
|
2376
|
+
const directory = findNode(tree.root, record.path);
|
|
2377
|
+
const children = directory?.isDirectory ? childrenFromEntries(directory, record.entries) : null;
|
|
2378
|
+
if (directory && children) {
|
|
2379
|
+
directory.children = children;
|
|
2380
|
+
draft.touch(tree);
|
|
2381
|
+
} else {
|
|
2382
|
+
draft.remove(tree);
|
|
2383
|
+
}
|
|
895
2384
|
}
|
|
896
|
-
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
897
2385
|
}
|
|
898
|
-
function
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
|
|
2386
|
+
function patchFetchedTree(draft, record) {
|
|
2387
|
+
if ((draft.tree(record.path)?.fetchedAt ?? 0) > record.fetchedAt) {
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
draft.put(record);
|
|
2391
|
+
for (const other of draft.within(record.path)) {
|
|
2392
|
+
if (other === record || other.fetchedAt > record.fetchedAt) {
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const node = findNode(record.root, other.path);
|
|
2396
|
+
if (other.kind === "tree") {
|
|
2397
|
+
if (node?.isDirectory) {
|
|
2398
|
+
draft.put({ ...other, root: structuredClone(node), fetchedAt: record.fetchedAt });
|
|
2399
|
+
} else {
|
|
2400
|
+
draft.remove(other);
|
|
2401
|
+
}
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
const entries = node?.isDirectory ? entriesFromNodes(other.entries, node.children) : null;
|
|
2405
|
+
if (entries) {
|
|
2406
|
+
other.entries = entries;
|
|
2407
|
+
draft.touch(other);
|
|
2408
|
+
} else {
|
|
2409
|
+
draft.remove(other);
|
|
902
2410
|
}
|
|
903
|
-
return value;
|
|
904
2411
|
}
|
|
905
|
-
if (
|
|
906
|
-
|
|
2412
|
+
if (record.path === "/") {
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const parentPath = parentOf(record.path);
|
|
2416
|
+
for (const tree of draft.treesContaining(parentPath)) {
|
|
2417
|
+
if (tree.fetchedAt > record.fetchedAt) {
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const parent = findNode(tree.root, parentPath);
|
|
2421
|
+
if (parent?.isDirectory) {
|
|
2422
|
+
parent.children = withChild(parent.children, structuredClone(record.root));
|
|
2423
|
+
draft.touch(tree);
|
|
2424
|
+
} else {
|
|
2425
|
+
draft.remove(tree);
|
|
2426
|
+
}
|
|
907
2427
|
}
|
|
908
|
-
return value;
|
|
909
2428
|
}
|
|
2429
|
+
var MetadataCache = class {
|
|
2430
|
+
maxEntries;
|
|
2431
|
+
store;
|
|
2432
|
+
/** Per workspace, how many writes this client has applied. */
|
|
2433
|
+
writeSeqs = /* @__PURE__ */ new Map();
|
|
2434
|
+
/** Per workspace, the tail of the queue that serializes cache updates. */
|
|
2435
|
+
queues = /* @__PURE__ */ new Map();
|
|
2436
|
+
constructor(options) {
|
|
2437
|
+
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2438
|
+
if (!Number.isSafeInteger(this.maxEntries) || this.maxEntries <= 0) {
|
|
2439
|
+
throw new RangeError("metadataCache.maxEntries must be a positive safe integer");
|
|
2440
|
+
}
|
|
2441
|
+
this.store = options.store ?? new IndexedDBMetadataCacheStore();
|
|
2442
|
+
}
|
|
2443
|
+
async get(workspace, kind, path) {
|
|
2444
|
+
try {
|
|
2445
|
+
const record = await this.store.get(workspace, kind, path);
|
|
2446
|
+
return record?.kind === kind && record.path === path ? record : null;
|
|
2447
|
+
} catch {
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
beginFetch(workspace) {
|
|
2452
|
+
return { startedAt: Date.now(), writeSeq: this.writeSeqs.get(workspace) ?? 0 };
|
|
2453
|
+
}
|
|
2454
|
+
/** Cache a fetched result, and bring the records it overlaps up to date with it. */
|
|
2455
|
+
async storeFetched(workspace, record, ticket) {
|
|
2456
|
+
await this.update(workspace, ticket, async () => {
|
|
2457
|
+
if (record.kind === "list") {
|
|
2458
|
+
const draft2 = await this.load(workspace, { lists: [record.path], trees: selfAndAncestors(record.path) });
|
|
2459
|
+
patchFetchedList(draft2, record);
|
|
2460
|
+
return draft2;
|
|
2461
|
+
}
|
|
2462
|
+
const draft = await this.load(workspace, { trees: selfAndAncestors(record.path), subtrees: [record.path] });
|
|
2463
|
+
patchFetchedTree(draft, record);
|
|
2464
|
+
return draft;
|
|
2465
|
+
}).catch(() => void 0);
|
|
2466
|
+
}
|
|
2467
|
+
/** Mountra reported that `path` does not exist. */
|
|
2468
|
+
async forgetFetched(workspace, path, ticket) {
|
|
2469
|
+
const change = { type: "remove", path };
|
|
2470
|
+
await this.update(workspace, ticket, async () => {
|
|
2471
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2472
|
+
patchChange(draft, change, Date.now());
|
|
2473
|
+
return draft;
|
|
2474
|
+
}).catch(() => void 0);
|
|
2475
|
+
}
|
|
2476
|
+
/** Reflect a write that Mountra completed. */
|
|
2477
|
+
async applyChange(workspace, change) {
|
|
2478
|
+
this.writeSeqs.set(workspace, (this.writeSeqs.get(workspace) ?? 0) + 1);
|
|
2479
|
+
await this.update(workspace, null, async () => {
|
|
2480
|
+
const draft = await this.load(workspace, changeScope(change));
|
|
2481
|
+
patchChange(draft, change, Date.now());
|
|
2482
|
+
return draft;
|
|
2483
|
+
}).catch(
|
|
2484
|
+
() => (
|
|
2485
|
+
// A write that cannot be reflected must not leave records that contradict it.
|
|
2486
|
+
this.store.clear(workspace).catch(() => void 0)
|
|
2487
|
+
)
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
clear(workspace) {
|
|
2491
|
+
return this.store.clear(workspace);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Run updates of one workspace one at a time. An update for a fetch is
|
|
2495
|
+
* dropped when this client completed a write since the fetch started: the
|
|
2496
|
+
* result may predate that write.
|
|
2497
|
+
*/
|
|
2498
|
+
update(workspace, ticket, build) {
|
|
2499
|
+
const run = async () => {
|
|
2500
|
+
if (ticket && ticket.writeSeq !== (this.writeSeqs.get(workspace) ?? 0)) {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const batch = (await build()).batch();
|
|
2504
|
+
if (batch.put.length > 0 || batch.remove.length > 0) {
|
|
2505
|
+
await this.store.write(workspace, batch, this.maxEntries);
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
const result = (this.queues.get(workspace) ?? Promise.resolve()).then(run);
|
|
2509
|
+
const tail = result.catch(() => void 0);
|
|
2510
|
+
this.queues.set(workspace, tail);
|
|
2511
|
+
void tail.then(() => {
|
|
2512
|
+
if (this.queues.get(workspace) === tail) {
|
|
2513
|
+
this.queues.delete(workspace);
|
|
2514
|
+
}
|
|
2515
|
+
});
|
|
2516
|
+
return result;
|
|
2517
|
+
}
|
|
2518
|
+
async load(workspace, scope) {
|
|
2519
|
+
const [records, subtrees] = await Promise.all([
|
|
2520
|
+
Promise.all([
|
|
2521
|
+
...[...new Set(scope.lists)].map((path) => this.store.get(workspace, "list", path)),
|
|
2522
|
+
...[...new Set(scope.trees)].map((path) => this.store.get(workspace, "tree", path))
|
|
2523
|
+
]),
|
|
2524
|
+
Promise.all([...new Set(scope.subtrees)].map((path) => this.store.getSubtree(workspace, path)))
|
|
2525
|
+
]);
|
|
2526
|
+
return new MetadataDraft([
|
|
2527
|
+
...records.filter((record) => record !== null),
|
|
2528
|
+
...subtrees.flat()
|
|
2529
|
+
]);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
910
2532
|
|
|
911
2533
|
// src/upload.ts
|
|
912
2534
|
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
913
2535
|
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
914
2536
|
var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
|
|
915
2537
|
var MAX_S3_MULTIPART_PARTS = 1e4;
|
|
916
|
-
var MountraApiError = class extends Error {
|
|
917
|
-
status;
|
|
918
|
-
code;
|
|
919
|
-
body;
|
|
920
|
-
constructor(message, input) {
|
|
921
|
-
super(message);
|
|
922
|
-
this.name = "MountraApiError";
|
|
923
|
-
this.status = input.status;
|
|
924
|
-
this.code = input.code;
|
|
925
|
-
this.body = input.body;
|
|
926
|
-
}
|
|
927
|
-
};
|
|
928
2538
|
var memoryCheckpointRecords = /* @__PURE__ */ new Map();
|
|
929
2539
|
function parseJumboCheckpoint(value) {
|
|
930
|
-
if (!
|
|
2540
|
+
if (!isRecord3(value) || value.version !== 1 || typeof value.storageKey !== "string") {
|
|
931
2541
|
return null;
|
|
932
2542
|
}
|
|
933
2543
|
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)) {
|
|
@@ -935,7 +2545,7 @@ function parseJumboCheckpoint(value) {
|
|
|
935
2545
|
}
|
|
936
2546
|
const parts = [];
|
|
937
2547
|
for (const part of value.parts) {
|
|
938
|
-
if (!
|
|
2548
|
+
if (!isRecord3(part)) {
|
|
939
2549
|
return null;
|
|
940
2550
|
}
|
|
941
2551
|
if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
|
|
@@ -1114,11 +2724,11 @@ function throwIfAborted2(signal) {
|
|
|
1114
2724
|
}
|
|
1115
2725
|
throw new DOMException("The upload was aborted", "AbortError");
|
|
1116
2726
|
}
|
|
1117
|
-
function
|
|
2727
|
+
function isRecord3(value) {
|
|
1118
2728
|
return typeof value === "object" && value !== null;
|
|
1119
2729
|
}
|
|
1120
2730
|
function parseApiErrorBody(body) {
|
|
1121
|
-
if (!
|
|
2731
|
+
if (!isRecord3(body)) {
|
|
1122
2732
|
return {};
|
|
1123
2733
|
}
|
|
1124
2734
|
return {
|
|
@@ -1127,7 +2737,7 @@ function parseApiErrorBody(body) {
|
|
|
1127
2737
|
};
|
|
1128
2738
|
}
|
|
1129
2739
|
function ensurePrepareResponse(value) {
|
|
1130
|
-
if (!
|
|
2740
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1131
2741
|
throw new TypeError("Mountra prepare response is invalid");
|
|
1132
2742
|
}
|
|
1133
2743
|
if (!Array.isArray(value.upload_targets)) {
|
|
@@ -1136,13 +2746,13 @@ function ensurePrepareResponse(value) {
|
|
|
1136
2746
|
return value;
|
|
1137
2747
|
}
|
|
1138
2748
|
function ensureFinishResponse(value) {
|
|
1139
|
-
if (!
|
|
2749
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1140
2750
|
throw new TypeError("Mountra finish response is invalid");
|
|
1141
2751
|
}
|
|
1142
2752
|
return value;
|
|
1143
2753
|
}
|
|
1144
2754
|
function ensureUploadStatusResponse(value) {
|
|
1145
|
-
if (!
|
|
2755
|
+
if (!isRecord3(value) || typeof value.upload_id !== "string") {
|
|
1146
2756
|
throw new TypeError("Mountra upload status response is invalid");
|
|
1147
2757
|
}
|
|
1148
2758
|
if (!Array.isArray(value.chunks)) {
|
|
@@ -1160,8 +2770,7 @@ function splitFileIntoChunks(file, chunkCount = 1) {
|
|
|
1160
2770
|
}
|
|
1161
2771
|
const chunkSize = Math.ceil(file.size / chunkCount);
|
|
1162
2772
|
const chunks = [];
|
|
1163
|
-
for (let index = 0;
|
|
1164
|
-
const offset = index * chunkSize;
|
|
2773
|
+
for (let index = 0, offset = 0; offset < file.size; index += 1, offset += chunkSize) {
|
|
1165
2774
|
const end = Math.min(offset + chunkSize, file.size);
|
|
1166
2775
|
chunks.push({
|
|
1167
2776
|
index,
|
|
@@ -1194,6 +2803,7 @@ var MountraClient = class {
|
|
|
1194
2803
|
jumboChunkCheckpointStore;
|
|
1195
2804
|
jumboPartSize;
|
|
1196
2805
|
downloader;
|
|
2806
|
+
metadata;
|
|
1197
2807
|
constructor(options) {
|
|
1198
2808
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
1199
2809
|
this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
|
|
@@ -1217,8 +2827,16 @@ var MountraClient = class {
|
|
|
1217
2827
|
clientSelector: this.clientSelector,
|
|
1218
2828
|
fetchImpl: this.fetchImpl,
|
|
1219
2829
|
checkpointStore: options.downloadCheckpointStore ?? new IndexedDBDownloadCheckpointStore(),
|
|
2830
|
+
cache: options.downloadCache ? new DownloadCache(options.downloadCache) : null,
|
|
1220
2831
|
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1221
2832
|
});
|
|
2833
|
+
this.metadata = new MountraMetadata({
|
|
2834
|
+
baseUrl: this.baseUrl,
|
|
2835
|
+
clientSelector: this.clientSelector,
|
|
2836
|
+
cache: options.metadataCache ? new MetadataCache(options.metadataCache) : null,
|
|
2837
|
+
requestJson: (path, body, signal) => this.requestJson(path, body, signal),
|
|
2838
|
+
invalidateDownloads: (selector, paths) => this.downloader.invalidateCachedPaths(selector, paths)
|
|
2839
|
+
});
|
|
1222
2840
|
}
|
|
1223
2841
|
async exportFile(options) {
|
|
1224
2842
|
return this.downloader.exportFile(options);
|
|
@@ -1238,6 +2856,40 @@ var MountraClient = class {
|
|
|
1238
2856
|
clearDownloadCheckpoint(downloadKey) {
|
|
1239
2857
|
return this.downloader.clearDownloadCheckpoint(downloadKey);
|
|
1240
2858
|
}
|
|
2859
|
+
/** Clear the download cache of one workspace, or of every workspace when none is given. */
|
|
2860
|
+
clearDownloadCache(selector) {
|
|
2861
|
+
return this.downloader.clearCache(selector);
|
|
2862
|
+
}
|
|
2863
|
+
getDownloadCacheUsage(selector) {
|
|
2864
|
+
return this.downloader.cacheUsage(selector);
|
|
2865
|
+
}
|
|
2866
|
+
/** List the entries of a directory. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2867
|
+
list(options) {
|
|
2868
|
+
return this.metadata.list(options);
|
|
2869
|
+
}
|
|
2870
|
+
/** Return a directory with its complete subtree. See `MetadataReadMode` for how `mode` uses the metadata cache. */
|
|
2871
|
+
tree(options) {
|
|
2872
|
+
return this.metadata.tree(options);
|
|
2873
|
+
}
|
|
2874
|
+
mkdir(options) {
|
|
2875
|
+
return this.metadata.mkdir(options);
|
|
2876
|
+
}
|
|
2877
|
+
rename(options) {
|
|
2878
|
+
return this.metadata.rename(options);
|
|
2879
|
+
}
|
|
2880
|
+
move(options) {
|
|
2881
|
+
return this.metadata.move(options);
|
|
2882
|
+
}
|
|
2883
|
+
copy(options) {
|
|
2884
|
+
return this.metadata.copy(options);
|
|
2885
|
+
}
|
|
2886
|
+
delete(options) {
|
|
2887
|
+
return this.metadata.delete(options);
|
|
2888
|
+
}
|
|
2889
|
+
/** Clear the metadata cache of one workspace, or of every workspace when none is given. */
|
|
2890
|
+
clearMetadataCache(selector) {
|
|
2891
|
+
return this.metadata.clearCache(selector);
|
|
2892
|
+
}
|
|
1241
2893
|
/** Create a stable resource object for one file in this or another workspace. */
|
|
1242
2894
|
file(locator) {
|
|
1243
2895
|
return new MountraFile(
|
|
@@ -1418,6 +3070,27 @@ var MountraClient = class {
|
|
|
1418
3070
|
if (finished.finish_status !== "completed") {
|
|
1419
3071
|
throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
|
|
1420
3072
|
}
|
|
3073
|
+
await this.metadata.recordUpload(this.cacheWorkspace(options), finished);
|
|
3074
|
+
await this.downloader.cacheUploadedFile(
|
|
3075
|
+
this.cacheWorkspace(options),
|
|
3076
|
+
{
|
|
3077
|
+
path: finished.path,
|
|
3078
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {},
|
|
3079
|
+
size: context.fileSize,
|
|
3080
|
+
...context.contentType ? { contentType: context.contentType } : {},
|
|
3081
|
+
chunks: orderedTargets.map((target) => {
|
|
3082
|
+
const chunk = chunks[target.chunk_index];
|
|
3083
|
+
return {
|
|
3084
|
+
index: chunk.index,
|
|
3085
|
+
offset: chunk.offset,
|
|
3086
|
+
size: chunk.size,
|
|
3087
|
+
storageKey: target.storage_key,
|
|
3088
|
+
data: chunk.blob
|
|
3089
|
+
};
|
|
3090
|
+
})
|
|
3091
|
+
},
|
|
3092
|
+
options.cache
|
|
3093
|
+
);
|
|
1421
3094
|
await Promise.all(
|
|
1422
3095
|
[...jumboCheckpointKeys].map(
|
|
1423
3096
|
(checkpointKey) => this.jumboChunkCheckpointStore.remove(checkpointKey)
|
|
@@ -1593,12 +3266,27 @@ var MountraClient = class {
|
|
|
1593
3266
|
if (!uploadId.trim()) {
|
|
1594
3267
|
throw new TypeError("uploadId is required");
|
|
1595
3268
|
}
|
|
1596
|
-
|
|
3269
|
+
const finished = ensureFinishResponse(
|
|
1597
3270
|
await this.requestJson("/upload/finish", {
|
|
1598
3271
|
...getWorkspaceBody(this.clientSelector, selector),
|
|
1599
3272
|
upload_id: uploadId
|
|
1600
3273
|
})
|
|
1601
3274
|
);
|
|
3275
|
+
if (finished.finish_status === "completed") {
|
|
3276
|
+
await this.metadata.recordUpload(this.cacheWorkspace(selector), finished);
|
|
3277
|
+
await this.downloader.invalidateCachedFile(this.cacheWorkspace(selector), {
|
|
3278
|
+
path: finished.path,
|
|
3279
|
+
...finished.ino !== void 0 ? { ino: finished.ino } : {}
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
return finished;
|
|
3283
|
+
}
|
|
3284
|
+
/** Same workspace resolution as `getWorkspaceBody`, so the cache key matches what Mountra used. */
|
|
3285
|
+
cacheWorkspace(selector) {
|
|
3286
|
+
return {
|
|
3287
|
+
workspaceCuid: selector.workspaceCuid ?? this.clientSelector.workspaceCuid,
|
|
3288
|
+
workspaceId: selector.workspaceId ?? this.clientSelector.workspaceId
|
|
3289
|
+
};
|
|
1602
3290
|
}
|
|
1603
3291
|
endpoint(path) {
|
|
1604
3292
|
return `${this.baseUrl}${this.apiPrefix}${path}`;
|
|
@@ -1638,9 +3326,13 @@ function createMountraClient(options) {
|
|
|
1638
3326
|
export {
|
|
1639
3327
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
1640
3328
|
DownloadHttpError,
|
|
3329
|
+
InMemoryDownloadCacheStore,
|
|
1641
3330
|
InMemoryDownloadCheckpointStore,
|
|
1642
3331
|
InMemoryJumboChunkCheckpointStore,
|
|
3332
|
+
InMemoryMetadataCacheStore,
|
|
3333
|
+
IndexedDBDownloadCacheStore,
|
|
1643
3334
|
IndexedDBDownloadCheckpointStore,
|
|
3335
|
+
IndexedDBMetadataCacheStore,
|
|
1644
3336
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
1645
3337
|
LocalStorageJumboChunkCheckpointStore,
|
|
1646
3338
|
MAX_S3_MULTIPART_PARTS,
|