fix: 修复扫描/探测/移动三个健壮性问题

- scanner: filepath.Ext 替代 path.Ext,修复 Windows 点号目录名扩展名解析错误
- scanner: Walk 遇不可访问路径仅告警跳过,不再整体中止扫描
- probe: ffprobe 探测增加 30s 超时,避免损坏文件卡死程序
- mover: copyFile 失败时清理半截目标文件,消除双重 Close
- 新增代码审查报告 bug20260814.md
This commit is contained in:
2025-04-07 21:00:37 +08:00
parent b812abd345
commit c266da2858
4 changed files with 126 additions and 8 deletions
+106
View File
@@ -0,0 +1,106 @@
# kenaito-media-picker 代码审查问题报告
审查日期:2026-08-14
审查范围:`main.go`、`internal/` 全部包、`build.sh`、`README.md`
结论:功能主线可用,但存在若干 **数据安全 / 健壮性 / 一致性** 问题,按严重程度分级如下。
---
## 高危(可能导致数据丢失或程序卡死)
### 1. `internal/scanner/scanner.go:60` 使用 `path.Ext` 而非 `filepath.Ext`
```go
Ext: path.Ext(currentPath),
```
- `path.Ext` 只认 `/` 分隔符,Windows 路径分隔符是 `\`。
- 若目录名含点号(如 `C:\pics.v1\IMG001.jpg` 或 `C:\dir.v1\readme`),`path.Ext` 会把整个字符串当成一个路径段处理:
- `path.Ext("C:\\dir.v1\\readme")` → 返回 `".v1\\readme"`(错误!)
- 导致扩展名识别错误,文件被分到错误的归类目录。
- **修复**:改用 `filepath.Ext(currentPath)`。
### 2. `internal/probe/probe.go:44` ffprobe 子进程无超时控制
```go
cmd := exec.Command(ffprobePath, "-v", "error", ...)
output, err := cmd.Output()
```
- 对损坏/超大/异常媒体文件,ffprobe 可能长时间挂起,导致整个程序卡死。
- 视频/音频文件是用户主要处理对象,属高频路径。
- **修复**:使用 `exec.CommandContext` + `context.WithTimeout`(如 30s),超时后返回错误并跳过该文件。
### 3. `internal/scanner/scanner.go:42-63` Walk 遇到一个不可读目录即整体中止
```go
err := filepath.Walk(opts.RootDir, func(...) error {
if err != nil { return err } // ← 任何读取错误直接返回,终止整个扫描
```
- 只要目标目录下存在一个权限不足/损坏的目录,`Walk` 就整体失败,`main` 打印“扫描失败”并退出,什么都不整理。
- **修复**:Walk 回调中出错时打印警告并 `return nil` 继续遍历。
### 4. `internal/mover/mover.go:70-79` 复制失败残留半截文件 + 双重 Close
```go
out, err := os.Create(dst)
defer out.Close() // ① defer Close
if _, err := io.Copy(out, in); err != nil { return err } // ② 出错时残留不完整的 dst
return out.Close() // ③ 显式 Close,与 ① 重复
```
- `io.Copy` 失败时,目标目录残留一个**不完整文件**,用户会误以为文件已复制成功。
- `out.Close()` 被调用两次(①+③),虽然 Go 幂等,但属于编码缺陷。
- **修复**:`io.Copy` 失败时先 `os.Remove(dst)` 再返回;去掉 `defer out.Close()` 或去掉 ③ 的显式 Close(保留 defer,仅在 copy 成功后单独校验)。
---
## 中危(行为与文档不符 / 破坏性风险)
### 5. MIME 未知的文件(octet-stream / 空)会被**移动**进“视频_已忽略”
- `internal/scanner/scanner.go:107-110`:`isPotentialMedia` 对 `""` 与 `application/octet-stream` 一律返回 `true`。
- `internal/organize/organize.go:148-161`:此类文件交给 ffprobe 探测,探测失败且 MIME 非 `video/` 时归入 `dirVideoIgnored`,随后**被移动(或复制)到输出目录**。
- 后果:`.exe`、`.dll`、`.dat`、未知二进制、无法识别 MIME 的文件都会被移走,改变用户目录结构——与 README“明显的非媒体文件直接不处理”相矛盾,且**一旦移动不可轻易恢复**。
- **修复**:对 octet-stream/空 MIME 且 ffprobe 无法确认的文件,默认**原地忽略**(不移动),只有 MIME 明确是 `video/`/`audio/` 时才移入异常/忽略目录;或增加确认选项。
### 6. README 声称 TIFF 由 ffprobe 兜底,但实际永远不会走到该分支
- `internal/probe/image.go:50`:`if !IsSupportImage(ext) { return error }` 在 ffprobe 兜底**之前**返回;
- `internal/organize/organize.go:119`:`case !probe.IsSupportImage(f.Ext)` 直接归入“图片_已忽略”,连 `ProbeImage` 都不会调用。
- `.tiff`/`.tif` 不在 `supportImageTypes`(`image.go:12-22`),所以 TIFF 图片被直接“忽略”,**ffprobe 兜底逻辑成为死代码**,与 README 第 22 行“如 TIFF 由 ffprobe 兜底”矛盾。
- **修复**:在 ffprobe 可用时,允许非支持扩展名先走一次 ffprobe;否则删除 README 中相关描述,二选一。
### 7. `internal/organize/organize.go:115-118` PSD 文件不写数据库、不判明暗
- `.psd` 直接 `add(f, dirImagePSD)` 并 `continue`,跳过 `record()` 与 `AnalyzeBrightness()`。
- 结果:PSD 不在 `media-info.db` 中(README 声称“媒体信息入库”覆盖所有媒体),且无明暗信息。
- 若为有意设计,建议 README 注明;否则补上 `record`。
### 8. `internal/store/store.go:76-92` 指纹命中后 SaveMedia 会覆盖同名记录的源路径
- `organize.go:72` 指纹命中复用 `FileMd5`;`SaveMedia` 按 md5 查重后 `Update` 会**把该行 `file_path`/`file_name` 等覆盖为新文件的信息**。
- 若同一 md5 出现多个文件(重复文件),后一个文件的记录会覆盖前一个,前一个文件的 `file_path` 丢失,随后 `UpdateMediaDest`(按 `file_path` 匹配)将无法回填其去向。
- **修复**:重复文件不应覆盖已有记录,而是保留首条(或按 md5 生成关联表)。
---
## 低危(一致性 / 性能 / 健壮性)
### 9. `internal/probe/image.go:43-49` webp/bmp/psd/psb 走专用解析,缺少 ffprobe 兜底
- 只有 `decodeConfig` 失败的标准图片格式才有 ffprobe 兜底;`probeWebp`/`probeBmp`/`probePsd` 失败后直接返回错误,归入“读取异常”。
- 与通用路径不一致。建议失败后统一再尝试 ffprobe。
### 10. `internal/probe/brightness.go:25` 大图明暗分析整图解码,内存占用高
- `image.Decode(f)` 会把整张图载入内存,采样(`sampleStep`)是在解码**之后**进行的。
- 超大图(如 1 亿像素 TIFF/PNG)可能 OOM。
- 建议:超限尺寸直接采样跳过明暗分析,或改用逐行/缩略图解码。
### 11. `go.mod:3` 与 `README.md:34` Go 版本描述不一致
- `go.mod` 为 `go 1.25.0`,README 写“依赖 Go 1.19+(go.mod 指定 1.19)”。
- 二选一更正,避免误导。
### 12. `internal/probe/bmp.go:30-44` 仅支持 40 字节 BITMAPINFOHEADER
- OS/2 `BITMAPCOREHEADER`(12 字节)等变体解析会错位/失败,虽大概率进“读取异常”兜底,但建议按 `Size` 字段分支处理。
### 13. `internal/mover/mover.go:55-58` 复制成功但删除源失败时返回“成功”
- 源文件保留且目标已有副本(双份存在),`moved` 仍记录为成功。行为有注释说明,但返回值语义与实际不一致,建议返回特殊状态并在日志中明确告警。
### 14. `internal/scanner/scanner.go:108` 空 MIME 一律视为媒体候选
- `detectMime` 失败(权限、0 字节文件等)也返回 `""` 并被送入 ffprobe,增加无谓探测。建议对 0 字节文件直接跳过。
---
## 备注
- `ffprobe.exe` 文件在 `internal/probe/bin/`(未入库,见 `.gitignore`),构建需预先放置,README 已说明。
- 明暗判定 `neutralMargin = 0.15`、标准分辨率表等逻辑经核对与 README 描述一致。