@mountra/mountra-sdk 0.1.0 → 0.2.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 +19 -0
- package/README.md +62 -2
- package/dist/index.cjs +965 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +211 -1
- package/dist/index.d.ts +211 -1
- package/dist/index.js +960 -12
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,3 +1,913 @@
|
|
|
1
|
+
// src/download.ts
|
|
2
|
+
function copyCheckpoint(checkpoint) {
|
|
3
|
+
return {
|
|
4
|
+
...checkpoint,
|
|
5
|
+
request: { ...checkpoint.request },
|
|
6
|
+
chunks: checkpoint.chunks.map((chunk) => ({ ...chunk }))
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
var InMemoryDownloadCheckpointStore = class {
|
|
10
|
+
records = /* @__PURE__ */ new Map();
|
|
11
|
+
async load(key) {
|
|
12
|
+
const value = this.records.get(key);
|
|
13
|
+
return value ? copyCheckpoint(value) : null;
|
|
14
|
+
}
|
|
15
|
+
async save(key, checkpoint) {
|
|
16
|
+
this.records.set(key, copyCheckpoint(checkpoint));
|
|
17
|
+
}
|
|
18
|
+
async remove(key) {
|
|
19
|
+
this.records.delete(key);
|
|
20
|
+
}
|
|
21
|
+
async list() {
|
|
22
|
+
return [...this.records.values()].map(copyCheckpoint);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var DOWNLOAD_DATABASE_NAME = "mountra-sdk";
|
|
26
|
+
var DOWNLOAD_DATABASE_VERSION = 1;
|
|
27
|
+
var DOWNLOAD_STORE_NAME = "download-checkpoints";
|
|
28
|
+
function getIndexedDb() {
|
|
29
|
+
return globalThis.indexedDB ?? null;
|
|
30
|
+
}
|
|
31
|
+
var IndexedDBDownloadCheckpointStore = class {
|
|
32
|
+
fallback = new InMemoryDownloadCheckpointStore();
|
|
33
|
+
databasePromise;
|
|
34
|
+
async load(key) {
|
|
35
|
+
const database = await this.openDatabase();
|
|
36
|
+
if (!database) {
|
|
37
|
+
return this.fallback.load(key);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const value = await this.request(
|
|
41
|
+
database,
|
|
42
|
+
"readonly",
|
|
43
|
+
(store) => store.get(key)
|
|
44
|
+
);
|
|
45
|
+
return value ? copyCheckpoint(value) : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return this.fallback.load(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async save(key, checkpoint) {
|
|
51
|
+
const value = copyCheckpoint(checkpoint);
|
|
52
|
+
const database = await this.openDatabase();
|
|
53
|
+
if (!database) {
|
|
54
|
+
await this.fallback.save(key, value);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await this.request(database, "readwrite", (store) => store.put(value, key));
|
|
59
|
+
} catch {
|
|
60
|
+
await this.fallback.save(key, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async remove(key) {
|
|
64
|
+
const database = await this.openDatabase();
|
|
65
|
+
if (!database) {
|
|
66
|
+
await this.fallback.remove(key);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await this.request(database, "readwrite", (store) => store.delete(key));
|
|
71
|
+
} catch {
|
|
72
|
+
await this.fallback.remove(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async list() {
|
|
76
|
+
const database = await this.openDatabase();
|
|
77
|
+
if (!database) {
|
|
78
|
+
return this.fallback.list();
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const values = await this.request(
|
|
82
|
+
database,
|
|
83
|
+
"readonly",
|
|
84
|
+
(store) => store.getAll()
|
|
85
|
+
);
|
|
86
|
+
return values.map(copyCheckpoint);
|
|
87
|
+
} catch {
|
|
88
|
+
return this.fallback.list();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async openDatabase() {
|
|
92
|
+
if (!this.databasePromise) {
|
|
93
|
+
const indexedDb = getIndexedDb();
|
|
94
|
+
if (!indexedDb) {
|
|
95
|
+
this.databasePromise = Promise.resolve(null);
|
|
96
|
+
} else {
|
|
97
|
+
this.databasePromise = new Promise((resolve) => {
|
|
98
|
+
let request;
|
|
99
|
+
try {
|
|
100
|
+
request = indexedDb.open(DOWNLOAD_DATABASE_NAME, DOWNLOAD_DATABASE_VERSION);
|
|
101
|
+
} catch {
|
|
102
|
+
resolve(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
request.onupgradeneeded = () => {
|
|
106
|
+
if (!request.result.objectStoreNames.contains(DOWNLOAD_STORE_NAME)) {
|
|
107
|
+
request.result.createObjectStore(DOWNLOAD_STORE_NAME);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
request.onsuccess = () => resolve(request.result);
|
|
111
|
+
request.onerror = () => resolve(null);
|
|
112
|
+
request.onblocked = () => resolve(null);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return this.databasePromise;
|
|
117
|
+
}
|
|
118
|
+
request(database, mode, createRequest) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
let transaction;
|
|
121
|
+
let request;
|
|
122
|
+
try {
|
|
123
|
+
transaction = database.transaction(DOWNLOAD_STORE_NAME, mode);
|
|
124
|
+
request = createRequest(transaction.objectStore(DOWNLOAD_STORE_NAME));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
reject(error);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let result;
|
|
130
|
+
request.onsuccess = () => {
|
|
131
|
+
result = request.result;
|
|
132
|
+
};
|
|
133
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
134
|
+
transaction.oncomplete = () => resolve(result);
|
|
135
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB transaction failed"));
|
|
136
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var DownloadHttpError = class extends Error {
|
|
141
|
+
status;
|
|
142
|
+
constructor(status, message) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = "DownloadHttpError";
|
|
145
|
+
this.status = status;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function getShowSaveFilePicker() {
|
|
149
|
+
return globalThis.showSaveFilePicker ?? null;
|
|
150
|
+
}
|
|
151
|
+
function isRecord(value) {
|
|
152
|
+
return typeof value === "object" && value !== null;
|
|
153
|
+
}
|
|
154
|
+
function readSafeNumber(value, field) {
|
|
155
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
156
|
+
throw new TypeError(`Mountra export response field ${field} is invalid`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function readString(value, field) {
|
|
161
|
+
if (typeof value !== "string" || !value) {
|
|
162
|
+
throw new TypeError(`Mountra export response field ${field} is invalid`);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function ensureExportFileResponse(value) {
|
|
167
|
+
if (!isRecord(value)) {
|
|
168
|
+
throw new TypeError("Mountra export response is invalid");
|
|
169
|
+
}
|
|
170
|
+
const response = {
|
|
171
|
+
path: readString(value.path, "path"),
|
|
172
|
+
storageKey: readString(value.storage_key, "storage_key"),
|
|
173
|
+
url: readString(value.url, "url"),
|
|
174
|
+
timeout: readSafeNumber(value.timeout, "timeout"),
|
|
175
|
+
size: readSafeNumber(value.size, "size")
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
new URL(response.url);
|
|
179
|
+
} catch {
|
|
180
|
+
throw new TypeError("Mountra export response field url is invalid");
|
|
181
|
+
}
|
|
182
|
+
if (typeof value.content_type === "string" && value.content_type) {
|
|
183
|
+
response.contentType = value.content_type;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value.chunk_method === "string") {
|
|
186
|
+
response.chunkMethod = value.chunk_method;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value.is_fast_path === "boolean") {
|
|
189
|
+
response.isFastPath = value.is_fast_path;
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(value.chunks)) {
|
|
192
|
+
response.chunks = value.chunks.map((rawChunk, position) => {
|
|
193
|
+
if (!isRecord(rawChunk)) {
|
|
194
|
+
throw new TypeError(`Mountra export response chunk ${position} is invalid`);
|
|
195
|
+
}
|
|
196
|
+
const chunk = {
|
|
197
|
+
index: readSafeNumber(rawChunk.index, `chunks[${position}].index`),
|
|
198
|
+
offset: readSafeNumber(rawChunk.offset, `chunks[${position}].offset`),
|
|
199
|
+
size: readSafeNumber(rawChunk.size, `chunks[${position}].size`),
|
|
200
|
+
storageKey: readString(rawChunk.storage_key, `chunks[${position}].storage_key`),
|
|
201
|
+
url: readString(rawChunk.url, `chunks[${position}].url`)
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
new URL(chunk.url);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new TypeError(`Mountra export response chunk ${position} URL is invalid`);
|
|
207
|
+
}
|
|
208
|
+
return chunk;
|
|
209
|
+
});
|
|
210
|
+
response.chunks.sort((left, right) => left.index - right.index);
|
|
211
|
+
response.chunks.forEach((chunk, index) => {
|
|
212
|
+
if (chunk.index !== index || chunk.offset + chunk.size > response.size || index > 0 && response.chunks[index - 1].offset + response.chunks[index - 1].size !== chunk.offset) {
|
|
213
|
+
throw new TypeError("Mountra export response has an invalid chunk layout");
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const finalChunk = response.chunks[response.chunks.length - 1];
|
|
217
|
+
if (finalChunk && finalChunk.offset + finalChunk.size !== response.size) {
|
|
218
|
+
throw new TypeError("Mountra export response chunks do not cover the complete file");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return response;
|
|
222
|
+
}
|
|
223
|
+
function validateRetryOption(value, name) {
|
|
224
|
+
if (value === void 0) {
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
228
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
function throwIfAborted(signal) {
|
|
233
|
+
if (signal?.aborted) {
|
|
234
|
+
throw new DOMException("The download was aborted", "AbortError");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function isAbortError(error, signal) {
|
|
238
|
+
return signal?.aborted === true || error instanceof DOMException && error.name === "AbortError";
|
|
239
|
+
}
|
|
240
|
+
function isRetryableError(error) {
|
|
241
|
+
if (error instanceof DownloadHttpError) {
|
|
242
|
+
return [408, 425, 429, 500, 502, 503, 504].includes(error.status);
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function waitForRetry(delayMs, signal) {
|
|
247
|
+
if (delayMs <= 0) {
|
|
248
|
+
throwIfAborted(signal);
|
|
249
|
+
return Promise.resolve();
|
|
250
|
+
}
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
let timer;
|
|
253
|
+
const onAbort = () => {
|
|
254
|
+
if (timer !== void 0) {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
}
|
|
257
|
+
signal?.removeEventListener("abort", onAbort);
|
|
258
|
+
reject(new DOMException("The download was aborted", "AbortError"));
|
|
259
|
+
};
|
|
260
|
+
timer = setTimeout(() => {
|
|
261
|
+
signal?.removeEventListener("abort", onAbort);
|
|
262
|
+
resolve();
|
|
263
|
+
}, delayMs);
|
|
264
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
265
|
+
if (signal?.aborted) {
|
|
266
|
+
onAbort();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
var SpeedWindow = class {
|
|
271
|
+
samples = [];
|
|
272
|
+
record(bytes, at) {
|
|
273
|
+
this.samples.push({ at, bytes });
|
|
274
|
+
const cutoff = at - 1e3;
|
|
275
|
+
while (this.samples.length > 1 && this.samples[0].at < cutoff) {
|
|
276
|
+
this.samples.shift();
|
|
277
|
+
}
|
|
278
|
+
const first = this.samples[0];
|
|
279
|
+
if (!first || first.bytes === bytes) {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
const seconds = Math.max((at - first.at) / 1e3, 1e-3);
|
|
283
|
+
return (bytes - first.bytes) / seconds;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
var BlobDownloadSink = class {
|
|
287
|
+
parts = [];
|
|
288
|
+
async write(bytes) {
|
|
289
|
+
this.parts.push(bytes.slice());
|
|
290
|
+
}
|
|
291
|
+
async flush() {
|
|
292
|
+
}
|
|
293
|
+
async finalize() {
|
|
294
|
+
}
|
|
295
|
+
blob(contentType) {
|
|
296
|
+
const buffers = this.parts.map((part) => {
|
|
297
|
+
const buffer = new ArrayBuffer(part.byteLength);
|
|
298
|
+
new Uint8Array(buffer).set(part);
|
|
299
|
+
return buffer;
|
|
300
|
+
});
|
|
301
|
+
return new Blob(buffers, contentType ? { type: contentType } : void 0);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var FileSystemDownloadSink = class {
|
|
305
|
+
constructor(handle) {
|
|
306
|
+
this.handle = handle;
|
|
307
|
+
}
|
|
308
|
+
handle;
|
|
309
|
+
writable = null;
|
|
310
|
+
position = 0;
|
|
311
|
+
openedOnce = false;
|
|
312
|
+
async write(bytes, position) {
|
|
313
|
+
if (!this.writable) {
|
|
314
|
+
const keepExistingData = this.openedOnce || position > 0;
|
|
315
|
+
this.writable = await this.handle.createWritable({ keepExistingData });
|
|
316
|
+
if (position > 0) {
|
|
317
|
+
await this.writable.seek(position);
|
|
318
|
+
}
|
|
319
|
+
this.position = position;
|
|
320
|
+
this.openedOnce = true;
|
|
321
|
+
}
|
|
322
|
+
if (this.position !== position) {
|
|
323
|
+
throw new Error("File System Access writer position does not match the download checkpoint");
|
|
324
|
+
}
|
|
325
|
+
await this.writable.write(bytes);
|
|
326
|
+
this.position += bytes.byteLength;
|
|
327
|
+
}
|
|
328
|
+
async flush() {
|
|
329
|
+
if (this.writable) {
|
|
330
|
+
const writable = this.writable;
|
|
331
|
+
await writable.close();
|
|
332
|
+
this.writable = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async finalize(size) {
|
|
336
|
+
if (!this.writable) {
|
|
337
|
+
this.writable = await this.handle.createWritable({ keepExistingData: this.openedOnce });
|
|
338
|
+
this.openedOnce = true;
|
|
339
|
+
await this.writable.seek(size);
|
|
340
|
+
}
|
|
341
|
+
await this.writable.truncate(size);
|
|
342
|
+
const writable = this.writable;
|
|
343
|
+
this.writable = null;
|
|
344
|
+
await writable.close();
|
|
345
|
+
}
|
|
346
|
+
async abort() {
|
|
347
|
+
if (!this.writable) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const writable = this.writable;
|
|
351
|
+
this.writable = null;
|
|
352
|
+
if (writable.abort) {
|
|
353
|
+
await writable.abort();
|
|
354
|
+
} else {
|
|
355
|
+
await writable.close();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
function downloadRequestFromOptions(options) {
|
|
360
|
+
if (options.path === void 0 && options.ino === void 0) {
|
|
361
|
+
throw new TypeError("download target requires path or ino");
|
|
362
|
+
}
|
|
363
|
+
if (options.path !== void 0 && !options.path.trim()) {
|
|
364
|
+
throw new TypeError("download path cannot be empty");
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
...options.workspaceCuid !== void 0 ? { workspaceCuid: options.workspaceCuid } : {},
|
|
368
|
+
...options.workspaceId !== void 0 ? { workspaceId: options.workspaceId } : {},
|
|
369
|
+
...options.path !== void 0 ? { path: options.path } : {},
|
|
370
|
+
...options.ino !== void 0 ? { ino: options.ino } : {},
|
|
371
|
+
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
372
|
+
...options.contentDisposition !== void 0 ? { contentDisposition: options.contentDisposition } : {},
|
|
373
|
+
...options.downloadFilename !== void 0 ? { downloadFilename: options.downloadFilename } : {}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function requestBodyFromCheckpoint(request) {
|
|
377
|
+
return {
|
|
378
|
+
...request.workspaceCuid !== void 0 ? { workspace_cuid: request.workspaceCuid } : {},
|
|
379
|
+
...request.workspaceId !== void 0 ? { workspace_id: request.workspaceId } : {},
|
|
380
|
+
...request.path !== void 0 ? { path: request.path } : {},
|
|
381
|
+
...request.ino !== void 0 ? { ino: request.ino } : {},
|
|
382
|
+
...request.timeout !== void 0 ? { timeout: request.timeout } : {},
|
|
383
|
+
...request.contentDisposition !== void 0 ? { content_disposition: request.contentDisposition } : {},
|
|
384
|
+
...request.downloadFilename !== void 0 ? { download_filename: request.downloadFilename } : {}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function requestKey(request, baseUrl) {
|
|
388
|
+
return encodeURIComponent(
|
|
389
|
+
JSON.stringify({
|
|
390
|
+
baseUrl,
|
|
391
|
+
workspaceCuid: request.workspaceCuid ?? null,
|
|
392
|
+
workspaceId: request.workspaceId ?? null,
|
|
393
|
+
path: request.path ?? null,
|
|
394
|
+
ino: request.ino ?? null
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function buildDownloadChunks(exported, useClientAssembly) {
|
|
399
|
+
if (useClientAssembly && exported.chunks && exported.chunks.length > 1) {
|
|
400
|
+
return exported.chunks;
|
|
401
|
+
}
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
index: 0,
|
|
405
|
+
offset: 0,
|
|
406
|
+
size: exported.size,
|
|
407
|
+
storageKey: exported.storageKey,
|
|
408
|
+
url: exported.url
|
|
409
|
+
}
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
function buildDownloadKey(exported, chunks, baseUrl, request) {
|
|
413
|
+
return encodeURIComponent(
|
|
414
|
+
JSON.stringify({
|
|
415
|
+
request: requestKey(request, baseUrl),
|
|
416
|
+
path: exported.path,
|
|
417
|
+
size: exported.size,
|
|
418
|
+
chunks: chunks.map((chunk) => [chunk.index, chunk.offset, chunk.size, chunk.storageKey])
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
function checkpointMatches(checkpoint, exported, chunks) {
|
|
423
|
+
if (checkpoint.version !== 1 || checkpoint.path !== exported.path || checkpoint.size !== exported.size) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
if (checkpoint.chunks.length !== chunks.length) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return checkpoint.chunks.every((saved, index) => {
|
|
430
|
+
const current = chunks[index];
|
|
431
|
+
return Boolean(
|
|
432
|
+
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
|
+
);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function createCheckpoint(key, baseUrl, request, exported, chunks, fileHandle) {
|
|
437
|
+
return {
|
|
438
|
+
version: 1,
|
|
439
|
+
key,
|
|
440
|
+
requestKey: requestKey(request, baseUrl),
|
|
441
|
+
request: { ...request },
|
|
442
|
+
path: exported.path,
|
|
443
|
+
size: exported.size,
|
|
444
|
+
chunks: chunks.map((chunk) => ({
|
|
445
|
+
index: chunk.index,
|
|
446
|
+
offset: chunk.offset,
|
|
447
|
+
size: chunk.size,
|
|
448
|
+
storageKey: chunk.storageKey,
|
|
449
|
+
downloadedBytes: 0,
|
|
450
|
+
completed: chunk.size === 0
|
|
451
|
+
})),
|
|
452
|
+
...fileHandle ? { fileHandle } : {},
|
|
453
|
+
updatedAt: Date.now()
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function totalDownloadedBytes(checkpoint) {
|
|
457
|
+
return checkpoint.chunks.reduce((total, chunk) => total + chunk.downloadedBytes, 0);
|
|
458
|
+
}
|
|
459
|
+
function completedChunkCount(checkpoint) {
|
|
460
|
+
return checkpoint.chunks.filter((chunk) => chunk.completed).length;
|
|
461
|
+
}
|
|
462
|
+
function defaultFilename(exported, requested) {
|
|
463
|
+
const candidate = requested?.trim() || exported.path.split("/").filter(Boolean).pop() || "download";
|
|
464
|
+
return candidate.replace(/[\\/\r\n]/g, "_") || "download";
|
|
465
|
+
}
|
|
466
|
+
function triggerBrowserDownload(blob, filename) {
|
|
467
|
+
const documentValue = globalThis.document;
|
|
468
|
+
const urlApi = globalThis.URL;
|
|
469
|
+
if (!documentValue || !urlApi?.createObjectURL) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const url = urlApi.createObjectURL(blob);
|
|
473
|
+
const anchor = documentValue.createElement("a");
|
|
474
|
+
anchor.href = url;
|
|
475
|
+
anchor.download = filename;
|
|
476
|
+
anchor.style.display = "none";
|
|
477
|
+
documentValue.body?.appendChild(anchor);
|
|
478
|
+
anchor.click();
|
|
479
|
+
anchor.remove();
|
|
480
|
+
setTimeout(() => urlApi.revokeObjectURL(url), 0);
|
|
481
|
+
}
|
|
482
|
+
var MountraDownloader = class {
|
|
483
|
+
transport;
|
|
484
|
+
constructor(transport) {
|
|
485
|
+
this.transport = transport;
|
|
486
|
+
}
|
|
487
|
+
async exportFile(options) {
|
|
488
|
+
const request = downloadRequestFromOptions(options);
|
|
489
|
+
return ensureExportFileResponse(
|
|
490
|
+
await this.transport.requestJson(
|
|
491
|
+
"/file/export",
|
|
492
|
+
requestBodyFromCheckpoint(request),
|
|
493
|
+
options.signal
|
|
494
|
+
)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
async downloadFile(options) {
|
|
498
|
+
const preferFileSystemAccess = options.preferFileSystemAccess !== false;
|
|
499
|
+
const fileHandle = options.fileHandle ?? (preferFileSystemAccess && getShowSaveFilePicker() ? await this.pickSaveFile(options.downloadFilename ?? options.path?.split("/").filter(Boolean).pop()) : void 0);
|
|
500
|
+
const exported = await this.exportFile(options);
|
|
501
|
+
return this.downloadExport(exported, { ...options, ...fileHandle ? { fileHandle } : {} });
|
|
502
|
+
}
|
|
503
|
+
async downloadExport(exported, options = { path: exported.path }) {
|
|
504
|
+
let fileHandle = options.fileHandle;
|
|
505
|
+
if (!fileHandle && options.preferFileSystemAccess !== false && getShowSaveFilePicker()) {
|
|
506
|
+
fileHandle = await this.pickSaveFile(
|
|
507
|
+
options.downloadFilename ?? defaultFilename(exported),
|
|
508
|
+
exported.contentType
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const request = downloadRequestFromOptions(
|
|
512
|
+
options.path === void 0 && options.ino === void 0 ? { ...options, path: exported.path } : options
|
|
513
|
+
);
|
|
514
|
+
return this.downloadResolved(exported, request, options, fileHandle, false);
|
|
515
|
+
}
|
|
516
|
+
async resumeDownload(downloadKey, options = {}) {
|
|
517
|
+
if (!downloadKey.trim()) {
|
|
518
|
+
throw new TypeError("downloadKey is required");
|
|
519
|
+
}
|
|
520
|
+
const checkpoint = await this.transport.checkpointStore.load(downloadKey);
|
|
521
|
+
if (!checkpoint || !checkpoint.fileHandle) {
|
|
522
|
+
throw new Error("No resumable File System Access download was found");
|
|
523
|
+
}
|
|
524
|
+
await this.ensureWritePermission(checkpoint.fileHandle);
|
|
525
|
+
const exported = await this.exportFile({
|
|
526
|
+
...checkpoint.request,
|
|
527
|
+
signal: options.signal
|
|
528
|
+
});
|
|
529
|
+
return this.downloadResolved(
|
|
530
|
+
exported,
|
|
531
|
+
checkpoint.request,
|
|
532
|
+
options,
|
|
533
|
+
checkpoint.fileHandle,
|
|
534
|
+
true
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
listResumableDownloads() {
|
|
538
|
+
return this.transport.checkpointStore.list();
|
|
539
|
+
}
|
|
540
|
+
clearDownloadCheckpoint(downloadKey) {
|
|
541
|
+
return this.transport.checkpointStore.remove(downloadKey);
|
|
542
|
+
}
|
|
543
|
+
async pickSaveFile(requestedFilename, contentType) {
|
|
544
|
+
const picker = getShowSaveFilePicker();
|
|
545
|
+
if (!picker) {
|
|
546
|
+
throw new Error("File System Access API is not available");
|
|
547
|
+
}
|
|
548
|
+
const types = contentType ? [{ description: contentType, accept: { [contentType]: ["." + (requestedFilename?.split(".").pop() || "bin")] } }] : void 0;
|
|
549
|
+
return picker({
|
|
550
|
+
suggestedName: requestedFilename?.trim() || "download",
|
|
551
|
+
...types ? { types } : {}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async ensureWritePermission(handle) {
|
|
555
|
+
if (!handle.queryPermission && !handle.requestPermission) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const descriptor = { mode: "readwrite" };
|
|
559
|
+
const current = await handle.queryPermission?.(descriptor);
|
|
560
|
+
if (current === "granted") {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const requested = await handle.requestPermission?.(descriptor);
|
|
564
|
+
if (requested !== void 0 && requested !== "granted") {
|
|
565
|
+
throw new DOMException("Write permission was not granted", "NotAllowedError");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async downloadResolved(exported, request, options, fileHandle, forceResume) {
|
|
569
|
+
const useClientAssembly = Boolean(fileHandle && (exported.chunks?.length ?? 0) > 1);
|
|
570
|
+
const chunks = buildDownloadChunks(exported, useClientAssembly);
|
|
571
|
+
const key = buildDownloadKey(exported, chunks, this.transport.baseUrl, request);
|
|
572
|
+
const existing = await this.transport.checkpointStore.load(key);
|
|
573
|
+
const resumeRequested = !("resume" in options) || options.resume !== false;
|
|
574
|
+
let checkpoint = existing && checkpointMatches(existing, exported, chunks) && (forceResume || resumeRequested) && (forceResume || fileHandle && await this.isSameFileHandle(existing.fileHandle, fileHandle)) ? existing : createCheckpoint(key, this.transport.baseUrl, request, exported, chunks, fileHandle);
|
|
575
|
+
if (fileHandle) {
|
|
576
|
+
checkpoint.fileHandle = fileHandle;
|
|
577
|
+
}
|
|
578
|
+
const resumed = totalDownloadedBytes(checkpoint) > 0;
|
|
579
|
+
const maxRetries = validateRetryOption(options.maxRetries, "maxRetries") ?? 3;
|
|
580
|
+
const retryDelayMs = validateRetryOption(options.retryDelayMs, "retryDelayMs") ?? 500;
|
|
581
|
+
const sink = fileHandle ? new FileSystemDownloadSink(fileHandle) : new BlobDownloadSink();
|
|
582
|
+
const speedWindow = new SpeedWindow();
|
|
583
|
+
const startedAt = Date.now();
|
|
584
|
+
let retryCount = 0;
|
|
585
|
+
let lastCheckpointAt = 0;
|
|
586
|
+
const emit = (phase, currentChunkIndex) => {
|
|
587
|
+
const downloadedBytes = totalDownloadedBytes(checkpoint);
|
|
588
|
+
const speedBytesPerSecond = speedWindow.record(downloadedBytes, Date.now());
|
|
589
|
+
const remainingBytes = Math.max(exported.size - downloadedBytes, 0);
|
|
590
|
+
options.onProgress?.({
|
|
591
|
+
phase,
|
|
592
|
+
downloadedBytes,
|
|
593
|
+
totalBytes: exported.size,
|
|
594
|
+
downloadedChunks: completedChunkCount(checkpoint),
|
|
595
|
+
totalChunks: checkpoint.chunks.length,
|
|
596
|
+
...currentChunkIndex !== void 0 ? { currentChunkIndex } : {},
|
|
597
|
+
speedBytesPerSecond,
|
|
598
|
+
...speedBytesPerSecond > 0 && remainingBytes > 0 ? { estimatedRemainingSeconds: remainingBytes / speedBytesPerSecond } : {},
|
|
599
|
+
elapsedMs: Date.now() - startedAt,
|
|
600
|
+
retryCount
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
const persist = async (force) => {
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
if (!force && now - lastCheckpointAt < 1e3) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
await sink.flush();
|
|
609
|
+
checkpoint.updatedAt = now;
|
|
610
|
+
await this.transport.checkpointStore.save(key, checkpoint);
|
|
611
|
+
lastCheckpointAt = now;
|
|
612
|
+
};
|
|
613
|
+
await this.transport.checkpointStore.save(key, checkpoint);
|
|
614
|
+
try {
|
|
615
|
+
for (const chunk of chunks) {
|
|
616
|
+
const savedChunk = checkpoint.chunks[chunk.index];
|
|
617
|
+
if (!savedChunk) {
|
|
618
|
+
throw new Error(`Missing download checkpoint for chunk ${chunk.index}`);
|
|
619
|
+
}
|
|
620
|
+
if (savedChunk.completed) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
let attempt = 0;
|
|
624
|
+
while (true) {
|
|
625
|
+
throwIfAborted(options.signal);
|
|
626
|
+
const rangeStart = savedChunk.downloadedBytes;
|
|
627
|
+
try {
|
|
628
|
+
const response = await this.transport.fetchImpl(chunk.url, {
|
|
629
|
+
method: "GET",
|
|
630
|
+
...rangeStart > 0 ? { headers: { Range: `bytes=${rangeStart}-` } } : {},
|
|
631
|
+
...options.signal ? { signal: options.signal } : {}
|
|
632
|
+
});
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
throw new DownloadHttpError(
|
|
635
|
+
response.status,
|
|
636
|
+
`Download for chunk ${chunk.index} failed with HTTP ${response.status}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
await this.consumeResponse(
|
|
640
|
+
response,
|
|
641
|
+
chunk,
|
|
642
|
+
savedChunk,
|
|
643
|
+
sink,
|
|
644
|
+
exported.size,
|
|
645
|
+
emit,
|
|
646
|
+
persist,
|
|
647
|
+
options.signal
|
|
648
|
+
);
|
|
649
|
+
if (savedChunk.downloadedBytes !== savedChunk.size) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`Download for chunk ${chunk.index} ended at ${savedChunk.downloadedBytes} of ${savedChunk.size} bytes`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
savedChunk.completed = true;
|
|
655
|
+
await persist(true);
|
|
656
|
+
emit("downloading", chunk.index);
|
|
657
|
+
break;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
await persist(true);
|
|
660
|
+
if (isAbortError(error, options.signal) || !isRetryableError(error) || attempt >= maxRetries) {
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
attempt += 1;
|
|
664
|
+
retryCount += 1;
|
|
665
|
+
emit("retrying", chunk.index);
|
|
666
|
+
await waitForRetry(Math.min(retryDelayMs * 2 ** (attempt - 1), 3e4), options.signal);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (totalDownloadedBytes(checkpoint) !== exported.size) {
|
|
671
|
+
throw new Error("Download completed with an unexpected byte count");
|
|
672
|
+
}
|
|
673
|
+
if (fileHandle) {
|
|
674
|
+
await sink.finalize(exported.size);
|
|
675
|
+
} else {
|
|
676
|
+
await sink.finalize();
|
|
677
|
+
}
|
|
678
|
+
await this.transport.checkpointStore.remove(key);
|
|
679
|
+
emit("completed");
|
|
680
|
+
const blob = sink instanceof BlobDownloadSink ? sink.blob(exported.contentType) : void 0;
|
|
681
|
+
if (blob) {
|
|
682
|
+
const filename = "downloadFilename" in options ? options.downloadFilename : void 0;
|
|
683
|
+
triggerBrowserDownload(blob, defaultFilename(exported, filename));
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
export: exported,
|
|
687
|
+
downloadKey: key,
|
|
688
|
+
downloadedBytes: exported.size,
|
|
689
|
+
resumed,
|
|
690
|
+
...fileHandle ? { fileHandle } : {},
|
|
691
|
+
...blob ? { blob } : {}
|
|
692
|
+
};
|
|
693
|
+
} catch (error) {
|
|
694
|
+
await persist(true);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async consumeResponse(response, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
699
|
+
const sourceOffset = savedChunk.downloadedBytes;
|
|
700
|
+
let bytesToSkip = response.status === 200 && sourceOffset > 0 ? sourceOffset : 0;
|
|
701
|
+
const reader = response.body?.getReader();
|
|
702
|
+
if (!reader) {
|
|
703
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
704
|
+
await this.consumeBytes(body, bytesToSkip, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
while (true) {
|
|
709
|
+
throwIfAborted(signal);
|
|
710
|
+
const next = await reader.read();
|
|
711
|
+
if (next.done) {
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
const bytes = next.value;
|
|
715
|
+
if (bytesToSkip > 0) {
|
|
716
|
+
const skipped = Math.min(bytesToSkip, bytes.byteLength);
|
|
717
|
+
bytesToSkip -= skipped;
|
|
718
|
+
if (skipped === bytes.byteLength) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
await this.consumeBytes(
|
|
722
|
+
bytes.subarray(skipped),
|
|
723
|
+
0,
|
|
724
|
+
chunk,
|
|
725
|
+
savedChunk,
|
|
726
|
+
sink,
|
|
727
|
+
totalSize,
|
|
728
|
+
emit,
|
|
729
|
+
persist,
|
|
730
|
+
signal
|
|
731
|
+
);
|
|
732
|
+
} else {
|
|
733
|
+
await this.consumeBytes(bytes, 0, chunk, savedChunk, sink, totalSize, emit, persist, signal);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
} finally {
|
|
737
|
+
reader.releaseLock();
|
|
738
|
+
}
|
|
739
|
+
if (bytesToSkip > 0) {
|
|
740
|
+
throw new Error(`Download response did not contain the saved offset for chunk ${chunk.index}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async consumeBytes(bytes, offset, chunk, savedChunk, sink, totalSize, emit, persist, signal) {
|
|
744
|
+
const remaining = chunk.size - savedChunk.downloadedBytes;
|
|
745
|
+
const writableBytes = bytes.byteLength - offset;
|
|
746
|
+
if (writableBytes > remaining) {
|
|
747
|
+
throw new Error(`Download response for chunk ${chunk.index} is larger than expected`);
|
|
748
|
+
}
|
|
749
|
+
if (writableBytes === 0) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
throwIfAborted(signal);
|
|
753
|
+
const payload = bytes.subarray(offset);
|
|
754
|
+
const position = chunk.offset + savedChunk.downloadedBytes;
|
|
755
|
+
if (sink instanceof FileSystemDownloadSink) {
|
|
756
|
+
await sink.write(payload, position);
|
|
757
|
+
} else {
|
|
758
|
+
await sink.write(payload);
|
|
759
|
+
}
|
|
760
|
+
savedChunk.downloadedBytes += payload.byteLength;
|
|
761
|
+
emit("downloading", chunk.index);
|
|
762
|
+
await persist(false);
|
|
763
|
+
if (savedChunk.downloadedBytes > totalSize) {
|
|
764
|
+
throw new Error("Download byte count exceeded the exported file size");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async isSameFileHandle(savedHandle, selectedHandle) {
|
|
768
|
+
if (!savedHandle) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
if (savedHandle === selectedHandle) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
if (savedHandle.isSameEntry) {
|
|
775
|
+
try {
|
|
776
|
+
return await savedHandle.isSameEntry(selectedHandle);
|
|
777
|
+
} catch {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/file.ts
|
|
786
|
+
function normalizeMountraFileLocator(locator, fallbackWorkspace = {}) {
|
|
787
|
+
if (!locator || typeof locator !== "object") {
|
|
788
|
+
throw new TypeError("Mountra file locator is required");
|
|
789
|
+
}
|
|
790
|
+
const hasPath = locator.path !== void 0;
|
|
791
|
+
const hasIno = locator.ino !== void 0;
|
|
792
|
+
if (hasPath === hasIno) {
|
|
793
|
+
throw new TypeError("Mountra file locator requires exactly one of path or ino");
|
|
794
|
+
}
|
|
795
|
+
const workspaceId = locator.workspaceId ?? locator.wsid ?? fallbackWorkspace.workspaceId;
|
|
796
|
+
if (locator.workspaceId !== void 0 && locator.wsid !== void 0 && String(locator.workspaceId) !== String(locator.wsid)) {
|
|
797
|
+
throw new TypeError("workspaceId and wsid must identify the same workspace");
|
|
798
|
+
}
|
|
799
|
+
const workspaceCuid = locator.workspaceCuid ?? fallbackWorkspace.workspaceCuid;
|
|
800
|
+
if (workspaceCuid === void 0 && workspaceId === void 0) {
|
|
801
|
+
throw new TypeError("workspaceCuid, workspaceId, or wsid is required");
|
|
802
|
+
}
|
|
803
|
+
if (hasPath) {
|
|
804
|
+
const path = normalizePath(locator.path);
|
|
805
|
+
return {
|
|
806
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
807
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
808
|
+
path
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
...workspaceCuid !== void 0 ? { workspaceCuid } : {},
|
|
813
|
+
...workspaceId !== void 0 ? { workspaceId } : {},
|
|
814
|
+
ino: normalizeIno(locator.ino)
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
var MountraFile = class {
|
|
818
|
+
constructor(client, locator) {
|
|
819
|
+
this.client = client;
|
|
820
|
+
this.locator = Object.freeze({ ...locator });
|
|
821
|
+
}
|
|
822
|
+
client;
|
|
823
|
+
locator;
|
|
824
|
+
get workspaceCuid() {
|
|
825
|
+
return this.locator.workspaceCuid;
|
|
826
|
+
}
|
|
827
|
+
get workspaceId() {
|
|
828
|
+
return this.locator.workspaceId;
|
|
829
|
+
}
|
|
830
|
+
/** `wsid` is the short alias used by the file-object API. */
|
|
831
|
+
get wsid() {
|
|
832
|
+
return this.locator.workspaceId;
|
|
833
|
+
}
|
|
834
|
+
get path() {
|
|
835
|
+
return "path" in this.locator ? this.locator.path : void 0;
|
|
836
|
+
}
|
|
837
|
+
get ino() {
|
|
838
|
+
return "ino" in this.locator ? this.locator.ino : void 0;
|
|
839
|
+
}
|
|
840
|
+
export(options = {}) {
|
|
841
|
+
return this.client.exportFile({ ...options, ...this.locator });
|
|
842
|
+
}
|
|
843
|
+
download(options = {}) {
|
|
844
|
+
return this.client.downloadFile({ ...options, ...this.locator });
|
|
845
|
+
}
|
|
846
|
+
async resume(options = {}) {
|
|
847
|
+
const { downloadKey, ...resumeOptions } = options;
|
|
848
|
+
const checkpoints = await this.client.listResumableDownloads();
|
|
849
|
+
const candidates = checkpoints.filter(
|
|
850
|
+
(checkpoint2) => checkpoint2.fileHandle && matchesLocator(checkpoint2.request, this.locator)
|
|
851
|
+
);
|
|
852
|
+
const checkpoint = downloadKey ? candidates.find((candidate) => candidate.key === downloadKey) : [...candidates].sort((left, right) => right.updatedAt - left.updatedAt)[0];
|
|
853
|
+
if (!checkpoint) {
|
|
854
|
+
if (downloadKey) {
|
|
855
|
+
throw new Error("Download checkpoint does not belong to this Mountra file");
|
|
856
|
+
}
|
|
857
|
+
throw new Error("No resumable download was found for this Mountra file");
|
|
858
|
+
}
|
|
859
|
+
return this.client.resumeDownload(checkpoint.key, resumeOptions);
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
function matchesLocator(request, locator) {
|
|
863
|
+
if (locator.workspaceCuid !== void 0 && request.workspaceCuid !== locator.workspaceCuid) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
if (locator.workspaceId !== void 0 && String(request.workspaceId) !== String(locator.workspaceId)) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
if ("path" in locator) {
|
|
870
|
+
if (request.path === void 0 || request.ino !== void 0) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
return normalizePath(request.path) === locator.path;
|
|
875
|
+
} catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return String(request.ino) === String(locator.ino) && request.path === void 0;
|
|
880
|
+
}
|
|
881
|
+
function normalizePath(value) {
|
|
882
|
+
if (!value || !value.trim()) {
|
|
883
|
+
throw new TypeError("Mountra file path cannot be empty");
|
|
884
|
+
}
|
|
885
|
+
const raw = value.trim();
|
|
886
|
+
const absolute = raw.startsWith("/") ? raw : `/${raw}`;
|
|
887
|
+
const segments = absolute.split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
888
|
+
const stack = [];
|
|
889
|
+
for (const segment of segments) {
|
|
890
|
+
if (segment === "..") {
|
|
891
|
+
stack.pop();
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
stack.push(segment);
|
|
895
|
+
}
|
|
896
|
+
return stack.length === 0 ? "/" : `/${stack.join("/")}`;
|
|
897
|
+
}
|
|
898
|
+
function normalizeIno(value) {
|
|
899
|
+
if (typeof value === "number") {
|
|
900
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
901
|
+
throw new TypeError("Mountra file ino must be a non-negative safe integer");
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
if (!/^\d+$/.test(value)) {
|
|
906
|
+
throw new TypeError("Mountra file ino must be a non-negative integer string");
|
|
907
|
+
}
|
|
908
|
+
return value;
|
|
909
|
+
}
|
|
910
|
+
|
|
1
911
|
// src/upload.ts
|
|
2
912
|
var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
|
|
3
913
|
var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
|
|
@@ -17,7 +927,7 @@ var MountraApiError = class extends Error {
|
|
|
17
927
|
};
|
|
18
928
|
var memoryCheckpointRecords = /* @__PURE__ */ new Map();
|
|
19
929
|
function parseJumboCheckpoint(value) {
|
|
20
|
-
if (!
|
|
930
|
+
if (!isRecord2(value) || value.version !== 1 || typeof value.storageKey !== "string") {
|
|
21
931
|
return null;
|
|
22
932
|
}
|
|
23
933
|
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)) {
|
|
@@ -25,7 +935,7 @@ function parseJumboCheckpoint(value) {
|
|
|
25
935
|
}
|
|
26
936
|
const parts = [];
|
|
27
937
|
for (const part of value.parts) {
|
|
28
|
-
if (!
|
|
938
|
+
if (!isRecord2(part)) {
|
|
29
939
|
return null;
|
|
30
940
|
}
|
|
31
941
|
if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
|
|
@@ -198,17 +1108,17 @@ function getWorkspaceBody(clientSelector, requestSelector) {
|
|
|
198
1108
|
...workspaceId !== void 0 ? { workspace_id: workspaceId } : {}
|
|
199
1109
|
};
|
|
200
1110
|
}
|
|
201
|
-
function
|
|
1111
|
+
function throwIfAborted2(signal) {
|
|
202
1112
|
if (!signal?.aborted) {
|
|
203
1113
|
return;
|
|
204
1114
|
}
|
|
205
1115
|
throw new DOMException("The upload was aborted", "AbortError");
|
|
206
1116
|
}
|
|
207
|
-
function
|
|
1117
|
+
function isRecord2(value) {
|
|
208
1118
|
return typeof value === "object" && value !== null;
|
|
209
1119
|
}
|
|
210
1120
|
function parseApiErrorBody(body) {
|
|
211
|
-
if (!
|
|
1121
|
+
if (!isRecord2(body)) {
|
|
212
1122
|
return {};
|
|
213
1123
|
}
|
|
214
1124
|
return {
|
|
@@ -217,7 +1127,7 @@ function parseApiErrorBody(body) {
|
|
|
217
1127
|
};
|
|
218
1128
|
}
|
|
219
1129
|
function ensurePrepareResponse(value) {
|
|
220
|
-
if (!
|
|
1130
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
221
1131
|
throw new TypeError("Mountra prepare response is invalid");
|
|
222
1132
|
}
|
|
223
1133
|
if (!Array.isArray(value.upload_targets)) {
|
|
@@ -226,13 +1136,13 @@ function ensurePrepareResponse(value) {
|
|
|
226
1136
|
return value;
|
|
227
1137
|
}
|
|
228
1138
|
function ensureFinishResponse(value) {
|
|
229
|
-
if (!
|
|
1139
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
230
1140
|
throw new TypeError("Mountra finish response is invalid");
|
|
231
1141
|
}
|
|
232
1142
|
return value;
|
|
233
1143
|
}
|
|
234
1144
|
function ensureUploadStatusResponse(value) {
|
|
235
|
-
if (!
|
|
1145
|
+
if (!isRecord2(value) || typeof value.upload_id !== "string") {
|
|
236
1146
|
throw new TypeError("Mountra upload status response is invalid");
|
|
237
1147
|
}
|
|
238
1148
|
if (!Array.isArray(value.chunks)) {
|
|
@@ -283,6 +1193,7 @@ var MountraClient = class {
|
|
|
283
1193
|
jumboChunkUploader;
|
|
284
1194
|
jumboChunkCheckpointStore;
|
|
285
1195
|
jumboPartSize;
|
|
1196
|
+
downloader;
|
|
286
1197
|
constructor(options) {
|
|
287
1198
|
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
288
1199
|
this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
|
|
@@ -301,6 +1212,38 @@ var MountraClient = class {
|
|
|
301
1212
|
throw new Error("fetch is not available; provide fetch in MountraClientOptions");
|
|
302
1213
|
}
|
|
303
1214
|
this.fetchImpl = fetchImpl;
|
|
1215
|
+
this.downloader = new MountraDownloader({
|
|
1216
|
+
baseUrl: this.baseUrl,
|
|
1217
|
+
clientSelector: this.clientSelector,
|
|
1218
|
+
fetchImpl: this.fetchImpl,
|
|
1219
|
+
checkpointStore: options.downloadCheckpointStore ?? new IndexedDBDownloadCheckpointStore(),
|
|
1220
|
+
requestJson: (path, body, signal) => this.requestJson(path, body, signal)
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
async exportFile(options) {
|
|
1224
|
+
return this.downloader.exportFile(options);
|
|
1225
|
+
}
|
|
1226
|
+
async downloadFile(options) {
|
|
1227
|
+
return this.downloader.downloadFile(options);
|
|
1228
|
+
}
|
|
1229
|
+
async downloadExport(exported, options) {
|
|
1230
|
+
return this.downloader.downloadExport(exported, options);
|
|
1231
|
+
}
|
|
1232
|
+
async resumeDownload(downloadKey, options) {
|
|
1233
|
+
return this.downloader.resumeDownload(downloadKey, options);
|
|
1234
|
+
}
|
|
1235
|
+
listResumableDownloads() {
|
|
1236
|
+
return this.downloader.listResumableDownloads();
|
|
1237
|
+
}
|
|
1238
|
+
clearDownloadCheckpoint(downloadKey) {
|
|
1239
|
+
return this.downloader.clearDownloadCheckpoint(downloadKey);
|
|
1240
|
+
}
|
|
1241
|
+
/** Create a stable resource object for one file in this or another workspace. */
|
|
1242
|
+
file(locator) {
|
|
1243
|
+
return new MountraFile(
|
|
1244
|
+
this,
|
|
1245
|
+
normalizeMountraFileLocator(locator, this.clientSelector)
|
|
1246
|
+
);
|
|
304
1247
|
}
|
|
305
1248
|
async prepareUpload(file, options) {
|
|
306
1249
|
const chunks = splitFileIntoChunks(file, options.chunkCount ?? 1);
|
|
@@ -308,7 +1251,7 @@ var MountraClient = class {
|
|
|
308
1251
|
const contentHash = await sha256Hex(file);
|
|
309
1252
|
const chunkMethod = chunks.length === 1 ? "none" : "fix";
|
|
310
1253
|
const contentType = options.contentType ?? ("type" in file ? file.type : void 0);
|
|
311
|
-
|
|
1254
|
+
throwIfAborted2(options.signal);
|
|
312
1255
|
const response = ensurePrepareResponse(
|
|
313
1256
|
await this.requestJson("/upload/prepare", {
|
|
314
1257
|
...getWorkspaceBody(this.clientSelector, options),
|
|
@@ -411,7 +1354,7 @@ var MountraClient = class {
|
|
|
411
1354
|
if (!chunk || target.chunk_index !== position) {
|
|
412
1355
|
throw new Error("Mountra upload targets must contain contiguous chunk indexes");
|
|
413
1356
|
}
|
|
414
|
-
|
|
1357
|
+
throwIfAborted2(options.signal);
|
|
415
1358
|
if (isJumboChunk(chunk.size)) {
|
|
416
1359
|
const checkpointKey = this.buildJumboCheckpointKey(context, target, options);
|
|
417
1360
|
jumboCheckpointKeys.add(checkpointKey);
|
|
@@ -454,7 +1397,7 @@ var MountraClient = class {
|
|
|
454
1397
|
totalChunks: orderedTargets.length
|
|
455
1398
|
});
|
|
456
1399
|
}
|
|
457
|
-
|
|
1400
|
+
throwIfAborted2(options.signal);
|
|
458
1401
|
options.onProgress?.({
|
|
459
1402
|
phase: "finishing",
|
|
460
1403
|
uploadedBytes: file.size,
|
|
@@ -558,7 +1501,7 @@ var MountraClient = class {
|
|
|
558
1501
|
let uploadedBytes = 0;
|
|
559
1502
|
let uploadedParts = 0;
|
|
560
1503
|
for (let partNumber = 1; partNumber <= totalParts; partNumber += 1) {
|
|
561
|
-
|
|
1504
|
+
throwIfAborted2(options.signal);
|
|
562
1505
|
const start = (partNumber - 1) * this.jumboPartSize;
|
|
563
1506
|
const partBlob = chunk.blob.slice(start, Math.min(start + this.jumboPartSize, chunk.size));
|
|
564
1507
|
const partHash = await sha256Hex(partBlob);
|
|
@@ -694,13 +1637,18 @@ function createMountraClient(options) {
|
|
|
694
1637
|
}
|
|
695
1638
|
export {
|
|
696
1639
|
DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
|
|
1640
|
+
DownloadHttpError,
|
|
1641
|
+
InMemoryDownloadCheckpointStore,
|
|
697
1642
|
InMemoryJumboChunkCheckpointStore,
|
|
1643
|
+
IndexedDBDownloadCheckpointStore,
|
|
698
1644
|
JUMBO_CHUNK_THRESHOLD_BYTES,
|
|
699
1645
|
LocalStorageJumboChunkCheckpointStore,
|
|
700
1646
|
MAX_S3_MULTIPART_PARTS,
|
|
701
1647
|
MIN_S3_MULTIPART_PART_SIZE_BYTES,
|
|
702
1648
|
MountraApiError,
|
|
703
1649
|
MountraClient,
|
|
1650
|
+
MountraDownloader,
|
|
1651
|
+
MountraFile,
|
|
704
1652
|
MountraClient as MountraSDK,
|
|
705
1653
|
S3MultipartUploader,
|
|
706
1654
|
createMountraClient,
|