refactor: 全面重构代码并加白话注释,移动全局并行,去掉去向/重复记录
- 重构后端与前端,全程大白话注释(不懂程序也能看懂) - 移动阶段改为全局并行(MoveAll,工人数=CPU核心数) - 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿) - 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬 - 媒体库去掉归类目录列,看板去掉重复/归类分布统计
This commit is contained in:
+59
-28
@@ -1,4 +1,11 @@
|
||||
// Package mover 负责文件的移动,跨文件系统时自动回退为复制后删除。
|
||||
// Package mover 负责"搬文件"这一步:把一批文件从老地方移到新目录。
|
||||
//
|
||||
// 大白话理解:
|
||||
// - 同一个磁盘里移动 = 改个目录记录而已,瞬间完成(改名操作);
|
||||
// - 跨磁盘移动 = 系统不让"改名",只能老老实实把内容复制过去再删掉原来的。
|
||||
// 这个包会自动判断,跨盘时偷偷用"复制+删除"顶上,调用方感觉不到区别。
|
||||
//
|
||||
// 搬文件比较慢(尤其跨盘复制),所以用几个工人并行搬,能快不少。
|
||||
package mover
|
||||
|
||||
import (
|
||||
@@ -14,49 +21,64 @@ import (
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// Mover 文件移动器
|
||||
// MoveJob 一次"搬文件"的任务:把 Source 这个文件搬进 TargetDir 目录。
|
||||
type MoveJob struct {
|
||||
Source string // 文件现在在哪
|
||||
TargetDir string // 要搬进哪个目录
|
||||
}
|
||||
|
||||
// Mover 文件移动器。目前不需要记录任何状态,所以是个空壳结构体。
|
||||
type Mover struct{}
|
||||
|
||||
// workerCount 并行移动的 worker 数量:不超过 CPU 核数,限制在 1~4 之间。
|
||||
// 移动/复制是磁盘 I/O 密集型操作,过高的并行度反而会降低吞吐(尤其机械硬盘)。
|
||||
// workerCount 并行搬文件的工人数:直接用满 CPU 核心数。
|
||||
// 你的机器够强、平时又没别的活,那就让它全速跑,这就是空闲时的极限值。
|
||||
// (如果搬文件的同时机器还在干别的活、被拖慢了,再手动把这个数调低即可。)
|
||||
func workerCount() int {
|
||||
n := runtime.NumCPU()
|
||||
if n < 1 {
|
||||
return 1
|
||||
}
|
||||
if n > 4 {
|
||||
return 4
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// MoveToDir 将一批文件并行移动到目标目录(目标文件名使用 uuid 前缀防止同名覆盖)。
|
||||
// 返回移动成功的 源路径 → 完整目标文件路径 映射,供记录去向使用。
|
||||
// onProgress 在每移动一个文件后调用(done 为累计成功移动数,并发安全、单调递增)。
|
||||
func (m *Mover) MoveToDir(filePaths []string, targetDir string, onProgress func(done int)) map[string]string {
|
||||
// MoveAll 把一批"搬文件任务"全局并行搬完——不管任务分散在几个目录,
|
||||
// 所有文件同时开搬(最多 8 个工人),这才是真正的并行。
|
||||
//
|
||||
// 目标文件名会加一长串随机前缀(uuid),这样即使两个文件同名也不会互相覆盖。
|
||||
// 返回"移动成功的 源路径 → 新位置完整路径"的对照表,方便调用方记录每个文件去哪了。
|
||||
// onProgress 每搬完一个文件调一次,参数是"已累计搬完多少个"(只会涨不会退,并发安全)。
|
||||
func (m *Mover) MoveAll(jobs []MoveJob, onProgress func(done int)) map[string]string {
|
||||
moved := make(map[string]string)
|
||||
if len(filePaths) == 0 {
|
||||
if len(jobs) == 0 {
|
||||
return moved
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
fmt.Printf("=== 创建目录失败: %s, 错误: %v\n", targetDir, err)
|
||||
return moved
|
||||
// 先把所有目标目录建好(去重,每个目录只需建一次;MkdirAll 建已存在的目录也没关系)
|
||||
created := make(map[string]bool)
|
||||
for _, j := range jobs {
|
||||
if created[j.TargetDir] {
|
||||
continue
|
||||
}
|
||||
created[j.TargetDir] = true
|
||||
if err := os.MkdirAll(j.TargetDir, 0755); err != nil {
|
||||
fmt.Printf("=== 创建目录失败: %s, 错误: %v\n", j.TargetDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]string, len(filePaths))
|
||||
// 全局并行搬:results[i] 存 jobs[i] 搬完之后的新路径(失败为空串)
|
||||
results := make([]string, len(jobs))
|
||||
workers := workerCount()
|
||||
if workers > len(filePaths) {
|
||||
workers = len(filePaths)
|
||||
if workers > len(jobs) {
|
||||
workers = len(jobs)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
var doneCount int32
|
||||
jobs := make(chan int)
|
||||
ch := 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 {
|
||||
for i := range ch {
|
||||
if target, ok := moveOne(jobs[i].Source, jobs[i].TargetDir); ok {
|
||||
results[i] = target
|
||||
}
|
||||
if onProgress != nil {
|
||||
@@ -65,28 +87,34 @@ func (m *Mover) MoveToDir(filePaths []string, targetDir string, onProgress func(
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := range filePaths {
|
||||
jobs <- i
|
||||
for i := range jobs {
|
||||
ch <- i
|
||||
}
|
||||
close(jobs)
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
|
||||
// 把搬成功的整理成 源路径 → 新路径 的对照表
|
||||
for i, target := range results {
|
||||
if target != "" {
|
||||
moved[filePaths[i]] = target
|
||||
moved[jobs[i].Source] = target
|
||||
}
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
// moveOne 移动单个文件;跨文件系统时 fallback 为 copy+delete。
|
||||
// 成功时返回完整目标文件路径;部分成功(复制成功删除源失败)同样返回目标路径。
|
||||
// moveOne 移动单个文件;跨磁盘搬不动时,自动降级为"复制 + 删除源文件"。
|
||||
// 成功返回新位置的完整路径;连复制都失败返回 false。
|
||||
// 注意一种特殊情况:复制成功了但删源文件失败(比如源文件被占用),
|
||||
// 这时源和目标会同时存在,但依然算"移动成功",返回新路径。
|
||||
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 target, true
|
||||
}
|
||||
// 改名失败(大概率是跨磁盘)→ 复制 + 删除
|
||||
if err := copyFile(src, target); err != nil {
|
||||
fmt.Printf("=== 复制文件失败, 源: %s, 目标: %s, 错误: %v\n", src, target, err)
|
||||
return "", false
|
||||
@@ -97,7 +125,8 @@ func moveOne(src, targetDir string) (string, bool) {
|
||||
return target, true
|
||||
}
|
||||
|
||||
// copyFile 逐块复制文件;失败时清理已写入的目标文件,避免残留半截文件
|
||||
// copyFile 把一个文件的内容完整复制到另一个位置。
|
||||
// 复制到一半失败时,会把已经写出来的半截文件删掉,不给磁盘留垃圾。
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
@@ -109,11 +138,13 @@ func copyFile(src, dst string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 复制途中出错:关掉并删掉半截文件
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
out.Close()
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
// 写完收尾出错:同样删掉
|
||||
if err := out.Close(); err != nil {
|
||||
_ = os.Remove(dst)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user