SortImages

command module
v0.0.0-...-e27273d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

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 下的 JPGRAWHEIC 三类照片和 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 按拍摄日期归档,两者平级。

重名处理

ArchivesSorted 下的五个分类目录使用完全相同的规则,绝不覆盖已有文件:

目标位置的情况 处理方式
不存在 独占创建并写入
已有文件,内容完全相同 认为已经放过,跳过
已有文件,大小或内容不同 依次尝试 照片-1.jpg照片-2.jpg……
被目录或符号链接占用 同样视为占用,继续尝试下一个序号
连续 1000 个名字都冲突 报错并停止本轮整理

写入先落到同目录的 .sortimages-* 临时文件,再用 os.Link 定名。os.Link 在目标已存在时返回 EEXIST,因此两个进程同时整理到同一个输出目录也不会交错写坏同一个文件;进程被 Ctrl-C 打断时留下的是可识别的临时文件,而不是一个占着正确名字的半截文件。

注意事项

  • 输出目录取决于当前工作目录,而不是待扫描目录。程序会在遍历时跳过自己的 SortedArchives 两棵子树,因此即使输出目录位于待扫描目录内部,也不会把已经整理好的文件再处理一遍。
  • 输出目录采用扁平结构,不保留源目录层级,但同名文件不会互相覆盖,详见下面的「重名处理」。
  • HEIC 类文件名会在第一个点处截断,只保留首段:IMG.001.hif 变成 IMG.heic2024.06.01 shot.heif 会被压成 2024.heic。多个文件会因此撞成同名,但会按重名规则编号保存,不会丢失。
  • 只有无权限进入的目录会被警告并跳过。其余任何一次复制失败(包括源文件不可读、分类目录创建失败)都会中止整轮扫描并以退出码 1 结束,留下归档到一半的结果。因为只复制不移动,直接重跑一次即可补齐。
  • 错误信息打印到标准输出而非标准错误。
  • 照片和视频会被完整复制两份:一份进 Sorted 下的分类目录,一份进 Archives。两份互相独立、不共享 inode,因此其中一份被就地改写不会影响另一份;代价是整理 100 GB 素材实际读写各约 200 GB。
  • 备份日期按 DateTimeOriginalCreationDateCreateDateModifyDate → 文件修改时间的顺序取用。
  • 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 许可证。

Documentation

Overview

SortImages 递归扫描指定目录,按扩展名把其中的文件归档到当前工作目录下的分类桶中。

用法:

SortImages <待扫描目录>

程序先在当前工作目录下建好 Sorted 和 Archives 两个目录,再递归遍历待扫描目录,把遇到的 每个常规文件按扩展名复制进 Sorted 下对应的桶:

Sorted/{JPG,RAW,MP4,HEIC,Unknown}/<原文件名>

只复制,从不移动:源目录始终保持原样,最坏情况也只是多占一份磁盘,不会丢照片。

Sorted 与 Archives 建在当前工作目录,而不是待扫描目录旁边,因此预期用法是先 cd 到存放 结果的目录,再把源目录作为参数传入。

分类完全依据文件扩展名,从不检查文件内容。凡是没有命中扩展名表的文件一律进入 Unknown, 所以这不是一个只处理图片的工具:扫描树里的每个常规文件都会被复制到某个桶里。

除 Sorted 下的分类桶外,JPG、RAW、HEIC 三类照片和 MP4 类视频还会额外在与 Sorted 平级的 Archives 下留一份按拍摄日期组织的备份:

Archives/YYYY/YYYY-MM/YYYY-MM-DD/<原文件名>

详见 archiveFile。分类桶与 Archives 的重名处理规则完全一致,见 placeFile。

读取 EXIF 依赖外部命令 exiftool,程序启动时会先确认它在 PATH 中,找不到就打印安装方法 并以退出码 1 退出。选它而不是内嵌解析库,是因为它对 ORF、RW2 这类小众 RAW 的覆盖要完整 得多;代价是多一个系统依赖。

归档日期的取值顺序是 DateTimeOriginal、CreationDate、CreateDate、ModifyDate、文件修改 时间。注意 ModifyDate 记的是最后一次被软件写入的时间,也就是修图时间:只能拿到它时程序 会打印 date=modify 的警告,提醒这一天未必是拍摄日。

照片与视频的时间语义相反,这是最容易搞错的一点:EXIF 的拍摄时间是拍摄地的墙上时钟,而 QuickTime 的时间按规范是 UTC。视频因此会先按 UTC 解释,再用 QuickTime 的 TimeZone 标签 (没有则退到 GPS、本机)换算回拍摄地,这样一段当地 07:30 拍的视频不会被归到前一天。 换算刻意不用 exiftool 的 -api QuickTimeUTC:那个选项按运行机器的时区换算,同一段视频在 不同机器上会落到不同的日期。

确定年月日所用的时区依次取自 EXIF 偏移标签(视频是 QuickTime 的 TimeZone)、GPS 定位反查、 本机设置。EXIF 拍摄时间是墙上时钟,它的年月日不随运行机器的时区变化;退回文件修改时间时 则会变。

输出目录如果落在待扫描目录内部,遍历时会跳过 Sorted 和 Archives 这两棵子树,避免刚写好 的结果被当成新的源文件再处理一遍。macOS 在存储卡上生成的 ._ 开头 AppleDouble 文件同样直接忽略:它们不是 照片,却会跟着原文件的扩展名命中分类规则。

使用前需要知道的几处边界:

  • 目标文件名只保留 basename,不保留源目录层级。不同子目录下的同名文件会落成 照片.jpg、照片-1.jpg……绝不互相覆盖,详见 placeFile。
  • 除 filepath.Walk 自己报出的目录权限错误外,任何一次复制失败都会中止整轮归档, 留下归档到一半的结果。因为只复制不移动,直接重跑一次即可补齐。
  • 任何失败都以非零退出码结束,脚本可以据此判断这次整理是否真的完成。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL