@xiaoyuyu6420/dsh-backup 0.9.1 → 0.10.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 +19 -1
- package/README.zh.md +19 -1
- package/lib/client.js +39 -17
- package/lib/index.js +441 -79
- package/package.json +2 -1
- package/rescue/rescue.mjs +29 -5
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Backup, restore, download and GitHub-sync DeepSeek Harness user data (~/.dsh): /backup, scheduled auto-backup that survives restarts, sha256 checksums, integrity verify, rotation, credential redaction with a local vault (plaintext never leaves the machine), cross-machine restore preflight with github pull, and a visual Settings panel. Cross-platform (macOS/Linux/Windows). 一键备份与恢复 DSH 数据:定时自动备份、完整性校验、凭据默认脱敏(明文只存本机 vault)、跨机恢复预检与 github pull 拉取,附 Settings 可视面板。",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@11.24.0",
|
|
6
7
|
"main": "lib/index.js",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./lib/index.js",
|
package/rescue/rescue.mjs
CHANGED
|
@@ -104,7 +104,7 @@ async function listBackups(root) {
|
|
|
104
104
|
}
|
|
105
105
|
const backups = [];
|
|
106
106
|
for (const d of dirents) {
|
|
107
|
-
if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || !d.name.endsWith('.tar.gz')) continue;
|
|
107
|
+
if (!d.name.startsWith('dsh-') || d.name.startsWith('dsh-pre-restore-') || d.name.startsWith('dsh-t-') || !d.name.endsWith('.tar.gz')) continue;
|
|
108
108
|
let size;
|
|
109
109
|
try {
|
|
110
110
|
size = (await fs.stat(`${root}/${d.name}`)).size;
|
|
@@ -133,9 +133,9 @@ async function verifyOne(root, name) {
|
|
|
133
133
|
try {
|
|
134
134
|
expected = (await fs.readFile(`${root}/${name}.sha256`, 'utf8')).trim().split(/\s+/)[0];
|
|
135
135
|
} catch { /* 边车缺失 */ }
|
|
136
|
-
if (!/^[0-9a-f]{64}$/.test(expected)) return { name, ok: false, note: '
|
|
136
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) return { name, ok: false, note: '缺少或无效的配套校验文件(.sha256)——无法确认这份备份是否完好' };
|
|
137
137
|
const actual = await sha256File(`${root}/${name}`);
|
|
138
|
-
return { name, ok: actual === expected, note: actual === expected ? '完整' : '
|
|
138
|
+
return { name, ok: actual === expected, note: actual === expected ? '完整' : '校验和不匹配(归档或校验文件之一可能损坏)' };
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async function pickArchive(root, selector) {
|
|
@@ -430,6 +430,11 @@ async function restoreArchive(root, selector, apply) {
|
|
|
430
430
|
const parent = dshHome.slice(0, -(base.length + 1)) || '/';
|
|
431
431
|
const entries = safeEntries(run(['tar', '-tvzf', picked.name], root), base);
|
|
432
432
|
const { meta, redactedFiles } = await readArchiveMeta(root, picked.name);
|
|
433
|
+
// 分类型归档是子集,整包恢复会挪旁 ~/.dsh 后只解压子集 → 丢失未包含的类型
|
|
434
|
+
// 数据。灾时请用全量归档(dsh-)整包恢复;分类型恢复走 dsh 的 --types。
|
|
435
|
+
if (meta && Array.isArray(meta.types) && meta.types.length) {
|
|
436
|
+
throw new Error(`${picked.name} 是分类型归档(${meta.types.join(', ')}),整包恢复会丢失其他类型数据。请选一份全量归档(dsh- 开头)整包恢复,或用 dsh 的「/backup restore ${picked.name} --types ${meta.types.join(',')}」分类型恢复。`);
|
|
437
|
+
}
|
|
433
438
|
const home = userHome();
|
|
434
439
|
const preflight = [];
|
|
435
440
|
if (meta && typeof meta.home === 'string' && home && meta.home !== home) {
|
|
@@ -547,8 +552,27 @@ dialog::backdrop{background:rgba(0,0,0,.4)}
|
|
|
547
552
|
<button class="danger" id="confirmGo">确认恢复</button></div></dialog>
|
|
548
553
|
<p class="note">恢复是整体替换:现有数据会先自动快照再挪到一旁(.dsh.pre-restore-*),凭据从本机 vault 补回。只监听本机回环地址。</p>
|
|
549
554
|
<script>
|
|
550
|
-
const
|
|
555
|
+
const ERR_TEXT = {
|
|
556
|
+
'not-found': '接口不存在(可能是浏览器缓存了旧页面),请刷新后重试',
|
|
557
|
+
'missing-rescue-header': '页面安全校验未通过,请刷新页面后重试',
|
|
558
|
+
'confirm-required': '该操作需要先在确认框里确认',
|
|
559
|
+
'unknown-op': '未知的操作类型,请刷新页面后重试',
|
|
560
|
+
'bad-json': '请求内容无法解析,请刷新页面后重试',
|
|
561
|
+
'too-large': '请求内容过大',
|
|
562
|
+
'no-archive': '备份目录里没有可用的归档,请先确认备份目录位置',
|
|
563
|
+
};
|
|
564
|
+
// 失败结果按"出了什么问题 + 怎么办"展示;未知错误再回退 JSON 细节
|
|
565
|
+
const show = (r) => {
|
|
566
|
+
if (r.ok !== false) return typeof r === 'string' ? r : JSON.stringify(r, null, 2);
|
|
567
|
+
// 校验类失败(verify/restore 拦截)没有 error 码,note/summary 本身就是人话主文案
|
|
568
|
+
const note = typeof (r.note || r.summary) === 'string' ? (r.note || r.summary) : null;
|
|
569
|
+
const human = ERR_TEXT[r.error] || note || '操作失败';
|
|
570
|
+
const detail = r.summary || r.note || r.error;
|
|
571
|
+
return '❌ ' + human + (detail && detail !== r.error && detail !== human ? '\\n技术细节: ' + (typeof detail === 'string' ? detail : JSON.stringify(detail)) : '');
|
|
572
|
+
};
|
|
573
|
+
const log = (t) => { document.getElementById('log').textContent = typeof t === 'string' ? t : show(t); };
|
|
551
574
|
const dlg = document.getElementById('confirm');
|
|
575
|
+
const fmtSize = (n) => typeof n !== 'number' ? '?' : n >= 1048576 ? (n/1048576).toFixed(1) + 'MB' : Math.max(1, Math.round(n/1024)) + 'KB';
|
|
552
576
|
async function api(op, body) {
|
|
553
577
|
const res = await fetch('/api/' + op, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dsh-Rescue': '1' }, body: JSON.stringify(body || {}) });
|
|
554
578
|
return res.json();
|
|
@@ -556,7 +580,7 @@ async function api(op, body) {
|
|
|
556
580
|
async function renderList() {
|
|
557
581
|
const r = await api('list');
|
|
558
582
|
if (!r.ok) { log(r); return; }
|
|
559
|
-
const rows = r.backups.map(b => '<tr><td>' + b.name + '</td><td>' + (b.size
|
|
583
|
+
const rows = r.backups.map(b => '<tr><td>' + b.name + '</td><td>' + fmtSize(b.size) +
|
|
560
584
|
'</td><td><button onclick="verify(\\'' + b.name + '\\')">校验</button> ' +
|
|
561
585
|
'<button class="danger" onclick="restore(\\'' + b.name + '\\')">恢复…</button></td></tr>').join('');
|
|
562
586
|
document.getElementById('list').innerHTML = r.backups.length
|