主要功能:
- 输入操作(用户输入):
- A操作(添加任务数据):
- 一个协程程序读取源文件夹下所有文件绝对路径,生成相应的目标文件夹下的绝对路径。
- 将源文件绝对路径和目标文件绝对路径,存储在数据队列中。
- B操作(读取数据,并发比对备份文件):
- 通过循环判断数据队列中是否有数据,或者是否添加数据完成。如果没数据,同时B操作添加数据已完成,退出循环(退出C操作)。
- 如果添加数据未完成,循环读取数据队列中的值,如果有数据,按照输入的同步协程数,同步读取队列中数据,进行如下操作:
- 目标已经有重名文件,比对文件的md5和sha256(防止哈希碰撞),如果相同,证明文件相同,此文件不做任何操作,退出本次循环。
- 如果md5和sha256有不同,证明两文件只是文件名相同,文件内容有不同,对目标文件重命名,如果重命名后,继续循环上一操作,直到目标中没有重命名后的文件。(重命名就是对原文件名后加"(1)",多次重命名后就是:"文件名(1)(1)(1).扩展名")
- 目标没有重名文件,或者重命名完成后的文件,备份文件。
代码缺点
- 问题1:数据队列没有数据时,消费者需要不停的循环判断等待。
- 希望有解决方案:改为堵塞等待,性能会有进一步提升。
- 问题2:
if wq.Size() > 0
这行代码没法保证后续的操作确实大于零,因为判断同时,其他读取操作也在进行中,可能判断完了,队列就为空了,很容易读到空值。
- 欢迎提出解决方案。
主文件代码
go
复制代码
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
)
// 全局变量
var wq = NewWorkQueue() // 数据队列
var fz = false // 添加任务完毕后,设置为true
var wgroup = sync.WaitGroup{} // 用于同步等待协程完成
var readMax = 5 // 一次最多获取数据量
var ch = make(chan struct{}, readMax) // 控制获取数据量
var cn = NewCountNum() // 创建计数器,用于记录成功复制次数
func main() {
var srcDir, dstDir string
fmt.Print("源文件路径:")
fmt.Scanln(&srcDir)
fmt.Print("目标文件路径:")
fmt.Scanln(&dstDir)
srcAbs, _ := filepath.Abs(srcDir) // 源目录绝对路径
dstAbs, _ := filepath.Abs(dstDir) // 目标目录绝对路径
fmt.Print("同步数量:")
fmt.Scanln(&readMax)
wgroup.Add(1) // 添加数据协程+1
go A(srcAbs, dstAbs)
B() // 动态获取数据
wgroup.Wait() // 等待协程完成
logstr := fmt.Sprintf("[完成] %s %s 错误:%d,已存在:%d,成功:%d!\n", srcAbs, dstAbs, cn.ErrGet(), cn.WarnGet(), cn.OkGet())
SaveLog(logstr)
}
// 动态添加数据
func A(srcAbs, dstAbs string) {
os.MkdirAll(dstAbs, os.ModeDir) // 目录不存在时,创建
filepath.WalkDir(srcAbs, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
dstPath := DstPaht(path, srcAbs, dstAbs) // 目标文件绝对路径
if d.IsDir() { // 是目录
os.MkdirAll(dstPath, os.ModeDir) // 目标目录不存在时,创建
} else { // 不是目录
wq.Add(map[string]string{"src": path, "dst": dstPath})
}
return nil
})
fz = true // 添加数据完成,告知数据获取协程
wgroup.Done() // 添加数据完成
}
// 动态获取数据
func B() {
for {
if fz && wq.Size() == 0 { // 添加数据已完成,并且数据链长度为0
return // 退出获取数据操作
} // 添加数据已完成,并且队列为空时,退出获取数据
if wq.Size() > 0 { // 数据链上有数据节点
go func() {
defer wgroup.Done() // 完成后,协程计数-1
wgroup.Add(1) // 协程计数+1,防止退出
data := wq.Pop() // 从数据队列取出一个数据
if data != nil { // 数据存在时(因判断队列长度到取出数据过程中可能有其他协程取走数据,导致获取到空值)
// 从数据队列获取数据,通过信号量控制并发数量
ch <- struct{}{} // 获取信号量,占用一个并发资源,满时等待任务释放后继续执行
wgroup.Add(1) // 协程计数+1,防止退出
go func(data interface{}) { // 参数为源文件路径和目标文件路径
defer func() {
<-ch // 任务完成释放信号量,归还并发资源
wgroup.Done() // 完成后,协程计数-1
}()
val, _ := data.(map[string]string) // 将interface{}数据转换为map数据
srcPath := val["src"] // 源文件路径
dstPath := val["dst"] // 目标文件路径
for IsFileExist(dstPath) { // 目标文件存在
// 判断md5,sha256是否相同
srcMd5, srcSha256, _ := FileHash(srcPath)
dstMd5, dstSha256, _ := FileHash(dstPath)
if srcMd5 == dstMd5 && srcSha256 == dstSha256 { // md5和sha256都相同,防止产生哈希碰撞
cn.WarnAdd() // 目标文件已存在,计数器+1
fmt.Print("\r"+dstPath, " -> 已存在!")
return // 目标文件存在,不用复制文件
} else {
dstPath = FileRename(dstPath) // 目标文件存在,但是内容不一样,重命名保存
}
}
CopyFile(srcPath, dstPath) // 复制文件(目标文件不存在时直接复制,目标文件不一样时,重命名后复制)
cn.OkAdd() // 复制文件成功,复制计数器+1
fmt.Print("\r"+dstPath, " -> 完成!") // 完成一个文件复制
}(data)
}
}()
}
}
}
// 用于统计成功的数量和已存在的数量
// 变更数值时加锁,防止数据错误
type CountNum struct {
okNum int // 成功复制统计数
warnNum int // 目标文件已存在数
errNum int // 错误数
mutex sync.Mutex
}
func NewCountNum() *CountNum {
return &CountNum{okNum: 0, warnNum: 0, errNum: 0, mutex: sync.Mutex{}}
}
func (cn *CountNum) OkAdd() {
cn.mutex.Lock()
defer cn.mutex.Unlock()
cn.okNum++
}
func (cn *CountNum) OkGet() int {
return cn.okNum
}
func (cn *CountNum) WarnAdd() {
cn.mutex.Lock()
defer cn.mutex.Unlock()
cn.warnNum++
}
func (cn *CountNum) WarnGet() int {
return cn.warnNum
}
func (cn *CountNum) ErrAdd() {
cn.mutex.Lock()
defer cn.mutex.Unlock()
cn.errNum++
}
func (cn *CountNum) ErrGet() int {
return cn.errNum
}
文件、目录操作代码
go
复制代码
package main
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
)
// 根据源文件路径,生成目标文件路径
// 源文件绝对路径,源文件根目录,目标文件根目录->目标文件绝对路径
func DstPaht(srcPath, srcAbs, dstAbs string) string {
return strings.Replace(srcPath, srcAbs, dstAbs, -1)
}
// 文件重命名
// 接收一个路径,或者文件名,返回不带路径重命名后的文件名
// 返回格式:原文件名(1).扩展名
func FileRename(filePath string) string {
nameExt := filepath.Ext(filePath) // 文件扩展名,最后一个点后的字符串,包括点
nameSrart := strings.TrimSuffix(filePath, nameExt) // 去除文件扩展名后的绝对路径
return fmt.Sprint(nameSrart, "(1)", nameExt) // 新文件名加:(1)
}
// 返回文件md5、sha256、错误
// 输入文件路径
func FileHash(path string) (string, string, error) {
f, err := os.Open(path) // 打开文件
if err != nil {
return "", "", err
}
defer f.Close()
h5 := md5.New() // 创建md5
if _, err := io.Copy(h5, f); err != nil { // 将文件拷贝到md5
return "", "", err
}
h256 := sha256.New() // 创建sha256
f.Seek(0, 0) // 将文件指针指向开始位置
if _, err := io.Copy(h256, f); err != nil { // 将文件拷贝到sha256
return "", "", err
}
return hex.EncodeToString(h5.Sum(nil)), hex.EncodeToString(h256.Sum(nil)), err
}
// 文件是否存在
func IsFileExist(path string) bool {
_, err := os.Stat(path)
if err != nil { //文件不存在
return false
}
return true
}
// 文件复制
func CopyFile(src, dst string) {
rFile, err := os.Open(src) // 源文件
defer rFile.Close()
if err != nil {
log := "[err-a] " + src + " 空 读取源文件错误"
cn.ErrAdd() // 错误,计数器+1
SaveLog(log)
}
wFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0777) // 目标文件
defer wFile.Close()
if err != nil {
log := "[err-b] 空 " + dst + " 创建目标文件错误"
cn.ErrAdd() // 错误,计数器+1
SaveLog(log)
}
_, err = io.Copy(wFile, rFile) // 复制文件
if err != nil {
log := "[err-c] " + src + " " + dst + " 复制错误"
cn.ErrAdd() // 错误,计数器+1
SaveLog(log)
}
}
// 数据存数据库或文件
// 存储错误日志
func SaveLog(loginfo string) {
file, _ := os.OpenFile("run.Log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0777) // 打开文件
defer file.Close()
logger := log.New(file, "", log.Ldate|log.Ltime)
logger.Println(loginfo)
// 修改日志配置
logger.SetOutput(os.Stdout)
logger.Print("\r" + loginfo)
}
数据队列代码
go
复制代码
package main
import (
"sync"
)
// 数据队列以链表的形式存储数据,每个节点存储一个任意类型的数据,
// 创建数据队列、添加数据、删除数据、获取队列长度,每个数据存储在一个节点中。
// 先进先出
// 数据节点
type DataNode struct {
data interface{} // 节点中的数据
next *DataNode // 指向下一个节点
}
// 数据队列,存贮数据节点
type WorkQueue struct {
root *DataNode // 头结点
size int // 队列长度
mutex sync.Mutex // 锁
}
// 创建数据队列
func NewWorkQueue() *WorkQueue {
wq := &WorkQueue{root: nil, size: 0}
return wq
}
// 数据入队
// 切片数据:wq.Add([]string{"aaa", "bbb"})
// 字符串数据:wq.Add("ccc")
// 字典数据:wq.Add(map[string]string{"a": "aa", "b": "bb"})
func (wq *WorkQueue) Add(data interface{}) {
wq.mutex.Lock() // 加锁
defer wq.mutex.Unlock() // 解锁
if wq.root == nil { // 队列为空
wq.root = new(DataNode) // 创建节点,赋值给头节点
wq.root.data = data // 节点数据赋值
} else {
dn := new(DataNode) // 创建节点
dn.data = data // 节点数据赋值
node := wq.root // 获取队列头节点
for node.next != nil { // 从头节点开始向下寻找尾节点
node = node.next // 有下一个节点时,将下一个节点置为当前节点
}
node.next = dn // 将新节点连接到最后一个节点位置
}
wq.size++ // 数据队列长度+1
}
// 获取队首数据,并从队列中删除节点。
// 返回数据可能为nil,使用前需判断过滤nil值。
func (wq *WorkQueue) Pop() interface{} {
wq.mutex.Lock() // 加锁
defer wq.mutex.Unlock() // 解锁
if wq.root == nil { // 数据队列为空
return nil
} else {
node := wq.root // 获取首节点
v := node.data // 获取首节点数据
wq.root = node.next // 首节点设置为第二个节点
wq.size-- // 数据队列长度-1
return v // 返回首节点数据
}
}
// 获取数据队列长度
func (wq *WorkQueue) Size() int {
return wq.size
}