@finbheara/archive-client 0.40.0 → 0.43.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/README.md +55 -2
- package/dist/chunks/{index-bs939h4v.js → index-1nam25zf.js} +713 -164
- package/dist/cli.js +17 -4
- package/dist/index.js +87 -2
- package/dist/types/attach-basis.d.ts +23 -0
- package/dist/types/bytes.d.ts +12 -0
- package/dist/types/cli-spec.d.ts +1 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/dancers.d.ts +4 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/indexes.d.ts +2 -0
- package/dist/types/manager.d.ts +1 -0
- package/dist/types/progress.d.ts +30 -0
- package/dist/types/release.d.ts +34 -2
- package/dist/types/remote.d.ts +1 -0
- package/dist/types/results.d.ts +2 -1
- package/dist/types/strays.d.ts +4 -0
- package/dist/types/stream.d.ts +2 -0
- package/package.json +1 -1
|
@@ -51,7 +51,7 @@ function planAsset2(name, manifest) {
|
|
|
51
51
|
|
|
52
52
|
import { createHash } from "crypto";
|
|
53
53
|
import { createReadStream, createWriteStream } from "fs";
|
|
54
|
-
import { Transform } from "stream";
|
|
54
|
+
import { Readable, Transform } from "stream";
|
|
55
55
|
import { pipeline } from "stream/promises";
|
|
56
56
|
import { createZstdDecompress } from "zlib";
|
|
57
57
|
async function sha256File2(path) {
|
|
@@ -79,6 +79,55 @@ function sha256OfDigest(digest) {
|
|
|
79
79
|
const m = /^sha256:([0-9a-f]{64})$/i.exec(digest.trim());
|
|
80
80
|
return m === null ? null : m[1].toLowerCase();
|
|
81
81
|
}
|
|
82
|
+
async function streamTo2(source, dest, o) {
|
|
83
|
+
const rawHash = createHash("sha256");
|
|
84
|
+
const outHash = o.decompress ? createHash("sha256") : rawHash;
|
|
85
|
+
let received = 0;
|
|
86
|
+
let written = 0;
|
|
87
|
+
const keep = o.raw === undefined ? null : createWriteStream(o.raw);
|
|
88
|
+
const tell = () => o.onBytes?.(received, o.decompress ? written : received);
|
|
89
|
+
const inTap = new Transform({
|
|
90
|
+
transform(chunk, _enc, cb) {
|
|
91
|
+
rawHash.update(chunk);
|
|
92
|
+
received += chunk.length;
|
|
93
|
+
if (!o.decompress)
|
|
94
|
+
tell();
|
|
95
|
+
if (keep !== null && !keep.write(chunk))
|
|
96
|
+
keep.once("drain", () => cb(null, chunk));
|
|
97
|
+
else
|
|
98
|
+
cb(null, chunk);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const outTap = new Transform({
|
|
102
|
+
transform(chunk, _enc, cb) {
|
|
103
|
+
outHash.update(chunk);
|
|
104
|
+
written += chunk.length;
|
|
105
|
+
tell();
|
|
106
|
+
cb(null, chunk);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const chain = [
|
|
110
|
+
Readable.from(source),
|
|
111
|
+
inTap,
|
|
112
|
+
...o.decompress ? [createZstdDecompress(), outTap] : [],
|
|
113
|
+
createWriteStream(dest)
|
|
114
|
+
];
|
|
115
|
+
try {
|
|
116
|
+
await pipeline(chain, o.signal !== undefined ? { signal: o.signal } : {});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
keep?.destroy();
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
if (keep !== null)
|
|
122
|
+
await new Promise((res, rej) => keep.end((e) => e ? rej(e) : res()));
|
|
123
|
+
const rawSha256 = rawHash.digest("hex");
|
|
124
|
+
return {
|
|
125
|
+
rawSha256,
|
|
126
|
+
rawBytes: received,
|
|
127
|
+
sha256: o.decompress ? outHash.digest("hex") : rawSha256,
|
|
128
|
+
bytes: o.decompress ? written : received
|
|
129
|
+
};
|
|
130
|
+
}
|
|
82
131
|
|
|
83
132
|
import { Age, DANCE_UP_YEARS, birthYearsMeet } from "@finbheara/domain/age";
|
|
84
133
|
import { createIndex as createIndex2, indexSchema as indexSchema2, personEncoder } from "@finbheara/domain/noun-index";
|
|
@@ -503,6 +552,18 @@ var FEATURES2 = [
|
|
|
503
552
|
since: 345,
|
|
504
553
|
calls: "the same on a row of results.stream and results.entries (scope majors)"
|
|
505
554
|
},
|
|
555
|
+
{
|
|
556
|
+
feature: "results.attach_basis",
|
|
557
|
+
needs: ["results:results.attach_basis"],
|
|
558
|
+
since: 345,
|
|
559
|
+
calls: "the `attach_basis` a row of results.query, results.stream and results.entries carries (how its dancer_id was answered, or why not): absent, never refused, on a release without it"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
feature: "majors.attach_basis",
|
|
563
|
+
needs: ["majors:results.attach_basis"],
|
|
564
|
+
since: 345,
|
|
565
|
+
calls: "the same on a row of majors.results, and of results.stream and results.entries (scope majors)"
|
|
566
|
+
},
|
|
506
567
|
{
|
|
507
568
|
feature: "majors.tabs",
|
|
508
569
|
needs: ["majors:tabs.address", "majors:tabs.true_round"],
|
|
@@ -547,6 +608,21 @@ var FEATURES2 = [
|
|
|
547
608
|
since: 245,
|
|
548
609
|
calls: "dancers.years"
|
|
549
610
|
},
|
|
611
|
+
{
|
|
612
|
+
feature: "dancers.records.birth_consistency",
|
|
613
|
+
needs: ["results:dancers.birth_consistency", "results:dancers.birth_consistency_rows"],
|
|
614
|
+
since: 345,
|
|
615
|
+
calls: "the `birth_consistency` and `birth_consistency_rows` a record of dancers.records carries (the percent of its readable rows that fit its birth window, and how many were readable): absent, never refused, on a release without them"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
feature: "dancers.years.birth_consistency",
|
|
619
|
+
needs: [
|
|
620
|
+
"results:dancer_years.birth_consistency",
|
|
621
|
+
"results:dancer_years.birth_consistency_rows"
|
|
622
|
+
],
|
|
623
|
+
since: 345,
|
|
624
|
+
calls: "the `birth_consistency` and `birth_consistency_rows` a dancer-year of dancers.years carries: absent, never refused, on a release without them"
|
|
625
|
+
},
|
|
550
626
|
{
|
|
551
627
|
feature: "dancers.years.printed",
|
|
552
628
|
needs: ["results:dancer_years.printed_regions"],
|
|
@@ -1414,7 +1490,7 @@ function streamSql(q, shape = {}) {
|
|
|
1414
1490
|
if (q.primary === true)
|
|
1415
1491
|
where.push("(r.primary_source IS NULL OR r.primary_source = '')");
|
|
1416
1492
|
return {
|
|
1417
|
-
sql: `SELECT ${COLUMNS}${shape.competition === true ? COMPETITION_COLUMNS : ""}${shape.competitorsPrinted === true ? ", c.competitors_printed AS competitors_printed" : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
|
|
1493
|
+
sql: `SELECT ${COLUMNS}${shape.competition === true ? COMPETITION_COLUMNS : ""}${shape.competitorsPrinted === true ? ", c.competitors_printed AS competitors_printed" : ""}${shape.attachBasis === true ? ", r.attach_basis AS attach_basis" : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line${where.length === 0 ? "" : ` WHERE ${where.join(" AND ")}`} ORDER BY r.file, r.line`,
|
|
1418
1494
|
args
|
|
1419
1495
|
};
|
|
1420
1496
|
}
|
|
@@ -1495,7 +1571,7 @@ var COLUMNS2 = `r.address AS address, r.competition AS competition, c.event AS e
|
|
|
1495
1571
|
r.place_printed AS place_printed, c.competitors AS competitors, r.name_printed AS name_printed,
|
|
1496
1572
|
r.dancer_id AS dancer_id, r.school_printed AS school_printed, r.school_id AS school_id,
|
|
1497
1573
|
r.yob AS yob, r.qualifier AS qualifier`;
|
|
1498
|
-
function queryResults(rel, db, q) {
|
|
1574
|
+
function queryResults(rel, db, q, basis = false) {
|
|
1499
1575
|
const where = [];
|
|
1500
1576
|
const args = [];
|
|
1501
1577
|
if (q.dancer_id !== undefined) {
|
|
@@ -1521,7 +1597,7 @@ function queryResults(rel, db, q) {
|
|
|
1521
1597
|
args.push(q.year);
|
|
1522
1598
|
}
|
|
1523
1599
|
const asked = q.limit ?? 500;
|
|
1524
|
-
const all = rel.db(db).query(`SELECT ${COLUMNS2} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
|
|
1600
|
+
const all = rel.db(db).query(`SELECT ${COLUMNS2}${basis ? ", r.attach_basis AS attach_basis" : ""} FROM results r JOIN competitions c ON c.file = r.file AND c.line = r.competition_line
|
|
1525
1601
|
WHERE ${where.join(" AND ")}
|
|
1526
1602
|
ORDER BY c.year, c.event, r.competition, r.place IS NULL, r.place, r.address`).all(...args);
|
|
1527
1603
|
return {
|
|
@@ -1544,7 +1620,8 @@ class ResultFinder2 {
|
|
|
1544
1620
|
async query(q) {
|
|
1545
1621
|
await this.rel.fetch("results");
|
|
1546
1622
|
await this.rel.require("results.level");
|
|
1547
|
-
|
|
1623
|
+
const basis = (await this.rel.capability("results.attach_basis")).state === "full";
|
|
1624
|
+
return queryResults(this.rel, "results", q, basis);
|
|
1548
1625
|
}
|
|
1549
1626
|
async dancerLevel(q) {
|
|
1550
1627
|
await this.rel.fetch("results");
|
|
@@ -1557,7 +1634,8 @@ class ResultFinder2 {
|
|
|
1557
1634
|
await this.rel.require(db === "majors" ? "majors.stream" : "results.stream");
|
|
1558
1635
|
const competition = (await this.rel.capability(`${db}.stream.competition`)).state === "full";
|
|
1559
1636
|
const competitorsPrinted = (await this.rel.capability(`${db}.stream.competitors_printed`)).state === "full";
|
|
1560
|
-
|
|
1637
|
+
const attachBasis = (await this.rel.capability(`${db}.attach_basis`)).state === "full";
|
|
1638
|
+
return { db: this.rel.db(db), shape: { competition, competitorsPrinted, attachBasis } };
|
|
1561
1639
|
}
|
|
1562
1640
|
async* stream(q) {
|
|
1563
1641
|
const { db, shape } = await this.streamDb(q);
|
|
@@ -1621,7 +1699,8 @@ class MajorsFinder2 {
|
|
|
1621
1699
|
async results(q) {
|
|
1622
1700
|
await this.db();
|
|
1623
1701
|
await this.rel.require("results.level");
|
|
1624
|
-
|
|
1702
|
+
const basis = (await this.rel.capability("majors.attach_basis")).state === "full";
|
|
1703
|
+
return queryResults(this.rel, "majors", q, basis);
|
|
1625
1704
|
}
|
|
1626
1705
|
}
|
|
1627
1706
|
|
|
@@ -2222,7 +2301,8 @@ class DancerResolver2 {
|
|
|
2222
2301
|
await this.rel.require("dancers.records");
|
|
2223
2302
|
const db = this.rel.db("results");
|
|
2224
2303
|
const names = (await this.rel.capability("dancers.records.names")).state === "full";
|
|
2225
|
-
const
|
|
2304
|
+
const fit = (await this.rel.capability("dancers.records.birth_consistency")).state === "full";
|
|
2305
|
+
const sql = `SELECT dancer_id, display, appearances, birth_lo, birth_hi, birth_source, birth_conflict, genders${names ? ", names_printed" : ""}${fit ? ", birth_consistency, birth_consistency_rows" : ""} FROM dancers`;
|
|
2226
2306
|
const rows = q.ids === undefined ? db.query(`${sql} ORDER BY dancer_id`).all() : idChunks(q.ids).flatMap((c) => db.query(`${sql} WHERE dancer_id IN (${c.map(() => "?").join(",")})`).all(...c));
|
|
2227
2307
|
rows.sort((a, b) => a.dancer_id - b.dancer_id);
|
|
2228
2308
|
return {
|
|
@@ -2241,7 +2321,8 @@ class DancerResolver2 {
|
|
|
2241
2321
|
await this.rel.require("dancers.years");
|
|
2242
2322
|
const db = this.rel.db("results");
|
|
2243
2323
|
const printed = (await this.rel.capability("dancers.years.printed")).state === "full";
|
|
2244
|
-
const
|
|
2324
|
+
const fit = (await this.rel.capability("dancers.years.birth_consistency")).state === "full";
|
|
2325
|
+
const sql = `SELECT dancer_id, year, region, region_source, nationals, nationals_source, geohash${printed ? ", printed_regions" : ""}${fit ? ", birth_consistency, birth_consistency_rows" : ""} FROM dancer_years`;
|
|
2245
2326
|
const yearSql = q.year === undefined ? "" : " AND year = ?";
|
|
2246
2327
|
const yearArgs = q.year === undefined ? [] : [q.year];
|
|
2247
2328
|
const rows = q.ids === undefined ? db.query(`${sql} WHERE 1${yearSql} ORDER BY dancer_id, year`).all(...yearArgs) : idChunks(q.ids).flatMap((c) => db.query(`${sql} WHERE dancer_id IN (${c.map(() => "?").join(",")})${yearSql}`).all(...c, ...yearArgs));
|
|
@@ -3306,8 +3387,8 @@ class CompResolver2 {
|
|
|
3306
3387
|
|
|
3307
3388
|
import { Database } from "bun:sqlite";
|
|
3308
3389
|
import { createHash as createHash2 } from "crypto";
|
|
3309
|
-
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "fs";
|
|
3310
|
-
import { join } from "path";
|
|
3390
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, renameSync, rmSync as rmSync2, statSync } from "fs";
|
|
3391
|
+
import { join as join2 } from "path";
|
|
3311
3392
|
|
|
3312
3393
|
import {
|
|
3313
3394
|
createIndex as createIndex3,
|
|
@@ -3547,6 +3628,34 @@ class StaffFinder2 {
|
|
|
3547
3628
|
}
|
|
3548
3629
|
}
|
|
3549
3630
|
|
|
3631
|
+
import { existsSync, readdirSync, rmSync } from "fs";
|
|
3632
|
+
import { join } from "path";
|
|
3633
|
+
function pidAlive(pid) {
|
|
3634
|
+
if (pid === process.pid)
|
|
3635
|
+
return true;
|
|
3636
|
+
try {
|
|
3637
|
+
process.kill(pid, 0);
|
|
3638
|
+
return true;
|
|
3639
|
+
} catch (e) {
|
|
3640
|
+
return e.code === "EPERM";
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
var PART_FILE = /^\..+\.(\d+)-[0-9a-f]{8}\.part(?:\.zst|\.dl)?$/;
|
|
3644
|
+
var BUILDING_FILE = /^.+\.sqlite\.building\.(\d+)$/;
|
|
3645
|
+
function sweepDead(dir, re) {
|
|
3646
|
+
if (!existsSync(dir))
|
|
3647
|
+
return [];
|
|
3648
|
+
const gone = [];
|
|
3649
|
+
for (const f of readdirSync(dir)) {
|
|
3650
|
+
const m = re.exec(f);
|
|
3651
|
+
if (m === null || pidAlive(Number(m[1])))
|
|
3652
|
+
continue;
|
|
3653
|
+
rmSync(join(dir, f), { force: true });
|
|
3654
|
+
gone.push(f);
|
|
3655
|
+
}
|
|
3656
|
+
return gone.sort();
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3550
3659
|
import { parquetMetadata, parquetReadObjects, parquetSchema } from "hyparquet";
|
|
3551
3660
|
var COMPRESSORS = {
|
|
3552
3661
|
ZSTD: (input) => new Uint8Array(Bun.zstdDecompressSync(input))
|
|
@@ -3793,8 +3902,8 @@ class EventFinder2 {
|
|
|
3793
3902
|
function defaultBuilders2() {
|
|
3794
3903
|
return [schoolsIndex, feisesIndex, compsIndex, dancersIndex, staffIndex, teamsIndex, eventsIndex];
|
|
3795
3904
|
}
|
|
3796
|
-
var indexDir = (rel) =>
|
|
3797
|
-
var indexPath2 = (rel, name) =>
|
|
3905
|
+
var indexDir = (rel) => join2(rel.dir, "index");
|
|
3906
|
+
var indexPath2 = (rel, name) => join2(indexDir(rel), `${name}.sqlite`);
|
|
3798
3907
|
function indexKey2(rel, b) {
|
|
3799
3908
|
const files = rel.receipt().files;
|
|
3800
3909
|
const parts = [b.name, b.version, ...b.keyParts?.(rel) ?? []];
|
|
@@ -3805,7 +3914,7 @@ function indexKey2(rel, b) {
|
|
|
3805
3914
|
return createHash2("sha256").update(parts.join("\x00")).digest("hex");
|
|
3806
3915
|
}
|
|
3807
3916
|
function keyOf(path) {
|
|
3808
|
-
if (!
|
|
3917
|
+
if (!existsSync2(path))
|
|
3809
3918
|
return null;
|
|
3810
3919
|
try {
|
|
3811
3920
|
const db = new Database(path, { readonly: true });
|
|
@@ -3830,8 +3939,9 @@ function buildIndex2(rel, b, o) {
|
|
|
3830
3939
|
if (b.load !== undefined && o.loaded === undefined)
|
|
3831
3940
|
throw new Error(`archive-client: index "${b.name}" reads its inputs first: build it with ensureIndex (or rel.index)`);
|
|
3832
3941
|
mkdirSync(indexDir(rel), { recursive: true });
|
|
3942
|
+
sweepDead(indexDir(rel), BUILDING_FILE);
|
|
3833
3943
|
const tmp = `${out}.building.${process.pid}`;
|
|
3834
|
-
|
|
3944
|
+
rmSync2(tmp, { force: true });
|
|
3835
3945
|
const t0 = performance.now();
|
|
3836
3946
|
const db = new Database(tmp, { create: true });
|
|
3837
3947
|
try {
|
|
@@ -3847,7 +3957,7 @@ function buildIndex2(rel, b, o) {
|
|
|
3847
3957
|
db.run("VACUUM");
|
|
3848
3958
|
} catch (e) {
|
|
3849
3959
|
db.close();
|
|
3850
|
-
|
|
3960
|
+
rmSync2(tmp, { force: true });
|
|
3851
3961
|
throw e;
|
|
3852
3962
|
}
|
|
3853
3963
|
db.close();
|
|
@@ -3858,16 +3968,42 @@ function buildIndex2(rel, b, o) {
|
|
|
3858
3968
|
async function ensureIndex2(rel, b, o) {
|
|
3859
3969
|
if (!o.force && isCurrent2(rel, b))
|
|
3860
3970
|
return indexPath2(rel, b.name);
|
|
3861
|
-
const
|
|
3862
|
-
|
|
3971
|
+
const t0 = performance.now();
|
|
3972
|
+
const tell = (state, error) => o.progress?.({
|
|
3973
|
+
tag: rel.tag,
|
|
3974
|
+
kind: "index",
|
|
3975
|
+
item: b.name,
|
|
3976
|
+
state,
|
|
3977
|
+
bytes: 0,
|
|
3978
|
+
total: null,
|
|
3979
|
+
unpacked: 0,
|
|
3980
|
+
ms: performance.now() - t0,
|
|
3981
|
+
...error !== undefined ? { error } : {}
|
|
3982
|
+
});
|
|
3983
|
+
tell("indexing");
|
|
3984
|
+
try {
|
|
3985
|
+
const loaded = b.load === undefined ? undefined : await b.load(rel);
|
|
3986
|
+
if (o.progress !== undefined)
|
|
3987
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
3988
|
+
const path = buildIndex2(rel, b, {
|
|
3989
|
+
force: o.force,
|
|
3990
|
+
loaded,
|
|
3991
|
+
...o.progress === undefined && o.log !== undefined ? { log: o.log } : {}
|
|
3992
|
+
});
|
|
3993
|
+
tell("done");
|
|
3994
|
+
return path;
|
|
3995
|
+
} catch (e) {
|
|
3996
|
+
tell("failed", e.message);
|
|
3997
|
+
throw e;
|
|
3998
|
+
}
|
|
3863
3999
|
}
|
|
3864
4000
|
function listIndexes2(rel) {
|
|
3865
4001
|
const dir = indexDir(rel);
|
|
3866
|
-
if (!
|
|
4002
|
+
if (!existsSync2(dir))
|
|
3867
4003
|
return [];
|
|
3868
|
-
return
|
|
4004
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".sqlite")).sort().map((f) => {
|
|
3869
4005
|
const name = f.slice(0, -".sqlite".length);
|
|
3870
|
-
const path =
|
|
4006
|
+
const path = join2(dir, f);
|
|
3871
4007
|
const b = rel.builders().find((x) => x.name === name);
|
|
3872
4008
|
return {
|
|
3873
4009
|
name,
|
|
@@ -3881,35 +4017,35 @@ function listIndexes2(rel) {
|
|
|
3881
4017
|
import { createHash as createHash3 } from "crypto";
|
|
3882
4018
|
import {
|
|
3883
4019
|
copyFileSync as copyFileSync2,
|
|
3884
|
-
existsSync as
|
|
4020
|
+
existsSync as existsSync7,
|
|
3885
4021
|
linkSync as linkSync2,
|
|
3886
4022
|
mkdirSync as mkdirSync5,
|
|
3887
4023
|
readFileSync as readFileSync5,
|
|
3888
4024
|
readdirSync as readdirSync4,
|
|
3889
4025
|
renameSync as renameSync3,
|
|
3890
|
-
rmSync as
|
|
4026
|
+
rmSync as rmSync6,
|
|
3891
4027
|
statSync as statSync4
|
|
3892
4028
|
} from "fs";
|
|
3893
|
-
import { join as
|
|
4029
|
+
import { join as join6 } from "path";
|
|
3894
4030
|
|
|
3895
4031
|
import {
|
|
3896
4032
|
copyFileSync,
|
|
3897
|
-
existsSync as
|
|
4033
|
+
existsSync as existsSync6,
|
|
3898
4034
|
linkSync,
|
|
3899
4035
|
mkdirSync as mkdirSync4,
|
|
3900
4036
|
readFileSync as readFileSync4,
|
|
3901
4037
|
readdirSync as readdirSync3,
|
|
3902
|
-
rmSync as
|
|
4038
|
+
rmSync as rmSync5,
|
|
3903
4039
|
statSync as statSync3
|
|
3904
4040
|
} from "fs";
|
|
3905
|
-
import { isAbsolute, join as
|
|
4041
|
+
import { isAbsolute, join as join5, relative, resolve, sep } from "path";
|
|
3906
4042
|
|
|
3907
|
-
import { existsSync as
|
|
3908
|
-
import { join as
|
|
3909
|
-
var pinPath = (root) =>
|
|
4043
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync as rmSync3 } from "fs";
|
|
4044
|
+
import { join as join3 } from "path";
|
|
4045
|
+
var pinPath = (root) => join3(root, "PINNED.json");
|
|
3910
4046
|
function readPin2(root) {
|
|
3911
4047
|
const p = pinPath(root);
|
|
3912
|
-
if (!
|
|
4048
|
+
if (!existsSync3(p))
|
|
3913
4049
|
return null;
|
|
3914
4050
|
try {
|
|
3915
4051
|
const v = JSON.parse(readFileSync2(p, "utf8"));
|
|
@@ -3925,23 +4061,24 @@ function writePin(root, pin) {
|
|
|
3925
4061
|
}
|
|
3926
4062
|
function clearPin(root) {
|
|
3927
4063
|
const p = pinPath(root);
|
|
3928
|
-
if (!
|
|
4064
|
+
if (!existsSync3(p))
|
|
3929
4065
|
return false;
|
|
3930
|
-
|
|
4066
|
+
rmSync3(p);
|
|
3931
4067
|
return true;
|
|
3932
4068
|
}
|
|
3933
4069
|
|
|
3934
4070
|
import { Database as Database2 } from "bun:sqlite";
|
|
4071
|
+
import { randomBytes } from "crypto";
|
|
3935
4072
|
import {
|
|
3936
|
-
|
|
4073
|
+
createReadStream as createReadStream2,
|
|
4074
|
+
existsSync as existsSync5,
|
|
3937
4075
|
mkdirSync as mkdirSync3,
|
|
3938
4076
|
readFileSync as readFileSync3,
|
|
3939
|
-
readdirSync as readdirSync2,
|
|
3940
4077
|
renameSync as renameSync2,
|
|
3941
|
-
rmSync as
|
|
4078
|
+
rmSync as rmSync4,
|
|
3942
4079
|
statSync as statSync2
|
|
3943
4080
|
} from "fs";
|
|
3944
|
-
import { join as
|
|
4081
|
+
import { basename, dirname as dirname2, join as join4 } from "path";
|
|
3945
4082
|
|
|
3946
4083
|
var CALENDAR_STAGES2 = [
|
|
3947
4084
|
"announced",
|
|
@@ -4277,9 +4414,9 @@ class RegistrationResolver2 {
|
|
|
4277
4414
|
}
|
|
4278
4415
|
|
|
4279
4416
|
import { spawn } from "child_process";
|
|
4280
|
-
import { createWriteStream as createWriteStream2, existsSync as
|
|
4417
|
+
import { createWriteStream as createWriteStream2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
4281
4418
|
import { dirname } from "path";
|
|
4282
|
-
import { Readable } from "stream";
|
|
4419
|
+
import { Readable as Readable2 } from "stream";
|
|
4283
4420
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
4284
4421
|
function run(cmd, args) {
|
|
4285
4422
|
return new Promise((resolve) => {
|
|
@@ -4345,8 +4482,59 @@ var ghRemote2 = {
|
|
|
4345
4482
|
dest,
|
|
4346
4483
|
"--clobber"
|
|
4347
4484
|
]);
|
|
4348
|
-
if (r.status !== 0 || !
|
|
4485
|
+
if (r.status !== 0 || !existsSync4(dest))
|
|
4349
4486
|
throw new Error(`gh release download ${tag} -p ${name}: ${firstLine(r.stderr)}`);
|
|
4487
|
+
},
|
|
4488
|
+
async bytes(repo, tag, name, signal) {
|
|
4489
|
+
const what = `gh release download ${tag} -p ${name}`;
|
|
4490
|
+
let child;
|
|
4491
|
+
try {
|
|
4492
|
+
child = spawn("gh", ["release", "download", tag, "-R", repo, "-p", name, "-O", "-"], {
|
|
4493
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4494
|
+
});
|
|
4495
|
+
} catch (e) {
|
|
4496
|
+
throw new Error(`${what}: ${e.message}`);
|
|
4497
|
+
}
|
|
4498
|
+
let stderr = "";
|
|
4499
|
+
child.stderr?.on("data", (d) => {
|
|
4500
|
+
stderr += String(d);
|
|
4501
|
+
});
|
|
4502
|
+
const closed = new Promise((resolve) => {
|
|
4503
|
+
child.on("error", (e) => {
|
|
4504
|
+
stderr += e.message;
|
|
4505
|
+
resolve(null);
|
|
4506
|
+
});
|
|
4507
|
+
child.on("close", (status) => resolve(status));
|
|
4508
|
+
});
|
|
4509
|
+
const kill = () => {
|
|
4510
|
+
child.kill("SIGTERM");
|
|
4511
|
+
};
|
|
4512
|
+
signal?.addEventListener("abort", kill, { once: true });
|
|
4513
|
+
const out = child.stdout;
|
|
4514
|
+
if (out === null || out === undefined)
|
|
4515
|
+
throw new Error(`${what}: no stdout`);
|
|
4516
|
+
const it = out[Symbol.asyncIterator]();
|
|
4517
|
+
const failed = async () => {
|
|
4518
|
+
const status = await closed;
|
|
4519
|
+
signal?.removeEventListener("abort", kill);
|
|
4520
|
+
if (status !== 0)
|
|
4521
|
+
throw new Error(`${what}: ${firstLine(stderr) || `exit ${status}`}`);
|
|
4522
|
+
};
|
|
4523
|
+
const head = await it.next();
|
|
4524
|
+
if (head.done === true) {
|
|
4525
|
+
await failed();
|
|
4526
|
+
return async function* () {}();
|
|
4527
|
+
}
|
|
4528
|
+
return async function* () {
|
|
4529
|
+
yield head.value;
|
|
4530
|
+
for (;; ) {
|
|
4531
|
+
const n = await it.next();
|
|
4532
|
+
if (n.done === true)
|
|
4533
|
+
break;
|
|
4534
|
+
yield n.value;
|
|
4535
|
+
}
|
|
4536
|
+
await failed();
|
|
4537
|
+
}();
|
|
4350
4538
|
}
|
|
4351
4539
|
};
|
|
4352
4540
|
var API = "https://api.github.com";
|
|
@@ -4399,7 +4587,19 @@ function httpsRemote2(env = process.env, fetchImpl = fetch) {
|
|
|
4399
4587
|
if (!res.ok || res.body === null)
|
|
4400
4588
|
throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
|
|
4401
4589
|
mkdirSync2(dirname(dest), { recursive: true });
|
|
4402
|
-
await pipeline2(
|
|
4590
|
+
await pipeline2(Readable2.fromWeb(nodeWebStream(res.body)), createWriteStream2(dest));
|
|
4591
|
+
},
|
|
4592
|
+
async bytes(repo, tag, name, signal) {
|
|
4593
|
+
const a = (await release(repo, tag)).assets.find((x) => x.name === name);
|
|
4594
|
+
if (a === undefined)
|
|
4595
|
+
throw new Error(`${tag} has no asset ${name}`);
|
|
4596
|
+
const res = await fetchImpl(a.url, {
|
|
4597
|
+
headers: headers(env, "application/octet-stream"),
|
|
4598
|
+
...signal !== undefined ? { signal } : {}
|
|
4599
|
+
});
|
|
4600
|
+
if (!res.ok || res.body === null)
|
|
4601
|
+
throw new Error(`GET ${a.url}: ${res.status} ${res.statusText}`);
|
|
4602
|
+
return Readable2.fromWeb(nodeWebStream(res.body));
|
|
4403
4603
|
}
|
|
4404
4604
|
};
|
|
4405
4605
|
}
|
|
@@ -4419,7 +4619,10 @@ function fallbackRemote2(first, second) {
|
|
|
4419
4619
|
name: `${first.name}+${second.name}`,
|
|
4420
4620
|
tags: both((r) => r.tags.bind(r)),
|
|
4421
4621
|
assets: both((r) => r.assets.bind(r)),
|
|
4422
|
-
download: both((r) => r.download.bind(r))
|
|
4622
|
+
download: both((r) => r.download.bind(r)),
|
|
4623
|
+
...first.bytes !== undefined && second.bytes !== undefined ? {
|
|
4624
|
+
bytes: both((r) => r.bytes.bind(r))
|
|
4625
|
+
} : {}
|
|
4423
4626
|
};
|
|
4424
4627
|
}
|
|
4425
4628
|
var installed = null;
|
|
@@ -4658,12 +4861,12 @@ class UpcomingFinder2 {
|
|
|
4658
4861
|
function tagDir(root, tag) {
|
|
4659
4862
|
if (parseArchiveTag2(tag) === null)
|
|
4660
4863
|
throw new Error(`archive-client: "${tag}" is not a release tag (archive-YYYY.MM.DD[-N[.k]])`);
|
|
4661
|
-
return
|
|
4864
|
+
return join4(root, tag);
|
|
4662
4865
|
}
|
|
4663
|
-
var receiptPath = (dir) =>
|
|
4866
|
+
var receiptPath = (dir) => join4(dir, ".client.json");
|
|
4664
4867
|
function readReceipt(dir) {
|
|
4665
4868
|
const p = receiptPath(dir);
|
|
4666
|
-
if (!
|
|
4869
|
+
if (!existsSync5(p))
|
|
4667
4870
|
return null;
|
|
4668
4871
|
try {
|
|
4669
4872
|
return JSON.parse(readFileSync3(p, "utf8"));
|
|
@@ -4680,7 +4883,7 @@ async function resolveTag2(spec, opts = {}) {
|
|
|
4680
4883
|
const pin = readPin2(root);
|
|
4681
4884
|
return resolveTag2(pin === null ? { kind: "latest" } : { kind: "tag", tag: pin.tag }, opts);
|
|
4682
4885
|
}
|
|
4683
|
-
if (s.kind === "tag" &&
|
|
4886
|
+
if (s.kind === "tag" && existsSync5(join4(tagDir(root, s.tag), "MANIFEST.json")))
|
|
4684
4887
|
return s.tag;
|
|
4685
4888
|
const repo = requireRepo2(opts);
|
|
4686
4889
|
const tags = await (opts.remote ?? defaultRemote()).tags(repo);
|
|
@@ -4689,9 +4892,30 @@ async function resolveTag2(spec, opts = {}) {
|
|
|
4689
4892
|
throw new Error(`archive-client: ${repo} publishes no release ${s.kind === "tag" ? s.tag : s.kind === "cut" ? `of cut ${s.cut}${s.fraction > 0 ? `.${s.fraction}` : ""}` : "at all"}`);
|
|
4690
4893
|
return tag;
|
|
4691
4894
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4895
|
+
var DEFAULT_CONCURRENCY2 = 4;
|
|
4896
|
+
function partPath(dest) {
|
|
4897
|
+
return join4(dirname2(dest), `.${basename(dest)}.${process.pid}-${randomBytes(4).toString("hex")}.part`);
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
class Slots {
|
|
4901
|
+
n;
|
|
4902
|
+
running = 0;
|
|
4903
|
+
waiting = [];
|
|
4904
|
+
constructor(n) {
|
|
4905
|
+
this.n = n;
|
|
4906
|
+
}
|
|
4907
|
+
async run(job, signal) {
|
|
4908
|
+
if (this.running >= this.n)
|
|
4909
|
+
await new Promise((r) => this.waiting.push(r));
|
|
4910
|
+
this.running++;
|
|
4911
|
+
try {
|
|
4912
|
+
signal.throwIfAborted();
|
|
4913
|
+
return await job();
|
|
4914
|
+
} finally {
|
|
4915
|
+
this.running--;
|
|
4916
|
+
this.waiting.shift()?.();
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4695
4919
|
}
|
|
4696
4920
|
|
|
4697
4921
|
class Release2 {
|
|
@@ -4718,7 +4942,7 @@ class Release2 {
|
|
|
4718
4942
|
feisPanels;
|
|
4719
4943
|
opts;
|
|
4720
4944
|
handles = new Map;
|
|
4721
|
-
|
|
4945
|
+
listing = null;
|
|
4722
4946
|
constructor(tag, root, manifest, opts) {
|
|
4723
4947
|
const t = parseArchiveTag2(tag);
|
|
4724
4948
|
this.tag = tag;
|
|
@@ -4751,19 +4975,65 @@ class Release2 {
|
|
|
4751
4975
|
return planAsset2(asset, this.manifest);
|
|
4752
4976
|
}
|
|
4753
4977
|
has(asset) {
|
|
4754
|
-
return
|
|
4978
|
+
return existsSync5(join4(this.dir, this.plan(asset).local));
|
|
4755
4979
|
}
|
|
4756
4980
|
path(asset) {
|
|
4757
|
-
const p =
|
|
4758
|
-
if (!
|
|
4981
|
+
const p = join4(this.dir, this.plan(asset).local);
|
|
4982
|
+
if (!existsSync5(p))
|
|
4759
4983
|
throw new Error(`archive-client: ${this.tag} has no ${asset} in the cache: fetch it first (\`await rel.fetch("${asset}")\`, or \`fetch --release ${this.tag} --assets ${asset}\`)`);
|
|
4760
4984
|
return p;
|
|
4761
4985
|
}
|
|
4762
4986
|
async fetch(...assets) {
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4987
|
+
return this.fetching(assets).all;
|
|
4988
|
+
}
|
|
4989
|
+
inflight = new Map;
|
|
4990
|
+
slots = null;
|
|
4991
|
+
swept = false;
|
|
4992
|
+
fetching(assets) {
|
|
4993
|
+
this.slots ??= new Slots(Math.max(1, this.opts.concurrency ?? DEFAULT_CONCURRENCY2));
|
|
4994
|
+
const slots = this.slots;
|
|
4995
|
+
const ac = new AbortController;
|
|
4996
|
+
const mine = new Map;
|
|
4997
|
+
let first = null;
|
|
4998
|
+
for (const a of assets) {
|
|
4999
|
+
if (mine.has(a))
|
|
5000
|
+
continue;
|
|
5001
|
+
const local = this.plan(a).local;
|
|
5002
|
+
let p = this.inflight.get(local);
|
|
5003
|
+
if (p === undefined) {
|
|
5004
|
+
p = slots.run(() => this.fetchOne(a, ac.signal), ac.signal);
|
|
5005
|
+
this.inflight.set(local, p);
|
|
5006
|
+
const started = p;
|
|
5007
|
+
started.then(() => this.inflight.delete(local), () => this.inflight.delete(local));
|
|
5008
|
+
}
|
|
5009
|
+
p.catch((e) => {
|
|
5010
|
+
first ??= { e };
|
|
5011
|
+
ac.abort();
|
|
5012
|
+
});
|
|
5013
|
+
mine.set(a, p);
|
|
5014
|
+
}
|
|
5015
|
+
const all = (async () => {
|
|
5016
|
+
const settled = await Promise.allSettled([...mine.values()]);
|
|
5017
|
+
if (first !== null)
|
|
5018
|
+
throw first.e;
|
|
5019
|
+
return assets.map((a) => {
|
|
5020
|
+
const s = settled[[...mine.keys()].indexOf(a)];
|
|
5021
|
+
return s.value;
|
|
5022
|
+
});
|
|
5023
|
+
})();
|
|
5024
|
+
return {
|
|
5025
|
+
each: (asset) => {
|
|
5026
|
+
const p = mine.get(asset);
|
|
5027
|
+
if (p === undefined)
|
|
5028
|
+
throw new Error(`archive-client: ${asset} was not asked of this fetch`);
|
|
5029
|
+
return p;
|
|
5030
|
+
},
|
|
5031
|
+
all,
|
|
5032
|
+
cancel: () => {
|
|
5033
|
+
first ??= { e: new Error("archive-client: the fetch was cancelled") };
|
|
5034
|
+
ac.abort();
|
|
5035
|
+
}
|
|
5036
|
+
};
|
|
4767
5037
|
}
|
|
4768
5038
|
async publishes(asset) {
|
|
4769
5039
|
const plan = this.plan(asset);
|
|
@@ -4819,25 +5089,29 @@ class Release2 {
|
|
|
4819
5089
|
if (c.state === "missing")
|
|
4820
5090
|
throw needsRelease2(this.tag, c);
|
|
4821
5091
|
}
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
if (
|
|
5092
|
+
listed() {
|
|
5093
|
+
this.listing ??= (async () => {
|
|
5094
|
+
const assets = await (this.opts.remote ?? defaultRemote()).assets(requireRepo2(this.opts), this.tag);
|
|
5095
|
+
if (existsSync5(this.dir)) {
|
|
4826
5096
|
const r = this.receipt();
|
|
4827
|
-
r.published =
|
|
5097
|
+
r.published = assets.map((a) => a.name).sort();
|
|
4828
5098
|
writeJsonAtomic2(receiptPath(this.dir), r);
|
|
4829
5099
|
}
|
|
4830
|
-
|
|
4831
|
-
|
|
5100
|
+
return assets;
|
|
5101
|
+
})();
|
|
5102
|
+
this.listing.catch(() => {
|
|
5103
|
+
this.listing = null;
|
|
5104
|
+
});
|
|
5105
|
+
return this.listing;
|
|
4832
5106
|
}
|
|
4833
|
-
async fetchOne(asset) {
|
|
5107
|
+
async fetchOne(asset, signal) {
|
|
4834
5108
|
const plan = this.plan(asset);
|
|
4835
|
-
const dest =
|
|
5109
|
+
const dest = join4(this.dir, plan.local);
|
|
4836
5110
|
const have = this.receipt().files[plan.local];
|
|
4837
|
-
if (
|
|
5111
|
+
if (existsSync5(dest) && have !== undefined)
|
|
4838
5112
|
return have;
|
|
4839
5113
|
const log = this.opts.log ?? (() => {});
|
|
4840
|
-
if (
|
|
5114
|
+
if (existsSync5(dest) && plan.contentSha256 !== null) {
|
|
4841
5115
|
const sha = await sha256File2(dest);
|
|
4842
5116
|
if (sha === plan.contentSha256) {
|
|
4843
5117
|
const adopted = {
|
|
@@ -4846,7 +5120,8 @@ class Release2 {
|
|
|
4846
5120
|
bytes: statSync2(dest).size,
|
|
4847
5121
|
verified: ["manifest"]
|
|
4848
5122
|
};
|
|
4849
|
-
|
|
5123
|
+
if (this.opts.progress === undefined)
|
|
5124
|
+
log(`adopt ${this.tag} ${plan.local} (the manifest's sha256)`);
|
|
4850
5125
|
this.record(plan.local, adopted);
|
|
4851
5126
|
return adopted;
|
|
4852
5127
|
}
|
|
@@ -4855,14 +5130,24 @@ class Release2 {
|
|
|
4855
5130
|
const pub = plan.published.map((n) => listed.find((a) => a.name === n)).find((a) => a !== undefined);
|
|
4856
5131
|
if (pub === undefined)
|
|
4857
5132
|
throw new Error(`archive-client: ${this.tag} publishes no ${plan.published.join(" or ")} (asked: ${asset})`);
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
const landed = await
|
|
5133
|
+
mkdirSync3(this.dir, { recursive: true });
|
|
5134
|
+
if (!this.swept) {
|
|
5135
|
+
sweepDead(this.dir, PART_FILE);
|
|
5136
|
+
this.swept = true;
|
|
5137
|
+
}
|
|
5138
|
+
const landed = await fetchAsset({
|
|
5139
|
+
remote: this.opts.remote ?? defaultRemote(),
|
|
5140
|
+
repo: requireRepo2(this.opts),
|
|
5141
|
+
tag: this.tag,
|
|
5142
|
+
pub,
|
|
5143
|
+
plan,
|
|
5144
|
+
dest,
|
|
5145
|
+
opts: this.opts,
|
|
5146
|
+
log,
|
|
5147
|
+
...this.opts.progress !== undefined ? { progress: this.opts.progress } : {},
|
|
5148
|
+
...signal !== undefined ? { signal } : {}
|
|
5149
|
+
});
|
|
4864
5150
|
this.record(plan.local, landed);
|
|
4865
|
-
dropIfEmpty(part);
|
|
4866
5151
|
return landed;
|
|
4867
5152
|
}
|
|
4868
5153
|
record(local, f) {
|
|
@@ -4891,7 +5176,11 @@ class Release2 {
|
|
|
4891
5176
|
if (FEATURES2.some((f) => f.feature === b.name))
|
|
4892
5177
|
await this.require(b.name);
|
|
4893
5178
|
await this.fetch(...b.inputs);
|
|
4894
|
-
return ensureIndex2(this, b, {
|
|
5179
|
+
return ensureIndex2(this, b, {
|
|
5180
|
+
force: o.force ?? false,
|
|
5181
|
+
log: this.opts.log,
|
|
5182
|
+
...this.opts.progress !== undefined ? { progress: this.opts.progress } : {}
|
|
5183
|
+
});
|
|
4895
5184
|
}
|
|
4896
5185
|
async indexDb(name) {
|
|
4897
5186
|
const key = `index:${name}`;
|
|
@@ -4911,69 +5200,172 @@ class Release2 {
|
|
|
4911
5200
|
this.close();
|
|
4912
5201
|
}
|
|
4913
5202
|
}
|
|
4914
|
-
|
|
5203
|
+
function verifyLanded(got, pub, plan, strict) {
|
|
4915
5204
|
const verified = [];
|
|
4916
5205
|
const host = sha256OfDigest(pub.digest);
|
|
4917
5206
|
if (host !== null) {
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
rmSync3(raw, { force: true });
|
|
4921
|
-
throw new Error(`archive-client: ${pub.name} does not match the host's digest (sha256 ${got}, the host says ${host})`);
|
|
4922
|
-
}
|
|
5207
|
+
if (got.rawSha256 !== host)
|
|
5208
|
+
throw new Error(`archive-client: ${pub.name} does not match the host's digest (sha256 ${got.rawSha256}, the host says ${host})`);
|
|
4923
5209
|
verified.push("host-digest");
|
|
4924
5210
|
}
|
|
4925
|
-
let landedAt = raw;
|
|
4926
|
-
let sha;
|
|
4927
|
-
if (plan.decompress && pub.name.endsWith(".zst")) {
|
|
4928
|
-
landedAt = `${raw.slice(0, -".zst".length)}.plain`;
|
|
4929
|
-
log(`decompress ${pub.name}`);
|
|
4930
|
-
sha = await unzstdTo2(raw, landedAt);
|
|
4931
|
-
} else
|
|
4932
|
-
sha = await sha256File2(raw);
|
|
4933
5211
|
if (plan.contentSha256 !== null) {
|
|
4934
|
-
if (
|
|
4935
|
-
|
|
4936
|
-
throw new Error(`archive-client: ${plan.local} does not match MANIFEST.json (sha256 ${sha}, the manifest says ${plan.contentSha256})`);
|
|
4937
|
-
}
|
|
5212
|
+
if (got.sha256 !== plan.contentSha256)
|
|
5213
|
+
throw new Error(`archive-client: ${plan.local} does not match MANIFEST.json (sha256 ${got.sha256}, the manifest says ${plan.contentSha256})`);
|
|
4938
5214
|
verified.push("manifest");
|
|
4939
5215
|
}
|
|
4940
|
-
if (verified.length === 0 &&
|
|
4941
|
-
rmSync3(landedAt, { force: true });
|
|
5216
|
+
if (verified.length === 0 && strict)
|
|
4942
5217
|
throw new Error(`archive-client: nothing verifies ${pub.name} (no host digest, no manifest hash) and strict is set`);
|
|
5218
|
+
return verified;
|
|
5219
|
+
}
|
|
5220
|
+
async function fetchAsset(j) {
|
|
5221
|
+
const t0 = performance.now();
|
|
5222
|
+
const { pub, plan, tag } = j;
|
|
5223
|
+
const decompress = plan.decompress && pub.name.endsWith(".zst");
|
|
5224
|
+
const tmp = partPath(j.dest);
|
|
5225
|
+
const raw = decompress && j.opts.keepCompressed === true ? `${tmp}.zst` : undefined;
|
|
5226
|
+
let dl = null;
|
|
5227
|
+
let bytes = 0;
|
|
5228
|
+
let unpacked = 0;
|
|
5229
|
+
let state = "downloading";
|
|
5230
|
+
const total = Number.isFinite(pub.size) ? pub.size : null;
|
|
5231
|
+
const tell = (s, error) => j.progress?.({
|
|
5232
|
+
tag,
|
|
5233
|
+
kind: "asset",
|
|
5234
|
+
item: pub.name,
|
|
5235
|
+
state: s,
|
|
5236
|
+
bytes,
|
|
5237
|
+
total,
|
|
5238
|
+
unpacked,
|
|
5239
|
+
ms: performance.now() - t0,
|
|
5240
|
+
...error !== undefined ? { error } : {}
|
|
5241
|
+
});
|
|
5242
|
+
const onBytes = (received, written) => {
|
|
5243
|
+
if (state === "downloading")
|
|
5244
|
+
bytes = received;
|
|
5245
|
+
unpacked = decompress ? written : 0;
|
|
5246
|
+
tell(state);
|
|
5247
|
+
};
|
|
5248
|
+
if (j.progress === undefined)
|
|
5249
|
+
j.log(`fetch ${tag} ${pub.name} (${pub.size} bytes)`);
|
|
5250
|
+
tell("downloading");
|
|
5251
|
+
try {
|
|
5252
|
+
let got;
|
|
5253
|
+
if (j.remote.bytes !== undefined) {
|
|
5254
|
+
const body = await j.remote.bytes(j.repo, tag, pub.name, j.signal);
|
|
5255
|
+
if (decompress && j.progress === undefined)
|
|
5256
|
+
j.log(`decompress ${pub.name}`);
|
|
5257
|
+
got = await streamTo2(body, tmp, {
|
|
5258
|
+
decompress,
|
|
5259
|
+
...raw !== undefined ? { raw } : {},
|
|
5260
|
+
onBytes,
|
|
5261
|
+
...j.signal !== undefined ? { signal: j.signal } : {}
|
|
5262
|
+
});
|
|
5263
|
+
} else {
|
|
5264
|
+
dl = `${tmp}.dl`;
|
|
5265
|
+
await j.remote.download(j.repo, tag, pub.name, dl);
|
|
5266
|
+
j.signal?.throwIfAborted();
|
|
5267
|
+
bytes = statSync2(dl).size;
|
|
5268
|
+
if (decompress) {
|
|
5269
|
+
if (j.progress === undefined)
|
|
5270
|
+
j.log(`decompress ${pub.name}`);
|
|
5271
|
+
state = "unpacking";
|
|
5272
|
+
tell(state);
|
|
5273
|
+
got = await streamTo2(createReadStream2(dl), tmp, {
|
|
5274
|
+
decompress,
|
|
5275
|
+
onBytes,
|
|
5276
|
+
...j.signal !== undefined ? { signal: j.signal } : {}
|
|
5277
|
+
});
|
|
5278
|
+
if (raw !== undefined)
|
|
5279
|
+
renameSync2(dl, raw);
|
|
5280
|
+
else
|
|
5281
|
+
rmSync4(dl, { force: true });
|
|
5282
|
+
} else {
|
|
5283
|
+
state = "verifying";
|
|
5284
|
+
tell(state);
|
|
5285
|
+
const sha = await sha256File2(dl);
|
|
5286
|
+
got = { rawSha256: sha, rawBytes: bytes, sha256: sha, bytes };
|
|
5287
|
+
renameSync2(dl, tmp);
|
|
5288
|
+
}
|
|
5289
|
+
dl = null;
|
|
5290
|
+
}
|
|
5291
|
+
state = "verifying";
|
|
5292
|
+
tell(state);
|
|
5293
|
+
const verified = verifyLanded(got, pub, plan, j.opts.strict === true);
|
|
5294
|
+
renameSync2(tmp, j.dest);
|
|
5295
|
+
if (raw !== undefined)
|
|
5296
|
+
renameSync2(raw, `${j.dest}.zst`);
|
|
5297
|
+
tell("done");
|
|
5298
|
+
return { from: pub.name, sha256: got.sha256, bytes: got.bytes, verified };
|
|
5299
|
+
} catch (e) {
|
|
5300
|
+
for (const f of [tmp, raw, dl])
|
|
5301
|
+
if (f !== undefined && f !== null)
|
|
5302
|
+
rmSync4(f, { force: true });
|
|
5303
|
+
const cancelled = j.signal?.aborted === true;
|
|
5304
|
+
tell(cancelled ? "cancelled" : "failed", cancelled ? undefined : e.message);
|
|
5305
|
+
throw e;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
async function land(raw, pub, plan, dest, opts, log) {
|
|
5309
|
+
const decompress = plan.decompress && pub.name.endsWith(".zst");
|
|
5310
|
+
let got;
|
|
5311
|
+
let landedAt = raw;
|
|
5312
|
+
if (decompress) {
|
|
5313
|
+
landedAt = partPath(dest);
|
|
5314
|
+
log(`decompress ${pub.name}`);
|
|
5315
|
+
got = await streamTo2(createReadStream2(raw), landedAt, { decompress: true });
|
|
5316
|
+
} else {
|
|
5317
|
+
const sha = await sha256File2(raw);
|
|
5318
|
+
const n = statSync2(raw).size;
|
|
5319
|
+
got = { rawSha256: sha, rawBytes: n, sha256: sha, bytes: n };
|
|
5320
|
+
}
|
|
5321
|
+
let verified;
|
|
5322
|
+
try {
|
|
5323
|
+
verified = verifyLanded(got, pub, plan, opts.strict === true);
|
|
5324
|
+
} catch (e) {
|
|
5325
|
+
rmSync4(landedAt, { force: true });
|
|
5326
|
+
if (landedAt !== raw)
|
|
5327
|
+
rmSync4(raw, { force: true });
|
|
5328
|
+
throw e;
|
|
4943
5329
|
}
|
|
4944
5330
|
renameSync2(landedAt, dest);
|
|
4945
5331
|
if (landedAt !== raw) {
|
|
4946
5332
|
if (opts.keepCompressed === true)
|
|
4947
5333
|
renameSync2(raw, `${dest}.zst`);
|
|
4948
5334
|
else
|
|
4949
|
-
|
|
5335
|
+
rmSync4(raw, { force: true });
|
|
4950
5336
|
}
|
|
4951
|
-
return { from: pub.name, sha256:
|
|
5337
|
+
return { from: pub.name, sha256: got.sha256, bytes: statSync2(dest).size, verified };
|
|
4952
5338
|
}
|
|
4953
5339
|
async function openRelease2(spec = "pinned", opts = {}) {
|
|
4954
5340
|
const root = cacheRoot2(opts);
|
|
4955
5341
|
const tag = await resolveTag2(spec, opts);
|
|
4956
5342
|
const dir = tagDir(root, tag);
|
|
4957
|
-
const manifestPath =
|
|
4958
|
-
if (!
|
|
5343
|
+
const manifestPath = join4(dir, "MANIFEST.json");
|
|
5344
|
+
if (!existsSync5(manifestPath)) {
|
|
4959
5345
|
const remote = opts.remote ?? defaultRemote();
|
|
4960
5346
|
const repo = requireRepo2(opts);
|
|
4961
5347
|
const listed = await remote.assets(repo, tag);
|
|
4962
5348
|
const pub = listed.find((a) => a.name === "MANIFEST.json");
|
|
4963
5349
|
if (pub === undefined)
|
|
4964
5350
|
throw new Error(`archive-client: ${tag} publishes no MANIFEST.json`);
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
5351
|
+
mkdirSync3(dir, { recursive: true });
|
|
5352
|
+
const landed = await fetchAsset({
|
|
5353
|
+
remote,
|
|
5354
|
+
repo,
|
|
5355
|
+
tag,
|
|
5356
|
+
pub,
|
|
5357
|
+
plan: planAsset2("MANIFEST.json", null),
|
|
5358
|
+
dest: manifestPath,
|
|
5359
|
+
opts,
|
|
5360
|
+
log: () => {},
|
|
5361
|
+
...opts.progress !== undefined ? { progress: opts.progress } : {}
|
|
5362
|
+
});
|
|
4970
5363
|
writeJsonAtomic2(receiptPath(dir), {
|
|
4971
5364
|
tag,
|
|
4972
5365
|
source: repo,
|
|
4973
5366
|
files: { "MANIFEST.json": landed },
|
|
4974
5367
|
published: listed.map((a) => a.name).sort()
|
|
4975
5368
|
});
|
|
4976
|
-
dropIfEmpty(part);
|
|
4977
5369
|
}
|
|
4978
5370
|
const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
4979
5371
|
const rel = new Release2(tag, root, manifest, opts);
|
|
@@ -4982,8 +5374,8 @@ async function openRelease2(spec = "pinned", opts = {}) {
|
|
|
4982
5374
|
}
|
|
4983
5375
|
function openCached2(tag, opts = {}) {
|
|
4984
5376
|
const root = cacheRoot2(opts);
|
|
4985
|
-
const p =
|
|
4986
|
-
if (!
|
|
5377
|
+
const p = join4(tagDir(root, tag), "MANIFEST.json");
|
|
5378
|
+
if (!existsSync5(p))
|
|
4987
5379
|
return null;
|
|
4988
5380
|
return new Release2(tag, root, JSON.parse(readFileSync3(p, "utf8")), opts);
|
|
4989
5381
|
}
|
|
@@ -4995,14 +5387,14 @@ function checkKeep(keep) {
|
|
|
4995
5387
|
return keep;
|
|
4996
5388
|
}
|
|
4997
5389
|
function duBytes(p) {
|
|
4998
|
-
if (!
|
|
5390
|
+
if (!existsSync6(p))
|
|
4999
5391
|
return 0;
|
|
5000
5392
|
const s = statSync3(p);
|
|
5001
5393
|
if (!s.isDirectory())
|
|
5002
5394
|
return s.size;
|
|
5003
5395
|
let n = 0;
|
|
5004
5396
|
for (const e of readdirSync3(p))
|
|
5005
|
-
n += duBytes(
|
|
5397
|
+
n += duBytes(join5(p, e));
|
|
5006
5398
|
return n;
|
|
5007
5399
|
}
|
|
5008
5400
|
|
|
@@ -5014,9 +5406,9 @@ class ReleaseManager2 {
|
|
|
5014
5406
|
this.root = cacheRoot2(opts);
|
|
5015
5407
|
}
|
|
5016
5408
|
cachedTags() {
|
|
5017
|
-
if (!
|
|
5409
|
+
if (!existsSync6(this.root))
|
|
5018
5410
|
return [];
|
|
5019
|
-
return readdirSync3(this.root).filter((t) => parseArchiveTag2(t) !== null &&
|
|
5411
|
+
return readdirSync3(this.root).filter((t) => parseArchiveTag2(t) !== null && existsSync6(join5(this.root, t, "MANIFEST.json"))).sort(compareTagsNewestFirst2);
|
|
5020
5412
|
}
|
|
5021
5413
|
cached(tag) {
|
|
5022
5414
|
const r = openCached2(tag, this.opts);
|
|
@@ -5056,10 +5448,14 @@ class ReleaseManager2 {
|
|
|
5056
5448
|
async use(spec, o = {}) {
|
|
5057
5449
|
const keep = o.prune === false ? null : this.keepCount(o.keep);
|
|
5058
5450
|
const was = readPin2(this.root)?.tag ?? null;
|
|
5059
|
-
const rel = await
|
|
5451
|
+
const rel = await openRelease2(spec, { ...this.opts, assets: [] });
|
|
5060
5452
|
const tag = rel.tag;
|
|
5061
|
-
|
|
5062
|
-
|
|
5453
|
+
let built;
|
|
5454
|
+
try {
|
|
5455
|
+
built = await this.buildAsLanded(rel, [], o.assets ?? ["results"], false);
|
|
5456
|
+
} finally {
|
|
5457
|
+
rel.close();
|
|
5458
|
+
}
|
|
5063
5459
|
const pin = await this.pin(tag);
|
|
5064
5460
|
if (keep === null)
|
|
5065
5461
|
return { tag, was, pin, built, pruned: null };
|
|
@@ -5093,22 +5489,22 @@ class ReleaseManager2 {
|
|
|
5093
5489
|
async add(dir, tag) {
|
|
5094
5490
|
if (parseArchiveTag2(tag) === null)
|
|
5095
5491
|
throw new Error(`add: "${tag}" is not a release tag`);
|
|
5096
|
-
const mf =
|
|
5097
|
-
if (!
|
|
5492
|
+
const mf = join5(dir, "MANIFEST.json");
|
|
5493
|
+
if (!existsSync6(mf))
|
|
5098
5494
|
throw new Error(`add: ${dir} holds no MANIFEST.json`);
|
|
5099
5495
|
const dest = tagDir(this.root, tag);
|
|
5100
|
-
if (
|
|
5496
|
+
if (existsSync6(join5(dest, "MANIFEST.json")))
|
|
5101
5497
|
throw new Error(`add: ${tag} is already in the cache (${dest}); \`rm ${tag}\` first`);
|
|
5102
5498
|
const manifest = JSON.parse(readFileSync4(mf, "utf8"));
|
|
5103
|
-
const part =
|
|
5499
|
+
const part = join5(dest, ".part");
|
|
5104
5500
|
mkdirSync4(part, { recursive: true });
|
|
5105
5501
|
const receipt = { tag, source: `local:${dir}`, files: {} };
|
|
5106
5502
|
const log = this.opts.log ?? (() => {});
|
|
5107
5503
|
for (const name of readdirSync3(dir).sort()) {
|
|
5108
|
-
const src =
|
|
5504
|
+
const src = join5(dir, name);
|
|
5109
5505
|
if (!statSync3(src).isFile())
|
|
5110
5506
|
continue;
|
|
5111
|
-
const raw =
|
|
5507
|
+
const raw = join5(part, name);
|
|
5112
5508
|
try {
|
|
5113
5509
|
linkSync(src, raw);
|
|
5114
5510
|
} catch {
|
|
@@ -5118,10 +5514,10 @@ class ReleaseManager2 {
|
|
|
5118
5514
|
const side = Object.entries(manifest.sideFiles ?? {}).find(([k, f]) => (f.file ?? `${k}.zst`) === name)?.[0];
|
|
5119
5515
|
const asked = base !== name && isDatabase(base) ? base : side ?? name;
|
|
5120
5516
|
const plan = planAsset2(asked, manifest);
|
|
5121
|
-
const landed = await land(raw, { name, digest: null }, plan,
|
|
5517
|
+
const landed = await land(raw, { name, digest: null }, plan, join5(dest, plan.local), this.opts, log);
|
|
5122
5518
|
receipt.files[plan.local] = landed;
|
|
5123
5519
|
}
|
|
5124
|
-
|
|
5520
|
+
rmSync5(part, { recursive: true, force: true });
|
|
5125
5521
|
writeJsonAtomic2(receiptPath(dest), receipt);
|
|
5126
5522
|
const rel = openCached2(tag, this.opts);
|
|
5127
5523
|
if (rel === null)
|
|
@@ -5130,20 +5526,51 @@ class ReleaseManager2 {
|
|
|
5130
5526
|
}
|
|
5131
5527
|
async index(spec, names = [], o = {}) {
|
|
5132
5528
|
const rel = await openRelease2(spec, { ...this.opts, assets: [] });
|
|
5529
|
+
try {
|
|
5530
|
+
return { tag: rel.tag, built: await this.buildAsLanded(rel, names, [], o.force ?? false) };
|
|
5531
|
+
} finally {
|
|
5532
|
+
rel.close();
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
async buildAsLanded(rel, names, extra, force) {
|
|
5133
5536
|
const want = names.length > 0 ? names : rel.builders().map((b) => b.name);
|
|
5134
|
-
const
|
|
5135
|
-
for (const name of want) {
|
|
5537
|
+
const builders = want.map((name) => {
|
|
5136
5538
|
const b = rel.builders().find((x) => x.name === name);
|
|
5137
5539
|
if (b === undefined)
|
|
5138
5540
|
throw new Error(`index: no index "${name}"`);
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5541
|
+
return b;
|
|
5542
|
+
});
|
|
5543
|
+
const assets = [...new Set([...extra, ...builders.flatMap((b) => b.inputs)])];
|
|
5544
|
+
const f = rel.fetching(assets);
|
|
5545
|
+
f.all.catch(() => {});
|
|
5546
|
+
const waiting = new Map(builders.map((b) => [b.name, Promise.all(b.inputs.map((a) => f.each(a))).then(() => b.name)]));
|
|
5547
|
+
for (const p of waiting.values())
|
|
5548
|
+
p.catch(() => {});
|
|
5549
|
+
const paths = new Map;
|
|
5550
|
+
while (waiting.size > 0) {
|
|
5551
|
+
let name;
|
|
5552
|
+
try {
|
|
5553
|
+
name = await Promise.race(waiting.values());
|
|
5554
|
+
} catch {
|
|
5555
|
+
await f.all;
|
|
5556
|
+
throw new Error("archive-client: a fetch failed and said nothing");
|
|
5557
|
+
}
|
|
5558
|
+
waiting.delete(name);
|
|
5559
|
+
const b = builders.find((x) => x.name === name);
|
|
5560
|
+
try {
|
|
5561
|
+
paths.set(name, await ensureIndex2(rel, b, {
|
|
5562
|
+
force,
|
|
5563
|
+
...this.opts.log !== undefined ? { log: this.opts.log } : {},
|
|
5564
|
+
...this.opts.progress !== undefined ? { progress: this.opts.progress } : {}
|
|
5565
|
+
}));
|
|
5566
|
+
} catch (e) {
|
|
5567
|
+
f.cancel();
|
|
5568
|
+
await f.all.catch(() => {});
|
|
5569
|
+
throw e;
|
|
5570
|
+
}
|
|
5144
5571
|
}
|
|
5145
|
-
|
|
5146
|
-
return {
|
|
5572
|
+
await f.all;
|
|
5573
|
+
return want.map((name) => ({ name, path: paths.get(name) }));
|
|
5147
5574
|
}
|
|
5148
5575
|
plan(dryRun) {
|
|
5149
5576
|
return { dryRun, remove: [], keep: [], bytes: 0 };
|
|
@@ -5158,7 +5585,7 @@ class ReleaseManager2 {
|
|
|
5158
5585
|
p.bytes = p.remove.reduce((n, r) => n + r.bytes, 0);
|
|
5159
5586
|
if (!p.dryRun)
|
|
5160
5587
|
for (const r of p.remove)
|
|
5161
|
-
|
|
5588
|
+
rmSync5(r.path, { recursive: true, force: true });
|
|
5162
5589
|
return p;
|
|
5163
5590
|
}
|
|
5164
5591
|
prune(o = {}) {
|
|
@@ -5180,7 +5607,7 @@ class ReleaseManager2 {
|
|
|
5180
5607
|
}
|
|
5181
5608
|
remove(tag, o = {}) {
|
|
5182
5609
|
const path = tagDir(this.root, tag);
|
|
5183
|
-
if (!
|
|
5610
|
+
if (!existsSync6(path))
|
|
5184
5611
|
throw new Error(`${tag} is not in the cache`);
|
|
5185
5612
|
if (readPin2(this.root)?.tag === tag)
|
|
5186
5613
|
throw new Error(`${tag} is pinned: \`unpin\` first`);
|
|
@@ -5192,7 +5619,7 @@ class ReleaseManager2 {
|
|
|
5192
5619
|
const p = this.plan(o.dryRun ?? false);
|
|
5193
5620
|
for (const t of tag === undefined ? this.cachedTags() : [tag]) {
|
|
5194
5621
|
const path = indexDir({ dir: tagDir(this.root, t) });
|
|
5195
|
-
if (
|
|
5622
|
+
if (existsSync6(path))
|
|
5196
5623
|
this.planRemove(p, path, `${t}'s indexes`);
|
|
5197
5624
|
}
|
|
5198
5625
|
return this.apply(p);
|
|
@@ -5201,8 +5628,8 @@ class ReleaseManager2 {
|
|
|
5201
5628
|
const rel = this.cached(tag);
|
|
5202
5629
|
const bad = [];
|
|
5203
5630
|
for (const [name, f] of Object.entries(rel.receipt().files)) {
|
|
5204
|
-
const p =
|
|
5205
|
-
const got =
|
|
5631
|
+
const p = join5(rel.dir, name);
|
|
5632
|
+
const got = existsSync6(p) ? await sha256File2(p) : "missing";
|
|
5206
5633
|
if (got !== f.sha256)
|
|
5207
5634
|
bad.push({ file: name, want: f.sha256, got });
|
|
5208
5635
|
}
|
|
@@ -5214,11 +5641,11 @@ function releases2(opts = {}) {
|
|
|
5214
5641
|
}
|
|
5215
5642
|
|
|
5216
5643
|
var LOCAL_DIR2 = "local";
|
|
5217
|
-
var localRoot2 = (root) =>
|
|
5644
|
+
var localRoot2 = (root) => join6(root, LOCAL_DIR2);
|
|
5218
5645
|
var newerThan2 = (a, b) => a.cut > b.cut || a.cut === b.cut && a.fraction > b.fraction;
|
|
5219
5646
|
function readManifest(dir) {
|
|
5220
|
-
const p =
|
|
5221
|
-
if (!
|
|
5647
|
+
const p = join6(dir, "MANIFEST.json");
|
|
5648
|
+
if (!existsSync7(p))
|
|
5222
5649
|
return null;
|
|
5223
5650
|
try {
|
|
5224
5651
|
const v = JSON.parse(readFileSync5(p, "utf8"));
|
|
@@ -5240,8 +5667,8 @@ function incomplete2(dir) {
|
|
|
5240
5667
|
const d = dbs[name];
|
|
5241
5668
|
if (d === undefined)
|
|
5242
5669
|
continue;
|
|
5243
|
-
const p =
|
|
5244
|
-
if (!
|
|
5670
|
+
const p = join6(dir, d.file ?? `${name}.sqlite`);
|
|
5671
|
+
if (!existsSync7(p))
|
|
5245
5672
|
return `no ${d.file ?? `${name}.sqlite`}`;
|
|
5246
5673
|
if (d.bytes !== undefined && statSync4(p).size !== d.bytes)
|
|
5247
5674
|
return `${d.file ?? `${name}.sqlite`} is ${statSync4(p).size} bytes, the manifest says ${d.bytes}`;
|
|
@@ -5274,18 +5701,18 @@ function pickBuild2(builds, base) {
|
|
|
5274
5701
|
return best;
|
|
5275
5702
|
}
|
|
5276
5703
|
function buildTag2(p) {
|
|
5277
|
-
const at = typeof p.manifest.emittedAt === "string" && !Number.isNaN(Date.parse(p.manifest.emittedAt)) ? new Date(p.manifest.emittedAt) : statSync4(
|
|
5704
|
+
const at = typeof p.manifest.emittedAt === "string" && !Number.isNaN(Date.parse(p.manifest.emittedAt)) ? new Date(p.manifest.emittedAt) : statSync4(join6(p.build.dir, "MANIFEST.json")).mtime;
|
|
5278
5705
|
const date = at.toISOString().slice(0, 10).replaceAll("-", ".");
|
|
5279
5706
|
return renderArchiveTag2({ date, cut: p.version.cut, fraction: p.version.fraction });
|
|
5280
5707
|
}
|
|
5281
5708
|
function buildFiles(dir) {
|
|
5282
|
-
return readdirSync4(dir).filter((n) => statSync4(
|
|
5709
|
+
return readdirSync4(dir).filter((n) => statSync4(join6(dir, n)).isFile()).filter((n) => {
|
|
5283
5710
|
const m = /^(.*)\.sqlite(\.zst)?$/.exec(n);
|
|
5284
5711
|
return m === null || isDatabase(m[1]);
|
|
5285
5712
|
}).sort();
|
|
5286
5713
|
}
|
|
5287
5714
|
function lacks(b, manifest, inputs) {
|
|
5288
|
-
return inputs.filter((a) => !planAsset2(a, manifest).published.some((n) =>
|
|
5715
|
+
return inputs.filter((a) => !planAsset2(a, manifest).published.some((n) => existsSync7(join6(b.dir, n))));
|
|
5289
5716
|
}
|
|
5290
5717
|
function buildRemote(label) {
|
|
5291
5718
|
const refuse = async () => {
|
|
@@ -5302,7 +5729,7 @@ async function openBuild2(root, p, opts) {
|
|
|
5302
5729
|
const o = buildOptions(root, p.build.label, opts);
|
|
5303
5730
|
const local = localRoot2(root);
|
|
5304
5731
|
const tag = buildTag2(p);
|
|
5305
|
-
const want = sha256Of(
|
|
5732
|
+
const want = sha256Of(join6(p.build.dir, "MANIFEST.json"));
|
|
5306
5733
|
const current = () => {
|
|
5307
5734
|
const rel = openCached2(tag, o);
|
|
5308
5735
|
return rel !== null && rel.receipt().files["MANIFEST.json"]?.sha256 === want ? rel : null;
|
|
@@ -5311,33 +5738,33 @@ async function openBuild2(root, p, opts) {
|
|
|
5311
5738
|
if (have !== null)
|
|
5312
5739
|
return have;
|
|
5313
5740
|
mkdirSync5(local, { recursive: true });
|
|
5314
|
-
const work =
|
|
5741
|
+
const work = join6(local, scratchName("add"));
|
|
5315
5742
|
try {
|
|
5316
|
-
const stage =
|
|
5743
|
+
const stage = join6(work, "stage");
|
|
5317
5744
|
mkdirSync5(stage, { recursive: true });
|
|
5318
5745
|
for (const f of buildFiles(p.build.dir)) {
|
|
5319
5746
|
try {
|
|
5320
|
-
linkSync2(
|
|
5747
|
+
linkSync2(join6(p.build.dir, f), join6(stage, f));
|
|
5321
5748
|
} catch {
|
|
5322
|
-
copyFileSync2(
|
|
5749
|
+
copyFileSync2(join6(p.build.dir, f), join6(stage, f));
|
|
5323
5750
|
}
|
|
5324
5751
|
}
|
|
5325
|
-
const added = await new ReleaseManager2({ ...o, cache:
|
|
5752
|
+
const added = await new ReleaseManager2({ ...o, cache: join6(work, "cache") }).add(stage, tag);
|
|
5326
5753
|
const receipt = added.receipt();
|
|
5327
5754
|
receipt.source = `local:${p.build.dir}`;
|
|
5328
5755
|
writeJsonAtomic2(receiptPath(added.dir), receipt);
|
|
5329
5756
|
added.close();
|
|
5330
|
-
const dest =
|
|
5331
|
-
if (
|
|
5757
|
+
const dest = join6(local, tag);
|
|
5758
|
+
if (existsSync7(dest) && current() === null) {
|
|
5332
5759
|
try {
|
|
5333
|
-
renameSync3(dest,
|
|
5760
|
+
renameSync3(dest, join6(local, scratchName("old")));
|
|
5334
5761
|
} catch {}
|
|
5335
5762
|
}
|
|
5336
5763
|
try {
|
|
5337
5764
|
renameSync3(added.dir, dest);
|
|
5338
5765
|
} catch {}
|
|
5339
5766
|
} finally {
|
|
5340
|
-
|
|
5767
|
+
rmSync6(work, { recursive: true, force: true });
|
|
5341
5768
|
}
|
|
5342
5769
|
const rel = current();
|
|
5343
5770
|
if (rel === null)
|
|
@@ -5354,7 +5781,7 @@ function alive(pid) {
|
|
|
5354
5781
|
}
|
|
5355
5782
|
function sweepBuilds2(root, keep) {
|
|
5356
5783
|
const local = localRoot2(root);
|
|
5357
|
-
if (!
|
|
5784
|
+
if (!existsSync7(local))
|
|
5358
5785
|
return [];
|
|
5359
5786
|
const gone = [];
|
|
5360
5787
|
for (const name of readdirSync4(local).sort()) {
|
|
@@ -5365,23 +5792,23 @@ function sweepBuilds2(root, keep) {
|
|
|
5365
5792
|
continue;
|
|
5366
5793
|
if (m === null && parseArchiveTag2(name) === null)
|
|
5367
5794
|
continue;
|
|
5368
|
-
const path =
|
|
5795
|
+
const path = join6(local, name);
|
|
5369
5796
|
try {
|
|
5370
|
-
|
|
5797
|
+
rmSync6(path, { recursive: true, force: true });
|
|
5371
5798
|
gone.push(path);
|
|
5372
5799
|
} catch {}
|
|
5373
5800
|
}
|
|
5374
5801
|
return gone;
|
|
5375
5802
|
}
|
|
5376
5803
|
|
|
5377
|
-
import { existsSync as
|
|
5378
|
-
import { join as
|
|
5804
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
5805
|
+
import { join as join7 } from "path";
|
|
5379
5806
|
var LATEST_CHECK_MS2 = 60 * 60 * 1000;
|
|
5380
5807
|
var LATEST_CHECK_BUDGET_MS2 = 1500;
|
|
5381
|
-
var latestCheckPath2 = (root) =>
|
|
5808
|
+
var latestCheckPath2 = (root) => join7(root, "LATEST.json");
|
|
5382
5809
|
function readLatestCheck2(root) {
|
|
5383
5810
|
const p = latestCheckPath2(root);
|
|
5384
|
-
if (!
|
|
5811
|
+
if (!existsSync8(p))
|
|
5385
5812
|
return null;
|
|
5386
5813
|
try {
|
|
5387
5814
|
const v = JSON.parse(readFileSync6(p, "utf8"));
|
|
@@ -5463,4 +5890,126 @@ function staleWarning2(answered, latest, floor, tags = [], command = "idq") {
|
|
|
5463
5890
|
return `answered from ${answered}. ${floorTag ?? `cut ${floor.cut}`} is required${why}: run \`${command} use ${floor.cut}\` to get it`;
|
|
5464
5891
|
}
|
|
5465
5892
|
|
|
5466
|
-
|
|
5893
|
+
var PROGRESS_MODES2 = ["tty", "plain", "none"];
|
|
5894
|
+
function progressMode2(flag, env, isTTY) {
|
|
5895
|
+
for (const [v, from] of [
|
|
5896
|
+
[flag, "--progress"],
|
|
5897
|
+
[env.IDQ_PROGRESS, "IDQ_PROGRESS"]
|
|
5898
|
+
]) {
|
|
5899
|
+
if (v === undefined || v === "")
|
|
5900
|
+
continue;
|
|
5901
|
+
if (PROGRESS_MODES2.includes(v))
|
|
5902
|
+
return v;
|
|
5903
|
+
throw new Error(`${from} ${v}: expected ${PROGRESS_MODES2.join(", ")}`);
|
|
5904
|
+
}
|
|
5905
|
+
return isTTY ? "tty" : "plain";
|
|
5906
|
+
}
|
|
5907
|
+
var ENDS = new Set(["done", "failed", "cancelled"]);
|
|
5908
|
+
function bytesText(n) {
|
|
5909
|
+
if (n >= 1e9)
|
|
5910
|
+
return `${(n / 1e9).toFixed(2)} GB`;
|
|
5911
|
+
if (n >= 1e6)
|
|
5912
|
+
return `${(n / 1e6).toFixed(1)} MB`;
|
|
5913
|
+
if (n >= 1000)
|
|
5914
|
+
return `${(n / 1000).toFixed(1)} kB`;
|
|
5915
|
+
return `${n} B`;
|
|
5916
|
+
}
|
|
5917
|
+
var secs = (ms) => `${(ms / 1000).toFixed(1)} s`;
|
|
5918
|
+
function plainLine(e, started) {
|
|
5919
|
+
const what = e.kind === "index" ? `${e.tag} index ${e.item}` : `${e.tag} ${e.item}`;
|
|
5920
|
+
if (e.state === "done") {
|
|
5921
|
+
if (e.kind === "index")
|
|
5922
|
+
return `done ${what} in ${secs(e.ms)}`;
|
|
5923
|
+
const size = e.unpacked > 0 ? `${bytesText(e.bytes)} -> ${bytesText(e.unpacked)}` : bytesText(e.bytes);
|
|
5924
|
+
return `done ${what} ${size} in ${secs(e.ms)}`;
|
|
5925
|
+
}
|
|
5926
|
+
if (e.state === "failed")
|
|
5927
|
+
return `failed ${what} after ${secs(e.ms)}: ${e.error ?? "failed"}`;
|
|
5928
|
+
if (e.state === "cancelled")
|
|
5929
|
+
return `cancelled ${what} after ${secs(e.ms)}`;
|
|
5930
|
+
if (started)
|
|
5931
|
+
return null;
|
|
5932
|
+
return `start ${what}${e.total !== null ? ` (${bytesText(e.total)})` : ""}`;
|
|
5933
|
+
}
|
|
5934
|
+
function ttyLine(e, width) {
|
|
5935
|
+
const name = e.kind === "index" ? `index ${e.item}` : e.item;
|
|
5936
|
+
let amount = "";
|
|
5937
|
+
if (e.kind === "asset") {
|
|
5938
|
+
amount = e.total !== null ? `${bytesText(e.bytes)} / ${bytesText(e.total)}` : bytesText(e.bytes);
|
|
5939
|
+
if (e.unpacked > 0)
|
|
5940
|
+
amount += ` unpacked ${bytesText(e.unpacked)}`;
|
|
5941
|
+
if (!ENDS.has(e.state) && e.ms > 0)
|
|
5942
|
+
amount += ` ${bytesText(e.bytes * 1000 / e.ms)}/s`;
|
|
5943
|
+
}
|
|
5944
|
+
const state = e.state === "failed" ? `failed: ${e.error ?? ""}` : ENDS.has(e.state) ? `${e.state} in ${secs(e.ms)}` : e.state;
|
|
5945
|
+
const line = `${name.padEnd(34)} ${amount.padEnd(44)} ${state}`;
|
|
5946
|
+
return line.length > width ? line.slice(0, Math.max(0, width - 1)) : line;
|
|
5947
|
+
}
|
|
5948
|
+
function progressRenderer2(mode, io) {
|
|
5949
|
+
if (mode === "none")
|
|
5950
|
+
return { mode, sink: () => {}, close: () => {} };
|
|
5951
|
+
if (mode === "plain") {
|
|
5952
|
+
const started = new Set;
|
|
5953
|
+
return {
|
|
5954
|
+
mode,
|
|
5955
|
+
sink: (e) => {
|
|
5956
|
+
const key = `${e.tag}\x00${e.kind}\x00${e.item}`;
|
|
5957
|
+
const l = plainLine(e, started.has(key));
|
|
5958
|
+
started.add(key);
|
|
5959
|
+
if (ENDS.has(e.state))
|
|
5960
|
+
started.delete(key);
|
|
5961
|
+
if (l !== null)
|
|
5962
|
+
io.line(l);
|
|
5963
|
+
},
|
|
5964
|
+
close: () => {}
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
const write = io.write ?? ((s) => process.stderr.write(s));
|
|
5968
|
+
const columns = io.columns ?? (() => process.stderr.columns ?? 0);
|
|
5969
|
+
const interval = io.intervalMs ?? 100;
|
|
5970
|
+
const items = new Map;
|
|
5971
|
+
let drawn = 0;
|
|
5972
|
+
let timer = null;
|
|
5973
|
+
let dirty = false;
|
|
5974
|
+
const draw = () => {
|
|
5975
|
+
timer = null;
|
|
5976
|
+
if (!dirty)
|
|
5977
|
+
return;
|
|
5978
|
+
dirty = false;
|
|
5979
|
+
const width = Math.max(40, columns() || 120);
|
|
5980
|
+
let out = drawn > 0 ? `\x1B[${drawn}A` : "";
|
|
5981
|
+
for (const e of items.values())
|
|
5982
|
+
out += `\r\x1B[2K${ttyLine(e, width)}
|
|
5983
|
+
`;
|
|
5984
|
+
drawn = items.size;
|
|
5985
|
+
write(out);
|
|
5986
|
+
if ([...items.values()].every((e) => ENDS.has(e.state))) {
|
|
5987
|
+
items.clear();
|
|
5988
|
+
drawn = 0;
|
|
5989
|
+
}
|
|
5990
|
+
};
|
|
5991
|
+
return {
|
|
5992
|
+
mode,
|
|
5993
|
+
sink: (e) => {
|
|
5994
|
+
const key = `${e.tag}\x00${e.kind}\x00${e.item}`;
|
|
5995
|
+
const moved = items.get(key)?.state !== e.state;
|
|
5996
|
+
items.set(key, e);
|
|
5997
|
+
dirty = true;
|
|
5998
|
+
if (moved) {
|
|
5999
|
+
if (timer !== null)
|
|
6000
|
+
clearTimeout(timer);
|
|
6001
|
+
draw();
|
|
6002
|
+
} else if (timer === null) {
|
|
6003
|
+
timer = setTimeout(draw, interval);
|
|
6004
|
+
timer.unref?.();
|
|
6005
|
+
}
|
|
6006
|
+
},
|
|
6007
|
+
close: () => {
|
|
6008
|
+
if (timer !== null)
|
|
6009
|
+
clearTimeout(timer);
|
|
6010
|
+
draw();
|
|
6011
|
+
}
|
|
6012
|
+
};
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
export { DATABASES2, planAsset2, sha256File2, unzstdTo2, streamTo2, NAME_LEVELS2, nameLevel2, TabFinder2, PanelFinder2, FeisPanelFinder2, columnsOf2, FEATURES2, capabilityOf2, needsRelease2, rosterOf2, NO_CALENDAR2, NO_UPCOMING2, FeisFinder2, ADVANCEMENT_DANCES2, entryRank2, FEIS_LOCAL_ID_FLOOR2, ResultFinder2, MajorsFinder2, LINEAGE_FILE2, linksOfLineage2, releaseSchoolGraph2, readsSpanishNames2, SchoolResolver2, initialsOnly2, nameOrderOf, DancerResolver2, readPopulation2, syllabusSourceOf2, canonicalOf2, CompResolver2, StaffFinder2, TeamFinder2, EventFinder2, defaultBuilders2, indexPath2, indexKey2, isCurrent2, buildIndex2, ensureIndex2, listIndexes2, readPin2, CALENDAR_STAGES2, CALENDAR_STAGE_STATES2, CALENDAR_TERMINALS2, CalendarFinder2, youngest2, dancerResolverOf2, RegistrationResolver2, ghRemote2, httpsRemote2, fallbackRemote2, setRemote2, defaultRemote, FIELD_STATES2, DEPARTURE_REASONS2, UpcomingFinder2, resolveTag2, DEFAULT_CONCURRENCY2, Release2, openRelease2, openCached2, DEFAULT_KEEP2, ReleaseManager2, releases2, LOCAL_DIR2, localRoot2, newerThan2, incomplete2, releaseVersion2, pickBuild2, buildTag2, lacks, openBuild2, sweepBuilds2, LATEST_CHECK_MS2, LATEST_CHECK_BUDGET_MS2, latestCheckPath2, readLatestCheck2, latestCheck2, within, staleWarning2, PROGRESS_MODES2, progressMode2, progressRenderer2 };
|