ntfs文件镜像挂载

目标与现状

把「旧中国老照片」归档(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
确认挂上了且是 rogrep /mnt/photo /proc/mounts 或 findmnt /mnt/photo
只读自检(预期被拒)touch /mnt/photo/__probe__ ⇒ Read-only file system
看某个文件的 mtimestat -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 年代照片的「修改日期」。