当前位置: 首页 > news >正文

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

主要功能:

  • 输入操作(用户输入):
    • 输入源文件夹地址,目标文件夹地址,同步协程数。
  • A操作(添加任务数据):
    • 一个协程程序读取源文件夹下所有文件绝对路径,生成相应的目标文件夹下的绝对路径。
    • 将源文件绝对路径和目标文件绝对路径,存储在数据队列中。
  • B操作(读取数据,并发比对备份文件):
    • 通过循环判断数据队列中是否有数据,或者是否添加数据完成。如果没数据,同时B操作添加数据已完成,退出循环(退出C操作)。
    • 如果添加数据未完成,循环读取数据队列中的值,如果有数据,按照输入的同步协程数,同步读取队列中数据,进行如下操作:
      • 目标已经有重名文件,比对文件的md5和sha256(防止哈希碰撞),如果相同,证明文件相同,此文件不做任何操作,退出本次循环。
      • 如果md5和sha256有不同,证明两文件只是文件名相同,文件内容有不同,对目标文件重命名,如果重命名后,继续循环上一操作,直到目标中没有重命名后的文件。(重命名就是对原文件名后加“(1)”,多次重命名后就是:“文件名(1)(1)(1).扩展名”)
      • 目标没有重名文件,或者重命名完成后的文件,备份文件。

代码缺点

  • 问题:数据队列没有数据时,消费者需要不停的循环判断等待。
  • 希望有解决方案:改为堵塞等待,性能会有进一步提升。欢迎提出宝贵意见。

主文件代码

package mainimport ("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 stringfmt.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) // 添加数据协程+1go 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 { // 添加数据已完成,并且数据链长度为0return // 退出获取数据操作} // 添加数据已完成,并且队列为空时,退出获取数据if wq.Size() > 0 { // 数据链上有数据节点go func() {defer wgroup.Done() // 完成后,协程计数-1wgroup.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() // 目标文件已存在,计数器+1fmt.Print("\r"+dstPath, " -> 已存在!")return // 目标文件存在,不用复制文件} else {dstPath = FileRename(dstPath) // 目标文件存在,但是内容不一样,重命名保存}}CopyFile(srcPath, dstPath)         // 复制文件(目标文件不存在时直接复制,目标文件不一样时,重命名后复制)cn.OkAdd()                         // 复制文件成功,复制计数器+1fmt.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
}

文件、目录操作代码

package mainimport ("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()                           // 创建md5if _, err := io.Copy(h5, f); err != nil { // 将文件拷贝到md5return "", "", err}h256 := sha256.New()                        // 创建sha256f.Seek(0, 0)                                // 将文件指针指向开始位置if _, err := io.Copy(h256, f); err != nil { // 将文件拷贝到sha256return "", "", 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() // 错误,计数器+1SaveLog(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() // 错误,计数器+1SaveLog(log)}_, err = io.Copy(wFile, rFile) // 复制文件if err != nil {log := "[err-c] " + src + " " + dst + " 复制错误"cn.ErrAdd() // 错误,计数器+1SaveLog(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)
}

数据队列代码

package mainimport ("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--           // 数据队列长度-1return v            // 返回首节点数据}
}// 获取数据队列长度
func (wq *WorkQueue) Size() int {return wq.size
}

http://www.mrgr.cn/news/81318.html

相关文章:

  • 汇编语言运行环境搭建及简单使用
  • 华为Ascend产品
  • 查找地理处理工具
  • Final2x--开源AI图片放大工具
  • HTTP/HTTPS ⑤-CA证书 || 中间人攻击 || SSL/TLS
  • 【CSS】HTML页面定位CSS - position 属性 relative 、absolute、fixed 、sticky
  • Require:离线部署 Sourcegraph
  • Linux驱动开发--字符设备驱动开发
  • STM32 高级 谈一下IPV4/默认网关/子网掩码/DNS服务器/MAC
  • c++类型判断和获取原始类型
  • Flutter 实现全局悬浮按钮学习
  • Linux自动挂载与卸载USB设备
  • 菜鸟带新鸟——基于EPlan2022的部件库制作
  • 免费 IP 归属地接口
  • C++程序启动报错和启动失败的常见原因分析与排查经验总结
  • Linux -- 从抢票逻辑理解线程互斥
  • 开发场景中Java 集合的最佳选择
  • 深入理解批量归一化(BN):原理、缺陷与跨小批量归一化(CmBN)
  • 数据库安全-redisCouchdb
  • 鸿蒙-expandSafeArea使用
  • QT程序发布后,mysql在其它电脑设备无法连接数据库
  • Marscode AI辅助编程
  • Python超能力:高级技巧让你的代码飞起来
  • HTMLCSSJavaScriptDOM 之间的关系?
  • Kubernetes 架构图和组件
  • 医疗信息系统有哪些