Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 49 additions & 79 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,105 +1,75 @@
# 更新记录

版本号用 0.x:界面与接口仍在演进,暂不承诺兼容性;稳定后再发 1.0。
版本号用 0.x:界面和接口还在变,暂不承诺兼容;稳定之后再发 1.0。

## v0.2.1 — 2026-11-02

一次「评审驱动」的修复版:把 v0.2.0 逐行读完后确认的问题按优先级清掉一部分。
大文件断点续传不在本版(前置验证还没做,见文末「下一步」)。

### 修复

- **排除项对文件不生效(承诺了却没做到)**:界面写的是「排除的目录 / 文件」,
占位符还给了 `*.tmp`,但 `runner.py` 传给引擎的是 `excludes_files=None`——
任务排除项只对目录生效,文件一条都没排掉;而孤儿扫描对目录和文件都生效。
现在同一个列表同时喂给两边,口径统一,运行日志会把生效的排除规则打出来。
v0.2.0 的 CHANGELOG 已写过「孤儿扫描的排除规则与同步一致」,那句话从本版起才成立。
- **空文件语义两边相反**:dir 扫描里 `sz<=0` 静默并入 `skip`(`.gitkeep` 这类占位
文件被丢掉,日志里还看不出来);push 则是前置校验整批否决(100 个文件里混一个
空文件,一个都不传)。现在:dir 扫描单独计数 + 明确日志,不再并进「跳过」;
push 改为单文件失败计入 `fail`、其余继续传,失败的一轮不做保留策略清理。
- **分片数少算一片**:`total = size // PART` 向下取整——20MB 切成 8MB + 12MB,
8–16MB 的文件直接整包单片发(`total` 算成 1,客户端自己认为「只有一片」)。
服务端目前收得下,但这属于「客户端自称的片数与实际不符」,一旦服务端收紧
`partSize` 校验,表现就是「小文件能传、大文件必挂」。改为向上取整,抽出
`part_count()` 纯函数。
- **任务源目录校验只做了一半**:`create_task` 用字符串前缀判断(`/vol10/photos`
会被 `/vol1` 放行,`/vol1` 里指向外部的软链也拦不住),`update_task` 一行校验
都没有——建的时候拦、改的时候不拦,等于没拦。现抽成 `app/paths.py` 的
`within()`(realpath + 按路径段比较),create / update 共用;顺手把
`/api/fs/list`、`/api/jobs/push`、`/api/restore` 三处重复实现收敛到同一函数。
- **update_task 能改成重名**:改名时没查唯一约束,撞名直接抛 500。补上重名判断。
- **令牌文件的 644 窗口**:`token.json` 先 `os.replace` 再 `chmod 600`,中间有
一个极短的可读窗口。改为先给临时文件 chmod 再换入。
修了一批问题,都是把 v0.2.0 逐行读完之后确认过的。断点续传这一版没做,原因写在最后。

### 修好的问题

- 排除项对文件不起作用。界面写的是「排除的目录 / 文件」,示例还给了 *.tmp,但程序只拿它排目录,文件一个都没排掉。现在目录和文件按同一份规则排,运行日志里会写明这次用了哪些规则。
- 空文件两边处理相反。目录同步是悄悄跳过,日志里看不出来,.gitkeep 这种文件就这么丢了;脚本推送则是一个空文件卡住整批,100 个文件里混一个就一个都不传。现在目录同步单独计数并写日志,推送只把这一个记成失败,其余照传。
- 分片数算少了一片。20MB 的文件会被切成 8MB + 12MB,8–16MB 的文件直接整包发一片。云盘目前收得下,但片数和实际对不上,服务端哪天较真就会变成「小文件能传、大文件全挂」。改成向上取整。
- 任务源目录的校验只做了一半。新建任务只比字符串前缀,/vol10 会被 /vol1 放过,软链也能绕到目录外面;编辑任务完全不校验,还能改成重名。现在统一按真实路径比对,新建和编辑共用一处,重名直接拒绝。
- 改任务名可能撞车。以前改名不查重名,撞上直接报 500,现在提前拦住。
- 令牌文件有个很短的 644 窗口。以前先换文件再改权限,中间有一瞬间谁都能读。改成先改临时文件的权限再换。

### 新增

- **孤儿扫描「含已排除」视图**:文件排除修好之后,历史上传的 `*.tmp` 会被排除规则
过滤掉——从孤儿清单里消失,却仍然占着配额。新增 `include_excluded` 参数
(`POST /api/tasks/{tid}/orphans?include_excluded=1`)与界面上的「含已排除重扫」
入口:不套用任务的排除规则(`DEFAULT_EXCLUDE_DIRS/FILES` 仍生效),仍然只列不删。
先看清单,再决定清不清——和孤儿扫描「只列不删」的哲学一致。
- 孤儿扫描多了「含已排除」的视角。文件排除生效之后,以前传上去的 *.tmp 会被规则挡住,从孤儿清单里消失,可它还在占配额。现在可以专门扫一遍这些被规则藏起来的条目,仍然只列不删。

