trajectories-sh 1.10.2 → 1.11.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/dist/cli.js +1 -1
- package/dist/{upload-K2GBG3J3.js → upload-YKSKGJET.js} +152 -136
- package/package.json +1 -1
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-YKSKGJET.js");
|
|
85
85
|
const result = await uploadJob(directory, {
|
|
86
86
|
slug,
|
|
87
87
|
name: opts.name,
|
|
@@ -10,7 +10,10 @@ 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 ?? "32");
|
|
14
|
+
var BATCH_SIZE = 100;
|
|
15
|
+
var WEBP_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_WEBP_CONCURRENCY ?? "4");
|
|
16
|
+
var HASH_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_HASH_CONCURRENCY ?? "16");
|
|
14
17
|
function collectFiles(dir) {
|
|
15
18
|
const files = [];
|
|
16
19
|
function walk(current) {
|
|
@@ -84,26 +87,30 @@ async function convertToWebP(dir) {
|
|
|
84
87
|
sharp.cache({ memory: 0, files: 0, items: 0 });
|
|
85
88
|
let converted = 0;
|
|
86
89
|
let savedBytes = 0;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
90
|
+
let processed = 0;
|
|
91
|
+
const total = pngs.length;
|
|
92
|
+
async function convertOne(f) {
|
|
93
|
+
const srcPath = join(tmpDir, f.relPath);
|
|
94
|
+
const webpPath = srcPath.replace(/\.png$/i, ".webp");
|
|
95
|
+
try {
|
|
96
|
+
await pngToWebp(srcPath, webpPath);
|
|
97
|
+
const origSize = statSync(srcPath).size;
|
|
98
|
+
const newSize = statSync(webpPath).size;
|
|
99
|
+
if (newSize < origSize) {
|
|
100
|
+
unlinkSync(srcPath);
|
|
101
|
+
savedBytes += origSize - newSize;
|
|
102
|
+
converted++;
|
|
103
|
+
} else {
|
|
104
|
+
unlinkSync(webpPath);
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
} catch {
|
|
107
|
+
} finally {
|
|
108
|
+
processed++;
|
|
109
|
+
process.stdout.write(` Converting: ${processed}/${total} PNGs\r`);
|
|
106
110
|
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await runWithConcurrency(pngs, WEBP_CONCURRENCY, convertOne);
|
|
107
114
|
console.log("");
|
|
108
115
|
} finally {
|
|
109
116
|
try {
|
|
@@ -134,12 +141,34 @@ async function convertToWebP(dir) {
|
|
|
134
141
|
}
|
|
135
142
|
return { convertedDir: tmpDir, pngsConverted: converted, savedBytes };
|
|
136
143
|
}
|
|
137
|
-
function
|
|
138
|
-
|
|
144
|
+
async function runWithConcurrency(items, n, worker) {
|
|
145
|
+
if (items.length === 0) return;
|
|
146
|
+
let next = 0;
|
|
147
|
+
const runners = [];
|
|
148
|
+
for (let k = 0; k < Math.min(n, items.length); k++) {
|
|
149
|
+
runners.push(
|
|
150
|
+
(async () => {
|
|
151
|
+
while (true) {
|
|
152
|
+
const i = next++;
|
|
153
|
+
if (i >= items.length) return;
|
|
154
|
+
await worker(items[i], i);
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
await Promise.all(runners);
|
|
160
|
+
}
|
|
161
|
+
async function computeContentHash(files) {
|
|
162
|
+
const { readFile } = await import("fs/promises");
|
|
139
163
|
const sorted = [...files].sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
const contents = new Array(sorted.length);
|
|
165
|
+
await runWithConcurrency(sorted, HASH_CONCURRENCY, async (f, i) => {
|
|
166
|
+
contents[i] = await readFile(f.absPath);
|
|
167
|
+
});
|
|
168
|
+
const hash = createHash("sha256");
|
|
169
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
170
|
+
hash.update(sorted[i].relPath);
|
|
171
|
+
hash.update(contents[i]);
|
|
143
172
|
}
|
|
144
173
|
return hash.digest("hex");
|
|
145
174
|
}
|
|
@@ -279,9 +308,11 @@ async function uploadJob(dir, opts) {
|
|
|
279
308
|
}
|
|
280
309
|
const files = collectFiles(convertedDir);
|
|
281
310
|
const totalSize = files.reduce((s, f) => s + f.size, 0);
|
|
282
|
-
const contentHash = computeContentHash(files);
|
|
283
311
|
console.log(` ${files.length} files, ${(totalSize / 1024 / 1024).toFixed(1)} MB`);
|
|
284
|
-
|
|
312
|
+
process.stdout.write(` Hashing...\r`);
|
|
313
|
+
const t0 = Date.now();
|
|
314
|
+
const contentHash = await computeContentHash(files);
|
|
315
|
+
console.log(` Hash: ${contentHash.slice(0, 16)}... (${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
|
|
285
316
|
try {
|
|
286
317
|
const auth = await getAuthHeader();
|
|
287
318
|
const usageResp = await fetch(`${getApiUrl()}/api/cli/usage`, {
|
|
@@ -322,7 +353,7 @@ async function uploadJob(dir, opts) {
|
|
|
322
353
|
}
|
|
323
354
|
const { job_id: jobId } = await initResp.json();
|
|
324
355
|
console.log(` Job ID: ${jobId}`);
|
|
325
|
-
await
|
|
356
|
+
await uploadDirect(jobId, files);
|
|
326
357
|
console.log(" Finalizing...");
|
|
327
358
|
const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
|
|
328
359
|
if (!finalResp.ok) {
|
|
@@ -353,135 +384,120 @@ async function uploadJob(dir, opts) {
|
|
|
353
384
|
viewerUrl: `${siteUrl}/t/${jobId}`
|
|
354
385
|
};
|
|
355
386
|
}
|
|
356
|
-
async function
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
387
|
+
async function uploadDirect(jobId, files) {
|
|
388
|
+
const { ProgressBar } = await import("./progress-H7WNKHDR.js");
|
|
389
|
+
const totalBytes = files.reduce((s, f) => s + f.size, 0);
|
|
390
|
+
const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2191" });
|
|
391
|
+
const failedFiles = [];
|
|
392
|
+
const failureReasons = [];
|
|
393
|
+
const batches = [];
|
|
394
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
395
|
+
batches.push(files.slice(i, i + BATCH_SIZE));
|
|
396
|
+
}
|
|
397
|
+
let nextUrlsPromise = batches.length > 0 ? fetchSignedUrlsForBatch(jobId, batches[0].map((f) => f.relPath)) : null;
|
|
398
|
+
const pending = /* @__PURE__ */ new Set();
|
|
399
|
+
for (let bi = 0; bi < batches.length; bi++) {
|
|
400
|
+
const batch = batches[bi];
|
|
401
|
+
const urlMap = await nextUrlsPromise;
|
|
402
|
+
nextUrlsPromise = bi + 1 < batches.length ? fetchSignedUrlsForBatch(jobId, batches[bi + 1].map((f) => f.relPath)) : null;
|
|
403
|
+
for (const file of batch) {
|
|
404
|
+
if (!urlMap.has(file.relPath)) {
|
|
405
|
+
failedFiles.push(file.relPath);
|
|
406
|
+
failureReasons.push({
|
|
407
|
+
path: file.relPath,
|
|
408
|
+
reason: "no signed URL from server after retries"
|
|
409
|
+
});
|
|
410
|
+
bar.tick(0);
|
|
411
|
+
continue;
|
|
377
412
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (attempt < maxAttempts) {
|
|
388
|
-
console.log(` \u26A0 ${msg}`);
|
|
389
|
-
await sleep(3e3 * attempt);
|
|
413
|
+
const signedUrl = urlMap.get(file.relPath);
|
|
414
|
+
const task = (async () => {
|
|
415
|
+
const result = await uploadOneFileWithError(file, signedUrl);
|
|
416
|
+
if (result.ok) {
|
|
417
|
+
bar.tick(file.size);
|
|
418
|
+
} else {
|
|
419
|
+
failedFiles.push(file.relPath);
|
|
420
|
+
failureReasons.push({ path: file.relPath, reason: result.error });
|
|
421
|
+
bar.tick(0);
|
|
390
422
|
}
|
|
423
|
+
})().finally(() => {
|
|
424
|
+
pending.delete(task);
|
|
425
|
+
});
|
|
426
|
+
pending.add(task);
|
|
427
|
+
if (pending.size >= CONCURRENCY) {
|
|
428
|
+
await Promise.race(pending);
|
|
391
429
|
}
|
|
392
430
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
431
|
+
}
|
|
432
|
+
await Promise.all(pending);
|
|
433
|
+
bar.done();
|
|
434
|
+
if (failedFiles.length > 0) {
|
|
435
|
+
const byReason = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const { reason } of failureReasons) {
|
|
437
|
+
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
398
438
|
}
|
|
439
|
+
const reasonLines = [...byReason.entries()].sort((a, b) => b[1] - a[1]).map(([reason, count]) => ` ${count}\xD7 ${reason}`).join("\n");
|
|
440
|
+
const samplePath = failedFiles[0];
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Upload incomplete: ${failedFiles.length}/${files.length} file(s) failed. First failed file: ${samplePath}.
|
|
443
|
+
Failure breakdown:
|
|
444
|
+
${reasonLines}
|
|
445
|
+
Retry the upload or check your network connection.`
|
|
446
|
+
);
|
|
399
447
|
}
|
|
400
448
|
}
|
|
401
|
-
async function
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
449
|
+
async function fetchSignedUrlsForBatch(jobId, paths) {
|
|
450
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
451
|
+
let pending = paths.slice();
|
|
452
|
+
const attempts = 4;
|
|
453
|
+
for (let attempt = 0; attempt < attempts && pending.length > 0; attempt++) {
|
|
454
|
+
if (attempt > 0) {
|
|
455
|
+
const wait = 1500 * attempt;
|
|
456
|
+
await sleep(wait);
|
|
457
|
+
}
|
|
458
|
+
const resp = await apiPost(`/api/cli/push/${jobId}/upload-urls`, {
|
|
459
|
+
paths: pending
|
|
460
|
+
});
|
|
461
|
+
if (!resp.ok) {
|
|
462
|
+
throw new Error(`Failed to get upload URLs: ${await resp.text()}`);
|
|
463
|
+
}
|
|
464
|
+
const { urls } = await resp.json();
|
|
465
|
+
const stillPending = [];
|
|
466
|
+
for (const u of urls) {
|
|
467
|
+
if (u.signed_url) {
|
|
468
|
+
urlMap.set(u.path, u.signed_url);
|
|
469
|
+
} else {
|
|
470
|
+
stillPending.push(u.path);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
pending = stillPending;
|
|
410
474
|
}
|
|
411
|
-
|
|
412
|
-
|
|
475
|
+
return urlMap;
|
|
476
|
+
}
|
|
477
|
+
async function uploadOneFileWithError(file, signedUrl) {
|
|
478
|
+
const data = readFileSync(file.absPath);
|
|
479
|
+
let lastError = "unknown error";
|
|
413
480
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
414
|
-
const bar = new ProgressBar({ total: 1, totalBytes: archiveSize, label: "\u2191 archive" });
|
|
415
|
-
bar.setBytes(archiveSize);
|
|
416
481
|
try {
|
|
417
482
|
const resp = await fetch(signedUrl, {
|
|
418
483
|
method: "PUT",
|
|
419
|
-
headers: { "Content-Type": "application/
|
|
420
|
-
body:
|
|
421
|
-
signal: AbortSignal.timeout(
|
|
484
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
485
|
+
body: data,
|
|
486
|
+
signal: AbortSignal.timeout(12e4)
|
|
422
487
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (resp.ok) {
|
|
426
|
-
putError = null;
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
putError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
488
|
+
if (resp.ok) return { ok: true };
|
|
489
|
+
lastError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
430
490
|
try {
|
|
431
491
|
const body = await resp.text();
|
|
432
|
-
if (body)
|
|
492
|
+
if (body) lastError += ` \u2014 ${body.slice(0, 150).replace(/\s+/g, " ")}`;
|
|
433
493
|
} catch {
|
|
434
494
|
}
|
|
435
495
|
} catch (e) {
|
|
436
|
-
|
|
437
|
-
putError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
|
|
438
|
-
}
|
|
439
|
-
if (attempt < 2) await sleep(2e3 * (attempt + 1));
|
|
440
|
-
}
|
|
441
|
-
if (putError) {
|
|
442
|
-
throw new Error(`Archive upload failed: ${putError}`);
|
|
443
|
-
}
|
|
444
|
-
const extractResp = await apiPost(`/api/cli/push/${jobId}/extract`, {});
|
|
445
|
-
if (!extractResp.ok) {
|
|
446
|
-
throw new Error(`Failed to start extraction: ${await extractResp.text()}`);
|
|
447
|
-
}
|
|
448
|
-
const extractBar = new ProgressBar({ total: entryCount, totalBytes: 0, label: "\u2699 extract" });
|
|
449
|
-
const pollStart = Date.now();
|
|
450
|
-
const pollTimeoutMs = 30 * 60 * 1e3;
|
|
451
|
-
let lastStatus = null;
|
|
452
|
-
while (Date.now() - pollStart < pollTimeoutMs) {
|
|
453
|
-
await sleep(1500);
|
|
454
|
-
const statusResp = await apiGet(`/api/cli/push/${jobId}/extract-status`);
|
|
455
|
-
if (!statusResp.ok) {
|
|
456
|
-
throw new Error(`Failed to poll extract status: ${await statusResp.text()}`);
|
|
457
|
-
}
|
|
458
|
-
lastStatus = await statusResp.json();
|
|
459
|
-
const uploaded = Number(lastStatus?.uploaded ?? 0);
|
|
460
|
-
const errors = Number(lastStatus?.errors ?? 0);
|
|
461
|
-
extractBar.setCount(uploaded + errors);
|
|
462
|
-
if (lastStatus?.status === "done") break;
|
|
463
|
-
if (lastStatus?.status === "error") {
|
|
464
|
-
throw new Error(`Archive extraction failed: ${lastStatus?.detail ?? "unknown"}`);
|
|
496
|
+
lastError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
|
|
465
497
|
}
|
|
498
|
+
if (attempt < 2) await sleep(1e3 * (attempt + 1));
|
|
466
499
|
}
|
|
467
|
-
|
|
468
|
-
if (!lastStatus || lastStatus.status !== "done") {
|
|
469
|
-
throw new Error("Archive extraction timed out");
|
|
470
|
-
}
|
|
471
|
-
if (lastStatus.errors && lastStatus.errors > 0) {
|
|
472
|
-
throw new Error(
|
|
473
|
-
`Archive extraction completed but ${lastStatus.errors} file(s) failed to store. Retry the upload or check server logs.`
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
async function apiGet(path, timeout = 6e4) {
|
|
478
|
-
const auth = await getAuthHeader();
|
|
479
|
-
if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
|
|
480
|
-
return fetch(`${getApiUrl()}${path}`, {
|
|
481
|
-
method: "GET",
|
|
482
|
-
headers: { Authorization: auth },
|
|
483
|
-
signal: AbortSignal.timeout(timeout)
|
|
484
|
-
});
|
|
500
|
+
return { ok: false, error: lastError };
|
|
485
501
|
}
|
|
486
502
|
function sleep(ms) {
|
|
487
503
|
return new Promise((r) => setTimeout(r, ms));
|