@mison/wecom-cleaner 1.2.0 → 1.2.1
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 +23 -15
- package/docs/TEST_MATRIX.md +106 -0
- package/docs/releases/v1.2.1.md +33 -0
- package/native/manifest.json +3 -3
- package/package.json +2 -1
- package/skills/wecom-cleaner-agent/scripts/cleanup_monthly_report.sh +11 -1
- package/skills/wecom-cleaner-agent/scripts/recycle_maintain_report.sh +1 -1
- package/skills/wecom-cleaner-agent/scripts/restore_batch_report.sh +1 -1
- package/skills/wecom-cleaner-agent/scripts/space_governance_report.sh +3 -1
- package/src/recycle-maintenance.js +44 -3
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
- [常用参数](#常用参数)
|
|
28
28
|
- [数据与审计文件](#数据与审计文件)
|
|
29
29
|
- [开发与质量门禁](#开发与质量门禁)
|
|
30
|
+
- [测试矩阵](#测试矩阵)
|
|
30
31
|
- [发布与打包](#发布与打包)
|
|
31
32
|
- [FAQ](#faq)
|
|
32
33
|
|
|
@@ -158,10 +159,10 @@ GitHub 备选方式(无 npm 包依赖):
|
|
|
158
159
|
curl -fsSL https://raw.githubusercontent.com/MisonL/wecom-cleaner/main/scripts/install-skill.sh | bash
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
若需安装指定版本标签(例如 `v1.1
|
|
162
|
+
若需安装指定版本标签(例如 `v1.2.1`):
|
|
162
163
|
|
|
163
164
|
```bash
|
|
164
|
-
curl -fsSL https://raw.githubusercontent.com/MisonL/wecom-cleaner/main/scripts/install-skill.sh | bash -s -- --ref v1.1
|
|
165
|
+
curl -fsSL https://raw.githubusercontent.com/MisonL/wecom-cleaner/main/scripts/install-skill.sh | bash -s -- --ref v1.2.1
|
|
165
166
|
```
|
|
166
167
|
|
|
167
168
|
Agent 侧统一任务入口脚本(位于 `skills/wecom-cleaner-agent/scripts/`):
|
|
@@ -384,19 +385,25 @@ npm run format:check
|
|
|
384
385
|
发布前推荐全量门禁:
|
|
385
386
|
|
|
386
387
|
```bash
|
|
388
|
+
npm run format:check
|
|
387
389
|
npm run check
|
|
388
390
|
npm run test:coverage:check
|
|
389
|
-
npm run
|
|
391
|
+
npm run release:gate
|
|
390
392
|
npm run e2e:smoke -- --keep
|
|
391
393
|
npm run pack:tgz:dry-run
|
|
392
394
|
```
|
|
393
395
|
|
|
394
396
|
当前基线(主分支):
|
|
395
397
|
|
|
396
|
-
- 单元测试:`
|
|
397
|
-
- 覆盖率:`statements 88.
|
|
398
|
+
- 单元测试:`91/91` 通过。
|
|
399
|
+
- 覆盖率:`statements 88.49%`,`branches 75.01%`,`functions 95.02%`,`lines 88.49%`。
|
|
398
400
|
- 全菜单 smoke:通过(含恢复冲突分支与 doctor JSON 分支)。
|
|
399
401
|
|
|
402
|
+
## 测试矩阵
|
|
403
|
+
|
|
404
|
+
- 场景矩阵与断言模板见:[`docs/TEST_MATRIX.md`](./docs/TEST_MATRIX.md)
|
|
405
|
+
- 新增动作或输出字段时,需同步更新矩阵并补对应测试。
|
|
406
|
+
|
|
400
407
|
## 发布与打包
|
|
401
408
|
|
|
402
409
|
`prepack` 会自动执行:
|
|
@@ -423,26 +430,27 @@ npm run pack:tgz
|
|
|
423
430
|
|
|
424
431
|
```bash
|
|
425
432
|
# 1) 发布前检查
|
|
433
|
+
npm run format:check
|
|
426
434
|
npm run check
|
|
427
435
|
npm run test:coverage:check
|
|
428
|
-
npm run
|
|
436
|
+
npm run release:gate
|
|
429
437
|
npm run e2e:smoke
|
|
430
438
|
npm run pack:tgz
|
|
431
439
|
npm run pack:release-assets
|
|
432
440
|
|
|
433
441
|
# 2) 推送主分支与标签
|
|
434
442
|
git push origin main
|
|
435
|
-
git tag v1.2.
|
|
436
|
-
git push origin v1.2.
|
|
443
|
+
git tag v1.2.1
|
|
444
|
+
git push origin v1.2.1
|
|
437
445
|
|
|
438
446
|
# 3) 发布 GitHub Release(附 npm 包 + 双架构核心附件)
|
|
439
|
-
gh release create v1.2.
|
|
440
|
-
--title "v1.2.
|
|
441
|
-
--notes-file docs/releases/v1.2.
|
|
442
|
-
wecom-cleaner-1.2.
|
|
443
|
-
dist/release/wecom-cleaner-core-v1.2.
|
|
444
|
-
dist/release/wecom-cleaner-core-v1.2.
|
|
445
|
-
dist/release/wecom-cleaner-core-v1.2.
|
|
447
|
+
gh release create v1.2.1 \
|
|
448
|
+
--title "v1.2.1" \
|
|
449
|
+
--notes-file docs/releases/v1.2.1.md \
|
|
450
|
+
wecom-cleaner-1.2.1.tgz \
|
|
451
|
+
dist/release/wecom-cleaner-core-v1.2.1-darwin-x64 \
|
|
452
|
+
dist/release/wecom-cleaner-core-v1.2.1-darwin-arm64 \
|
|
453
|
+
dist/release/wecom-cleaner-core-v1.2.1-SHA256SUMS.txt
|
|
446
454
|
|
|
447
455
|
# 4) 发布 npm
|
|
448
456
|
npm publish --access public
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# 测试矩阵(稳定性与极端场景)
|
|
2
|
+
|
|
3
|
+
本文用于约束 `wecom-cleaner` 在复杂组合场景与极端输入下的行为一致性,避免“看似成功但语义错误”。
|
|
4
|
+
|
|
5
|
+
## 目标
|
|
6
|
+
|
|
7
|
+
- 覆盖高风险动作:年月清理、全量空间治理、恢复、回收区治理。
|
|
8
|
+
- 锁定无交互契约:JSON 字段兼容、退出码语义稳定、text 卡片结论可读。
|
|
9
|
+
- 验证边界安全:路径越界、符号链接逃逸、索引异常、权限失败、并发竞争。
|
|
10
|
+
|
|
11
|
+
## 维度定义
|
|
12
|
+
|
|
13
|
+
### 维度 A:入口
|
|
14
|
+
|
|
15
|
+
- 交互模式(TUI)
|
|
16
|
+
- 无交互 CLI(`--output json|text`)
|
|
17
|
+
- Agent 脚本(`skills/wecom-cleaner-agent/scripts/*.sh`)
|
|
18
|
+
|
|
19
|
+
### 维度 B:动作
|
|
20
|
+
|
|
21
|
+
- `cleanup_monthly`
|
|
22
|
+
- `analysis_only`
|
|
23
|
+
- `space_governance`
|
|
24
|
+
- `restore`
|
|
25
|
+
- `recycle_maintain`
|
|
26
|
+
- `doctor`
|
|
27
|
+
|
|
28
|
+
### 维度 C:范围组合
|
|
29
|
+
|
|
30
|
+
- 账号:`current` / `all` / 指定 ID 集合
|
|
31
|
+
- 月份:显式 `--months` / `--cutoff-month` / 自动窗口
|
|
32
|
+
- 类别:默认 / 指定列表 / `all`
|
|
33
|
+
- 文件存储目录来源:`preset` / `configured` / `auto` / `all`
|
|
34
|
+
|
|
35
|
+
### 维度 D:执行模式
|
|
36
|
+
|
|
37
|
+
- dry-run(预演)
|
|
38
|
+
- 真实执行(`--dry-run false --yes`)
|
|
39
|
+
- 真实执行后复核(同条件二次运行)
|
|
40
|
+
|
|
41
|
+
### 维度 E:异常与边界
|
|
42
|
+
|
|
43
|
+
- 路径越界(`../`、绝对路径、非法根目录)
|
|
44
|
+
- 符号链接逃逸(raw 路径在根内,realpath 在根外)
|
|
45
|
+
- 索引异常(损坏行、批次根不一致、异常 `batchId`)
|
|
46
|
+
- 系统失败(`EACCES`、`ENOENT`、`ENOSPC`)
|
|
47
|
+
- 并发锁冲突与陈旧锁恢复
|
|
48
|
+
|
|
49
|
+
### 维度 F:规模
|
|
50
|
+
|
|
51
|
+
- 小样本(< 50 目录)
|
|
52
|
+
- 中样本(50~500 目录)
|
|
53
|
+
- 大样本(> 500 目录,强调耗时与稳定性)
|
|
54
|
+
|
|
55
|
+
## 关键断言模板(每个场景最少验证)
|
|
56
|
+
|
|
57
|
+
### 1) 退出码
|
|
58
|
+
|
|
59
|
+
- `0`:动作完成(可含业务失败明细)
|
|
60
|
+
- `2`:参数/用法错误
|
|
61
|
+
- `3`:真实执行缺少 `--yes`
|
|
62
|
+
|
|
63
|
+
### 2) JSON 契约(无交互)
|
|
64
|
+
|
|
65
|
+
必须存在且类型稳定:
|
|
66
|
+
|
|
67
|
+
- `ok: boolean`
|
|
68
|
+
- `action: string`
|
|
69
|
+
- `dryRun: boolean | null`
|
|
70
|
+
- `summary: object`
|
|
71
|
+
- `warnings: array`
|
|
72
|
+
- `errors: array`
|
|
73
|
+
- `meta.durationMs: number`
|
|
74
|
+
- `meta.engine: string`
|
|
75
|
+
|
|
76
|
+
### 3) 业务语义
|
|
77
|
+
|
|
78
|
+
- dry-run 不修改源目录
|
|
79
|
+
- 真实执行删除采用“移动到回收区”,可按批次恢复
|
|
80
|
+
- 无目标场景不得生成真实批次(或写入误导性“成功删除”)
|
|
81
|
+
- 部分失败时必须可见失败统计与错误明细
|
|
82
|
+
|
|
83
|
+
### 4) 审计一致性
|
|
84
|
+
|
|
85
|
+
- `index.jsonl` 记录动作、状态、路径、错误类型
|
|
86
|
+
- 越界/异常路径必须落审计(`error_type=PATH_VALIDATION_FAILED`)
|
|
87
|
+
- 回收区治理异常批次不得触发越界删除
|
|
88
|
+
|
|
89
|
+
## 最小必跑清单(回归基线)
|
|
90
|
+
|
|
91
|
+
1. `cleanup_monthly`:`--cutoff-month` + `--output json` + dry-run(有目标 / 无目标)
|
|
92
|
+
2. `cleanup_monthly`:真实执行 + 复核(复核命中应下降或归零)
|
|
93
|
+
3. `space_governance`:`suggested-only` 与 `allow-recent-active` 组合
|
|
94
|
+
4. `restore`:`skip/overwrite/rename` 三冲突策略
|
|
95
|
+
5. `recycle_maintain`:`disabled` / `no_candidate` / `partial_failed`
|
|
96
|
+
6. `doctor`:只读模式不创建状态目录
|
|
97
|
+
7. Agent 报告脚本:成功、无目标、失败三态退出码与卡片完整性
|
|
98
|
+
|
|
99
|
+
## 当前门禁(执行顺序)
|
|
100
|
+
|
|
101
|
+
1. `npm run check`
|
|
102
|
+
2. `npm run test:coverage:check`
|
|
103
|
+
3. `shellcheck skills/wecom-cleaner-agent/scripts/*.sh`
|
|
104
|
+
4. `npm run e2e:smoke`
|
|
105
|
+
|
|
106
|
+
说明:若新增动作/字段,需先补此文档矩阵与断言,再提交实现。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# v1.2.1 发布说明
|
|
2
|
+
|
|
3
|
+
发布日期:2026-02-26
|
|
4
|
+
|
|
5
|
+
## 亮点
|
|
6
|
+
|
|
7
|
+
- 发布流程新增一键门禁:`npm run release:gate`,统一覆盖格式检查、语法检查、覆盖率门禁、Shell 脚本静态检查、E2E smoke 与打包预演。
|
|
8
|
+
- 回收区治理安全性进一步增强:新增 `realpath` 级边界校验,拦截符号链接越界路径。
|
|
9
|
+
- Agent 无交互脚本失败语义加固:失败时稳定返回非 0 退出码并输出可读错误原因,避免自动化误判成功。
|
|
10
|
+
|
|
11
|
+
## 重要修复
|
|
12
|
+
|
|
13
|
+
- 修复回收区治理的符号链接逃逸风险(`recycle_path_symlink_escape` / `batch_root_symlink_escape` 等)。
|
|
14
|
+
- 修复 `recycle_maintain` 业务失败场景下无交互契约测试断言,确保“退出码 + JSON 输出”语义一致。
|
|
15
|
+
- 修复报告脚本在 `set -u` 下错误分支变量展开异常,避免失败被错误吞掉。
|
|
16
|
+
|
|
17
|
+
## 质量状态
|
|
18
|
+
|
|
19
|
+
- 发布门禁:全部通过。
|
|
20
|
+
- 单元测试:`91/91` 通过。
|
|
21
|
+
- 覆盖率(门禁):
|
|
22
|
+
- statements:`88.49%`
|
|
23
|
+
- branches:`75.01%`
|
|
24
|
+
- functions:`95.02%`
|
|
25
|
+
- lines:`88.49%`
|
|
26
|
+
|
|
27
|
+
## 资产
|
|
28
|
+
|
|
29
|
+
- npm 包:`wecom-cleaner-1.2.1.tgz`
|
|
30
|
+
- GitHub Release 附件:
|
|
31
|
+
- `wecom-cleaner-core-v1.2.1-darwin-x64`
|
|
32
|
+
- `wecom-cleaner-core-v1.2.1-darwin-arm64`
|
|
33
|
+
- `wecom-cleaner-core-v1.2.1-SHA256SUMS.txt`
|
package/native/manifest.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"targets": {
|
|
5
5
|
"darwin-x64": {
|
|
6
6
|
"binaryName": "wecom-cleaner-core",
|
|
7
7
|
"sha256": "c703a8bdb6df39df776c6a3ab28b3553916d487cf7c38612989566b4ecea34c9",
|
|
8
|
-
"url": "https://github.com/MisonL/wecom-cleaner/releases/download/v1.2.
|
|
8
|
+
"url": "https://github.com/MisonL/wecom-cleaner/releases/download/v1.2.1/wecom-cleaner-core-v1.2.1-darwin-x64"
|
|
9
9
|
},
|
|
10
10
|
"darwin-arm64": {
|
|
11
11
|
"binaryName": "wecom-cleaner-core",
|
|
12
12
|
"sha256": "8d0a15efcb70a266e15c752212f15570c006a931681249d6cd41a1c438941f72",
|
|
13
|
-
"url": "https://github.com/MisonL/wecom-cleaner/releases/download/v1.2.
|
|
13
|
+
"url": "https://github.com/MisonL/wecom-cleaner/releases/download/v1.2.1/wecom-cleaner-core-v1.2.1-darwin-arm64"
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mison/wecom-cleaner",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "企业微信本地聊天缓存清理工具(交互式 CLI/TUI)",
|
|
5
5
|
"author": "MisonL",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"format:check": "prettier --check .",
|
|
44
44
|
"check": "node --check src/*.js",
|
|
45
45
|
"e2e:smoke": "bash scripts/e2e-smoke.sh",
|
|
46
|
+
"release:gate": "bash scripts/release-gate.sh",
|
|
46
47
|
"prepack": "npm run build:native:release && npm run check",
|
|
47
48
|
"pack:tgz": "node scripts/pack-tgz.js",
|
|
48
49
|
"pack:tgz:dry-run": "node scripts/pack-tgz.js --dry-run",
|
|
@@ -190,7 +190,7 @@ run_cmd_to_file() {
|
|
|
190
190
|
if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
|
|
191
191
|
local err_head
|
|
192
192
|
err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
|
|
193
|
-
echo "执行失败(dry-run=$dry_run):${err_head:-未知错误}" >&2
|
|
193
|
+
echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
|
|
194
194
|
return 1
|
|
195
195
|
fi
|
|
196
196
|
}
|
|
@@ -275,6 +275,7 @@ if [[ -z "$selected_categories_human" ]]; then
|
|
|
275
275
|
fi
|
|
276
276
|
|
|
277
277
|
printf '\n=== 清理结果(给用户)===\n'
|
|
278
|
+
printf -- '- 执行结论:%s(%s)\n' "$conclusion" "$reason"
|
|
278
279
|
if [[ "$executed" == "true" ]]; then
|
|
279
280
|
printf -- '- 已完成:已清理 %s 项聊天缓存,释放 %s。\n' "$execute_success" "$(human_bytes "$execute_reclaimed")"
|
|
280
281
|
elif [[ "$preview_matched" -eq 0 ]]; then
|
|
@@ -287,6 +288,7 @@ printf -- '- 你的目标:清理 %s 及之前的企业微信聊天缓存。\n'
|
|
|
287
288
|
printf '\n你关心的范围\n'
|
|
288
289
|
printf -- '- 账号:%s(识别到 %s 个账号)\n' "$account_scope_label" "$scope_accounts"
|
|
289
290
|
printf -- '- 数据类型:%s\n' "$selected_categories_human"
|
|
291
|
+
printf -- '- 筛选月份桶:%s,筛选类别数:%s\n' "$scope_months" "$scope_categories"
|
|
290
292
|
if [[ -n "$matched_month_start" && -n "$matched_month_end" ]]; then
|
|
291
293
|
printf -- '- 实际命中月份:%s ~ %s\n' "$matched_month_start" "$matched_month_end"
|
|
292
294
|
else
|
|
@@ -300,6 +302,7 @@ printf -- '- 命中字节:%s(命中目录当前大小)\n' "$(human_bytes "
|
|
|
300
302
|
printf -- '- 预计释放:%s(预演估算)\n' "$(human_bytes "$preview_reclaimed")"
|
|
301
303
|
if [[ "$executed" == "true" ]]; then
|
|
302
304
|
printf -- '- 实际释放:%s(真实执行结果)\n' "$(human_bytes "$execute_reclaimed")"
|
|
305
|
+
printf -- '- 执行明细:成功 %s / 跳过 %s / 失败 %s\n' "$execute_success" "$execute_skipped" "$execute_failed"
|
|
303
306
|
printf -- '- 清理批次:%s(可用于恢复)\n' "$execute_batch"
|
|
304
307
|
printf -- '- 复核结果:剩余可清理 %s 项\n' "$verify_matched"
|
|
305
308
|
else
|
|
@@ -431,6 +434,13 @@ if [[ "$executed" == "true" ]]; then
|
|
|
431
434
|
fi
|
|
432
435
|
fi
|
|
433
436
|
|
|
437
|
+
printf '\n运行状态\n'
|
|
438
|
+
printf -- '- 扫描引擎:%s\n' "$engine"
|
|
439
|
+
printf -- '- 总耗时:%s ms\n' "$duration_total"
|
|
440
|
+
printf -- '- 告警:%s\n' "$warnings_total"
|
|
441
|
+
printf -- '- 错误:%s\n' "$errors_total"
|
|
442
|
+
printf -- '- 预演失败项:%s\n' "$preview_failed"
|
|
443
|
+
|
|
434
444
|
if [[ "$warnings_total" -gt 0 || "$errors_total" -gt 0 ]]; then
|
|
435
445
|
printf '\n异常与提示\n'
|
|
436
446
|
printf -- '- 告警:%s\n' "$warnings_total"
|
|
@@ -143,7 +143,7 @@ run_cmd_to_file() {
|
|
|
143
143
|
fi
|
|
144
144
|
if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
|
|
145
145
|
err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
|
|
146
|
-
echo "执行失败(dry-run=$dry_run):${err_head:-未知错误}" >&2
|
|
146
|
+
echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
|
|
147
147
|
return 1
|
|
148
148
|
fi
|
|
149
149
|
}
|
|
@@ -145,7 +145,7 @@ run_cmd_to_file() {
|
|
|
145
145
|
fi
|
|
146
146
|
if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
|
|
147
147
|
err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
|
|
148
|
-
echo "执行失败(dry-run=$dry_run):${err_head:-未知错误}" >&2
|
|
148
|
+
echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
|
|
149
149
|
return 1
|
|
150
150
|
fi
|
|
151
151
|
}
|
|
@@ -173,7 +173,7 @@ run_cmd_to_file() {
|
|
|
173
173
|
fi
|
|
174
174
|
if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
|
|
175
175
|
err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
|
|
176
|
-
echo "执行失败(dry-run=$dry_run):${err_head:-未知错误}" >&2
|
|
176
|
+
echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
|
|
177
177
|
return 1
|
|
178
178
|
fi
|
|
179
179
|
}
|
|
@@ -241,6 +241,7 @@ warnings_total=$((warnings_preview + warnings_exec + warnings_verify))
|
|
|
241
241
|
errors_total=$((errors_preview + errors_exec + errors_verify))
|
|
242
242
|
|
|
243
243
|
printf '\n=== 全量空间治理结果(给用户)===\n'
|
|
244
|
+
printf -- '- 执行结论:%s(%s)\n' "$conclusion" "$reason"
|
|
244
245
|
if [[ "$executed" == "true" ]]; then
|
|
245
246
|
printf -- '- 已完成:已治理 %s 项空间目标,释放 %s。\n' "$execute_success" "$(human_bytes "$execute_reclaimed")"
|
|
246
247
|
elif [[ "$matched_targets" -eq 0 ]]; then
|
|
@@ -263,6 +264,7 @@ printf -- '- 命中体积:%s\n' "$(human_bytes "$matched_bytes")"
|
|
|
263
264
|
printf -- '- 预计释放:%s\n' "$(human_bytes "$preview_reclaimed")"
|
|
264
265
|
if [[ "$executed" == "true" ]]; then
|
|
265
266
|
printf -- '- 实际释放:%s\n' "$(human_bytes "$execute_reclaimed")"
|
|
267
|
+
printf -- '- 执行明细:成功 %s / 跳过 %s / 失败 %s\n' "$execute_success" "$execute_skipped" "$execute_failed"
|
|
266
268
|
printf -- '- 清理批次:%s(可用于恢复)\n' "$execute_batch"
|
|
267
269
|
printf -- '- 复核结果:剩余可治理 %s 项\n' "$verify_matched"
|
|
268
270
|
else
|
|
@@ -51,8 +51,23 @@ function isPathWithinRoot(rootPath, targetPath) {
|
|
|
51
51
|
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
54
|
+
async function safeRealpath(targetPath) {
|
|
55
|
+
try {
|
|
56
|
+
return await fs.realpath(targetPath);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
55
63
|
const recycleRootAbs = path.resolve(String(recycleRoot || ''));
|
|
64
|
+
const recycleRootReal = await safeRealpath(recycleRootAbs);
|
|
65
|
+
if (!recycleRootReal) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
invalidReason: 'missing_recycle_root',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
56
71
|
const entries = Array.isArray(batch?.entries) ? batch.entries : [];
|
|
57
72
|
if (entries.length === 0) {
|
|
58
73
|
return {
|
|
@@ -78,6 +93,19 @@ function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
|
78
93
|
invalidReason: 'recycle_path_outside_recycle_root',
|
|
79
94
|
};
|
|
80
95
|
}
|
|
96
|
+
const recyclePathReal = await safeRealpath(recyclePathAbs);
|
|
97
|
+
if (!recyclePathReal) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
invalidReason: 'recycle_path_unresolvable',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!isPathWithinRoot(recycleRootReal, recyclePathReal)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
invalidReason: 'recycle_path_symlink_escape',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
81
109
|
|
|
82
110
|
const batchRootAbs = path.dirname(recyclePathAbs);
|
|
83
111
|
if (!isPathWithinRoot(recycleRootAbs, batchRootAbs)) {
|
|
@@ -92,7 +120,20 @@ function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
|
92
120
|
invalidReason: 'batch_root_is_recycle_root',
|
|
93
121
|
};
|
|
94
122
|
}
|
|
95
|
-
|
|
123
|
+
const batchRootReal = await safeRealpath(batchRootAbs);
|
|
124
|
+
if (!batchRootReal) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
invalidReason: 'batch_root_unresolvable',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!isPathWithinRoot(recycleRootReal, batchRootReal)) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
invalidReason: 'batch_root_symlink_escape',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
rootSet.add(batchRootReal);
|
|
96
137
|
}
|
|
97
138
|
|
|
98
139
|
if (rootSet.size !== 1) {
|
|
@@ -282,7 +323,7 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
|
|
|
282
323
|
onProgress(i + 1, selected.candidates.length);
|
|
283
324
|
}
|
|
284
325
|
|
|
285
|
-
const resolvedBatchRoot = resolveBatchRootFromEntries(recycleRoot, batch);
|
|
326
|
+
const resolvedBatchRoot = await resolveBatchRootFromEntries(recycleRoot, batch);
|
|
286
327
|
if (!resolvedBatchRoot.ok) {
|
|
287
328
|
summary.failBatches += 1;
|
|
288
329
|
summary.operations.push({
|