--- title: UnifyDrive2NAS の現状 tags: - homelab - UnifyDrive2NAS status: 本番運用開始済み --- # 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|Dagu]]: Step Type は `shell` / `ssh.run` のみ、スクリプト実体はリモートホスト側の固定パス)に従う | | 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](https://gitea.kazu634.com/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 を手動実行しない。 通常実行とドライランの例: ```bash python3 unifydrive2nas.py python3 unifydrive2nas.py --dry-run ``` ### 配置手順 ローカルから NAS へ直接接続する鍵は無いため、踏み台、Dagu VM、NAS の順に SSH を中継する。以下はリポジトリのルートで実行する。 1. `unifydrive2nas.py` と `vendor/` だけを NAS へ配置する。`tests/` は転送対象に含めない。 ```bash 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 上のスクリプトをドライランし、対象と失敗件数を確認する。 ```bash 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 回だけ実行する。 ```bash 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` へ一時配置する。 ```bash 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 を検証する。 ```bash 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 コマンドを実行しない。 ```bash 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 の失敗通知に委ねる。 ```bash 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/` へ配置してスケジュールを有効にする。一時ファイルも削除する。 ```bash 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 を含めない(決定事項) ## 未決事項 方式を決める前に決める必要がある項目。 - [x] **純 Python で日付が取れるかの実機確認**: 2026-10-12 確認。JPG・HEIC・PNG・MOV・MP4 とも取れる([[#純 Python での日時の取得(実現可能性の確認)]]) - [x] **マウントのプロトコル**: 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 の再起動後にマウントし直されるかは未確認 [要確認] - [x] **Dagu → NAS の SSH 鍵**: 2026-10-12 に登録・ログインを確認([[#NAS 側の実行環境]]) - [x] **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](https://gitea.kazu634.com/kazu634/UnifyDrive2NAS) へ `main` を push ## 確かめ方 ローカルの単体テスト: ```bash python3 -m unittest discover -s tests -v ``` DAG と配置手順の静的契約テスト: ```bash python3 -m unittest tests.test_deployment -v ``` CLI と引数の確認: ```bash python3 unifydrive2nas.py --help ``` NAS の本番配置には `tests/` を置かない。NAS で単体テストを再実行するときは、`unifydrive2nas.py`・`vendor/`・`tests/` を `/tmp/unifydrive2nas-test` へ一時展開して実行し、終了後に削除する。その後、本番配置したスクリプトのドライランで対象と日付の出どころを確認する。 ```bash 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 で確認する。 ```bash 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)が有効 |