refactor: 按模块重构项目结构
- 删除 vars 全局状态,按职责拆分 internal/ 包:scanner/probe/classify/organize/mover/store/digest - 数据驱动归类规则替代图片/视频重复分支,store 改为结构体实例 - build.sh 按系统类型编译:Windows 用 upx 压缩,macOS/Linux 不压缩 - 忽略目录拆分:图片_已忽略 / 视频_已忽略 - 输出构建产物加入 .gitignore
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// Package scanner 负责递归扫描文件、读取 MIME 并按类型过滤。
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"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 {
|
||||
return err
|
||||
}
|
||||
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: path.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)
|
||||
}
|
||||
Reference in New Issue
Block a user