@xiaoyuyu6420/dsh-backup 0.13.2 → 0.13.4

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/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
- const SESSION_LOG_NAMES = new Set(['session.jsonl.zstd', 'session.jsonl']);
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;
@@ -173,24 +177,48 @@ function parseTarEntry(line) {
173
177
  }
174
178
  }
175
179
  if (nameIdx < 0 || nameIdx >= f.length) return null;
176
- return { type: f[0][0], name: f.slice(nameIdx).join(' ').replace(/\/$/, '') };
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
- if (
188
- !parsed || !name
189
- || (parsed.type !== '-' && parsed.type !== 'd')
190
- || name.startsWith('/') || /^[A-Za-z]:/.test(name)
191
- || name.split('/').includes('..')
192
- || (name !== base && !name.startsWith(`${base}/`))
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 (SESSION_LOG_NAMES.has(d.name)) {
369
+ } else if (isSessionLogName(d.name)) {
342
370
  out.push({ abs: `${dir}/${d.name}`, rel });
343
371
  }
344
372
  }