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