目标与现状
把「旧中国老照片」归档(Hyper-V 动态 VHDX)在 NixOS 上直接挂载、全程只读、mtime 不失真——判据是显示 1800 年代的拍摄年代,不能被夹成 1970。
| 项 | 值 |
|---|---|
| 归档(保真载体) | /s500data/旧中国老照片/photo-archive.vhdx |
| raw 形态(宿主用) | /s500data/旧中国老照片/photo-archive.img |
| 挂载点 | /mnt/photo(内核 ntfs3 + loop,只读) |
| 声明式模块 | /etc/nixos/modules/system/service/photo-archive.nix |
| 侧车文件 | manifest_old-china-photo.csv(38,265 行)、photo-archive.vhdx.sha256 |
双形态各自一等公民:宿主走 raw(内核 loop 内建,挂载时零外部工具);VHDX 形态留给 Windows 原生「附加 VHD」只读挂载。两者同一内容,等价性用 qemu-img compare 核对。只读是红线:宿主 ro、Windows 侧 Read-only——两边同时可写挂同一份即双写损坏。
前置事实(实测)
| 项 | 实测值 |
|---|---|
| VHDX 形态 | 动态盘,虚拟 21.475 GB / 实占 9.232 GB(43%),块 32 MB,无父盘 |
| 分区表 | MBR,单分区 type=0x07(NTFS),逻辑扇区 512;分区起始 LBA 2048 → 偏移 1048576 字节 |
| raw 形态 | 表观 21.475 GB / 实占 9.129 GB(稀疏省 57.5%) |
| 侧车 manifest | 列 RelPath, Size, MTime, CTime, MTimeUtc;MTime 是本地 CST,MTimeUtc 才是 UTC |
| 现场副本(非本归档) | 38,265 个文件 mtime 已被改写(1970 出现 947 次、无任何早于 1900)⇒ 归档才是保真载体 |
| 宿主能力 | 内核 ntfs3 / loop / nbd 齐备(挂载本身不需要装任何包) |
实现过程
raw 形态生成
qemu-img convert -p -f vhdx -O raw -t none -T none \
photo-archive.vhdx photo-archive.img
实测 3.2 秒完成;-O raw 默认按零块识别跳过 ⇒ 输出保稀疏(表观 21.475 GB / 实占 9.129 GB),不需要额外加参数。
声明式挂载(NixOS 模块)
模块 /etc/nixos/modules/system/service/photo-archive.nix,在 configuration.nix 的 imports 清单里放开一行即启用。两件事都由声明式承担:
systemd.services.photo-archive-raw = { # raw 缺了就生成(幂等、可重放)
unitConfig.ConditionPathExists = "!${raw}";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.qemu-utils}/bin/qemu-img convert -p -f vhdx -O raw -t none -T none ${vhdx} ${raw}";
};
before = [ "mnt-photo.mount" ];
requiredBy = [ "mnt-photo.mount" ];
};
fileSystems."/mnt/photo" = {
device = raw;
fsType = "ntfs3"; # 内核驱动(非 FUSE)
options = [
"loop" "ro" "noatime"
"offset=1048576" # MBR 分区1 起始 LBA 2048;整盘 raw 直接挂会报「引导签名不是 NTFS」
"uid=1000" "gid=100" "umask=022" # 只读挂载下让普通用户可浏览
"nofail" # 归档缺失时不阻塞启动(只读数据挂载,非系统盘)
];
};
生效:
sudo nixos-rebuild switch
为什么必须是内核 ntfs3,而不是 ntfs-3g(FUSE)
归档里是 1860 年代的拍摄年代(mtime 保真是本项验收判据「不是 1970」)。内核 ntfs3 读写前置 1970 的时间戳不夹取,FUSE 路线有夹取风险。
实测证据
| 判据 | 实测值 |
|---|---|
| 挂载形态 | /dev/loop0 /mnt/photo ntfs3 ro,noatime,uid=1000,gid=100,dmask=0022,fmask=0022,iocharset=utf8 |
| 只读生效 | 写入被拒:EROFS (30) Read-only file system |
| mtime 保真 | 38,266 个文件;直方含 1840:1 / 1860:111 / 1870:46 / 1900:581 / 1924:2540 / 1930:5020 …,未被夹成 1970 |
| manifest 对照 | 用 MTimeUtc + Size 抽查 6 项(3 项 1860s + 3 项随机)⇒ 6/6 完全一致 |
| raw 与 VHDX 等价 | qemu-img compare -f vhdx -F raw ⇒ Images are identical. |
管理命令
| 用途 | 命令 |
|---|---|
| 挂载 / 卸载(走声明式单元) | systemctl start|stop mnt-photo.mount |
| 看单元状态 | systemctl status mnt-photo.mount |
| 手动挂载(不依赖 fstab) | sudo mount -t ntfs3 -o loop,ro,offset=1048576,uid=1000,gid=100,umask=022 /s500data/旧中国老照片/photo-archive.img /mnt/photo |
| 确认挂上了且是 ro | grep /mnt/photo /proc/mounts 或 findmnt /mnt/photo |
| 只读自检(预期被拒) | touch /mnt/photo/__probe__ ⇒ Read-only file system |
| 看某个文件的 mtime | stat -c '%y %n' /mnt/photo/<路径> |
| 重建 raw(归档更新后) | rm photo-archive.img 然后 systemctl start photo-archive-raw.service(已存在时会跳过) |
| 等价性核对 | qemu-img compare -f vhdx -F raw photo-archive.vhdx photo-archive.img |
| 空间/稀疏度 | df -h /mnt/photo;du -h --apparent-size photo-archive.img; du -h photo-archive.img |
| 备选只读直挂(不落 raw,需 qemu 工具) | qemu-nbd --read-only --format=vhdx --connect=/dev/nbd0 photo-archive.vhdx → partx -a /dev/nbd0 → mount -t ntfs3 -o ro /dev/nbd0p1 /mnt/photo → umount /mnt/photo; qemu-nbd --disconnect /dev/nbd0 |
挂载单元的名字由挂载路径转义而来(/mnt/photo → mnt-photo.mount);raw 生成单元用 before/requiredBy 挂到它前面,所以 systemctl start mnt-photo.mount 会自动先备好 .img。
踩坑记录
- 整盘 raw 直接按 ntfs3 挂会失败:报
ntfs3(loop0): Primary boot signature is not NTFS—— NTFS 卷在 MBR 分区里,必须加offset=(本档 = 1048576 = LBA 2048 × 512)。 - manifest 的
MTime是本地时间:与挂载视图会差 8 小时(CST vs NTFS 内部 UTC)⇒ 判读/对照一律用MTimeUtc,否则会误判成「时间不一致」。 - mtime 里出现 1970 的那 947 个文件是原始数据本身没有时间戳(与现场副本同数),不是本次挂载造成的夹取。
- 稀疏不用手动保:
qemu-img convert -O raw默认按零块识别跳过(实测省 57.5%);加-S 0反而会关掉这层检测、把整盘写满。 nofail的用意:归档文件缺失时不阻塞启动(这是只读数据挂载、不是系统盘);属于明确设计选择,不是静默兜底。- 声明式挂载的参数传递:
fileSystems.<路径>.options里的项会原样进/etc/fstab,所以offset=、loop、ro都必须写在这一处;只写挂载命令不会生效到开机。
边界
本档只做只读;任何写入需求(补元数据、整理目录等)不在范围内。Windows 侧的「附加 VHD」只读挂载与宿主 /mnt/photo 是同一内容的两形态,可用于交叉核对同一张 1800 年代照片的「修改日期」。