使用golang删除重复文件

本文介绍了如何使用Go语言编写一个文件去重工具,通过计算文件的SHA256摘要和比较文件大小来找出并处理重复文件。程序首先遍历指定目录,读取每个文件的部分内容计算摘要,然后对摘要相同而大小不同的文件读取全部内容再次计算摘要以确保准确性。最终将重复文件信息输出到Excel文件,并提供了删除重复文件的选项。该工具在作者的NAS系统上成功删除了50多GB的重复文件。
摘要由CSDN通过智能技术生成

背景

      最近搞了一台NAS,使用了两只珍藏多年的500G组Raid1(废物利用),把积累多年的照片放了上去,发现有100G多,其中半数重复,经年累月备份的结果,于是想释放这些空间,能节省就节省。

      老早前使用golang写过一个去重工具,过于久远,源码找不到了,就重新撸了一个,以后随时使用。

过程

1.定义文件信息结构

      定义以下结构体保存读到的文件信息,并为后续文件处理提供方便。

// FileInfo 文件信息保存
type FileInfo struct {
	FullPath string // 文件的全路径
	Sha256   string // 文件的sha256值 用于文件排重
	Size     int64  // 文件的大小 单位字节
	fullRead bool   // 计算sha256时是否读取了文件的全部内容
}

      FullPath 是文件的全路径名即绝对路径名,在确定是重复的文件时执行删除操作时用的到。

      Sha256 是文件内容的sha256摘要,用于确定此文件与其他文件内容上是否相同,为了速度这里参与计算的文件内容并不是文件内容的全部,只是文件开头的一部分。这样不用读取全部,但存在问题,两个文件如果只有后部分不同则无法有效判断。我这里只有照片问题不大,但为了保险,还是做了相应处理。

     Size 是文件的大小,单位字节,这也是判断文件内容是否一样的重要依据之一。

     为了解决上述内容前部分相同的问题,添加了fullRead字段,用于指示在计算sha256时是否主动读取了全部文件,仅在文件大小不一样且sha256一样时才会主动读取全部信息重新计算sha256。

2.遍历文件信息

      使用filepath.Walk遍历文件。返回所有文件信息,供后续处理。

func readAllFile() []FileInfo {
	// 当前文件夹
	var pwd = GetWorkDir()
	// 文件信息保存
	var fileInfos = make([]FileInfo, 0, 10000)
	// 文件序号 在遍历的过程中输出信息
	var index int
	err := filepath.Walk("./", func(path string, info os.FileInfo, err error) error {
		// 有错误直接退出
		if err != nil {
			panic(err)
		}
		// 是否是文件夹 是文件夹直接跳过
		if info.IsDir() {
			return nil
		}
		// 是否是正常文件,如果是文件夹、设备文件、链接文件或其他非常规文件则直接跳过。
		if !info.Mode().IsRegular() {
			// 可能是链接文件 也可能是文件夹
			println("get no regular file and pass:" + path)
			return nil
		}
		// 使用当前文件夹路径拼接得到文件的绝对路径
		var fullPath = fmt.Sprintf("%v%c%v", pwd, os.PathSeparator, path)
		fileInfos = append(fileInfos, FileInfo{
			FullPath: fullPath,
			Sha256:   GenSha256For32KB(fullPath), // 开始只读取文件开头的32kb内容,不足32k全读取
			Size:     info.Size(),
			fullRead: false, // 默认false
		})
		// 通过序号每50个文件输出一条提示
		index++
		if index%50 == 0 {
			fmt.Printf("%v-%v\n", index, fullPath)
		}
		return nil
	})
	if err != nil {
		println(err)
	}
	return fileInfos
}

      以下为获取当前文件夹的函数,如果出现错误则直接退出。os get work dir。

func GetWorkDir() string {
	wd, err := os.Getwd()
	if err != nil {
		panic(err)
	}
	return wd
}

       是否是文件夹,文件夹跳过,文件夹中的内容并没有跳过。

		// 是否是文件夹 是文件夹直接跳过
		if info.IsDir() {
			return nil
		}

      判断是否是常规文件 ls -alh文件属性 第一个字符为‘-’的是常规文件。d为文件夹,l为链接文件,链接文件可以链接到文件夹也可以链接到文件。

		// 是否是正常文件,如果是文件夹、设备文件、链接文件或其他非常规文件则直接跳过。
		if !info.Mode().IsRegular() {
			// 可能是链接文件 也可能是文件夹
			println("get no regular file and pass:" + path)
			return nil
		}

 3.读取文件内容,计算sha256摘要

      使用golang标准库中的工具,读取文件与计算摘要。以下只读取32KB大小。返回的是十六进制的字面字符串。

