- scanner: filepath.Ext 替代 path.Ext,修复 Windows 点号目录名扩展名解析错误 - scanner: Walk 遇不可访问路径仅告警跳过,不再整体中止扫描 - probe: ffprobe 探测增加 30s 超时,避免损坏文件卡死程序 - mover: copyFile 失败时清理半截目标文件,消除双重 Close - 新增代码审查报告 bug20260814.md
6.9 KiB
6.9 KiB
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 描述一致。