@xiaoyuyu6420/dsh-backup 0.13.1 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +8 -1
- package/README.md +10 -1
- package/lib/client.js +2 -2
- package/lib/index.js +532 -145
- package/package.json +8 -8
- package/rescue/rescue.mjs +48 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "一键备份与恢复 DeepSeek Harness(dsh)的全部用户数据(~/.dsh):定时自动备份、升级快照、会话日志体检修复、迁移预检、宿主起不来也能用的救援通道、凭据脱敏、GitHub 同步跨机迁移,附可视化面板。 One command to back up & restore all of ~/.dsh: /backup, scheduled auto-backup, upgrade snapshots, session doctor & repair, migrate precheck, out-of-process rescue console, credential redaction, GitHub sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.24.0",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
],
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
-
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
57
|
-
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
58
|
-
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
59
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
60
|
-
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
61
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1",
|
|
62
62
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": ">=20",
|
|
72
|
-
"dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1"
|
|
72
|
+
"dsh": "^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-rc.1"
|
|
73
73
|
},
|
|
74
74
|
"license": "MIT",
|
|
75
75
|
"repository": {
|
package/rescue/rescue.mjs
CHANGED
|
@@ -37,7 +37,11 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
|
37
37
|
const SENSITIVE_DEFAULTS = ['.credentials.yaml', '.env', 'qq-bridge/config.json'];
|
|
38
38
|
const VAULT_DIR = 'vault';
|
|
39
39
|
const ZSTD_MAGIC = 0xfd2fb528;
|
|
40
|
-
|
|
40
|
+
// 与 lib/index.js 同源:会话日志名形如 session[.v<N>].jsonl[.zstd](宿主
|
|
41
|
+
// sessionFormatLogFilename 的形态)。写死具体名字会在格式换代当天整体失明
|
|
42
|
+
// (#117)——救援台是宿主起不来时的唯一通道,更不能看不见会话。
|
|
43
|
+
const SESSION_LOG_RE = /^session(?:\.v([1-9][0-9]*))?\.jsonl(?:\.zstd)?$/;
|
|
44
|
+
const isSessionLogName = (name) => SESSION_LOG_RE.test(name);
|
|
41
45
|
const SCAN_SKIP_DIRS = new Set(['node_modules', '.system', '.git', 'vault']);
|
|
42
46
|
const SCAN_MAX_FILES = 4000;
|
|
43
47
|
const SCAN_MAX_DEPTH = 8;
|
|
@@ -108,7 +112,7 @@ async function listBackups(root) {
|
|
|
108
112
|
}
|
|
109
113
|
const backups = [];
|
|
110
114
|
for (const d of dirents) {
|
|
111
|
-
if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || d.name.startsWith('dsh-t-') || !d.name.endsWith('.tar.gz')) continue;
|
|
115
|
+
if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || d.name.startsWith('dsh-pre-upgrade-') || d.name.startsWith('dsh-t-') || !d.name.endsWith('.tar.gz')) continue;
|
|
112
116
|
let size;
|
|
113
117
|
try {
|
|
114
118
|
size = (await fs.stat(`${root}/${d.name}`)).size;
|
|
@@ -173,24 +177,48 @@ function parseTarEntry(line) {
|
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
179
|
if (nameIdx < 0 || nameIdx >= f.length) return null;
|
|
176
|
-
|
|
180
|
+
const rest = f.slice(nameIdx).join(' ');
|
|
181
|
+
// 链接目标单独摘出(硬链接 `<名> link to <目标>`、符号链接 `<名> -> <目标>`):
|
|
182
|
+
// 与插件侧 parseTarEntry 同语义,别把目标混进名字(#112)。
|
|
183
|
+
let name = rest;
|
|
184
|
+
let link = null;
|
|
185
|
+
const hardAt = rest.indexOf(' link to ');
|
|
186
|
+
if (hardAt >= 0) {
|
|
187
|
+
name = rest.slice(0, hardAt);
|
|
188
|
+
link = rest.slice(hardAt + ' link to '.length);
|
|
189
|
+
} else {
|
|
190
|
+
const symAt = rest.lastIndexOf(' -> ');
|
|
191
|
+
if (symAt >= 0) {
|
|
192
|
+
name = rest.slice(0, symAt);
|
|
193
|
+
link = rest.slice(symAt + 4);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
type: f[0][0],
|
|
198
|
+
name: name.replace(/\/$/, ''),
|
|
199
|
+
link: link === null ? null : link.replace(/\/$/, ''),
|
|
200
|
+
};
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
function safeEntries(listedText, base) {
|
|
180
204
|
const entries = [];
|
|
181
205
|
const bad = [];
|
|
206
|
+
const insideBase = (p) => Boolean(p)
|
|
207
|
+
&& !p.startsWith('/') && !/^[A-Za-z]:/.test(p)
|
|
208
|
+
&& !p.split('/').includes('..')
|
|
209
|
+
&& (p === base || p.startsWith(`${base}/`));
|
|
182
210
|
for (const line of listedText.split('\n')) {
|
|
183
211
|
const s = line.trim();
|
|
184
212
|
if (!s) continue;
|
|
185
213
|
const parsed = parseTarEntry(s);
|
|
186
214
|
const name = parsed ? parsed.name.replace(/\\/g, '/').replace(/\/$/, '') : null;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) {
|
|
215
|
+
const link = parsed && parsed.link ? parsed.link.replace(/\\/g, '/').replace(/\/$/, '') : null;
|
|
216
|
+
// 与插件侧同语义:放行目标也在备份根内的硬链接 'h'(DSH 附件库的正常形态,
|
|
217
|
+
// 见 issue #112),符号链接/设备等仍拒绝。救援台是宿主起不来时的唯一通路,
|
|
218
|
+
// 这里漏判会让"插件写出的归档自己都恢复不了"。
|
|
219
|
+
const plainOk = Boolean(parsed) && Boolean(name) && (parsed.type === '-' || parsed.type === 'd') && insideBase(name);
|
|
220
|
+
const hardOk = Boolean(parsed) && parsed.type === 'h' && insideBase(name) && insideBase(link);
|
|
221
|
+
if (!plainOk && !hardOk) {
|
|
194
222
|
bad.push(parsed ? name : s);
|
|
195
223
|
} else {
|
|
196
224
|
entries.push(name);
|
|
@@ -338,7 +366,7 @@ async function collectSessionLogs(dir, relPrefix, depth, out) {
|
|
|
338
366
|
if (d.isDirectory()) {
|
|
339
367
|
if (SCAN_SKIP_DIRS.has(d.name)) continue;
|
|
340
368
|
await collectSessionLogs(`${dir}/${d.name}`, rel, depth + 1, out);
|
|
341
|
-
} else if (
|
|
369
|
+
} else if (isSessionLogName(d.name)) {
|
|
342
370
|
out.push({ abs: `${dir}/${d.name}`, rel });
|
|
343
371
|
}
|
|
344
372
|
}
|
|
@@ -610,23 +638,27 @@ const show = (r) => {
|
|
|
610
638
|
const log = (t) => { document.getElementById('log').textContent = typeof t === 'string' ? t : show(t); };
|
|
611
639
|
const dlg = document.getElementById('confirm');
|
|
612
640
|
const fmtSize = (n) => typeof n !== 'number' ? '?' : n >= 1048576 ? (n/1048576).toFixed(1) + 'MB' : Math.max(1, Math.round(n/1024)) + 'KB';
|
|
641
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
613
642
|
async function api(op, body) {
|
|
614
643
|
const res = await fetch('/api/' + op, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dsh-Rescue': '1' }, body: JSON.stringify(body || {}) });
|
|
615
644
|
return res.json();
|
|
616
645
|
}
|
|
646
|
+
let listNames = [];
|
|
617
647
|
async function renderList() {
|
|
618
648
|
const r = await api('list');
|
|
619
649
|
if (!r.ok) { log(r); return; }
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
'
|
|
650
|
+
listNames = r.backups.map(b => b.name);
|
|
651
|
+
const rows = r.backups.map((b, i) => '<tr><td>' + esc(b.name) + '</td><td>' + fmtSize(b.size) +
|
|
652
|
+
'</td><td><button onclick="verify(' + i + ')">校验</button> ' +
|
|
653
|
+
'<button class="danger" onclick="restore(' + i + ')">恢复…</button></td></tr>').join('');
|
|
623
654
|
document.getElementById('list').innerHTML = r.backups.length
|
|
624
655
|
? '<table><tr><th>备份</th><th>大小</th><th></th></tr>' + rows + '</table>'
|
|
625
656
|
: '<p>备份目录里没有找到归档。</p>';
|
|
626
657
|
log(r.summary || '');
|
|
627
658
|
}
|
|
628
|
-
async function verify(
|
|
629
|
-
async function restore(
|
|
659
|
+
async function verify(i) { log(await api('verify', { selector: listNames[i] })); }
|
|
660
|
+
async function restore(i) {
|
|
661
|
+
const name = listNames[i];
|
|
630
662
|
const pre = await api('restore', { selector: name, dryRun: true });
|
|
631
663
|
log(pre);
|
|
632
664
|
if (!pre.ok) return;
|