// 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) }