feat: 扫描整理并行化、WebSocket 实时进度与统计看板
- 移动阶段并行处理文件,按原子计数上报累计进度 - 扫描遍历阶段实时上报已发现文件数,界面不再卡在 0/0 - 新增 /api/scan/ws WebSocket 实时推送扫描状态;前端断线指数退避自动重连,并以低频轮询兜底 - 新增统计看板与媒体库筛选/排序,目录选择器路径不存在时回退到最近可用父目录 - 存放目录支持自定义并持久化,默认输出到 KMP已整理
This commit is contained in:
+56
-36
@@ -6,27 +6,34 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/redmask-hb/GoSimplePrint/goPrint"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// Mover 文件移动器
|
||||
type Mover struct{}
|
||||
|
||||
// moveResult 移动结果状态
|
||||
type moveResult int
|
||||
// workerCount 并行移动的 worker 数量:不超过 CPU 核数,限制在 1~4 之间。
|
||||
// 移动/复制是磁盘 I/O 密集型操作,过高的并行度反而会降低吞吐(尤其机械硬盘)。
|
||||
func workerCount() int {
|
||||
n := runtime.NumCPU()
|
||||
if n < 1 {
|
||||
return 1
|
||||
}
|
||||
if n > 4 {
|
||||
return 4
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
const (
|
||||
moveOK moveResult = iota // 移动成功(rename 或 copy+delete 完整完成)
|
||||
movePartial // 复制成功但删除源失败,源与目标同时存在
|
||||
moveFailed // 移动/复制失败
|
||||
)
|
||||
|
||||
// MoveToDir 将一批文件移动到目标目录(目标文件名使用 uuid 前缀防止同名覆盖)。
|
||||
// 返回移动成功的 源路径 → 目标目录 映射,供记录去向使用。
|
||||
func (m *Mover) MoveToDir(filePaths []string, targetDir string) map[string]string {
|
||||
// MoveToDir 将一批文件并行移动到目标目录(目标文件名使用 uuid 前缀防止同名覆盖)。
|
||||
// 返回移动成功的 源路径 → 完整目标文件路径 映射,供记录去向使用。
|
||||
// onProgress 在每移动一个文件后调用(done 为累计成功移动数,并发安全、单调递增)。
|
||||
func (m *Mover) MoveToDir(filePaths []string, targetDir string, onProgress func(done int)) map[string]string {
|
||||
moved := make(map[string]string)
|
||||
if len(filePaths) == 0 {
|
||||
return moved
|
||||
@@ -36,37 +43,58 @@ func (m *Mover) MoveToDir(filePaths []string, targetDir string) map[string]strin
|
||||
return moved
|
||||
}
|
||||
|
||||
bar := goPrint.NewBar(100)
|
||||
bar.SetNotice("=== 移动文件到目录:")
|
||||
bar.SetGraph(">")
|
||||
total := len(filePaths)
|
||||
for i, src := range filePaths {
|
||||
switch moveOne(src, targetDir) {
|
||||
case moveOK, movePartial:
|
||||
moved[src] = targetDir
|
||||
results := make([]string, len(filePaths))
|
||||
workers := workerCount()
|
||||
if workers > len(filePaths) {
|
||||
workers = len(filePaths)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
var doneCount int32
|
||||
jobs := make(chan int)
|
||||
for w := 0; w < workers; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range jobs {
|
||||
if target, ok := moveOne(filePaths[i], targetDir); ok {
|
||||
results[i] = target
|
||||
}
|
||||
if onProgress != nil {
|
||||
onProgress(int(atomic.AddInt32(&doneCount, 1)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := range filePaths {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
for i, target := range results {
|
||||
if target != "" {
|
||||
moved[filePaths[i]] = target
|
||||
}
|
||||
bar.PrintBar(percent(i+1, total))
|
||||
}
|
||||
bar.PrintEnd("=== Finish")
|
||||
return moved
|
||||
}
|
||||
|
||||
// moveOne 移动单个文件;跨文件系统时 fallback 为 copy+delete
|
||||
func moveOne(src, targetDir string) moveResult {
|
||||
// moveOne 移动单个文件;跨文件系统时 fallback 为 copy+delete。
|
||||
// 成功时返回完整目标文件路径;部分成功(复制成功删除源失败)同样返回目标路径。
|
||||
func moveOne(src, targetDir string) (string, bool) {
|
||||
fileName := strings.ReplaceAll(uuid.NewV4().String(), "-", "") + "_" + filepath.Base(src)
|
||||
target := filepath.Join(targetDir, fileName)
|
||||
if err := os.Rename(src, target); err == nil {
|
||||
return moveOK
|
||||
return target, true
|
||||
}
|
||||
if err := copyFile(src, target); err != nil {
|
||||
fmt.Printf("=== 复制文件失败, 源: %s, 目标: %s, 错误: %v\n", src, target, err)
|
||||
return moveFailed
|
||||
return "", false
|
||||
}
|
||||
if err := os.Remove(src); err != nil {
|
||||
fmt.Printf("=== 警告: 复制成功但删除源文件失败, 源与目标同时存在, 源: %s, 错误: %v\n", src, err)
|
||||
return movePartial
|
||||
}
|
||||
return moveOK
|
||||
return target, true
|
||||
}
|
||||
|
||||
// copyFile 逐块复制文件;失败时清理已写入的目标文件,避免残留半截文件
|
||||
@@ -92,11 +120,3 @@ func copyFile(src, dst string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// percent 计算百分比(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