trajectories-sh 1.10.1 → 1.10.3
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/dist/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ async function uploadTrajectoryAction(directory, opts) {
|
|
|
81
81
|
Uploading trajectory ${directory} \u2192 ${chalk.cyan(slug)}
|
|
82
82
|
`));
|
|
83
83
|
try {
|
|
84
|
-
const { uploadJob } = await import("./upload-
|
|
84
|
+
const { uploadJob } = await import("./upload-UXM6LEBL.js");
|
|
85
85
|
const result = await uploadJob(directory, {
|
|
86
86
|
slug,
|
|
87
87
|
name: opts.name,
|
|
@@ -10,7 +10,8 @@ import { tmpdir } from "os";
|
|
|
10
10
|
import { randomBytes, createHash } from "crypto";
|
|
11
11
|
var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
|
|
12
12
|
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
13
|
-
var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "
|
|
13
|
+
var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "15");
|
|
14
|
+
var BATCH_SIZE = 100;
|
|
14
15
|
function collectFiles(dir) {
|
|
15
16
|
const files = [];
|
|
16
17
|
function walk(current) {
|
|
@@ -322,7 +323,7 @@ async function uploadJob(dir, opts) {
|
|
|
322
323
|
}
|
|
323
324
|
const { job_id: jobId } = await initResp.json();
|
|
324
325
|
console.log(` Job ID: ${jobId}`);
|
|
325
|
-
await
|
|
326
|
+
await uploadDirect(jobId, files);
|
|
326
327
|
console.log(" Finalizing...");
|
|
327
328
|
const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
|
|
328
329
|
if (!finalResp.ok) {
|
|
@@ -353,149 +354,116 @@ async function uploadJob(dir, opts) {
|
|
|
353
354
|
viewerUrl: `${siteUrl}/t/${jobId}`
|
|
354
355
|
};
|
|
355
356
|
}
|
|
356
|
-
async function
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let lastError = null;
|
|
374
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
375
|
-
if (attempt > 1) {
|
|
376
|
-
console.log(` Retrying archive upload (attempt ${attempt}/${maxAttempts})\u2026`);
|
|
357
|
+
async function uploadDirect(jobId, files) {
|
|
358
|
+
const { ProgressBar } = await import("./progress-H7WNKHDR.js");
|
|
359
|
+
const totalBytes = files.reduce((s, f) => s + f.size, 0);
|
|
360
|
+
const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2191" });
|
|
361
|
+
const failedFiles = [];
|
|
362
|
+
const failureReasons = [];
|
|
363
|
+
for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
|
|
364
|
+
const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
|
|
365
|
+
const urlMap = await fetchSignedUrlsForBatch(jobId, batch.map((f) => f.relPath));
|
|
366
|
+
for (const file of batch) {
|
|
367
|
+
if (!urlMap.has(file.relPath)) {
|
|
368
|
+
failedFiles.push(file.relPath);
|
|
369
|
+
failureReasons.push({
|
|
370
|
+
path: file.relPath,
|
|
371
|
+
reason: "no signed URL from server after retries"
|
|
372
|
+
});
|
|
373
|
+
bar.tick(0);
|
|
377
374
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
375
|
+
}
|
|
376
|
+
const pending = /* @__PURE__ */ new Set();
|
|
377
|
+
for (const file of batch) {
|
|
378
|
+
const signedUrl = urlMap.get(file.relPath);
|
|
379
|
+
if (!signedUrl) continue;
|
|
380
|
+
const task = (async () => {
|
|
381
|
+
const result = await uploadOneFileWithError(file, signedUrl);
|
|
382
|
+
if (result.ok) {
|
|
383
|
+
bar.tick(file.size);
|
|
384
|
+
} else {
|
|
385
|
+
failedFiles.push(file.relPath);
|
|
386
|
+
failureReasons.push({ path: file.relPath, reason: result.error });
|
|
387
|
+
bar.tick(0);
|
|
390
388
|
}
|
|
389
|
+
})().finally(() => {
|
|
390
|
+
pending.delete(task);
|
|
391
|
+
});
|
|
392
|
+
pending.add(task);
|
|
393
|
+
if (pending.size >= CONCURRENCY) {
|
|
394
|
+
await Promise.race(pending);
|
|
391
395
|
}
|
|
392
396
|
}
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
await Promise.all(pending);
|
|
398
|
+
}
|
|
399
|
+
bar.done();
|
|
400
|
+
if (failedFiles.length > 0) {
|
|
401
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
402
|
+
for (const { reason } of failureReasons) {
|
|
403
|
+
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
398
404
|
}
|
|
405
|
+
const reasonLines = [...byReason.entries()].sort((a, b) => b[1] - a[1]).map(([reason, count]) => ` ${count}\xD7 ${reason}`).join("\n");
|
|
406
|
+
const samplePath = failedFiles[0];
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Upload incomplete: ${failedFiles.length}/${files.length} file(s) failed. First failed file: ${samplePath}.
|
|
409
|
+
Failure breakdown:
|
|
410
|
+
${reasonLines}
|
|
411
|
+
Retry the upload or check your network connection.`
|
|
412
|
+
);
|
|
399
413
|
}
|
|
400
414
|
}
|
|
401
|
-
async function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
415
|
+
async function fetchSignedUrlsForBatch(jobId, paths) {
|
|
416
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
417
|
+
let pending = paths.slice();
|
|
418
|
+
const attempts = 4;
|
|
419
|
+
for (let attempt = 0; attempt < attempts && pending.length > 0; attempt++) {
|
|
420
|
+
if (attempt > 0) {
|
|
421
|
+
const wait = 1500 * attempt;
|
|
422
|
+
await sleep(wait);
|
|
423
|
+
}
|
|
424
|
+
const resp = await apiPost(`/api/cli/push/${jobId}/upload-urls`, {
|
|
425
|
+
paths: pending
|
|
426
|
+
});
|
|
427
|
+
if (!resp.ok) {
|
|
428
|
+
throw new Error(`Failed to get upload URLs: ${await resp.text()}`);
|
|
429
|
+
}
|
|
430
|
+
const { urls } = await resp.json();
|
|
431
|
+
const stillPending = [];
|
|
432
|
+
for (const u of urls) {
|
|
433
|
+
if (u.signed_url) {
|
|
434
|
+
urlMap.set(u.path, u.signed_url);
|
|
435
|
+
} else {
|
|
436
|
+
stillPending.push(u.path);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
pending = stillPending;
|
|
411
440
|
}
|
|
412
|
-
|
|
441
|
+
return urlMap;
|
|
442
|
+
}
|
|
443
|
+
async function uploadOneFileWithError(file, signedUrl) {
|
|
444
|
+
const data = readFileSync(file.absPath);
|
|
445
|
+
let lastError = "unknown error";
|
|
413
446
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
414
|
-
const bar = new ProgressBar({ total: 1, totalBytes: archiveSize, label: "\u2191 archive" });
|
|
415
|
-
const stream = createReadStream(archivePath);
|
|
416
|
-
let bytesSent = 0;
|
|
417
|
-
stream.on("data", (chunk) => {
|
|
418
|
-
bytesSent += chunk.length;
|
|
419
|
-
bar.setBytes(bytesSent);
|
|
420
|
-
});
|
|
421
447
|
try {
|
|
422
448
|
const resp = await fetch(signedUrl, {
|
|
423
449
|
method: "PUT",
|
|
424
|
-
headers: {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
},
|
|
428
|
-
// undici accepts Node streams here
|
|
429
|
-
body: stream,
|
|
430
|
-
duplex: "half",
|
|
431
|
-
signal: AbortSignal.timeout(30 * 60 * 1e3)
|
|
450
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
451
|
+
body: data,
|
|
452
|
+
signal: AbortSignal.timeout(12e4)
|
|
432
453
|
});
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (resp.ok) {
|
|
436
|
-
putError = null;
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
putError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
454
|
+
if (resp.ok) return { ok: true };
|
|
455
|
+
lastError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
440
456
|
try {
|
|
441
457
|
const body = await resp.text();
|
|
442
|
-
if (body)
|
|
458
|
+
if (body) lastError += ` \u2014 ${body.slice(0, 150).replace(/\s+/g, " ")}`;
|
|
443
459
|
} catch {
|
|
444
460
|
}
|
|
445
461
|
} catch (e) {
|
|
446
|
-
|
|
447
|
-
putError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
|
|
448
|
-
}
|
|
449
|
-
try {
|
|
450
|
-
stream.destroy?.();
|
|
451
|
-
} catch {
|
|
452
|
-
}
|
|
453
|
-
if (attempt < 2) await sleep(2e3 * (attempt + 1));
|
|
454
|
-
}
|
|
455
|
-
if (putError) {
|
|
456
|
-
throw new Error(`Archive upload failed: ${putError}`);
|
|
457
|
-
}
|
|
458
|
-
const extractResp = await apiPost(`/api/cli/push/${jobId}/extract`, {});
|
|
459
|
-
if (!extractResp.ok) {
|
|
460
|
-
throw new Error(`Failed to start extraction: ${await extractResp.text()}`);
|
|
461
|
-
}
|
|
462
|
-
const extractBar = new ProgressBar({ total: entryCount, totalBytes: 0, label: "\u2699 extract" });
|
|
463
|
-
const pollStart = Date.now();
|
|
464
|
-
const pollTimeoutMs = 30 * 60 * 1e3;
|
|
465
|
-
let lastStatus = null;
|
|
466
|
-
while (Date.now() - pollStart < pollTimeoutMs) {
|
|
467
|
-
await sleep(1500);
|
|
468
|
-
const statusResp = await apiGet(`/api/cli/push/${jobId}/extract-status`);
|
|
469
|
-
if (!statusResp.ok) {
|
|
470
|
-
throw new Error(`Failed to poll extract status: ${await statusResp.text()}`);
|
|
471
|
-
}
|
|
472
|
-
lastStatus = await statusResp.json();
|
|
473
|
-
const uploaded = Number(lastStatus?.uploaded ?? 0);
|
|
474
|
-
const errors = Number(lastStatus?.errors ?? 0);
|
|
475
|
-
extractBar.setCount(uploaded + errors);
|
|
476
|
-
if (lastStatus?.status === "done") break;
|
|
477
|
-
if (lastStatus?.status === "error") {
|
|
478
|
-
throw new Error(`Archive extraction failed: ${lastStatus?.detail ?? "unknown"}`);
|
|
462
|
+
lastError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
|
|
479
463
|
}
|
|
464
|
+
if (attempt < 2) await sleep(1e3 * (attempt + 1));
|
|
480
465
|
}
|
|
481
|
-
|
|
482
|
-
if (!lastStatus || lastStatus.status !== "done") {
|
|
483
|
-
throw new Error("Archive extraction timed out");
|
|
484
|
-
}
|
|
485
|
-
if (lastStatus.errors && lastStatus.errors > 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`Archive extraction completed but ${lastStatus.errors} file(s) failed to store. Retry the upload or check server logs.`
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
async function apiGet(path, timeout = 6e4) {
|
|
492
|
-
const auth = await getAuthHeader();
|
|
493
|
-
if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
|
|
494
|
-
return fetch(`${getApiUrl()}${path}`, {
|
|
495
|
-
method: "GET",
|
|
496
|
-
headers: { Authorization: auth },
|
|
497
|
-
signal: AbortSignal.timeout(timeout)
|
|
498
|
-
});
|
|
466
|
+
return { ok: false, error: lastError };
|
|
499
467
|
}
|
|
500
468
|
function sleep(ms) {
|
|
501
469
|
return new Promise((r) => setTimeout(r, ms));
|