func GenSha256For32KB(fullPath string) string {
	// 打开文件
	open, err := os.Open(fullPath)
	if err != nil {
		panic(err)
	}
	// 关闭文件
	defer func() {
		err := open.Close()
		if err != nil {
			println("关闭文件错误:" + err.Error())
		}
	}()
	// 读取Buff 长度32KB
	var readLimit = [1024 * 32]byte{} // 32KB
	// 读取文件
	readLen, err := open.Read(readLimit[:])
	if err != nil && err != io.EOF {
		panic(err)
	}
	// 计算sha256
	sum256 := sha256.Sum256(readLimit[:readLen])
	// 转化为十六进制字符串形式反回
	return hex.EncodeToString(sum256[:])
}

      读取全文件,与上相同,只在读取时有区别,使用ioutil工具把文件直接整个读到内存中。同样如果有错误产生则直接结束运行。

func GenSha256FullRead(fullPath string) string {
	open, err := os.Open(fullPath)
	if err != nil {
		panic(err)
	}
	defer func() {
		err := open.Close()
		if err != nil {
			println("关闭文件错误:" + err.Error())
		}
	}()
	allContent, err := ioutil.ReadAll(open)
	if err != nil {
		panic(err)
	}
	sum256 := sha256.Sum256(allContent)
	return hex.EncodeToString(sum256[:])
}

4.把字节转化为人可读的格式

      如果小于1024字节,就显示字节数,其他类推。%.3f 格式化输出float类型,保留三位小数。

func ToHumanReadSize(size int64) string {
	if size < 1024 {
		return strconv.FormatInt(size, 10) + "Byte"
	} else if size < 1024*1024 {
		return fmt.Sprintf("%.3fKB", float32(size)/1024.0)
	} else if size < 1024*1024*1024 {
		return fmt.Sprintf("%.3fMB", float32(size)/(1024.0*1024))
	} else {
		return fmt.Sprintf("%.3fGB", float32(size)/(1024.0*1024*1024))
	}
}

5.确保文件sha256的有效性,必要时读取整个文件内容计算摘要

       对于sha256摘要相同,而文件大小不同的文件,采取重新读取全部内容计算摘要的方式,保证对比的准确。对于照片来说,这足够了。文件内容不同而摘要相同的概率极小。这里使用函数递归,每处理一个文件后即要从头重新对比。

func CheckSha256OnFullRead(files []FileInfo) {
	println("check size and sha256")
	// 是否要重新检查
	var reCheck = false
	// 文件map记录 记录sha256相同的文件
	var fileMap = make(map[string][]*FileInfo)
	// 遍历文件
	for index := range files {
		// 检查文件map记录
		fileSlice, ok := fileMap[files[index].Sha256]
		if ok {
			// 有相同sha256摘要的文件 检查文件大小
			if files[index].Size != fileSlice[0].Size {
				println("need recheck size and sha256")
				reCheck = true // 重新检查
				// 读取全部文件计算摘要
				if !fileSlice[0].fullRead {
					fileSlice[0].Sha256 = GenSha256FullRead(fileSlice[0].FullPath) // 读取全文件
					fileSlice[0].fullRead = true // 设置读取了全文件
					fmt.Printf("full read first %v %v\n", fileSlice[0].Size, fileSlice[0].FullPath)
				}
				if !files[index].fullRead {
					files[index].Sha256 = GenSha256FullRead(files[index].FullPath) // 读取全文件
					files[index].fullRead = true // 设置读取了全文件
					fmt.Printf("full read other %v %v\n", files[index].Size, files[index].FullPath)
				}
			}
			// 添加文件到组中
			fileMap[files[index].Sha256] = append(fileSlice, &files[index])
		} else {
			// 添加此摘要组第一个文件到组中
			fileMap[files[index].Sha256] = []*FileInfo{&files[index]}
		}
	}
	// 重新检查
	if reCheck {
		println("begin recheck")
		CheckSha256OnFullRead(files)
	}
}

6.整理成map备用

      把相同sha256的文件分组,备后续处理。与上代码类似。此时组内都是内容一样的文件,为了保险,处理时判断一下文件大小,文件大小不一致则文件内容肯定不一样。

func GenToHashMapVarSha(files []FileInfo) map[string][]*FileInfo {
	var fileMap = make(map[string][]*FileInfo)
	for index := range files {
		fileSlice, ok := fileMap[files[index].Sha256]
		if ok {
			// 检查文件大小
			if files[index].Size != fileSlice[0].Size {
				fmt.Printf("%v %v %v\n", fileSlice[0].Size, fileSlice[0].Sha256, fileSlice[0].FullPath)
				fmt.Printf("%v %v %v\n", files[index].Size, files[index].Sha256, files[index].FullPath)
				panic("同组内文件大小不一致,构建删除map失败")
			}
			fileMap[files[index].Sha256] = append(fileSlice, &files[index])
		} else {
			fileMap[files[index].Sha256] = []*FileInfo{&files[index]}
		}
	}
	return fileMap
}

7.导出到xls文件,确定要删除的文件是哪些

      使用三方xls库,这个库简单好用,功能够用。

