Files
kenaito-media-picker/bug20260814.md
T
odboy c266da2858 fix: 修复扫描/探测/移动三个健壮性问题
- scanner: filepath.Ext 替代 path.Ext,修复 Windows 点号目录名扩展名解析错误
- scanner: Walk 遇不可访问路径仅告警跳过,不再整体中止扫描
- probe: ffprobe 探测增加 30s 超时,避免损坏文件卡死程序
- mover: copyFile 失败时清理半截目标文件,消除双重 Close
- 新增代码审查报告 bug20260814.md
2025-04-07 21:00:37 +08:00

107 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 描述一致。