### 其他

- CI 的语法检查从硬编码文件清单改为 `compileall` 扫目录:v0.2.0 的清单漏了
`app/orphans.py`(pyflakes 覆盖到了,编译这一步没编过它)。
- `main.py` 云端占用的 docstring 写「6 小时缓存」、代码是 `24 * 3600`,按代码改注释。
- 前端那行孤零零的 `<!-- 孤儿扫描 -->` 注释挪回它该在的位置。
- CI 的编译检查改成扫目录。以前是手写文件清单,漏了 app/orphans.py。
- 云端占用那行注释写「6 小时缓存」,代码其实是 24 小时,按代码改了注释。

### 评审存档
### 这一版的来历

本版来自一份对 v0.2.0 的逐行评审(6000 行读完后按优先级排的 6 条 + 若干小项)。
逐条核验与处置:
这些改动来自对 v0.2.0 的一次逐行评审:原文提了 6 条,核验时又补了 4 条。处理情况如下。

| 评审项 | 核验结论 | 本版处置 |
| 评审提到的问题 | 核对结果 | 这一版 |
|---|---|---|
| 大文件无断点续传 | 成立,最大的可靠性缺口 | 未做,见「下一步」 |
| 排除项口径不一致 | 成立,且 v0.2.0 的 CHANGELOG 已背书「一致」 | 已修 |
| 空文件在 dir 同步里静默跳过 | 成立;原文说 push「明确报错」偏轻,实为整批否决 | 已修 |
| CI 无自动化测试 | 成立 | 下一步:纯函数单测进 CI |
| 分片数应向上取整 | 成立;但「22GB 实测证明服务端容忍」的举证不成立——22GiB = 2816 × 8MiB 正好整除,没走到超大末片那条路径 | 已修,定性为修隐患 |
| 每次同步全量重列云端树 | 成立 | 未做(需要本地索引,架构级改动) |
| `create_task` 未做 realpath | 原文未提,评审补充 | 已修,update 同修 |
| `update_task` 无任何校验 | 原文未提,评审补充 | 已修 |
| push 空文件整批否决 | 原文表述偏轻,评审补充 | 已修 |
| 云端占用注释 6h / 代码 24h | 原文未提,评审补充 | 已修 |

### 下一步

1. `tools/probe_resume.py`:验证服务端在数小时后是否仍认同一个 `uniqueId`。
认 → 做分片级断点续传(进度 journal 落 SQLite);不认 → 退化为文件级续传。
探完把云端残片删掉,别留在配额里。
2. 纯函数单测进 CI:`excludes` 匹配、`part_count`、`within` 边界、空文件计数。
3. 已知未动(都有明确理由,不是忘了):`engine.py` 里进程级的 IPv4 monkeypatch
(要真机验证传输路径,不适合混在补丁版)、`main.py` 两处 `db._conn()` 直接写
SQL(等单测到位再抽)、每次运行结束全量 reload 调度器(任务量小,先不动)。
| 大文件没有断点续传 | 成立,是目前最大的可靠性缺口 | 没做,见最后 |
| 排除项对文件不生效 | 成立,v0.2.0 的说明里还写过「与同步一致」 | 已修 |
| 空文件悄悄跳过 | 成立;原文说推送是「明确报错」,其实是整批否决 | 已修 |
| CI 没有自动化测试 | 成立 | 单测写好了,等断点续传定了再合 |
| 分片数应该向上取整 | 成立;但举证不成立,22GiB 正好是 2816 × 8MiB,没走到末片超长那条路 | 已修 |
| 每次同步都重列云端目录 | 成立 | 没做,要加本地索引,改动太大 |
| 新建任务没做真实路径校验 | 原文没提,核验时补的 | 已修,编辑任务一起修 |
| 编辑任务完全不校验 | 原文没提,核验时补的 | 已修 |
| 推送遇到空文件整批否决 | 原文说得偏轻,核验时补的 | 已修 |
| 云端占用注释和代码对不上 | 原文没提,核验时补的 | 已修 |

### 还没做的

1. 断点续传。要先测服务端认不认几小时前的上传会话,探针脚本已经写好,等结果出来再定方案。
2. 纯函数单测。分片数、排除规则、路径校验、空文件计数四个点已经写好,等这版收尾时一起合。
3. 三处暂时不动,都是改动面大于收益:engine.py 里那个全局 IPv4 补丁(要真机验证传输路径)、main.py 两处直接写 SQL 的地方、每次跑完都重建一遍调度任务。

## v0.2.0 — 2026-10-30

首个正式发布的版本。从这一版起,Releases 页面与本文的版本段落一一对应。
第一个正式版本。从这版起,每次发布都会写一份说明,说清楚改了什么、升级要不要动手。