"github.com/tealeg/xlsx"

     输出必要的信息,人肉查看有无疏漏。

func GenDuplicateFileToFile(files map[string][]*FileInfo) {
	file := xlsx.NewFile()
	sheet, err := file.AddSheet("Sheet1")
	if err != nil {
		fmt.Printf(err.Error())
	}
	row := sheet.AddRow()
	row.AddCell().Value = "总序号"
	row.AddCell().Value = "组内序号"
	row.AddCell().Value = "大小Human"
	row.AddCell().Value = "SHA256"
	row.AddCell().Value = "文件路径"
	row.AddCell().Value = "大小Byte"
	row.AddCell().Value = "完整读"
	var totalIndex = 0
	for _, fileInfos := range files {
		for index, fileInfo := range fileInfos {
			row := sheet.AddRow()
			totalIndex++
			row.AddCell().Value = strconv.Itoa(totalIndex)
			row.AddCell().Value = strconv.Itoa(index)
			row.AddCell().Value = ToHumanReadSize(fileInfo.Size)
			row.AddCell().Value = fileInfo.Sha256
			row.AddCell().Value = fileInfo.FullPath
			row.AddCell().Value = strconv.FormatInt(fileInfo.Size, 10)
			row.AddCell().Value = fmt.Sprintf("%v", fileInfo.fullRead)
		}
	}
	var statInfo = CalStatisticInfo(files)
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("总文件数:%d个\n", statInfo.TotalFileCount)
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("不重复文件数:%d个\n", statInfo.NoDumpFileCount)
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("重复文件数:%d个\n", statInfo.DumpFileCount)
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("总空间占用:%v\n", ToHumanReadSize(statInfo.TotalSize))
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("重复总空间占用:%v\n", ToHumanReadSize(statInfo.DumpSize))
	row = sheet.AddRow()
	row.AddCell().Value = fmt.Sprintf("不重复空间占用:%v\n", ToHumanReadSize(statInfo.NoDumpSize))

	err = file.Save("dump_file.xlsx")
	if err != nil {
		fmt.Printf(err.Error())
	}
}

8.删除重复文件

 dryRun=true 就只显示要删除的文件而不进行实际删除。确定正确后再执行删除。

func DeleteDuplicateFile(files map[string][]*FileInfo, dryRun bool) {
	for sha256Key, fileItems := range files {
		fmt.Printf("==sha256:%v=\n", sha256Key)
		for index, file := range fileItems {
			if file.Sha256 != sha256Key {
				panic("err map format")
			}
			if index == 0 {
				fmt.Printf(">>>>> first %v %v %v\n", index, ToHumanReadSize(file.Size), file.FullPath)
			} else {
				// 文件已经存在 删除
				fmt.Printf(">>>>> other %v %v %v\n", index, ToHumanReadSize(file.Size), file.FullPath)
				if !dryRun {
					err := os.Remove(file.FullPath)
					if err != nil {
						fmt.Printf("删除失败:path-%v %v \n", file.FullPath, err.Error())
					} else {
						fmt.Printf(">>>>> >>delete-ok %v\n", index)
					}
				}
			}
		}
	}
}

9.参数

      第二个参数是do则执行实际删除操作,否则不执行删除操作。

	var args = os.Args
	if len(args) == 2 && args[1] == "do" {
		DeleteDuplicateFile(mappedFiles, false) // 执行删除
	} else {
		DeleteDuplicateFile(mappedFiles, true) // 打印数据不删除
	}

      golang接受键盘输入,做到按任意键继续功能

func PauseWhileAnyKey() {
	println("按任意键继续...")
	var anyKey string
	_, err := fmt.Scanln(&anyKey)
	if err != nil {
		return
	}
}

10.在windows上交叉编译golang到linux可执行文件

      由于NAS是x86架构linux系统,通过ssh连接,所以交叉编译,复制可执行文件上去执行就可以了。以下是交叉编译的代码,本是放在build_linux.bat文件中的。


SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build

     还有在docker中编译程序的build_binary.sh脚本

#!/bin/bash

docker run -it -v "$PWD":/app -v "$PWD"/.cache/gopath:/go -v "$PWD"/.cache/gocache:/root/.cache/ golang:1.17 \
           /bin/sh -c "cd /app && export CGO_ENABLED=0 && export GOPROXY=https://goproxy.cn,direct && go build -v"
if [ "$?" -eq 0 ];then
  echo "build ok"
else
  echo "no"
  exit 1
fi

结尾

     使用这个工具程序删除了50多G的重复文件。心里舒畅了。威联通NAS也有去重工具,查资料说要自行从安装包安装,懒得弄了,自己动手,提升能力也解决问题,挺好。

代码

https://download.csdn.net/download/a34ErxV/80226203https://download.csdn.net/download/a34ErxV/80226203

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独杆小蓬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值