@mazjq/zhiro 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -2
- package/README.zh-CN.md +132 -0
- package/agent-skills/zhiro/SKILL.md +83 -0
- package/bin/zhiro.cjs +21 -0
- package/dist/acp-bridge.js +87 -0
- package/dist/chunk-2SSYR7VW.js +558 -0
- package/dist/chunk-4JJEOOW2.js +107 -0
- package/dist/chunk-5KCN32HQ.js +517 -0
- package/dist/chunk-F6DLONN7.js +328 -0
- package/dist/chunk-J2D7DYGW.js +680 -0
- package/dist/chunk-LH25QWMP.js +1154 -0
- package/dist/chunk-LUKCNS5M.js +180 -0
- package/dist/chunk-NNH4ZG6F.js +6 -0
- package/dist/chunk-OOHBM3BT.js +6 -0
- package/dist/chunk-UMCKC7XP.js +819 -0
- package/dist/chunk-YJNBYSVM.js +3738 -0
- package/dist/chunk-YYNUPNBH.js +2474 -0
- package/dist/chunk-ZL2RKXOF.js +44 -0
- package/dist/cli-WO7FHPTE.js +131 -0
- package/dist/login-2WAH5KPP.js +75 -0
- package/dist/main-KCG27VLD.js +7463 -0
- package/dist/research-data.js +46 -0
- package/dist/stdio-FZZLZI4P.js +32 -0
- package/dist/vault-7KJ34T7Q.js +117 -0
- package/dist/zhiro.js +62 -0
- package/package.json +57 -4
- package/ui/assets/core-CwxXejkd.js +1 -0
- package/ui/assets/index-DUzC3BlF.js +11 -0
- package/ui/assets/index-y_0yp8U5.css +2 -0
- package/ui/index.html +15 -0
- package/ui/tauri.svg +6 -0
- package/ui/vite.svg +1 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION
|
|
3
|
+
} from "./chunk-NNH4ZG6F.js";
|
|
4
|
+
import {
|
|
5
|
+
COUNTS_MAY_CHANGE,
|
|
6
|
+
MIGRATIONS,
|
|
7
|
+
SCHEMA_VERSION,
|
|
8
|
+
SchemaMismatch,
|
|
9
|
+
migrate,
|
|
10
|
+
openDb,
|
|
11
|
+
schemaOf
|
|
12
|
+
} from "./chunk-5KCN32HQ.js";
|
|
13
|
+
import {
|
|
14
|
+
configFile,
|
|
15
|
+
dbPath,
|
|
16
|
+
replaceConfig
|
|
17
|
+
} from "./chunk-4JJEOOW2.js";
|
|
18
|
+
import {
|
|
19
|
+
__name
|
|
20
|
+
} from "./chunk-2SSYR7VW.js";
|
|
21
|
+
|
|
22
|
+
// apps/daemon/src/upgrade.ts
|
|
23
|
+
import { DatabaseSync } from "node:sqlite";
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { closeSync, copyFileSync, existsSync, fsyncSync, lstatSync, openSync, readdirSync, readFileSync, readSync, renameSync, rmSync, statSync, statfsSync, writeSync } from "node:fs";
|
|
26
|
+
import { join, relative } from "node:path";
|
|
27
|
+
import YAML from "yaml";
|
|
28
|
+
var UpgradeFailed = class extends Error {
|
|
29
|
+
static {
|
|
30
|
+
__name(this, "UpgradeFailed");
|
|
31
|
+
}
|
|
32
|
+
code = "UPGRADE_FAILED";
|
|
33
|
+
};
|
|
34
|
+
var KEPT = {
|
|
35
|
+
application: "1",
|
|
36
|
+
application_event: "1",
|
|
37
|
+
stage: "1",
|
|
38
|
+
fact: "1",
|
|
39
|
+
proposal: "1",
|
|
40
|
+
resume: "1",
|
|
41
|
+
document: "1",
|
|
42
|
+
deadline_override: "1",
|
|
43
|
+
company: "1",
|
|
44
|
+
position: "watch_priority is not null"
|
|
45
|
+
};
|
|
46
|
+
var userTables = /* @__PURE__ */ __name((db) => db.prepare("select name from sqlite_master where type = 'table' and name not like 'sqlite_%' and name <> 'schema_version' order by name").all().map((r) => r.name), "userTables");
|
|
47
|
+
var columns = /* @__PURE__ */ __name((db, t) => db.prepare(`pragma table_info("${t}")`).all().map((c) => c.name), "columns");
|
|
48
|
+
function digest(db, t, cols) {
|
|
49
|
+
const h = createHash("sha256");
|
|
50
|
+
const list = cols.map((c) => `"${c}"`).join(", ");
|
|
51
|
+
for (const r of db.prepare(`select ${list} from "${t}" where ${KEPT[t]} order by ${list}`).all()) h.update(JSON.stringify(cols.map((c) => r[c])));
|
|
52
|
+
return h.digest("hex");
|
|
53
|
+
}
|
|
54
|
+
__name(digest, "digest");
|
|
55
|
+
function census(db, only) {
|
|
56
|
+
const counts2 = {}, digests = {};
|
|
57
|
+
for (const t of userTables(db)) counts2[t] = db.prepare(`select count(*) as n from "${t}"`).get().n;
|
|
58
|
+
for (const t of Object.keys(KEPT)) if (t in counts2) digests[t] = digest(db, t, only?.[t] ?? columns(db, t));
|
|
59
|
+
const structure = db.prepare("select type || ' ' || name as s from sqlite_master where name not like 'sqlite_%' order by s").all().map((r) => r.s);
|
|
60
|
+
return { counts: counts2, digests, structure };
|
|
61
|
+
}
|
|
62
|
+
__name(census, "census");
|
|
63
|
+
function expectedStructure(migrations) {
|
|
64
|
+
const db = new DatabaseSync(":memory:");
|
|
65
|
+
try {
|
|
66
|
+
migrate(db, migrations);
|
|
67
|
+
return census(db).structure;
|
|
68
|
+
} finally {
|
|
69
|
+
db.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
__name(expectedStructure, "expectedStructure");
|
|
73
|
+
function compare(before, after, from, migrations) {
|
|
74
|
+
const problems = [];
|
|
75
|
+
const may = /* @__PURE__ */ new Set();
|
|
76
|
+
for (let v = from + 1; v <= migrations.length; v++) for (const t of COUNTS_MAY_CHANGE[v] ?? []) may.add(t);
|
|
77
|
+
const shared = {};
|
|
78
|
+
for (const t of Object.keys(KEPT)) {
|
|
79
|
+
const a2 = columns(after, t);
|
|
80
|
+
shared[t] = columns(before, t).filter((c) => a2.includes(c));
|
|
81
|
+
}
|
|
82
|
+
const b = census(before, shared), a = census(after, shared);
|
|
83
|
+
for (const [t, n] of Object.entries(b.counts)) {
|
|
84
|
+
if (may.has(t)) continue;
|
|
85
|
+
if (!(t in a.counts)) problems.push(`表 ${t} 不见了`);
|
|
86
|
+
else if (a.counts[t] !== n) problems.push(`表 ${t}:升级前 ${n} 行,升级后 ${a.counts[t]} 行`);
|
|
87
|
+
}
|
|
88
|
+
for (const [t, d] of Object.entries(b.digests)) if (!may.has(t) && a.digests[t] !== d) problems.push(`表 ${t} 的内容和升级前对不上`);
|
|
89
|
+
const want = expectedStructure(migrations);
|
|
90
|
+
const missing = want.filter((s) => !a.structure.includes(s)), extra = a.structure.filter((s) => !want.includes(s));
|
|
91
|
+
if (missing.length) problems.push(`缺少:${missing.join("、")}`);
|
|
92
|
+
if (extra.length) problems.push(`多出:${extra.join("、")}`);
|
|
93
|
+
const integrity = after.prepare("pragma integrity_check").all().map((r) => r.integrity_check);
|
|
94
|
+
if (integrity.join() !== "ok") problems.push(`完整性检查:${integrity.join(";").slice(0, 200)}`);
|
|
95
|
+
const fk = after.prepare("pragma foreign_key_check").all();
|
|
96
|
+
if (fk.length) problems.push(`${fk.length} 条外键指向不存在的行`);
|
|
97
|
+
return problems;
|
|
98
|
+
}
|
|
99
|
+
__name(compare, "compare");
|
|
100
|
+
var journalPath = /* @__PURE__ */ __name((vault) => join(vault, "upgrade.json"), "journalPath");
|
|
101
|
+
function writeDurable(path, text) {
|
|
102
|
+
const tmp = `${path}.tmp`;
|
|
103
|
+
const fd = openSync(tmp, "w");
|
|
104
|
+
try {
|
|
105
|
+
writeSync(fd, text, null, "utf8");
|
|
106
|
+
fsyncSync(fd);
|
|
107
|
+
} finally {
|
|
108
|
+
closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
renameSync(tmp, path);
|
|
111
|
+
}
|
|
112
|
+
__name(writeDurable, "writeDurable");
|
|
113
|
+
function recoverUpgrade(vault) {
|
|
114
|
+
const live = dbPath(vault);
|
|
115
|
+
let j = null;
|
|
116
|
+
try {
|
|
117
|
+
j = JSON.parse(readFileSync(journalPath(vault), "utf8"));
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (e?.code !== "ENOENT") throw new UpgradeFailed(`升级记录 ${journalPath(vault)} 读不出来;数据没动,请把这个文件发给开发者,别删`);
|
|
120
|
+
}
|
|
121
|
+
const strays = /* @__PURE__ */ __name(() => {
|
|
122
|
+
for (const f of readdirSync(vault)) if (/^jobos\.db\.upgrade-[0-9a-z]+(-wal|-shm)?$/.test(f)) rmSync(join(vault, f), { force: true });
|
|
123
|
+
}, "strays");
|
|
124
|
+
if (!j) {
|
|
125
|
+
strays();
|
|
126
|
+
return "none";
|
|
127
|
+
}
|
|
128
|
+
const hasLive = existsSync(live), hasCand = existsSync(j.candidate), hasBackup = existsSync(j.backup);
|
|
129
|
+
let out;
|
|
130
|
+
if (hasLive && hasCand) out = "undone";
|
|
131
|
+
else if (!hasLive && hasCand && hasBackup) {
|
|
132
|
+
renameSync(j.candidate, live);
|
|
133
|
+
out = "finished";
|
|
134
|
+
} else if (hasLive && !hasCand) out = "finished";
|
|
135
|
+
else throw new UpgradeFailed(`升级在切换时中断,文件状态认不出来(数据库 ${hasLive ? "在" : "不在"},升级副本 ${hasCand ? "在" : "不在"},备份 ${hasBackup ? "在" : "不在"})。备份 ${j.backup} 是升级前的原库,请不要删除任何文件`);
|
|
136
|
+
rmSync(journalPath(vault), { force: true });
|
|
137
|
+
strays();
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
__name(recoverUpgrade, "recoverUpgrade");
|
|
141
|
+
var manifestOf = /* @__PURE__ */ __name((backup) => backup.replace(/\.db$/, ".json"), "manifestOf");
|
|
142
|
+
function sha256(path) {
|
|
143
|
+
const h = createHash("sha256"), fd = openSync(path, "r"), buf = Buffer.alloc(1 << 20);
|
|
144
|
+
try {
|
|
145
|
+
let n;
|
|
146
|
+
while ((n = readSync(fd, buf, 0, buf.length, null)) > 0) h.update(buf.subarray(0, n));
|
|
147
|
+
} finally {
|
|
148
|
+
closeSync(fd);
|
|
149
|
+
}
|
|
150
|
+
return h.digest("hex");
|
|
151
|
+
}
|
|
152
|
+
__name(sha256, "sha256");
|
|
153
|
+
function appOf(db) {
|
|
154
|
+
try {
|
|
155
|
+
return db.prepare("select value from meta where key = 'app_version'").get()?.value ?? "";
|
|
156
|
+
} catch {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
__name(appOf, "appOf");
|
|
161
|
+
var PRE = /^pre-v(\d+)-v(\d+)-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)\.db$/;
|
|
162
|
+
function stalePreUpgrade(names) {
|
|
163
|
+
const ours = names.filter((f) => PRE.test(f)).sort((a, b) => PRE.exec(a)[3].localeCompare(PRE.exec(b)[3]));
|
|
164
|
+
return ours.slice(1, Math.max(1, ours.length - 3));
|
|
165
|
+
}
|
|
166
|
+
__name(stalePreUpgrade, "stalePreUpgrade");
|
|
167
|
+
var preUpgradeBackups = /* @__PURE__ */ __name((vault) => {
|
|
168
|
+
try {
|
|
169
|
+
return readdirSync(join(vault, "backups")).filter((f) => PRE.test(f)).sort((a, b) => PRE.exec(b)[3].localeCompare(PRE.exec(a)[3]));
|
|
170
|
+
} catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}, "preUpgradeBackups");
|
|
174
|
+
function upgradeVault(vault, _lock, opts = {}) {
|
|
175
|
+
const migrations = opts.migrations ?? MIGRATIONS;
|
|
176
|
+
const target = migrations.length;
|
|
177
|
+
const log = opts.log ?? (() => {
|
|
178
|
+
});
|
|
179
|
+
recoverUpgrade(vault);
|
|
180
|
+
const live = dbPath(vault);
|
|
181
|
+
const have = schemaOf(live);
|
|
182
|
+
if (have === 0 || have === target) return null;
|
|
183
|
+
if (have > target) throw new SchemaMismatch("TOO_NEW", have, tooNewMessage(vault, have, target));
|
|
184
|
+
const t0 = Date.now();
|
|
185
|
+
const size = statSync(live).size + (existsSync(live + "-wal") ? statSync(live + "-wal").size : 0);
|
|
186
|
+
const fs = statfsSync(vault);
|
|
187
|
+
if (fs.bavail * fs.bsize < size * 2) throw new UpgradeFailed(`升级要先复制一份数据库,磁盘空间不够(需要约 ${(size * 2 / 1e9).toFixed(1)} GB)。数据没动;腾出空间后再启动`);
|
|
188
|
+
const id = Date.now().toString(36);
|
|
189
|
+
const candidate = `${live}.upgrade-${id}`;
|
|
190
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
191
|
+
const backup = join(vault, "backups", `pre-v${have}-v${target}-${stamp}.db`);
|
|
192
|
+
let before, fromApp;
|
|
193
|
+
log(`升级数据库 v${have} → v${target}:先复制一份(原库不动)…`);
|
|
194
|
+
const src = new DatabaseSync(live);
|
|
195
|
+
try {
|
|
196
|
+
src.exec("pragma wal_checkpoint(TRUNCATE)");
|
|
197
|
+
fromApp = appOf(src);
|
|
198
|
+
src.exec(`vacuum into '${candidate.replace(/'/g, "''")}'`);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
src.close();
|
|
201
|
+
rmSync(candidate, { force: true });
|
|
202
|
+
throw new UpgradeFailed(`复制数据库失败:${e?.message ?? e}。数据没动`);
|
|
203
|
+
}
|
|
204
|
+
src.close();
|
|
205
|
+
try {
|
|
206
|
+
opts.failAfterCopy?.();
|
|
207
|
+
const cand = new DatabaseSync(candidate);
|
|
208
|
+
const orig = new DatabaseSync(live, { readOnly: true });
|
|
209
|
+
try {
|
|
210
|
+
cand.exec("pragma foreign_keys = on");
|
|
211
|
+
log("在副本上升级…");
|
|
212
|
+
migrate(cand, migrations);
|
|
213
|
+
log("核对…");
|
|
214
|
+
const problems = compare(orig, cand, have, migrations);
|
|
215
|
+
if (problems.length) throw new UpgradeFailed(`升级后的副本没通过核对:${problems.join(";")}`);
|
|
216
|
+
before = census(orig);
|
|
217
|
+
cand.prepare("insert into upgrade_log(at, from_v, to_v, app_version, backup) values (?,?,?,?,?)").run((/* @__PURE__ */ new Date()).toISOString(), have, target, VERSION, relative(vault, backup));
|
|
218
|
+
cand.prepare("insert into meta(key, value) values ('app_version', ?) on conflict(key) do update set value = excluded.value").run(VERSION);
|
|
219
|
+
cand.exec("pragma wal_checkpoint(TRUNCATE)");
|
|
220
|
+
} finally {
|
|
221
|
+
orig.close();
|
|
222
|
+
cand.close();
|
|
223
|
+
}
|
|
224
|
+
} catch (e) {
|
|
225
|
+
for (const s of ["", "-wal", "-shm"]) rmSync(candidate + s, { force: true });
|
|
226
|
+
if (e instanceof UpgradeFailed) throw e;
|
|
227
|
+
throw new UpgradeFailed(`升级到 v${target} 失败:${String(e?.message ?? e).slice(0, 300)}。数据没动,原库照旧`);
|
|
228
|
+
}
|
|
229
|
+
if (existsSync(live + "-wal") && statSync(live + "-wal").size > 0) {
|
|
230
|
+
for (const s of ["", "-wal", "-shm"]) rmSync(candidate + s, { force: true });
|
|
231
|
+
throw new UpgradeFailed("原库还有没并进去的写入(别的进程在写?),这次不升级,数据没动");
|
|
232
|
+
}
|
|
233
|
+
const manifest = { kind: "pre-upgrade", fromSchema: have, toSchema: target, fromApp, toApp: VERSION, sha256: sha256(live), bytes: statSync(live).size, counts: before.counts, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
234
|
+
writeDurable(manifestOf(backup), JSON.stringify(manifest, null, 2));
|
|
235
|
+
writeDurable(journalPath(vault), JSON.stringify({ id, candidate, backup, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
236
|
+
for (const s of ["-wal", "-shm"]) rmSync(live + s, { force: true });
|
|
237
|
+
renameSync(live, backup);
|
|
238
|
+
renameSync(candidate, live);
|
|
239
|
+
rmSync(journalPath(vault), { force: true });
|
|
240
|
+
for (const f of stalePreUpgrade(preUpgradeBackups(vault))) {
|
|
241
|
+
rmSync(join(vault, "backups", f), { force: true });
|
|
242
|
+
rmSync(join(vault, "backups", manifestOf(f)), { force: true });
|
|
243
|
+
}
|
|
244
|
+
const ms = Date.now() - t0;
|
|
245
|
+
log(`升级完成(${(ms / 1e3).toFixed(1)} 秒)。升级前的库在 ${backup}`);
|
|
246
|
+
return { from: have, to: target, backup, ms };
|
|
247
|
+
}
|
|
248
|
+
__name(upgradeVault, "upgradeVault");
|
|
249
|
+
function tooNewMessage(vault, have, target) {
|
|
250
|
+
let app = "";
|
|
251
|
+
try {
|
|
252
|
+
const db = new DatabaseSync(dbPath(vault), { readOnly: true });
|
|
253
|
+
try {
|
|
254
|
+
app = appOf(db);
|
|
255
|
+
} finally {
|
|
256
|
+
db.close();
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return `数据是更新的版本${app ? `(${app})` : ""}写的:数据库 v${have},这个程序(${VERSION})只认到 v${target}。请装${app ? ` ${app} 或更新的版本` : "新版本"};要用这个旧版本,先运行 \`zhiro vault rollback\` 回到升级前`;
|
|
261
|
+
}
|
|
262
|
+
__name(tooNewMessage, "tooNewMessage");
|
|
263
|
+
function stampApp(db) {
|
|
264
|
+
db.prepare("insert into meta(key, value) values ('app_version', ?) on conflict(key) do update set value = excluded.value").run(VERSION);
|
|
265
|
+
}
|
|
266
|
+
__name(stampApp, "stampApp");
|
|
267
|
+
function verifyVault(vault, opts = {}) {
|
|
268
|
+
const live = dbPath(vault), problems = [];
|
|
269
|
+
if (!existsSync(vault)) throw new Error(`数据目录不存在:${vault}`);
|
|
270
|
+
if (!existsSync(live)) return { ok: false, schema: 0, program: SCHEMA_VERSION, problems: [`没有数据库:${live}`], counts: {} };
|
|
271
|
+
let schema = 0;
|
|
272
|
+
try {
|
|
273
|
+
schema = schemaOf(live);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return { ok: false, schema: 0, program: SCHEMA_VERSION, problems: [String(e?.message ?? e)], counts: {} };
|
|
276
|
+
}
|
|
277
|
+
if (schema > SCHEMA_VERSION) problems.push(`数据库 v${schema} 比这个程序(v${SCHEMA_VERSION})新`);
|
|
278
|
+
if (schema < SCHEMA_VERSION) problems.push(`数据库 v${schema} 还没升级到 v${SCHEMA_VERSION}(启动一次就会升级)`);
|
|
279
|
+
const db = new DatabaseSync(live, { readOnly: true });
|
|
280
|
+
let counts2 = {};
|
|
281
|
+
try {
|
|
282
|
+
db.exec("pragma busy_timeout = 5000; begin");
|
|
283
|
+
const c = census(db);
|
|
284
|
+
counts2 = c.counts;
|
|
285
|
+
const check = db.prepare(opts.quick ? "pragma quick_check" : "pragma integrity_check").all().map((r) => Object.values(r)[0]);
|
|
286
|
+
if (check.join() !== "ok") problems.push(`完整性检查:${check.join(";").slice(0, 200)}`);
|
|
287
|
+
const fk = db.prepare("pragma foreign_key_check").all();
|
|
288
|
+
if (fk.length) problems.push(`${fk.length} 条外键指向不存在的行`);
|
|
289
|
+
if (schema === SCHEMA_VERSION) {
|
|
290
|
+
const want = expectedStructure(MIGRATIONS);
|
|
291
|
+
const missing = want.filter((s) => !c.structure.includes(s));
|
|
292
|
+
if (missing.length) problems.push(`缺少:${missing.join("、")}`);
|
|
293
|
+
}
|
|
294
|
+
db.exec("commit");
|
|
295
|
+
} finally {
|
|
296
|
+
db.close();
|
|
297
|
+
}
|
|
298
|
+
const cfg = join(vault, "config", "jobos.yaml");
|
|
299
|
+
if (existsSync(cfg)) {
|
|
300
|
+
try {
|
|
301
|
+
YAML.parse(readFileSync(cfg, "utf8"));
|
|
302
|
+
} catch (e) {
|
|
303
|
+
problems.push(`配置文件读不出来:${String(e?.message ?? e).slice(0, 120)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { ok: problems.length === 0, schema, program: SCHEMA_VERSION, problems, counts: counts2 };
|
|
307
|
+
}
|
|
308
|
+
__name(verifyVault, "verifyVault");
|
|
309
|
+
function rollbackVault(vault, _lock, file) {
|
|
310
|
+
const name = file ?? preUpgradeBackups(vault)[0];
|
|
311
|
+
if (!name) throw new Error("没有升级前的备份(backups/pre-v*.db),无从回退");
|
|
312
|
+
const backup = name.includes("/") || name.includes("\\") ? name : join(vault, "backups", name);
|
|
313
|
+
if (!existsSync(backup)) throw new Error(`找不到备份:${backup}`);
|
|
314
|
+
let manifest;
|
|
315
|
+
try {
|
|
316
|
+
manifest = JSON.parse(readFileSync(manifestOf(backup), "utf8"));
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error(`备份旁边没有清单(${manifestOf(backup)}),不能确认它是完整的,不回退`);
|
|
319
|
+
}
|
|
320
|
+
if (sha256(backup) !== manifest.sha256) throw new Error("备份文件和它的清单对不上(文件被改过或损坏),不回退");
|
|
321
|
+
const probe = new DatabaseSync(backup, { readOnly: true });
|
|
322
|
+
try {
|
|
323
|
+
const r = probe.prepare("pragma integrity_check").all();
|
|
324
|
+
if (r.map((x) => x.integrity_check).join() !== "ok") throw new Error("备份没通过完整性检查,不回退");
|
|
325
|
+
} finally {
|
|
326
|
+
probe.close();
|
|
327
|
+
}
|
|
328
|
+
const live = dbPath(vault);
|
|
329
|
+
const kept = join(vault, "backups", `jobos.db.before-rollback-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
|
|
330
|
+
if (existsSync(live)) {
|
|
331
|
+
const cur = new DatabaseSync(live);
|
|
332
|
+
try {
|
|
333
|
+
cur.exec("pragma wal_checkpoint(TRUNCATE)");
|
|
334
|
+
} finally {
|
|
335
|
+
cur.close();
|
|
336
|
+
}
|
|
337
|
+
for (const s of ["-wal", "-shm"]) rmSync(live + s, { force: true });
|
|
338
|
+
renameSync(live, kept);
|
|
339
|
+
}
|
|
340
|
+
copyFileSync(backup, live + ".rollback");
|
|
341
|
+
renameSync(live + ".rollback", live);
|
|
342
|
+
return { restored: backup, kept, manifest };
|
|
343
|
+
}
|
|
344
|
+
__name(rollbackVault, "rollbackVault");
|
|
345
|
+
|
|
346
|
+
// apps/daemon/src/service/export.ts
|
|
347
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
348
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync, copyFileSync as copyFileSync2 } from "node:fs";
|
|
349
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
350
|
+
import { isAbsolute, join as join2, relative as relative2, resolve } from "node:path";
|
|
351
|
+
import YAML2 from "yaml";
|
|
352
|
+
|
|
353
|
+
// apps/daemon/src/service/zip.ts
|
|
354
|
+
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
355
|
+
var TABLE = (() => {
|
|
356
|
+
const t = new Uint32Array(256);
|
|
357
|
+
for (let n = 0; n < 256; n++) {
|
|
358
|
+
let c = n;
|
|
359
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
360
|
+
t[n] = c >>> 0;
|
|
361
|
+
}
|
|
362
|
+
return t;
|
|
363
|
+
})();
|
|
364
|
+
function crc32(buf) {
|
|
365
|
+
let c = 4294967295;
|
|
366
|
+
for (let i = 0; i < buf.length; i++) c = TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
367
|
+
return (c ^ 4294967295) >>> 0;
|
|
368
|
+
}
|
|
369
|
+
__name(crc32, "crc32");
|
|
370
|
+
function dosTime(d) {
|
|
371
|
+
return { time: (d.getHours() & 31) << 11 | (d.getMinutes() & 63) << 5 | d.getSeconds() >> 1 & 31, date: (d.getFullYear() - 1980 & 127) << 9 | (d.getMonth() + 1 & 15) << 5 | d.getDate() & 31 };
|
|
372
|
+
}
|
|
373
|
+
__name(dosTime, "dosTime");
|
|
374
|
+
function zipWrite(entries) {
|
|
375
|
+
if (entries.length > 65e3) throw new Error("ZIP_TOO_MANY: 文件超过 65,000 个");
|
|
376
|
+
const parts = [], central = [];
|
|
377
|
+
let offset = 0;
|
|
378
|
+
for (const e of entries) {
|
|
379
|
+
const name = Buffer.from(e.name.replace(/\\/g, "/"), "utf8");
|
|
380
|
+
const comp = deflateRawSync(e.data), store = comp.length >= e.data.length;
|
|
381
|
+
const body = store ? e.data : comp, method = store ? 0 : 8, crc = crc32(e.data), { time, date } = dosTime(e.mtime ?? /* @__PURE__ */ new Date());
|
|
382
|
+
const local = Buffer.alloc(30);
|
|
383
|
+
local.writeUInt32LE(67324752, 0);
|
|
384
|
+
local.writeUInt16LE(20, 4);
|
|
385
|
+
local.writeUInt16LE(2048, 6);
|
|
386
|
+
local.writeUInt16LE(method, 8);
|
|
387
|
+
local.writeUInt16LE(time, 10);
|
|
388
|
+
local.writeUInt16LE(date, 12);
|
|
389
|
+
local.writeUInt32LE(crc, 14);
|
|
390
|
+
local.writeUInt32LE(body.length, 18);
|
|
391
|
+
local.writeUInt32LE(e.data.length, 22);
|
|
392
|
+
local.writeUInt16LE(name.length, 26);
|
|
393
|
+
local.writeUInt16LE(0, 28);
|
|
394
|
+
const cd = Buffer.alloc(46);
|
|
395
|
+
cd.writeUInt32LE(33639248, 0);
|
|
396
|
+
cd.writeUInt16LE(20, 4);
|
|
397
|
+
cd.writeUInt16LE(20, 6);
|
|
398
|
+
cd.writeUInt16LE(2048, 8);
|
|
399
|
+
cd.writeUInt16LE(method, 10);
|
|
400
|
+
cd.writeUInt16LE(time, 12);
|
|
401
|
+
cd.writeUInt16LE(date, 14);
|
|
402
|
+
cd.writeUInt32LE(crc, 16);
|
|
403
|
+
cd.writeUInt32LE(body.length, 20);
|
|
404
|
+
cd.writeUInt32LE(e.data.length, 24);
|
|
405
|
+
cd.writeUInt16LE(name.length, 28);
|
|
406
|
+
cd.writeUInt16LE(0, 30);
|
|
407
|
+
cd.writeUInt16LE(0, 32);
|
|
408
|
+
cd.writeUInt16LE(0, 34);
|
|
409
|
+
cd.writeUInt16LE(0, 36);
|
|
410
|
+
cd.writeUInt32LE(0, 38);
|
|
411
|
+
cd.writeUInt32LE(offset, 42);
|
|
412
|
+
parts.push(local, name, body);
|
|
413
|
+
central.push(cd, name);
|
|
414
|
+
offset += local.length + name.length + body.length;
|
|
415
|
+
}
|
|
416
|
+
const cdSize = central.reduce((a, b) => a + b.length, 0);
|
|
417
|
+
const end = Buffer.alloc(22);
|
|
418
|
+
end.writeUInt32LE(101010256, 0);
|
|
419
|
+
end.writeUInt16LE(0, 4);
|
|
420
|
+
end.writeUInt16LE(0, 6);
|
|
421
|
+
end.writeUInt16LE(entries.length, 8);
|
|
422
|
+
end.writeUInt16LE(entries.length, 10);
|
|
423
|
+
end.writeUInt32LE(cdSize, 12);
|
|
424
|
+
end.writeUInt32LE(offset, 16);
|
|
425
|
+
end.writeUInt16LE(0, 20);
|
|
426
|
+
if (offset + cdSize + 22 > 4294967295) throw new Error("ZIP_TOO_BIG: 超过 4 GB");
|
|
427
|
+
return Buffer.concat([...parts, ...central, end]);
|
|
428
|
+
}
|
|
429
|
+
__name(zipWrite, "zipWrite");
|
|
430
|
+
function zipRead(buf) {
|
|
431
|
+
let eocd = -1;
|
|
432
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) if (buf.readUInt32LE(i) === 101010256) {
|
|
433
|
+
eocd = i;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (eocd < 0) throw new Error("ZIP_BAD: 不是 zip 文件");
|
|
437
|
+
const count = buf.readUInt16LE(eocd + 10), cdOffset = buf.readUInt32LE(eocd + 16);
|
|
438
|
+
const out = [];
|
|
439
|
+
let p = cdOffset;
|
|
440
|
+
for (let i = 0; i < count; i++) {
|
|
441
|
+
if (buf.readUInt32LE(p) !== 33639248) throw new Error("ZIP_BAD: 目录损坏");
|
|
442
|
+
const method = buf.readUInt16LE(p + 10), crc = buf.readUInt32LE(p + 16), csize = buf.readUInt32LE(p + 20), usize = buf.readUInt32LE(p + 24);
|
|
443
|
+
const nlen = buf.readUInt16LE(p + 28), xlen = buf.readUInt16LE(p + 30), clen = buf.readUInt16LE(p + 32), local = buf.readUInt32LE(p + 42);
|
|
444
|
+
const name = buf.subarray(p + 46, p + 46 + nlen).toString("utf8");
|
|
445
|
+
p += 46 + nlen + xlen + clen;
|
|
446
|
+
if (buf.readUInt32LE(local) !== 67324752) throw new Error("ZIP_BAD: 条目损坏");
|
|
447
|
+
const dataStart = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
448
|
+
const body = buf.subarray(dataStart, dataStart + csize);
|
|
449
|
+
const data = method === 8 ? inflateRawSync(body) : method === 0 ? Buffer.from(body) : (() => {
|
|
450
|
+
throw new Error(`ZIP_BAD: 不支持的压缩方式 ${method}`);
|
|
451
|
+
})();
|
|
452
|
+
if (data.length !== usize || crc32(data) !== crc) throw new Error(`ZIP_BAD: ${name} 校验失败`);
|
|
453
|
+
out.push({ name, data });
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
__name(zipRead, "zipRead");
|
|
458
|
+
|
|
459
|
+
// apps/daemon/src/service/export.ts
|
|
460
|
+
var FORMAT = 1;
|
|
461
|
+
var FILE_DIRS = ["research", "resume", "profile", "captures", "snapshots", "submissions"];
|
|
462
|
+
var SECRET_KEY = /secret|password|token|webhook|cookie|apikey|api_key/i;
|
|
463
|
+
var COUNTED = ["company", "campaign", "position", "application", "fact", "proposal", "agent_action"];
|
|
464
|
+
function counts(db) {
|
|
465
|
+
const out = {};
|
|
466
|
+
for (const t of COUNTED) {
|
|
467
|
+
try {
|
|
468
|
+
out[t] = db.prepare(`select count(*) as n from ${t}`).get().n;
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
__name(counts, "counts");
|
|
475
|
+
function redact(v) {
|
|
476
|
+
if (Array.isArray(v)) return v.map(redact);
|
|
477
|
+
if (v && typeof v === "object") {
|
|
478
|
+
const o = {};
|
|
479
|
+
for (const [k, x] of Object.entries(v)) o[k] = SECRET_KEY.test(k) ? "" : redact(x);
|
|
480
|
+
return o;
|
|
481
|
+
}
|
|
482
|
+
return v;
|
|
483
|
+
}
|
|
484
|
+
__name(redact, "redact");
|
|
485
|
+
function walk(vault) {
|
|
486
|
+
const out = [];
|
|
487
|
+
const visit = /* @__PURE__ */ __name((abs, rel) => {
|
|
488
|
+
const st = lstatSync2(abs);
|
|
489
|
+
if (st.isSymbolicLink()) return;
|
|
490
|
+
if (st.isDirectory()) {
|
|
491
|
+
for (const n of readdirSync2(abs)) visit(join2(abs, n), rel ? `${rel}/${n}` : n);
|
|
492
|
+
} else if (st.isFile()) out.push({ rel, abs });
|
|
493
|
+
}, "visit");
|
|
494
|
+
for (const d of FILE_DIRS) if (existsSync2(join2(vault, d))) visit(join2(vault, d), d);
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
__name(walk, "walk");
|
|
498
|
+
var realOf = /* @__PURE__ */ __name((p) => {
|
|
499
|
+
try {
|
|
500
|
+
return realpathSync(p);
|
|
501
|
+
} catch {
|
|
502
|
+
const up = resolve(p, "..");
|
|
503
|
+
return up === resolve(p) ? up : join2(realOf(up), relative2(up, resolve(p)));
|
|
504
|
+
}
|
|
505
|
+
}, "realOf");
|
|
506
|
+
var inside = /* @__PURE__ */ __name((dir, p) => {
|
|
507
|
+
const r = relative2(realOf(dir), realOf(p));
|
|
508
|
+
return r === "" || !r.startsWith("..") && !isAbsolute(r);
|
|
509
|
+
}, "inside");
|
|
510
|
+
function snapshotDb(db, to) {
|
|
511
|
+
rmSync2(to, { force: true });
|
|
512
|
+
db.exec(`vacuum into '${to.replace(/'/g, "''")}'`);
|
|
513
|
+
const copy = new DatabaseSync2(to, { readOnly: true });
|
|
514
|
+
try {
|
|
515
|
+
const r = copy.prepare("pragma integrity_check").all();
|
|
516
|
+
if (r.length !== 1 || r[0].integrity_check !== "ok") throw new Error(`SNAPSHOT_BAD: ${r.map((x) => x.integrity_check).join("; ").slice(0, 200)}`);
|
|
517
|
+
} finally {
|
|
518
|
+
copy.close();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
__name(snapshotDb, "snapshotDb");
|
|
522
|
+
function exportVault(db, vault, to) {
|
|
523
|
+
if (!/\.zip$/i.test(to)) throw new Error("BAD_PATH: 导出文件名要以 .zip 结尾");
|
|
524
|
+
if (inside(vault, to)) throw new Error("BAD_PATH: 不能导出到数据目录里面");
|
|
525
|
+
if (existsSync2(to)) throw new Error("EXISTS: 已有同名文件,换个名字");
|
|
526
|
+
mkdirSync(join2(vault, "backups"), { recursive: true });
|
|
527
|
+
const snap = join2(vault, "backups", `export-${Date.now()}.db`);
|
|
528
|
+
snapshotDb(db, snap);
|
|
529
|
+
try {
|
|
530
|
+
const dbBuf = readFileSync2(snap);
|
|
531
|
+
const schemaVersion = db.prepare("select max(v) as v from schema_version").get().v ?? 0;
|
|
532
|
+
const files = walk(vault);
|
|
533
|
+
const entries = [{ name: "jobos.db", data: dbBuf }];
|
|
534
|
+
const cfg = join2(vault, "config", "jobos.yaml");
|
|
535
|
+
if (existsSync2(cfg)) entries.push({ name: "config/jobos.yaml", data: Buffer.from(YAML2.stringify(redact(YAML2.parse(readFileSync2(cfg, "utf8")) ?? {})), "utf8") });
|
|
536
|
+
for (const f of files) entries.push({ name: `files/${f.rel}`, data: readFileSync2(f.abs), mtime: statSync2(f.abs).mtime });
|
|
537
|
+
const manifest = { format: FORMAT, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), schemaVersion, counts: counts(db), files: files.length, dbSha256: createHash2("sha256").update(dbBuf).digest("hex") };
|
|
538
|
+
entries.unshift({ name: "manifest.json", data: Buffer.from(JSON.stringify(manifest, null, 2), "utf8") });
|
|
539
|
+
const zip = zipWrite(entries);
|
|
540
|
+
mkdirSync(resolve(to, ".."), { recursive: true });
|
|
541
|
+
writeFileSync(to + ".part", zip);
|
|
542
|
+
renameSync2(to + ".part", to);
|
|
543
|
+
return { path: to, bytes: zip.length, manifest };
|
|
544
|
+
} finally {
|
|
545
|
+
rmSync2(snap, { force: true });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
__name(exportVault, "exportVault");
|
|
549
|
+
function importVault(vault, from, lock) {
|
|
550
|
+
const entries = zipRead(readFileSync2(from));
|
|
551
|
+
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
552
|
+
const mf = byName.get("manifest.json"), dbe = byName.get("jobos.db");
|
|
553
|
+
if (!mf || !dbe) throw new Error("NOT_EXPORT: 这不是 Zhiro 导出的 zip(缺 manifest.json 或 jobos.db)");
|
|
554
|
+
const manifest = JSON.parse(mf.data.toString("utf8"));
|
|
555
|
+
if (manifest.format !== FORMAT) throw new Error(`FORMAT: 导出格式 ${manifest.format},这个版本只认 ${FORMAT}`);
|
|
556
|
+
if (createHash2("sha256").update(dbe.data).digest("hex") !== manifest.dbSha256) throw new Error("CORRUPT: 数据库与清单的校验和不符");
|
|
557
|
+
for (const e of entries) if (/(^|\/)\.\.(\/|$)|^[A-Za-z]:|^\//.test(e.name)) throw new Error(`CORRUPT: 路径不对 ${e.name}`);
|
|
558
|
+
mkdirSync(join2(vault, "backups"), { recursive: true });
|
|
559
|
+
mkdirSync(join2(vault, "config"), { recursive: true });
|
|
560
|
+
const incoming = join2(vault, "backups", `import-${Date.now()}.db`);
|
|
561
|
+
writeFileSync(incoming, dbe.data);
|
|
562
|
+
const probe = new DatabaseSync2(incoming, { readOnly: true });
|
|
563
|
+
try {
|
|
564
|
+
const r = probe.prepare("pragma integrity_check").all();
|
|
565
|
+
if (r.length !== 1 || r[0].integrity_check !== "ok") throw new Error("CORRUPT: 导出里的数据库没通过完整性检查");
|
|
566
|
+
} finally {
|
|
567
|
+
probe.close();
|
|
568
|
+
}
|
|
569
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
570
|
+
const backup = join2(vault, "backups", `pre-import-${stamp}.db`);
|
|
571
|
+
const dbPath2 = join2(vault, "jobos.db");
|
|
572
|
+
let before = {};
|
|
573
|
+
if (existsSync2(dbPath2)) {
|
|
574
|
+
const cur = new DatabaseSync2(dbPath2, { readOnly: true });
|
|
575
|
+
try {
|
|
576
|
+
before = counts(cur);
|
|
577
|
+
snapshotDb(cur, backup);
|
|
578
|
+
} finally {
|
|
579
|
+
cur.close();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const cfg = join2(vault, "config", "jobos.yaml");
|
|
583
|
+
if (existsSync2(cfg)) copyFileSync2(cfg, join2(vault, "backups", `pre-import-${stamp}.yaml`));
|
|
584
|
+
for (const s of ["", "-wal", "-shm"]) rmSync2(dbPath2 + s, { force: true });
|
|
585
|
+
renameSync2(incoming, dbPath2);
|
|
586
|
+
let files = 0;
|
|
587
|
+
for (const e of entries) {
|
|
588
|
+
if (!e.name.startsWith("files/")) continue;
|
|
589
|
+
const p = join2(vault, e.name.slice(6));
|
|
590
|
+
mkdirSync(resolve(p, ".."), { recursive: true });
|
|
591
|
+
writeFileSync(p, e.data);
|
|
592
|
+
files++;
|
|
593
|
+
}
|
|
594
|
+
const cfge = byName.get("config/jobos.yaml");
|
|
595
|
+
if (cfge) {
|
|
596
|
+
const incomingCfg = YAML2.parse(cfge.data.toString("utf8")) ?? {};
|
|
597
|
+
const current = existsSync2(cfg) ? YAML2.parse(readFileSync2(cfg, "utf8")) ?? {} : {};
|
|
598
|
+
const keepSecrets = /* @__PURE__ */ __name((into, from2) => {
|
|
599
|
+
if (!from2 || typeof from2 !== "object") return;
|
|
600
|
+
for (const [k, v] of Object.entries(from2)) {
|
|
601
|
+
if (SECRET_KEY.test(k) && v && !into?.[k]) into[k] = v;
|
|
602
|
+
else if (v && typeof v === "object" && !Array.isArray(v) && into?.[k] && typeof into[k] === "object") keepSecrets(into[k], v);
|
|
603
|
+
}
|
|
604
|
+
}, "keepSecrets");
|
|
605
|
+
keepSecrets(incomingCfg, current);
|
|
606
|
+
replaceConfig(vault, incomingCfg);
|
|
607
|
+
}
|
|
608
|
+
upgradeVault(vault, lock);
|
|
609
|
+
const db = openDb(dbPath2);
|
|
610
|
+
try {
|
|
611
|
+
const after = counts(db);
|
|
612
|
+
const schemaVersion = db.prepare("select max(v) as v from schema_version").get().v ?? 0;
|
|
613
|
+
return { backup, before, after, files, schemaVersion };
|
|
614
|
+
} finally {
|
|
615
|
+
db.close();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
__name(importVault, "importVault");
|
|
619
|
+
var defaultExportPath = /* @__PURE__ */ __name(() => {
|
|
620
|
+
const d = /* @__PURE__ */ new Date();
|
|
621
|
+
return join2(process.env.USERPROFILE ?? process.env.HOME ?? ".", "Downloads", `Zhiro-${d.toISOString().slice(0, 10)}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}.zip`);
|
|
622
|
+
}, "defaultExportPath");
|
|
623
|
+
|
|
624
|
+
// apps/daemon/src/service/backup.ts
|
|
625
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
626
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync3, readdirSync as readdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
627
|
+
import { join as join3 } from "node:path";
|
|
628
|
+
import YAML3 from "yaml";
|
|
629
|
+
var OWN = /^jobos-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.db$/;
|
|
630
|
+
var takenAt = /* @__PURE__ */ __name((f) => Date.parse(f.slice(6, 16) + "T" + f.slice(17, 25).replace(/-/g, ":") + "." + f.slice(26, 29) + "Z"), "takenAt");
|
|
631
|
+
function staleBackups(names, now = Date.now(), keep = 5) {
|
|
632
|
+
const rest = names.filter((f) => OWN.test(f)).sort().reverse().slice(keep);
|
|
633
|
+
const older = rest.find((f) => now - takenAt(f) >= 7 * 864e5);
|
|
634
|
+
return rest.filter((f) => f !== older);
|
|
635
|
+
}
|
|
636
|
+
__name(staleBackups, "staleBackups");
|
|
637
|
+
var pairOf = /* @__PURE__ */ __name((db) => db.replace(/\.db$/, ".yaml"), "pairOf");
|
|
638
|
+
function runBackup(db, vault) {
|
|
639
|
+
const dir = join3(vault, "backups");
|
|
640
|
+
mkdirSync2(dir, { recursive: true });
|
|
641
|
+
const path = join3(dir, `jobos-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.db`);
|
|
642
|
+
const cfg = configFile(vault);
|
|
643
|
+
const config = existsSync3(cfg) ? pairOf(path) : "";
|
|
644
|
+
try {
|
|
645
|
+
snapshotDb(db, path);
|
|
646
|
+
const copy = new DatabaseSync3(path, { readOnly: true });
|
|
647
|
+
let same;
|
|
648
|
+
try {
|
|
649
|
+
same = JSON.stringify(counts(copy)) === JSON.stringify(counts(db));
|
|
650
|
+
} finally {
|
|
651
|
+
copy.close();
|
|
652
|
+
}
|
|
653
|
+
if (!same) throw new Error("BACKUP_BAD: 备份里的条数和库里对不上(备份期间库被写入了?),再跑一次");
|
|
654
|
+
if (config) writeFileSync2(config, YAML3.stringify(redact(YAML3.parse(readFileSync3(cfg, "utf8")) ?? {})), "utf8");
|
|
655
|
+
} catch (e) {
|
|
656
|
+
rmSync3(path, { force: true });
|
|
657
|
+
if (config) rmSync3(config, { force: true });
|
|
658
|
+
throw e;
|
|
659
|
+
}
|
|
660
|
+
for (const f of staleBackups(readdirSync3(dir))) {
|
|
661
|
+
rmSync3(join3(dir, f), { force: true });
|
|
662
|
+
rmSync3(join3(dir, pairOf(f)), { force: true });
|
|
663
|
+
}
|
|
664
|
+
return { path, bytes: statSync3(path).size, verified: true, config };
|
|
665
|
+
}
|
|
666
|
+
__name(runBackup, "runBackup");
|
|
667
|
+
|
|
668
|
+
export {
|
|
669
|
+
UpgradeFailed,
|
|
670
|
+
preUpgradeBackups,
|
|
671
|
+
upgradeVault,
|
|
672
|
+
stampApp,
|
|
673
|
+
verifyVault,
|
|
674
|
+
rollbackVault,
|
|
675
|
+
zipRead,
|
|
676
|
+
exportVault,
|
|
677
|
+
importVault,
|
|
678
|
+
defaultExportPath,
|
|
679
|
+
runBackup
|
|
680
|
+
};
|