diff --git a/CHANGELOG.md b/CHANGELOG.md index da23e70..e213bad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 且哈希一致。