### 新增

- **孤儿扫描**:dir 任务新增「孤儿扫描」,后台以云端树为准递归,找出云端有、本地已经没有同名文件(大小写不敏感)的条目。孤儿文件逐条列出,孤儿目录整棵统计文件数与字节数;**只列清单、不删除云端文件**;清单超 2000 条会截断提示。接口:`POST /api/tasks/{tid}/orphans`、`GET /api/orphans/{jid}`。
- **恢复演练进面板**:管理台新增演练卡片,展示上次演练时间、抽样通过/失败数、超 45 天未演练提示与失败样本。演练报告由 `tools/drill.py` 跑完自动上报(`POST /api/drill/report`),存于 `DATA/state/drill.json`,`GET /api/drill` 读取。
- **演练哈希抽查**:`drill.py` 在体积比对通过后,再算下载文件的 SHA256 与云端 `sha256` 比对;不一致判失败,云端无哈希时不推翻体积结论;上报失败只记 warning,不影响演练退出码。
- **界面粘贴令牌**:`POST /api/token/paste` + 首页「Token 剩余」卡片里的「粘贴令牌」入口。粘贴 `access_token` / `refresh_token` 后,**先用这对令牌真连一次云端(列一次根目录),通过才落盘**,成功即生效、无需重启容器;验不过不改动现有凭证。快速开始第 3 步的 SSH 写文件降级为备选方案。
- **截图进仓库**:三张实拍图放到 `docs/screenshots/`,README 改用仓库内路径,补中文 alt 与图注。

### 修复
- 孤儿扫描:云端有、本地已经删掉的文件,可以扫出来看看。文件逐个列,目录只给整棵树的大小和文件数,只列清单,不删云端任何东西。
- 恢复演练:首页会显示上次演练时间、抽查几个、通过几个,超过 45 天没练会提醒。
- 演练加查内容:以前只比文件大小,现在大小对了还要比 SHA256,内容不一致算失败。云端没返回哈希时,以大小为准,不会误判。
- 网页里换令牌:不用再 SSH 进去改文件。粘上 access_token 和 refresh_token,保存前先连一次云盘,连得上才写入,连不上不动你原来的。
- README 换成三张实拍图。

- 演练卡片的「过期」判断从未生效:`/api/drill` 用 `datetime.strptime(at, "%F %T")` 解析,而 `%F` 只在 `strftime` 里有效,`strptime` 会抛 `ValueError` 被 `except` 吞掉,导致 `age_days` 恒为 `None`、`stale` 恒为 `False`。改为 `"%Y-%m-%d %H:%M:%S"`。
- 演练过期阈值统一为 45 天(原先面板 35 天、`check.sh` 45 天,口径不一致;演练每月 3 号跑,35 天会在下次演练前就误报过期)。
- README 功能列表原先写着「界面直接浏览云端、单文件或整目录取回」,但界面入口已经移除。改为如实描述:取回走 `/api/restore` 接口,演练走 `tools/drill.py`。
### 修好的问题

### 升级注意
- 演练卡片的「过期」提醒一直不出现。判断时间时用错了格式,报错又被吞掉,结果永远是「没过期」。
- 「多久算过期」两处对不上:面板按 35 天,巡检脚本按 45 天。统一成 45 天。
- README 里写着能「在界面浏览云端、取回文件」,其实这个入口早就删了。改成实际写法:取回走 /api/restore 接口,演练走 tools/drill.py。

- 无破坏性变更:`data/state/token.json` 格式未变,旧的任务配置、运行记录直接可用。
- 放置令牌现在可以在页面里粘贴(Token 卡片 → 粘贴令牌),SSH 写文件的方式仍然有效。
- 合并部署后,演练卡片在下次 `drill.py` 跑完上报前显示「暂无报告」,属正常现象。
### 升级前看一眼

### 说明
- 可以直接覆盖升级。令牌文件格式没变,任务配置和运行记录都在。
- 升级后演练卡片先显示「暂无报告」,等下次 drill.py 跑完就有了。
- dir 模式是明文上传,云盘上能看到文件名和内容。密码库、证件这类东西请走 push 模式,先在宿主机打包加密再传。
- 仓库里只有源码和 Dockerfile,没有现成镜像。clone 下来执行 sudo docker compose up -d --build,几分钟能构建好。

- 孤儿扫描的排除规则与同步一致(`DEFAULT_EXCLUDE_DIRS/FILES` + 任务 `excludes`),云端根目录用只读方式解析,不会顺手建目录。
- 新增「安全边界」小节:明确 **dir 模式明文上传**,敏感数据请走 push 模式先加密;Web 端单账号 Basic Auth,只建议内网或套 HTTPS 反代。
这一版在真实部署上跑过:冒烟 10/10、每日巡检正常、演练 5/5 且哈希一致。
Loading