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

6.9 KiB
Raw Blame History

kenaito-media-picker 代码审查问题报告

审查日期:2026-08-14 审查范围:main.go、internal/ 全部包、build.sh、README.md 结论:功能主线可用,但存在若干 数据安全 / 健壮性 / 一致性 问题,按严重程度分级如下。


高危(可能导致数据丢失或程序卡死)

1. internal/scanner/scanner.go:60 使用 path.Ext 而非 filepath.Ext

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 子进程无超时控制

cmd := exec.Command(ffprobePath, "-v", "error", ...)
output, err := cmd.Output()
  • 对损坏/超大/异常媒体文件,ffprobe 可能长时间挂起,导致整个程序卡死。
  • 视频/音频文件是用户主要处理对象,属高频路径。
  • 修复:使用 exec.CommandContext + context.WithTimeout(如 30s),超时后返回错误并跳过该文件。

3. internal/scanner/scanner.go:42-63 Walk 遇到一个不可读目录即整体中止

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

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 描述一致。