使用golang删除重复文件

背景

      最近搞了一台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为链接文件,链接文件可以链接到文件夹也可以链接到文件。

使用golang删除重复文件_第1张图片

		// 是否是正常文件,如果是文件夹、设备文件、链接文件或其他非常规文件则直接跳过。
		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

你可能感兴趣的:(golang,开发语言,后端)