go语言并发文件备份,自动比对自动重命名(逐行注释)

主要功能:

  • 输入操作(用户输入):
    • 输入源文件夹地址,目标文件夹地址,同步协程数。
  • 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
}
相关推荐
梁山好汉(Ls_man)3 分钟前
鸿蒙_ArkTS解决Duplicate function implementation错误
开发语言·华为·typescript·harmonyos·鸿蒙
xiaoshuaishuai87 分钟前
Git二分法定位Bug
开发语言·python
so2F32hj214 分钟前
一款Go语言Gin框架DDD脚手架,适合快速搭建项目
开发语言·golang·gin
GetcharZp19 分钟前
还在用 Python 爬虫?Go 语言这款神器 Colly,性能直接拉满!
后端
LJianK139 分钟前
Java中的类、普通类,抽象类,接口的区别
java·开发语言
Dev7z39 分钟前
基于MATLAB的5G物理层文本传输系统仿真与性能分析
开发语言·5g·matlab
小智社群40 分钟前
贝壳获取小区的名称
开发语言·前端·javascript
lsx2024061 小时前
Python3 OS模块详解
开发语言
LiLiYuan.1 小时前
【Java线程 vs 虚拟机线程】
java·开发语言
FlDmr4i281 小时前
.NET 10 & C# 14 New Features 新增功能介绍-扩展成员Extension Members
开发语言·c#·.net