Files
UnifyDrive2NAS/PROJECT.md
T

32 KiB
Raw Blame History

title, tags, status
title tags status
UnifyDrive2NAS の現状
homelab
UnifyDrive2NAS
本番運用開始済み

UnifyDrive2NAS の現状

[!info] このファイルの位置づけ UnifyDrive2NAS プロジェクトの現状(決定事項・要件・構成・課題)を積み上げていく。運用規約は AGENTS、ホームラボ基盤は homelab/AGENTS を参照。

今の状態

  • Python 3.8 の標準ライブラリと同梱した exifread 3.5.1 だけで動くコピーコアを実装済み
  • コピー済み一覧、初期化、ドライラン、二重起動防止、マウント確認を含む
  • NAS へのスクリプト配置と 6,786 件の初期化が完了済み
  • Dagu VM へ DAG を配置済み。毎日 04:00(Asia/Tokyo)の定期実行が有効

目的

ホームラボ内の Unify Drive に格納されている画像ファイルを、Synology NAS(ds923p)へコピーする。そのための方式を検討して実装する。

決定事項

項目 決定
コピー元 Unify Drive(UT2-9EV9)のストレージ M.211 配下(詳細は #確認済みの事実)
コピー先 NAS ds923p の共有フォルダ homes 配下 kazu634/Photos/PhotoLibrary
コピー先のフォルダ構成 YYYY/YYYYMMDD/ のフォルダを作り、その中に画像を置く(例: PhotoLibrary/2026/20261010/)
実行方法 定期実行。その日に Unify Drive へ格納された分をコピーする(ユーザー判断、2026-10-10)
対象ファイル種別 RAW(arw・dng)は含めない。動画(mov・mp4)とスクリーンショット(png)は含める(ユーザー判断、2026-10-10)
コピー対象のフォルダ nvme11-admin(= M.211)配下の PlugBackup・iPhone18,1・iPad14,9・frankel のすべて。.samba_recycle は除く(ユーザー判断、2026-10-10)
新しいファイルの判定 コピー済みの一覧を持ち、まだコピーしていないファイルをコピーする(案 A。ユーザー判断、2026-10-10)。WebDAV の日時からは格納日が分からないため(#WebDAV で取れる日時)
初回の扱い 本番開始の前日に初期化だけを行う。既存のファイルをすべて「コピー済み」として一覧に記録し、コピーはしない。翌日からの定期実行で、一覧に無い新しいファイルだけをコピーする。初期化した日のうち、初期化より後に格納されたファイルは翌日の実行でコピーされる(ユーザー判断、2026-10-10)
実行場所 **Dagu(192.168.10.156)**から ssh.run で NAS 上の処理を呼ぶ(ユーザー判断、2026-10-10)。Dagu の方針([[ITProjects/homelab/Projects/Dagu/AGENTS
Unify Drive の読み方 Unify Drive を NAS にマウントする(ユーザー判断、2026-10-10)。プロトコルは #未決事項
コピー済みの一覧 SQLite に持つ。キーはコピー元のパス・サイズ・更新日時(ユーザー判断、2026-10-12)。マウント経由では WebDAV の ETag が取れないため
EXIF・動画メタデータの読み取り NAS 上の Python 3.8(標準ライブラリのみ)で読む。純 Python のライブラリ(exifread 等)をディレクトリごと配置し、動画(mov・mp4)の作成日時は QuickTime の mvhd を自前で読む(ユーザー判断、2026-10-12)。docker(root 相当)は使わない
同名で中身が違うファイル 両方残す。中身が同じなら何もしない。違えば連番を付ける(例: DSC00001_1.JPG)(ユーザー判断、2026-10-10)
コピー元のファイル 残す(コピーのみで、消さない)(ユーザー判断、2026-10-10)
YYYYMMDD の日付 EXIF の撮影日時 → 動画のメタデータ(QuickTime の作成日時等)→ ファイルの日時の順に、取れたものを使う(ユーザー判断、2026-10-10)
動画の日時の優先順 Apple の作成日時(moov/meta、タイムゾーン付き)→ moov/mvhd の作成日時(UTC を JST に変換)(ユーザー判断、2026-10-12)
隠しファイル 先頭が . のファイル・フォルダはコピーしない(.<元の名前>.z<16 進 32 桁> の書きかけと推定されるファイルを含む)(ユーザー判断、2026-10-12)
カメラの管理フォルダ内の動画 コピー対象とする。Sony の動画は PlugBackup/<回>/.../PRIVATE/M4ROOT/CLIP/*.MP4 にあり(2026-10-12 時点で 9 本・約 5.4 GB)、mvhd を JST に変換した時刻が同じフォルダの *M01.XML の CreationDate と一致することを確認済み(ユーザー判断、2026-10-12)
コピー済み DB の既定値 ~/.local/state/unifydrive2nas/copied.sqlite3。--database で変更できる
実行時刻 毎日 04:00(Asia/Tokyo)。Dagu の DAGU_TZ=Asia/Tokyo を前提に schedule: "0 4 * * *" とする(ユーザー判断、2026-10-12)
NAS 上の配置先 /volume1/homes/kazu634/bin/unifydrive2nas/。unifydrive2nas.py と vendor/ を置き、tests/ は配置しない。DB は既定の ~/.local/state/unifydrive2nas/copied.sqlite3 を使う(ユーザー判断、2026-10-12)
DAG の正本 このリポジトリの deploy/dagu/unifydrive2nas.yaml。Dagu VM(192.168.10.156)の /var/lib/dagu/dags/ へ手で配置し、Dagu 用リポジトリができたら移す(ユーザー判断、2026-10-12)
リポジトリ Gitea の非公開リポジトリ kazu634/UnifyDrive2NAS。main を push 済み(2026-10-12)
本番開始 配置した日に --init を 1 回だけ実行し、翌朝 04:00 から定期実行を始める(ユーザー判断、2026-10-12)

要件

機能要件

  • Unify Drive 上の画像ファイルを NAS の PhotoLibrary/YYYY/YYYYMMDD/ へコピーする
  • コピー先に YYYY / YYYYMMDD フォルダが無ければ作る

非機能要件

  • Python 3.8.15 で動作し、exifread 以外は標準ライブラリだけを使う
  • 1 ファイルの失敗で全体を止めず、終了時に非 0 を返す
  • コピー元は読み取りだけとし、コピー先は一時ファイルからの rename で確定する
  • コピー元がマウントポイントでない場合または空の場合は処理を開始しない
  • fcntl.flock により同じ DB を使う処理の二重起動を防ぐ

構成

パス 内容
unifydrive2nas.py ファイル列挙、日時判定、コピー、SQLite 記録、安全装置を持つ CLI
vendor/exifread/ exifread 3.5.1 と BSD-2-Clause ライセンス
tests/test_unifydrive2nas.py 最小の画像・動画バイト列を生成して実行する標準ライブラリ unittest
tests/test_deployment.py DAG と PROJECT.md の配置契約を確認する静的テスト
deploy/dagu/unifydrive2nas.yaml Dagu VM へ手動配置する DAG の正本

SQLite の copied_files は、コピー元からの相対パス・サイズ・更新日時(nanosecond)を複合主キーとする。コピー先、日付の出どころ、処理種別、処理日時も記録する。

CLI

既定値は次のとおり。

引数 既定値
--source /volume1/homes/kazu634/UnifyDrive
--destination /volume1/homes/kazu634/Photos/PhotoLibrary
--database ~/.local/state/unifydrive2nas/copied.sqlite3

[!warning] 初回運用 配置した日に python3 unifydrive2nas.py --init を 1 回実行する。--init は現在の対象を DB に登録するだけで、コピーしない。事前確認には --dry-run を付け、初期化が成功する前に DAG を手動実行しない。

通常実行とドライランの例:

python3 unifydrive2nas.py
python3 unifydrive2nas.py --dry-run

配置手順

ローカルから NAS へ直接接続する鍵は無いため、踏み台、Dagu VM、NAS の順に SSH を中継する。以下はリポジトリのルートで実行する。

  1. unifydrive2nas.py と vendor/ だけを NAS へ配置する。tests/ は転送対象に含めない。

    COPYFILE_DISABLE=1 tar --no-xattrs -cf - --exclude='__pycache__' unifydrive2nas.py vendor \
      | ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"mkdir -p /volume1/homes/kazu634/bin/unifydrive2nas && tar -C /volume1/homes/kazu634/bin/unifydrive2nas -xf -\""'
    
  2. NAS 上のスクリプトをドライランし、対象と失敗件数を確認する。

    ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"python3 /volume1/homes/kazu634/bin/unifydrive2nas/unifydrive2nas.py --dry-run\""'
    
  3. ドライランに問題が無ければ、配置した日に NAS 上で初期化を 1 回だけ実行する。

    ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"python3 /volume1/homes/kazu634/bin/unifydrive2nas/unifydrive2nas.py --init\""'
    
  4. DAG の正本を、スケジュール対象外の Dagu VM /tmp へ一時配置する。

    COPYFILE_DISABLE=1 tar --no-xattrs -cf - -C deploy/dagu unifydrive2nas.yaml \
      | ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "tar -C /tmp -xf -"'
    
  5. /tmp の YAML を検証する。

    ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "DAGU_HOME=/var/lib/dagu /usr/local/bin/dagu validate /tmp/unifydrive2nas.yaml"'
    
  6. dagu dry で変数展開後のコマンドを確認する。この操作は実際の SSH コマンドを実行しない。

    ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "DAGU_HOME=/var/lib/dagu /usr/local/bin/dagu dry /tmp/unifydrive2nas.yaml"'
    
  7. 初期化成功後に /tmp の YAML を手動実行し、接続と終了コードを確認する。初期化直後なのでコピーは 0 件となる想定。終了コード 1 または 2 は ssh.run の失敗となり、Dagu の失敗通知に委ねる。

    ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "DAGU_HOME=/var/lib/dagu /usr/local/bin/dagu start /tmp/unifydrive2nas.yaml"'
    
  8. 手動実行まで成功してから、DAG を /var/lib/dagu/dags/ へ配置してスケジュールを有効にする。一時ファイルも削除する。

    COPYFILE_DISABLE=1 tar --no-xattrs -cf - -C deploy/dagu unifydrive2nas.yaml \
      | ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "tar -C /var/lib/dagu/dags -xf - && rm -f /tmp/unifydrive2nas.yaml"'
    

[!danger] 対話シェルでは DAGU_HOME を必ず付ける Dagu サービスは root だけが読める /etc/dagu/dagu.env から DAGU_HOME=/var/lib/dagu を受け取るが、kazu634 の対話シェルには継承されない。付けずに dagu validate / dagu dry / dagu start を実行すると ~/.config/dagu が作られ、サービスとは別の設定・実行履歴を使ってしまう。

[!note] スケジュールと二重起動 Dagu VM は OS と Dagu の両方が Asia/Tokyo に設定済みで、0 4 * * * は JST 04:00 として解釈される。DAG 全体の上限は 7,200 秒。overlap_policy: skip は catchup の重複を抑え、通常の二重起動はスクリプト側の fcntl.flock が最終的に防ぐ。

外部とのつながり

相手 アドレス 用途 根拠
Unify Drive UT2-9EV9 192.168.10.200/24(固定 IP。Web UI http://192.168.10.200:5055) コピー元 ユーザーが DHCP(旧 192.168.10.37)から固定 IP に変更。[実機] 2026-10-10 ping 応答と :5055 の HTTP 302 を確認
Synology ds923p 192.168.10.201(DSM http://192.168.10.201:5000) コピー先 homelab/docs/inventory

[!note] Unify Drive の IP 当初は DHCP で 192.168.10.37 だったが、2026-10-10 にユーザーが 192.168.10.200/24 の固定 IP に変更した。homelab/docs/inventory と homelab/AGENTS の IP 割当に登録済み。

確認済みの事実

コピー元: Unify Drive

[実機] 2026-10-10 踏み台から WebDAV(https://192.168.10.200:5006/、~/.config/unifydrive.netrc で認証)で一覧を取得して確認。件数・容量・日付は PROPFIND の getcontentlength / getlastmodified(ファイルの更新日時)による。

  • デバイス名は UT2-9EV9
  • WebDAV のルート直下は nvme11-admin/・Shared Files/・public/・sharetome/。Web UI の M.211 は nvme11-admin/ にあたる(直下のフォルダ名が一致)。Shared Files・public・sharetome は空
  • nvme11-admin/ 直下は PlugBackup・frankel・iPhone18,1・iPad14,9 と、隠しフォルダ .samba_recycle
フォルダ 何のバックアップか(推定を含む) ファイル数 容量 拡張子 更新日時の範囲
PlugBackup/ Unify Drive に差した SD カードのバックアップ 5,059 211.27 GiB jpg 4,475 / arw 452 / dng 109 ほか 2025-11-08〜2026-10-10
iPhone18,1/iPhone18,1PhotoBackup/all photos/ iPhone のアプリからの写真バックアップ 2,241 15.75 GiB jpg 1,061 / heic 536 / jpeg 436 / mov 120 / png 72 / mp4 2 ほか 2023-10-26〜2026-10-04
iPad14,9/iPad14,9PhotoBackup/all photos/ iPad のアプリからの写真バックアップ 90 0.69 GiB png 82 / jpg 6 / mp4 2 2025-09-23〜2026-09-23
frankel/frankelPhotoBackup/Camera/ 別端末(frankel)の写真バックアップ 4 0.01 GiB jpg 4 2025-12-27〜2026-01-16
.samba_recycle/ SMB のごみ箱(削除済みファイル) 3,889 460.02 GiB arw 3,839 / dng 45 ほか 2025-11-08〜2026-05-17

PlugBackup/ の構成

バックアップ 1 回ごとにフォルダが 1 つできる。フォルダ名の形式は 2 通りある。

形式 例 画像の場所
YYYYMMDDHH + 連番 3 桁(と推定) 2026051623001 <回>/Generic MassStorageClass/<ボリューム名>/DCIM/<100MSDCF 等>/
SDCARD1_YYYYMMDD_HH_連番 SDCARD1_20261010_11_001 <回>/DCIM/100MSDCF/
  • ボリューム名は SDCARD1・AVPRO SD・RICOH GR の 3 種類を確認。カメラは Sony(100MSDCF、DSC*.ARW、PRIVATE/M4ROOT)と RICOH GR IIIx(101RICOH、R*.DNG)と推定
  • 画像以外に、カメラの管理ファイル(AVF_INFO/*.bnp|inp|int、PRIVATE/ 配下、.Spotlight-V100/)とバックアップごとの .txt がある
  • 2026022819001 と 2026050219001 には Synology のサムネイル用フォルダ @eaDir/ が混じっている
  • 2025102520001・2025110304001 には画像が無い(フォルダはあるがファイルは 0 件)
回 画像のあるフォルダ 件数
2025102520001 なし 0
2025110304001 SDCARD1/DCIM/100MSDCF/(空) 0
2025110806001 SDCARD1/DCIM/100MSDCF/ 821
2025112401001 SDCARD1/DCIM/100MSDCF/ 40
2025120719001 RICOH GR/DCIM/101RICOH/ 34
2026011119001 RICOH GR/DCIM/101RICOH/ 45
2026022819001 RICOH GR/DCIM/101RICOH/ 182
2026050219001 SDCARD1/DCIM/100MSDCF/ 56
2026051623001 SDCARD1/DCIM/100MSDCF/ 1,540
2026051723001 AVPRO SD/DCIM/100MSDCF/ 1,482(すべて jpg、51.88 GiB)
SDCARD1_20260913_14_001 DCIM/100MSDCF/ 208
SDCARD1_20261010_11_001 DCIM/100MSDCF/ 600(jpg 300 / arw 300、45.83 GiB)

iPhone / iPad / frankel の構成

  • all photos/ の下は YYYY-MM(月)フォルダに分かれている(iPhone は 2023-10〜2026-10、iPad は 2025-09〜2026-09)
  • iPhone には 1970-01/ フォルダ(2 件)がある。撮影日時が取れなかったファイルと推定
  • iPhone には、拡張子が z + 32 桁の 16 進数になっているファイルがある(少なくとも 4 件)。正体は未確認
  • 写真のほかに動画(mov / mp4)とスクリーンショットらしい png を含む

.samba_recycle/(SMB のごみ箱)

  • PlugBackup/ と同じ構成のフォルダの下に、ARW が 3,839 件、合計 460 GiB 残っている。PlugBackup/ から RAW を消した跡と推定
  • コピー対象から外すかどうかは #未決事項 で決める

WebDAV で取れる日時

[実機] 2026-10-10、PlugBackup/SDCARD1_20261010_11_001/DCIM/100MSDCF/ の 600 件で確認。

  • 取れるのは creationdate と getlastmodified の 2 つで、どちらもカメラで記録した日時と同じ値(例: DSC06493.JPG はどちらも 2026-08-05 11:43:30Z)
  • このバックアップは 2026-10-10 11 時に行われたが、ファイルの日時は 2026-08-05〜2026-10-07。WebDAV の日時からは「Unify Drive に格納された日」は分からない

SMB で取れる日時

[実機] 2026-10-10、smbclient -m SMB3 の allinfo で確認。

  • PlugBackup/SDCARD1_20261010_11_001/DCIM/100MSDCF/DSC06493.JPG は create_time・write_time・change_time がすべて 2026-08-05 20:43:30 JST で、カメラの日時と同じ。access_time だけが 2026-10-10 11:02:11 JST
  • SMB でも「Unify Drive に格納された日」は分からない(access_time は読み出しでも更新されうるので使わない)。新しいファイルの判定は、決定事項どおりコピー済みの一覧で行う

コピー元への到達手段

[実機] 2026-10-10 踏み台 bastion2404 から確認。

ポート 応答 解釈
22 閉じている SSH の標準ポートではない
10000 SSH(rsa / ecdsa / ed25519 のホスト鍵あり)。認証方式は publickey,password 2026-10-10 にユーザーが Web UI の「Remote assist/SSH」で有効化(ポート 10000、「LAN access only」・15 分で自動切断)。UI の注意書きでは管理者アカウントで接続し、root 権限は sudo。また SSH を有効にするとソフトウェアのサポート対象外になる(システムを復元すれば戻せる)。踏み台から kazu634 の鍵認証は Permission denied
21 220 ZFileShare 2.5.4 ready。anonymous は 530 FTP。認証が必要
139 / 445 匿名では Shared Files だけが見える(中身は空)。~/.config/unifydrive.smbauth で認証すると共有 Shared Files・nvme・sharetome・External Device-rescuefs が見える。SMB1 は無効 SMB。共有 nvme のルートが nvme11-admin(= M.211)の中身にあたる(直下に PlugBackup・iPad14,9・iPhone18,1・frankel・.samba_recycle)。External Device-rescuefs は空(容量 0)。[実機] 2026-10-10 確認
111 / 2049 開いている NFS。エクスポートは /tmp/zfsv3/share_fake/share(許可 *)の 1 件だけで、名前からダミーと推定 [要確認]
5005(http)/ 5006(https) 認証なしは 401(Basic realm="Restricted")。~/.config/unifydrive.netrc で認証すると PROPFIND に 207 WebDAV。一覧の取得に成功(#コピー元: Unify Drive)
5055 302 → /home/(openresty) Web UI(My Files)
5056 openresty(http は 400、https は 404) 用途不明

コピー先: Synology NAS

[実機] 2026-10-10 ユーザー提供のスクリーンショット(DSM File Station)で確認。

  • File Station 上のパスは homes > kazu634 > Photos > PhotoLibrary。ファイルシステム上の実パス(/volume1/homes/kazu634/... か)は [要確認]
  • PhotoLibrary には 27 項目あり、見えた範囲は以下
    • 年フォルダ: 2020〜2026
    • その他のフォルダ: 99999999_ios・99999999_misc・99999999_tumblr・iPad・Mobile
    • ファイル: upload2Flickr.sh
  • 年フォルダの中が YYYYMMDD 形式かどうかは、スクリーンショットからは確認できない [要確認]

NAS 側の実行環境

[実機] 2026-10-12、Dagu の VM から ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 で確認。

  • Dagu → NAS の SSH は鍵で通る(ポート 22、ユーザー kazu634、administrators グループ所属)。NAS のホスト鍵は Dagu の known_hosts に登録済み
  • DSM 7.4.1(build 90080)
  • マウントの実パスは /volume1/homes/kazu634/UnifyDrive($HOME は /var/services/homes/kazu634)。種別は cifs、vers=3.02、username=admin、uid=1026(kazu634)・gid=100(users) に固定、file_mode=0777、noexec、nocase
  • マウント経由の stat も、Modify・Change はカメラの日時(DSC06493.JPG は 2026-08-05 20:43:30 JST)。Birth は取れない
  • 使えるもの: python3(3.8.15、標準ライブラリのみで pip も無い)・sqlite3・rsync
  • 使えないもの: exiftool・ffprobe・perl は無い
  • docker の制約: docker は /usr/local/bin/docker にある(非対話 SSH の PATH には入っていない)。ただし /var/run/docker.sock は root:root の 660 で、kazu634 からは使えない。sudo はパスワードを要求する
  • インストール済みのパッケージに Node.js_v20・Node.js_v22・Python2・ContainerManager がある

純 Python での日時の取得(実現可能性の確認)

[実機] 2026-10-12、NAS 上の Python 3.8.15 + exifread 3.5.1(純 Python・依存なし・Requires-Python >=3.7)と、QuickTime の atom を読む自前のコードで確認。調査用のファイルは NAS の /tmp/unify-probe に置き、確認後に削除した。

取れた日時の出どころ(iPhone・iPad・frankel は全件、PlugBackup は 3 フォルダから数件ずつ)

形式 取れたもの 件数
JPG(Sony・RICOH GR・iPhone・iPad・Pixel) EXIF DateTimeOriginal iPhone 940 / iPad 6 / frankel 4 / PlugBackup のサンプルすべて
HEIC EXIF DateTimeOriginal 535
PNG(スクリーンショット) EXIF DateTimeOriginal(eXIf チャンク) iPhone 58 / iPad 82
MOV(iPhone) moov/meta の Apple の作成日時(例 2025-11-08T20:46:34+0900、タイムゾーン付き) 119
MOV・MP4(それ以外) moov/mvhd の作成日時(1904 年起点の UTC 秒を JST に変換) iPhone mov 1・mp4 2 / iPad mp4 2
  • MOV では、Apple の作成日時と mvhd は 1 秒ほどしか違わない(mvhd は書き出し終了時刻の可能性)。Apple の作成日時を優先し、無ければ mvhd を使うのがよい
  • DJI の MP4 は、mvhd を JST にした時刻がファイル名の時刻(DJI_20250518131016)と一致した。ただし、カメラによっては現地時刻を UTC として mvhd に書くものがあり、その場合は 9 時間ずれる(日付が変わりうる)[要確認]

日時が取れなかったもの(すべて iPhone。ファイルの中身を見て、exifread の読み落としではなく日時の情報自体が無いことを確認した)

種類 件数 中身 ファイルの日時(決定事項どおりの代替)
UUID 名の .jpeg(2024-02〜2024-03 等) 436 JFIF のみで EXIF 自体が無い すべて 2024-03-04 15:56:27。取り込んだ日時であって撮影日ではない
IMG_*.JPG 121 EXIF はあるが日時のタグが無い(編集・書き出し後と推定) 個別の日時
IMG_*.PNG 14 中身は JPEG(拡張子と中身が違う)。EXIF はあるが日時のタグが無い 個別の日時
IMG_2542.HEIC 1 EXIF が無い(XMP はあるが日時は無い) 個別の日時
1970-01/IMG_1824.JPG 等 2 12 KB ほどで、中身は JPEG を Base64 にしたテキスト(/9j/4AAQ...)。画像として壊れている 2025-03-23

設計に効く事実

  • 拡張子と中身が一致しないファイルがある(.PNG の中身が JPEG)。exifread は中身で形式を判定するので、拡張子に頼らず読める
  • 先頭が . の隠しファイル .<元の名前>.z<16 進 32 桁> が 14 件ある(例: .IMG_2584.MOV.zb0bad79...、.DSC07166.JPG.z6d2802...)。iPhone からのバックアップの途中のファイルか一時ファイルと推定。コピー対象から外さないと、書きかけのファイルを取り込むおそれがある
  • iPhone・iPad・frankel の全 2,331 件を読むのに約 78 秒(ほぼ iPhone)。日々の実行では新しいファイルだけを読むので、時間は問題にならない見込み

コピーコアの実機検証

[実機] 2026-10-12、NAS の Python 3.8.15 で単体テスト 37 件がすべて成功した。実マウントに対する --dry-run は、対象 6,786 件・失敗 0 件、日付の出どころは exif 6,081 件・apple 119 件・mvhd 14 件・mtime 572 件で、所要時間は約 3 分 47 秒だった。DB には /tmp の使い捨てパスを指定した。

本番デプロイ

[実機] 2026-10-12、NAS と Dagu VM への本番デプロイを実施した。

  • NAS の /volume1/homes/kazu634/bin/unifydrive2nas/ へ unifydrive2nas.py と vendor/ を配置し、ローカルの正本と SHA-256 が一致することを確認した
  • 配置後の --dry-run は終了コード 0、対象 6,786 件、失敗 0 件だった
  • --init は 18:19:51 に開始し、19:41 に終了した(約 82 分)。終了コード 0、対象 6,786 件、新規 6,786 件、失敗 0 件だった
  • DB ~/.local/state/unifydrive2nas/copied.sqlite3 は約 1.8 MB。action=initialized かつ date_source=init の記録が 6,786 行ある
  • Dagu VM では DAGU_HOME=/var/lib/dagu を指定した検証と dry-run がともに終了コード 0 となり、誤った既定パス ~/.config/dagu は作成されなかった
  • /tmp/unifydrive2nas.yaml からの手動実行は 10 秒で Succeeded となった。対象 6,786 件、新規 0 件、コピー 0 件、同一スキップ 0 件、連番 0 件、失敗 0 件だった
  • DAG を /var/lib/dagu/dags/unifydrive2nas.yaml へ配置した。SHA-256 は 4466e9a2…ac63 で正本と一致し、/tmp の一時 YAML は削除済み。毎日 04:00 のスケジュールが有効になっている

[!note] Dagu の SSH 終了時の警告 手動実行のログに SSH session close error err=EOF が 1 件出たが、ステップと DAG はともに Succeeded で終了した。

コピー先の現状

[実機] 2026-10-12 確認。

  • ~/Photos/PhotoLibrary/2026/ には 20260110〜20260517 の YYYYMMDD フォルダが既にある
  • 例として 20260517 には 385 項目があり、JPG と ARW の両方と @eaDir が入っている。これまで RAW も含めて手作業でコピーしていたと推定。今後は RAW を含めない(決定事項)

未決事項

方式を決める前に決める必要がある項目。

  • 純 Python で日付が取れるかの実機確認: 2026-10-12 確認。JPG・HEIC・PNG・MOV・MP4 とも取れる(#純 Python での日時の取得(実現可能性の確認))

  • マウントのプロトコル: SMB(CIFS)。2026-10-10 にユーザーが DSM の File Station「リモートフォルダのマウント」で \\192.168.10.200\nvme を /home/UnifyDrive(File Station 上のパス)にマウントした。Unify Drive 側のアカウントは admin、「起動時に自動的にマウントする」にチェック。DSM のリモートフォルダのマウントは SMB と NFS のみ対応 [公式] https://www.synology.com/en-global/dsm/7.4/software_spec/file_access (取得日: 2026-10-10)。[実機] 2026-10-10 ユーザー提供の File Station のスクリーンショットで、home > UnifyDrive の下に .samba_recycle・frankel・iPad14,9・iPhone18,1・PlugBackup(12 回分のフォルダ)が見えることを確認。実パスは /volume1/homes/kazu634/UnifyDrive(#NAS 側の実行環境)

  • マウントの再起動後の扱い: 「起動時に自動的にマウントする」は有効にした。実際に NAS の再起動後にマウントし直されるかは未確認 [要確認]

  • Dagu → NAS の SSH 鍵: 2026-10-12 に登録・ログインを確認(#NAS 側の実行環境)

  • Unify Drive の IP 固定とインベントリへの登録(2026-10-10、192.168.10.200/24)

課題

  • --init は NAS の btrfs 上で約 82 分かかった。1 ファイルごとの SQLite commit が遅く、実行中のプロセスは D 状態、処理速度は約 1.4 件/秒だった。再実行可能なため現行運用に支障はないが、1 トランザクション化または一定件数ごとの commit が改善候補。改善の実施は未着手・未決定
  • マウントが NAS 再起動後に自動復旧するか確認する(#未決事項)

経緯

  • 2026-10-10: プロジェクト開始。AGENTS.md を tver から流用し、PROJECT.md を作成
  • 2026-10-10: Unify Drive を DHCP(192.168.10.37)から固定 IP 192.168.10.200/24 に変更
  • 2026-10-10: 踏み台から WebDAV でコピー元のディレクトリ構成を調査
  • 2026-10-12: Dagu → NAS の SSH 鍵を登録。NAS 上の純 Python で撮影日時が取れることを確認
  • 2026-10-12: Python のコピーコア、SQLite のコピー済み一覧、安全装置、単体テストを実装
  • 2026-10-12: Apple 作成日時の読み取りを、version/flags の無い QuickTime 形式と FullBox 形式の両方の meta に対応
  • 2026-10-12: 毎日 04:00 に NAS 上のコピー処理を呼ぶ Dagu DAG と手動配置手順を作成
  • 2026-10-12: NAS へコピー処理を配置し、6,786 件を初期化。Dagu の検証・手動実行後、毎日 04:00 のスケジュールを有効化
  • 2026-10-12: Gitea の非公開リポジトリ kazu634/UnifyDrive2NAS へ main を push

確かめ方

ローカルの単体テスト:

python3 -m unittest discover -s tests -v

DAG と配置手順の静的契約テスト:

python3 -m unittest tests.test_deployment -v

CLI と引数の確認:

python3 unifydrive2nas.py --help

NAS の本番配置には tests/ を置かない。NAS で単体テストを再実行するときは、unifydrive2nas.py・vendor/・tests/ を /tmp/unifydrive2nas-test へ一時展開して実行し、終了後に削除する。その後、本番配置したスクリプトのドライランで対象と日付の出どころを確認する。

COPYFILE_DISABLE=1 tar --no-xattrs -cf - --exclude='__pycache__' unifydrive2nas.py vendor tests \
  | ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"rm -rf /tmp/unifydrive2nas-test && mkdir -p /tmp/unifydrive2nas-test && tar -C /tmp/unifydrive2nas-test -xf -\""'
ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"cd /tmp/unifydrive2nas-test && python3 -W error::ResourceWarning -m unittest discover -s tests -v\""'
ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"rm -rf /tmp/unifydrive2nas-test\""'
ssh bastion 'ssh -p 10022 kazu634@192.168.10.156 "ssh -i ~/.ssh/id_ed25519_nas kazu634@192.168.10.201 \"python3 /volume1/homes/kazu634/bin/unifydrive2nas/unifydrive2nas.py --dry-run\""'

Dagu VM では、スケジュールを有効にする前に /tmp の YAML を Dagu 2.13.0 の validator と dry-run で確認する。

DAGU_HOME=/var/lib/dagu /usr/local/bin/dagu validate /tmp/unifydrive2nas.yaml
DAGU_HOME=/var/lib/dagu /usr/local/bin/dagu dry /tmp/unifydrive2nas.yaml

本番デプロイ後は、次の状態を確認済み([実機] 2026-10-12)。

確認対象 結果
NAS 配置物 ローカルの正本と SHA-256 が一致
本番 DB initialized / init が 6,786 行
Dagu の手動実行 Succeeded、新規 0 件、失敗 0 件
DAG 配置物 正本と SHA-256 が一致し、一時 YAML は削除済み
定期実行 毎日 04:00(Asia/Tokyo)が有効