24 KiB
title, tags, status
| title | tags | status | ||
|---|---|---|---|---|
| UnifyDrive2NAS の現状 |
|
コピーコア実装済み |
UnifyDrive2NAS の現状
[!info] このファイルの位置づけ UnifyDrive2NAS プロジェクトの現状(決定事項・要件・構成・課題)を積み上げていく。運用規約は AGENTS、ホームラボ基盤は homelab/AGENTS を参照。
今の状態
- Python 3.8 の標準ライブラリと同梱した
exifread3.5.1 だけで動くコピーコアを実装済み - コピー済み一覧、初期化、ドライラン、二重起動防止、マウント確認を含む
- NAS への配置と Dagu の DAG 定義は未実施
目的
ホームラボ内の 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 で変更できる |
要件
機能要件
- 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 |
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を付ける。
通常実行とドライランの例:
python3 unifydrive2nas.py
python3 unifydrive2nas.py --dry-run
外部とのつながり
| 相手 | アドレス | 用途 | 根拠 |
|---|---|---|---|
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 確認。
~/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)
課題
- NAS への配置と Dagu の DAG 定義を行う
- マウントが NAS 再起動後に自動復旧するか確認する(#未決事項)
経緯
- 2026-10-10: プロジェクト開始。
AGENTS.mdを tver から流用し、PROJECT.mdを作成 - 2026-10-10: Unify Drive を DHCP(
192.168.10.37)から固定 IP192.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に対応
確かめ方
ローカルの単体テスト:
python3 -m unittest discover -s tests -v
CLI と引数の確認:
python3 unifydrive2nas.py --help
NAS では配置先で同じ単体テストを実行した後、実マウントを使うドライランで対象と日付の出どころを確認する。
python3 -m unittest discover -s tests -v
python3 unifydrive2nas.py --dry-run