字节笔记本

2026年2月22日

MomentsCleaner 源码解析:群晖 Moments 重复文件清理工具的实现

本文深入解析 MomentsCleaner 的核心源码实现,这是一个专为群晖 Moments 设计的重复文件清理工具。通过分析其 Go 语言实现,了解如何高效扫描重复文件、MD5 哈希计算、以及安全的文件删除策略。

项目概述

MomentsCleaner 是一个使用 Go 语言开发的小工具,主要解决群晖 Moments 照片管理中的重复文件问题。当 Moments 同步照片时,经常会在同一目录下生成重复文件(如 xxx_1.jpgxxx_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()
}

入口函数非常简洁:

  1. 初始化 Logrus 日志系统,日志文件名为 cleaner,保留 10 个备份
  2. 调用 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("删除")
	}
}

关键设计决策:

  1. 安全优先:使用 os.Rename 移动文件而非直接删除,用户可手动检查后再清理
  2. 保留策略:保留文件名最短的版本(通常是原始文件)
  3. 目录结构保留:备份目录保持原目录结构,便于追溯

工具函数 (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 结构化日志

使用建议

  1. 首次运行:建议在测试目录验证行为
  2. 备份检查:清理后检查 "被删除的文件" 目录,确认无误后再彻底删除
  3. 日志分析:查看 cleaner_info.log 了解处理详情
  4. 内存优化:最新版本已将运行时内存占用从 1GB 优化至 10MB

项目链接

分享: