字
字节笔记本
2026年2月22日
MomentsCleaner 源码解析:群晖 Moments 重复文件清理工具的实现
本文深入解析 MomentsCleaner 的核心源码实现,这是一个专为群晖 Moments 设计的重复文件清理工具。通过分析其 Go 语言实现,了解如何高效扫描重复文件、MD5 哈希计算、以及安全的文件删除策略。
项目概述
MomentsCleaner 是一个使用 Go 语言开发的小工具,主要解决群晖 Moments 照片管理中的重复文件问题。当 Moments 同步照片时,经常会在同一目录下生成重复文件(如 xxx_1.jpg、xxx_2.jpg),该工具通过 MD5 哈希值识别重复文件,自动保留文件名最短的版本,将其他重复文件移动到备份目录。
核心代码结构
项目主要由三个文件组成:
| 文件 | 功能 |
|---|---|
main.go | 程序入口,初始化日志并调用清理逻辑 |
cleaner/cleaner.go | 核心清理逻辑,文件扫描与重复检测 |
cleaner/util.go | 工具函数,MD5 计算、目录操作、隐藏文件检测 |
入口函数分析 (main.go)
go
package main
import (
"github.com/0990/momentscleaner/cleaner"
"github.com/0990/momentscleaner/logconfig"
_ "net/http/pprof"
)
func main() {
logconfig.InitLogrus("cleaner", 10)
cleaner.DoClean()
}入口函数非常简洁:
- 初始化 Logrus 日志系统,日志文件名为
cleaner,保留 10 个备份 - 调用
cleaner.DoClean()执行清理任务
核心清理逻辑 (cleaner.go)
全局统计变量
go
const BACKUP_DIR_NAME = "被删除的文件"
var allDelCount int32 // 总删除文件数
var allFileCount int32 // 总扫描文件数
var allDirCount int32 // 总扫描目录数
var ignoreCount int32 // 忽略文件数(.log 文件)使用 int32 配合 atomic 包实现线程安全的计数器。
主入口函数 DoClean
go
func DoClean() {
t := time.Now()
dirWalk("./")
logrus.Infof("总扫描文件数:%d", allFileCount)
logrus.Infof("总扫描文件夹数:%d", allDirCount)
logrus.Infof("总重复文件被删除数:%d", allDelCount)
logrus.Infof("总耗时%v", time.Since(t))
}记录开始时间,从当前目录 ./ 开始递归扫描,最后输出统计信息。
递归扫描函数 dirWalk
这是核心算法实现:
go
func dirWalk(dirPath string) {
// 跳过备份目录,避免循环处理
if strings.Contains(dirPath, BACKUP_DIR_NAME) {
return
}
log := logrus.WithField("目录", dirPath)
// 检测隐藏文件/目录
hidden, err := isFileHidden(dirPath)
if err != nil {
log.WithError(err).Info("isFileHidden")
}
if hidden {
return
}
// 读取目录内容
fs, err := ioutil.ReadDir(dirPath)
if err != nil {
logrus.Panic(err)
}
atomic.AddInt32(&allDirCount, 1)
// 使用 map 存储 MD5 -> 文件列表的映射
hash2files := make(map[string][]os.FileInfo, 0)
for _, file := range fs {
if file.IsDir() {
// 递归处理子目录
dirWalk(dirPath + file.Name() + "/")
} else {
// 跳过 .log 日志文件
if path.Ext(file.Name()) == ".log" {
atomic.AddInt32(&ignoreCount, 1)
continue
}
name := dirPath + file.Name()
md5, err := md5File(name)
if err != nil {
logrus.Panic(err)
}
// 将文件按 MD5 值分组
hash2files[md5] = append(hash2files[md5], file)
atomic.AddInt32(&allFileCount, 1)
}
}
// 处理重复文件
// ...
}重复文件处理逻辑
go
var delCount int32
for _, files := range hash2files {
// 文件数量小于 2,说明没有重复
if len(files) < 2 {
continue
}
// 保留名称最短的文件
min := len(files[0].Name())
for i := 1; i < len(files); i++ {
lname := len(files[i].Name())
if lname < min {
min = lname
}
}
// 删除其他重复文件
for _, file := range files {
if len(file.Name()) == min {
log.WithField("filename", file.Name()).Info("保留")
continue
}
// 创建备份目录
backupDir := "./" + BACKUP_DIR_NAME + "/" + dirPath[2:]
createDirIfNoExist(backupDir)
// 移动文件到备份目录(而非直接删除)
err = os.Rename(dirPath+file.Name(), backupDir+file.Name())
if err != nil {
logrus.Panic(err)
}
delCount++
atomic.AddInt32(&allDelCount, 1)
log.WithField("filename", file.Name()).Info("删除")
}
}关键设计决策:
- 安全优先:使用
os.Rename移动文件而非直接删除,用户可手动检查后再清理 - 保留策略:保留文件名最短的版本(通常是原始文件)
- 目录结构保留:备份目录保持原目录结构,便于追溯
工具函数 (util.go)
MD5 文件哈希计算
go
func md5File(filename string) (string, error) {
file, err := os.Open(filename)
defer file.Close()
if err != nil {
return "", err
}
md5h := md5.New()
io.Copy(md5h, file)
return fmt.Sprintf("%x", md5h.Sum([]byte(""))), nil
}使用 io.Copy 将文件内容流式写入 MD5 哈希器,避免大文件内存占用问题。
Windows 隐藏文件检测
go
func isFileHidden(path string) (bool, error) {
name := utf16.Encode([]rune(path + "\x00"))
attributes, err := syscall.GetFileAttributes((*uint16)(unsafe.Pointer(&name[0])))
if err != nil {
return false, err
}
return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil
}使用 Windows API GetFileAttributes 检测隐藏文件,通过 UTF-16 编码和 unsafe.Pointer 进行系统调用。
目录创建
go
func createDirIfNoExist(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
return
}
return
}
}使用 os.MkdirAll 递归创建目录,权限设置为 os.ModePerm(0777)。
技术亮点总结
| 特性 | 实现方式 |
|---|---|
| 线程安全计数 | sync/atomic 包操作 int32 |
| 内存优化 | 流式 MD5 计算,避免加载整个文件 |
| 安全删除 | 移动文件到备份目录,非直接删除 |
| 跨平台 | Windows 隐藏文件检测(syscall) |
| 日志记录 | Logrus 结构化日志 |
使用建议
- 首次运行:建议在测试目录验证行为
- 备份检查:清理后检查
"被删除的文件"目录,确认无误后再彻底删除 - 日志分析:查看
cleaner_info.log了解处理详情 - 内存优化:最新版本已将运行时内存占用从 1GB 优化至 10MB
项目链接
分享: