Files
kenaito-media-picker/internal/scanner/scanner.go
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

140 lines
4.0 KiB
Go
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.
// Package scanner 负责递归扫描文件、读取 MIME 并按类型过滤。
package scanner
import (
"fmt"
"os"
"path/filepath"
"strings"
"kenaito-media-picker/internal/probe"
"github.com/gabriel-vasile/mimetype"
"github.com/redmask-hb/GoSimplePrint/goPrint"
)
// File 扫描到的文件信息
type File struct {
Path string // 完整路径
Name string // 文件名(含扩展名)
Ext string // 扩展名(含点)
Mime string // MIME 类型
}
// Result 过滤结果
type Result struct {
Files []File // 全部文件
Images []File // 图片文件
Media []File // 待 ffprobe 确认的媒体候选
}
// Options 扫描选项
type Options struct {
RootDir string // 扫描根目录
SkipDir string // 白名单输出目录,整目录跳过
SkipNames []string // 按文件名跳过(数据库、ffprobe 等)
}
// Scan 递归扫描目录并读取每个文件的 MIME 信息
func Scan(opts Options) (*Result, error) {
res := &Result{}
err := filepath.Walk(opts.RootDir, func(currentPath string, info os.FileInfo, err error) error {
if err != nil {
// 单个路径访问失败(权限不足/损坏)不中止整体扫描,仅告警后跳过该子树
fmt.Printf("=== 扫描跳过(无法访问): %s, 错误: %v\n", currentPath, err)
return nil
}
if info.IsDir() {
if opts.SkipDir != "" && currentPath == opts.SkipDir {
return filepath.SkipDir
}
return nil
}
for _, n := range opts.SkipNames {
if info.Name() == n {
return nil
}
}
res.Files = append(res.Files, File{
Path: currentPath,
Name: info.Name(),
Ext: filepath.Ext(currentPath),
})
return nil
})
if err != nil {
return nil, err
}
total := len(res.Files)
fmt.Printf("=== 文件总数: %d \n", total)
bar := goPrint.NewBar(100)
bar.SetNotice("=== 扫描文件Mime:")
bar.SetGraph(">")
bar.SetNoticeColor(goPrint.FontColor.Red)
for i := range res.Files {
res.Files[i].Mime = detectMime(res.Files[i].Path)
bar.PrintBar(percent(i+1, total))
}
bar.PrintEnd("=== Finish")
return res, nil
}
// Filter 将文件分为图片与媒体候选
func (r *Result) Filter() {
for _, f := range r.Files {
if strings.Contains(f.Mime, "image/") || probe.IsSupportImage(f.Ext) || strings.EqualFold(f.Ext, ".psd") {
r.Images = append(r.Images, f)
continue
}
if isPotentialMedia(f.Mime) {
r.Media = append(r.Media, f)
continue
}
// 明显的非媒体文件(代码/文档/压缩包等)直接不处理
}
}
// detectMime 读取文件的 MIME 类型
func detectMime(filePath string) string {
mt, err := mimetype.DetectFile(filePath)
if err != nil {
return ""
}
return mt.String()
}
// isPotentialMedia 判断是否可能是媒体文件(交由 ffprobe 进一步确认)
func isPotentialMedia(fileMime string) bool {
if fileMime == "" || fileMime == "application/octet-stream" {
return true
}
if strings.HasPrefix(fileMime, "text/") {
return false
}
if strings.HasPrefix(fileMime, "video/") || strings.HasPrefix(fileMime, "audio/") || strings.HasPrefix(fileMime, "image/") {
return true
}
switch fileMime {
case "application/zip", "application/gzip", "application/x-gzip", "application/pdf",
"application/json", "application/xml", "application/x-httpd-php", "application/x-sh",
"application/javascript", "application/x-7z-compressed", "application/x-rar-compressed",
"application/x-tar", "application/x-bzip2", "application/x-archive", "application/vnd.sqlite3",
"application/vnd.microsoft.portable-executable", "application/x-dosexec", "application/x-executable",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint":
return false
}
return true
}
// percent 计算百分比(0~100),total 为 0 时返回 100
func percent(part, total int) int {
if total <= 0 {
return 100
}
return int(float64(part) / float64(total) * 100)
}