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
}
相关推荐
黑风风8 分钟前
使用 `@Async` 实现 Spring Boot 异步编程
java·spring boot·后端
等一场春雨10 分钟前
Spring Boot 3 文件下载、多文件下载以及大文件分片下载、文件流处理、批量操作 和 分片技术
java·spring boot·后端
码喽不秃头14 分钟前
java中的特殊文件
java·开发语言
新手小袁_J14 分钟前
Python的列表基础知识点(超详细流程)
开发语言·python·numpy·pip·基础知识·基础知识点
jf加菲猫14 分钟前
条款35:考虑虚函数以外的其它选择(Consider alternatives to virtual functions)
开发语言·c++
听风吟丶18 分钟前
深入探究 Java hashCode:核心要点与实战应用
java·开发语言
【D'accumulation】21 分钟前
深入聊聊typescript、ES6和JavaScript的关系与前瞻技术发展
java·开发语言·前端·javascript·typescript·es6
AI人H哥会Java1 小时前
【Spring】Spring DI(依赖注入)详解——自动装配——手动装配与自动装配的区别
java·开发语言·后端·spring·架构
GGBondlctrl1 小时前
【SpringBoot】深度解析 Spring Boot 拦截器:实现统一功能处理的关键路径
java·spring boot·后端·拦截器
计算机学姐1 小时前
基于SpringBoot+Vue的旅游推荐系统
java·vue.js·spring boot·后端·mysql·tomcat·旅游