字
字节笔记本
2026年2月22日
MomentsCleaner:群晖 Moments 重复文件清理工具
本文介绍 MomentsCleaner,一个专为群晖 Moments 设计的重复文件清理工具。该工具通过 MD5 哈希值扫描重复文件,自动保留文件名最短的版本,安全高效地释放存储空间。
项目简介
MomentsCleaner 是一个开源的 Go 语言项目,由开发者 0990 创建维护。该项目专门解决群晖 Moments 照片管理中常见的重复文件问题。
群晖 Moments 在同步过程中经常会在同一文件夹下生成重复文件(如 xxx_1.jpg、xxx_2.jpg),占用大量存储空间。虽然群晖自带的存储空间分析器能够列出重复文件,但需要手动逐一判别删除,当重复文件数量庞大时操作非常繁琐。MomentsCleaner 正是为解决这一痛点而生。
核心特性
- 智能扫描:递归扫描当前目录及所有子目录下的文件
- MD5 去重:基于文件内容 MD5 哈希值精确识别重复文件
- 安全删除:仅删除同目录下的重复文件,避免误删
- 自动备份:被删除的文件自动移动到"被删除的文件"目录,便于复查
- 保留策略:自动保留文件名最短的版本(通常是最原始的文件)
- 详细日志:使用 logrus 记录完整的扫描和清理过程
技术栈
- Go 语言:核心开发语言,支持跨平台编译
- logrus:结构化日志库,提供清晰的执行记录
- syscall:Windows 系统调用,支持检测隐藏文件
- crypto/md5:标准库 MD5 哈希计算
安装指南
前置要求
- Windows 系统(工具主要针对 Windows 环境设计)
- 已安装 Synology Drive 客户端并同步 Moments 照片
下载安装
- 访问 GitHub Releases 下载最新版本
- 将
MomentsCleaner.exe放入 Moments 同步文件夹或子文件夹中
源码编译
bash
# 克隆仓库
git clone https://github.com/0990/momentscleaner.git
cd momentscleaner
# 编译(需要 Go 环境)
go build -o MomentsCleaner.exe快速开始
使用步骤
-
同步照片:在 Windows 系统中使用 Synology Drive 将 Moments 照片同步到本地
-
执行清理:将
MomentsCleaner.exe放入 Moments 文件夹或子文件夹,双击执行 -
检查结果:执行完成后会生成
被删除的文件目录,所有被删除的文件都保存在这里 -
确认删除:检查
被删除的文件目录,确认无误后手动删除 -
同步回群晖:清理完成后,开启 Drive 的双向同步(或单向上传),将更新同步到群晖
执行示例
bash
# 将可执行文件放入 Moments 目录
MomentsCleaner.exe
# 查看输出日志
type cleaner_info.log代码分析
项目结构
text
momentscleaner/
├── cleaner/
│ ├── cleaner.go # 核心清理逻辑
│ └── util.go # 工具函数
├── logconfig/ # 日志配置
├── main.go # 程序入口
├── go.mod
└── README.md核心代码解析
main.go - 程序入口
go
package main
import (
"github.com/0990/momentscleaner/cleaner"
"github.com/0990/momentscleaner/logconfig"
)
func main() {
logconfig.InitLogrus("cleaner", 10)
cleaner.DoClean()
}程序初始化日志配置后,调用 cleaner.DoClean() 开始清理流程。
cleaner.go - 核心清理逻辑
go
const BACKUP_DIR_NAME = "被删除的文件"
func DoClean() {
t := time.Now()
dirWalk("./")
logrus.Infof("总扫描文件数:%d", allFileCount)
logrus.Infof("总扫描文件夹数:%d", allDirCount)
logrus.Infof("总重复文件被删除数:%d", allDelCount)
logrus.Infof("总耗时%v", time.Since(t))
}DoClean 函数是清理流程的入口,它调用 dirWalk 递归遍历目录,最后输出统计信息。
目录遍历与重复检测
go
func dirWalk(dirPath string) {
// 跳过备份目录
if strings.Contains(dirPath, BACKUP_DIR_NAME) {
return
}
// 跳过隐藏文件/目录
hidden, err := isFileHidden(dirPath)
if hidden {
return
}
// 读取目录内容
fs, err := ioutil.ReadDir(dirPath)
// 使用 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" {
continue
}
// 计算 MD5
md5, err := md5File(name)
hash2files[md5] = append(hash2files[md5], file)
}
}
// 处理重复文件
for _, files := range hash2files {
if len(files) < 2 {
continue
}
// 保留名称最短的文件
min := len(files[0].Name())
for i := 1; i < len(files); i++ {
if len(files[i].Name()) < min {
min = len(files[i].Name())
}
}
// 移动其他重复文件到备份目录
for _, file := range files {
if len(file.Name()) == min {
log.WithField("filename", file.Name()).Info("保留")
continue
}
// 移动到备份目录...
}
}
}util.go - 工具函数
go
// 创建目录(如果不存在)
func createDirIfNoExist(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
}
}
}
// 检测文件是否为隐藏文件(Windows)
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
}
// 计算文件 MD5
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
}设计说明
为什么只删除同目录下的重复文件?
群晖 Moments 的重复文件问题主要集中在同一文件夹内(如同一张照片被多次导入生成 xxx_1.jpg、xxx_2.jpg)。限制在同目录下处理:
- 安全性高:避免误删不同目录下可能需要的相同文件
- 针对性强:精准解决 Moments 的主要痛点
- 符合使用场景:大部分重复文件确实位于同一目录
删除策略:保留文件名最短的版本
当检测到重复文件时,工具保留文件名最短的文件,删除其他版本。这是因为:
- 原始文件名通常最短(如
photo.jpg) - 重复文件往往带有序号后缀(如
photo_1.jpg、photo_2.jpg) - 此策略能最大程度保留原始文件
注意事项
- 备份重要数据:首次使用前建议备份重要照片
- 检查删除结果:清理后务必检查"被删除的文件"目录
- 日志文件:执行过程中会生成
cleaner_info.log记录详细操作 - 存档状态:该项目已被作者存档(2025年8月),现为只读状态
项目统计
- Stars: 41
- Forks: 5
- 主要语言: Go
- 状态: 已存档(Archived)
项目链接
分享: