SortImages
一个简单的 Go 命令行工具,用于递归扫描指定目录,并按文件扩展名整理照片和视频。只复制,从不移动,源目录始终保持原样。
除按格式分类外,程序还会读取照片的 EXIF 信息,按拍摄日期在 Archives 下生成一份备份。
EXIF 读取依赖外部命令行工具 exiftool,GPS 反查时区依赖 tzf。所有输出目录都会创建在运行命令时的当前工作目录中。
功能特点
- 递归扫描指定目录及其子目录。
- 扩展名匹配不区分大小写。
- 将不同来源目录中的文件汇总到扁平的分类目录,同名文件自动编号,绝不互相覆盖。
- 整理结束后打印
Archives 备份树。
- 未识别的文件统一归入
Unknown。
- 照片和视频按拍摄日期额外备份到
Archives,日期所用时区依次取自 EXIF、GPS 定位、本机设置。
支持的格式
Sorted 下的桶 |
文件扩展名 |
JPG |
.jpg、.jpeg |
RAW |
.raf、.dng、.orf、.arw、.3fr、.cr3、.cr2、.crw、.nef、.nrw、.rw2、.raw |
MP4 |
.mp4、.mov、.m4v、.mts、.m2ts、.avi、.mpg、.mpeg、.mkv、.wmv、.3gp、.insv、.lrv、.lrf |
HEIC |
.heic、.heif、.hif |
Unknown |
其他扩展名 |
Sorted 下的 JPG、RAW、HEIC 三类照片和 MP4 类视频都会额外备份一份到 Archives,只有 Unknown 不备份。
进入 HEIC 目录的文件扩展名会统一改为 .heic,本来就是 .heic 的文件也会走同样的改名逻辑。
安装
前置条件
程序启动时会检查 exiftool 是否在 PATH 中,找不到会打印安装方法并退出。
| 系统 |
安装命令 |
| macOS(Homebrew) |
brew install exiftool |
| Ubuntu / Debian |
sudo apt update && sudo apt install libimage-exiftool-perl |
| Omarchy Linux(Arch 系) |
sudo pacman -S perl-image-exiftool |
装好后可以用 exiftool -ver 确认。
从源码构建还需要 Go 1.27 或更高版本。内嵌的全球时区边界数据会让构建产物达到约 16 MB。
从源码构建
git clone https://github.com/huahang/SortImages.git
cd SortImages
go build
安装当前源码
进入仓库根目录后执行:
go install .
未单独配置 GOBIN 时,可执行文件通常会安装到 $(go env GOPATH)/bin/SortImages。如需明确安装到该目录,可以执行:
GOBIN="$(go env GOPATH)/bin" go install .
也可以不克隆仓库,直接安装远程最新版:
go install github.com/huahang/SortImages@latest
使用方法
SortImages <待扫描目录>
| 参数 |
说明 |
<待扫描目录> |
要递归扫描的目录,只能指定一个 |
程序只复制文件,不提供移动选项:源目录永远不会被改动。
退出码
| 退出码 |
含义 |
0 |
整理成功完成 |
1 |
参数错误、缺少 exiftool、目录创建失败,或遍历中途被错误中止 |
2 |
传了未定义的命令行选项 |
失败时退出码一定非零,因此 SortImages src && do_something 这类写法是安全的。
建议使用一个不在待扫描目录内的独立目录保存整理结果。
运行
假设构建得到的 SortImages 位于项目根目录:
mkdir sorted
cd sorted
../SortImages /absolute/path/to/photos
执行后,当前目录会生成以下结构:
sorted/
├── Sorted/
│ ├── JPG/
│ ├── RAW/
│ ├── MP4/
│ ├── HEIC/
│ └── Unknown/
└── Archives/
└── 2024/
└── 2024-06/
└── 2024-06-01/
└── DSCF1234.raf
Sorted 按格式分类,Archives 按拍摄日期归档,两者平级。
重名处理
Archives 和 Sorted 下的五个分类目录使用完全相同的规则,绝不覆盖已有文件:
| 目标位置的情况 |
处理方式 |
| 不存在 |
独占创建并写入 |
| 已有文件,内容完全相同 |
认为已经放过,跳过 |
| 已有文件,大小或内容不同 |
依次尝试 照片-1.jpg、照片-2.jpg…… |
| 被目录或符号链接占用 |
同样视为占用,继续尝试下一个序号 |
| 连续 1000 个名字都冲突 |
报错并停止本轮整理 |
写入先落到同目录的 .sortimages-* 临时文件,再用 os.Link 定名。os.Link 在目标已存在时返回 EEXIST,因此两个进程同时整理到同一个输出目录也不会交错写坏同一个文件;进程被 Ctrl-C 打断时留下的是可识别的临时文件,而不是一个占着正确名字的半截文件。
注意事项
- 输出目录取决于当前工作目录,而不是待扫描目录。程序会在遍历时跳过自己的
Sorted 和 Archives 两棵子树,因此即使输出目录位于待扫描目录内部,也不会把已经整理好的文件再处理一遍。
- 输出目录采用扁平结构,不保留源目录层级,但同名文件不会互相覆盖,详见下面的「重名处理」。
- HEIC 类文件名会在第一个点处截断,只保留首段:
IMG.001.hif 变成 IMG.heic,2024.06.01 shot.heif 会被压成 2024.heic。多个文件会因此撞成同名,但会按重名规则编号保存,不会丢失。
- 只有无权限进入的目录会被警告并跳过。其余任何一次复制失败(包括源文件不可读、分类目录创建失败)都会中止整轮扫描并以退出码 1 结束,留下归档到一半的结果。因为只复制不移动,直接重跑一次即可补齐。
- 错误信息打印到标准输出而非标准错误。
- 照片和视频会被完整复制两份:一份进
Sorted 下的分类目录,一份进 Archives。两份互相独立、不共享 inode,因此其中一份被就地改写不会影响另一份;代价是整理 100 GB 素材实际读写各约 200 GB。
- 备份日期按
DateTimeOriginal → CreationDate → CreateDate → ModifyDate → 文件修改时间的顺序取用。
ModifyDate 是修图时间,不是拍摄时间。经 Photoshop、Lightroom 处理过的照片常常只剩这个标签,此时程序会打印 date=modify 警告——这一天是编辑那天,未必是按快门那天。
- 确定年月日所用的时区按三级顺序解析:EXIF 中的时区偏移 → 照片 GPS 定位反查出的时区 → 本机时区。
- EXIF 拍摄时间按规范记录的是拍摄地的墙上时钟,因此它的年月日不随运行机器的时区变化,换台电脑重跑结果一致。
- 视频的时间语义与照片相反:QuickTime 的
CreateDate 按规范是 UTC。程序会用视频自带的 TimeZone 标签(没有则用 GPS、本机时区)把它换算回拍摄地时间,因此一段当地 07:30 拍的视频不会被归到前一天。换算不依赖运行机器的时区,换台电脑结果一致。
- 退回文件修改时间时年月日会随时区变化:例如一张 GPS 在火奴鲁鲁的照片,在上海的电脑上按本机时区会归到 6 月 15 日,按 GPS 反查出的时区则归到 6 月 14 日。
- 日期或时区任一项不是从照片本身读出来的,程序会打印
[Warning] date=... tz=... 提示该归档日期不完全可信。
- 备份失败会中止本轮扫描。因为只复制不移动,直接重跑一次即可补齐。
Archives 中目标已存在时不覆盖:内容完全相同则跳过(重复运行幂等),内容不同则追加 -1、-2。因此对已备份的照片重跑会完整读两遍文件。
- 扫描树中的所有常规文件都会被复制到某个分类目录,不只是照片和视频;未识别的一律进入
Unknown。
- macOS 在存储卡上生成的
._ 开头 AppleDouble 文件会被直接忽略,既不整理也不备份。
- 整理结束后打印的树只包含本次新增的备份;重复运行会显示「本次无新增备份」。
开发
gofmt -w SortImages.go
go vet ./...
go build
Archives 备份写入使用 O_EXCL 独占创建,因此并发运行、目标位置存在符号链接、复制中途失败等情况都不会破坏已有备份。遍历时会跳过自己的六个输出目录,所以把输出目录放在待扫描目录内部也不会把备份回吞。
当前项目尚未包含自动化测试。
许可证
本项目使用 Apache License 2.0 许可证。