前回の記事で、Synology NASにClaude Codeを常駐させました。24時間、NASの中でAIが待機している状態です。
こうなると、人間は良からぬことを考えます。
「じゃあ、何を作らせようか」
このブログもPOSレジも野鳥のシューティングゲームも、全部NASの上で動かしてきました。今回もNAS内AIで、動くものを作りたい。そう思って身の回りを見回したときに、目に留まったのが——Synology純正の「USB Copy」でした。
結論から言います。本家より使いやすいものが、できてしまいました。
純正USB Copyは便利。でも、ひとつだけ困る
まず名誉のために言っておくと、Synology USB Copyは名パッケージです。
野鳥撮影から帰ってきて、疲れ切った体でMacを起動する気力がないとき。カードリーダーをNASに挿すだけで勝手に取り込んでくれるあの機能は、PCを介さずにバックアップできるという一点で本当に価値があります。撮影後の「とりあえずカードから逃がしておく」が儀式ではなく自動になるのは、精神衛生上とても大きい。
でも、ひとつだけ。ひとつだけ、どうしても困ることがあります。
メモリカードをフォーマットすると、登録していたタスクが呼び出せなくなる。
USB Copy 2.0のタスクは、デバイスの登録情報に紐づいています。カードをフォーマットすると別デバイス扱いになり、せっかく作ったタスクが適用されなくなるわけです。
……これ、野鳥界隈のかたなら、たぶん全員うなずいてくれると思います。カードのフォーマットって、大事ですよね。
削除ではなくカメラ本体でフォーマット。カードの性能維持のため、書き込みエラーを防ぐため、僕らは撮影のたびにこれをやります。連写でカードを酷使する野鳥撮影では、ほとんど作法に近い。
つまり僕にとって、純正USB Copyは「カードをフォーマットするたびに設定し直すツール」でした。せっかくの自動化なのに、そこだけ手動。この矛盾に、僕は数年間モヤモヤしていたわけです。
Kay’s Voice: 「まあ、そういうものだし」と諦めていました。でもNASの中にAIが常駐した今、「そういうもの」を疑っていい環境が手元にあるんですよね。これが本当に大きい変化でした。
その前に:僕のLightroom取り込みフローの話
自作の話に入る前に、大前提として「僕が写真をどう管理しているか」を書かせてください。ここが分からないと、なぜあんなスクリプトになったのかが伝わらないので。
僕はLightroom Classicのカタログ1つに、約20万枚のRAWを突っ込んで管理しています。
面白いのはここからで、取り込み先は状況によって毎回変えています。
- 出先で急いで編集したいとき:Mac本体のSSDに読み込む
- 旅行中など枚数が多いとき:外付けSSDに読み込む
- 自宅にいるとき:NASに直接読み込む
- 出先でも枚数が少なければ:Tailscale経由でNASに直接放り込むことも
バラバラに見えますが、ゴールはひとつです。最終的には、すべてNASに集約する。
Mac本体や外付けSSDはあくまで一時的な避難場所で、編集が落ち着いたらLightroomの「フォルダを移動」でNASに寄せます。そしてTailscaleを入れてあるので、外出先からでもNAS上のRAWを開いて編集できる。母艦がひとつに定まっていると、この身軽さが手に入ります。
📎 あわせて読みたい: SynologyとTailscaleで外出先からLightroom編集する話
で、その母艦側のフォルダ構造がこれです。
/volume1/homes/Kay/
└── Creatives/ ← クリエイティブ系のファイル置き場
└── Lightroom/
├── 2025/
│ ├── 2025-11-03/
│ └── 2025-11-14/
└── 2026/
├── 2026-07-19/
└── 2026-07-21/
Creatives/Lightroom/YYYY/YYYY-MM-DD。撮影日ベースの、極めて素直な構造です。何年やっても飽きない、そして壊れない構造だと思っています。
🔍 Kay’s Check: このルールで統一しておくと、Lightroom側は「フォルダを同期」を押すだけで新しい日付フォルダがカタログに載ります。取り込みダイアログすら開かなくていい。フォルダ構造の一貫性は、そのまま作業時間になります。
そして今回の自作USB Copyは、このルールをそのままスクリプトにしただけです。ここ、大事なところなので後でもう一度触れます。
設計方針:デバイスIDをやめて「DCIMがあるか」で見る
さて、自作です。iPhoneのClaudeアプリから、NAS上のセッションに話しかけながら設計を詰めていきました。
いちばんの争点はもちろん、フォーマット問題をどう回避するか。
答えはシンプルでした。デバイスIDを見るのをやめる。
純正がカードを「どのデバイスか」で識別するから、フォーマットで別人になってしまう。ならば、「マウントされたUSBの中にDCIMフォルダがあるか」を見るだけにすることにしました。
カメラのメモリカードには、必ずDCIMフォルダがあります。規格で決まっています。つまり——
- フォーマット直後のカードでもOK
- 買ったばかりの新品カードでもOK
- 事前登録は一切不要
問題を回避したのではなく、問題そのものを消しました。 ここが今回いちばん気持ちよかった設計です。
そのうえで、以下を条件にしました。
- 取り込み先は
Creatives/Lightroom/YYYY/YYYY-MM-DD(撮影日=ファイルのmtime基準) - 冪等(べきとう)にする:同名かつ同サイズのファイルはスキップ。何度実行しても結果が変わらない
- サイズ不一致は「壊れファイル」とみなして再コピー:転送が途中で切れた中途半端なファイルを自動修復する
- 新規コピーが0件なら完全に無音・無動作:1分毎に動くタスクなので、空回りのたびにビープが鳴ったら家庭が崩壊します
- 1件でも失敗したら、完了ビープも自動排出もしない:音が鳴らない=何かあった、のサイン。カードは挿さったまま保持され、あとで調査できる
最後の1つ、地味ですが実は最重要です。バックアップツールで最悪なのは「失敗したのに成功したように見える」ことなので。
トリガーは2系統:自動でも、チャットでも
ここが本家との差別化ポイントです。純正USB Copyは「挿したら自動」。それはそれで完璧なので、それは残したまま、チャットからも呼べるようにしました。
系統1:挿すだけで自動実行
DSMのタスクスケジューラに1分毎に走るタスクを登録し、本体スクリプトを呼びます。カードを挿すと、最大1分以内に検知して動き出します。
系統2:iPhoneから「USB Copyして」
こちらが新機軸です。iPhoneのClaudeアプリのCodeタブから、NAS常駐のセッションにこう話しかけます。
「USB Copyして」
すると、こう動きます。
- Claudeが
bridgeフォルダに「依頼ファイル」を作る - NAS側で1分毎に走っている監視タスクが、そのファイルを検知して削除し、本体スクリプトを実行
- Claudeが実行ログを読んで、「◯件コピーしました」と件数を報告してくる
なぜこんな回りくどいことをするのか。理由があります。
コンテナの中にいるClaudeは、NAS本体のUSBデバイスやDSMコマンドに直接触れないからです。 前回の記事で「見せる範囲はマウントで制御する」と書きましたが、その安全性の裏返しでもあります。
そこで、両者が共有できるフォルダを1つだけ用意して、そこに「依頼ファイル」を置く。NAS側は定期的にそこを覗きに行く。手紙をポストに入れて、郵便屋さんが回収しに来る方式です。
🔍 Kay’s Check: この「依頼ファイル方式の橋渡し」、USB Copyに限らずコンテナ⇔NAS本体連携の汎用パターンになります。コンテナの安全性を保ったまま、DSM側の操作を頼める。今後いろいろ応用できそうで、個人的にはUSB Copy本体より収穫でした。
なお、この手順書はコンテナ内のグローバルCLAUDE.md(/root/.claude/CLAUDE.md)に書いています。プロジェクトごとのCLAUDE.mdだと、そのセッションでしか通じないので。
これに気づくまで、僕は業務システム開発用のセッションに向かって「USB Copyして」と話しかけ続けていました。当然、無反応。 AIが壊れたかと思いましたが、壊れていたのは僕の理解でした。
完成スクリプト全文
長々と語りましたが、実体はこれだけです。
bash
#!/bin/bash
DEST=/volume1/homes/Kay/Creatives/Lightroom
LOG=/volume1/docker/claude/bridge/usb-eject.log
BEEP() { local n=$1; while [ $n -gt 0 ]; do echo 2 > /dev/ttyS1; sleep 1; n=$((n-1)); done; }
COPIED=0
FAILED=0
for MOUNT in /volumeUSB*/usbshare*; do
[ -d "$MOUNT/DCIM" ] || continue
TODO=()
while IFS= read -r f; do
d=$(date -r "$f" +%Y/%Y-%m-%d)
t="$DEST/$d/$(basename "$f")"
if [ ! -e "$t" ] || [ "$(stat -c%s "$f" 2>/dev/null)" != "$(stat -c%s "$t" 2>/dev/null)" ]; then
TODO+=("$f")
fi
done < <(find "$MOUNT/DCIM" -type f \
\( -iname '*.nef' -o -iname '*.jpg' -o -iname '*.jpeg' \
-o -iname '*.heif' -o -iname '*.hif' \
-o -iname '*.mov' -o -iname '*.mp4' \) )
[ ${#TODO[@]} -eq 0 ] && continue
BEEP 1
for f in "${TODO[@]}"; do
d=$(date -r "$f" +%Y/%Y-%m-%d)
mkdir -p "$DEST/$d"
if cp -p "$f" "$DEST/$d/"; then COPIED=$((COPIED+1)); else FAILED=$((FAILED+1)); fi
done
if [ "$FAILED" -eq 0 ]; then
BEEP 2
sync
BASE=$(grep -m1 " $MOUNT " /proc/mounts | awk '{print $1}' | sed 's|/dev/||; s|p[0-9]*$||')
/usr/syno/bin/synowebapi --exec api=SYNO.Core.ExternalDevice.Storage.USB \
method=eject version=1 dev_id="$BASE" >> "$LOG" 2>&1
fi
done
echo "copied: $COPIED files, failed: $FAILED"
対応拡張子にニコンの.nefを入れてあるのは、まあ、そういうことです。.hif(HEIF)も入れてあります。
登録するタスクは2つ、いずれもユーザーはroot。
- 自動検知タスク:1分毎に
usb-import.shを実行 - bridge監視タスク:1分毎、依頼ファイルがあれば削除して
usb-import.shをログ付きで実行
⚠️ ビープ音の仕組みと、絶対に間違えてはいけない数字
完了通知は、NAS内蔵ブザーの「ピッ」にしました。仕組みはこれです。
bash
echo 2 > /dev/ttyS1
/dev/ttyS1に2を書き込むと、内蔵ブザーが短く鳴ります。実にシンプル。
……ただし、ここで重大な警告があります。
🔍 Kay’s Check: 同じ場所に
1を書き込むと、NASが即座に電源断します。 テストのために手打ちするときは、絶対に、絶対に数字を間違えないでください。稼働中のNASが無言で落ちます。僕は指が震えました。
なぜこの通知方式になったかというと、当初はDSMの通知機能を使おうとしてsynodsmnotifyを叩いたところ、neither mail string key nor i18n formatというエラーで弾かれたためです。DSM 7では自由文のタイトルが使えなくなっていました。潔く諦めて、ビープ+チャット報告に一本化しています。
自動排出の迷宮:5段階のハマりポイント
さて、ここからが本記事の白眉です。
やりたいことは、たった一言でした。「コピーが終わったらカードを安全に外して」
……これに、丸一晩かかりました。
第1段階:素のumountでは外れない
まず普通にumount。表側のマウントは外れます。でもDSM上のUSBアイコンは残ったまま。
原因は、Container Managerが動いているNASでは、USB共有が/volume1/@appdata/ContainerManager/all_shares/配下にミラーマウントされていること。表を外しても、裏でデバイスを掴んだままだったわけです。
Dockerを入れているNASならではの罠でした。前回の記事の自分に恨み言を言いたい。
第2段階:synousbdiskは形骸化していた
「DSM専用コマンドがあるはずだ」と探して見つけたsynousbdisk。
-umountはsucceededを返すのにUI表示は残る(つまり嘘)-rmtabentryは引数を受け付けない- そもそも
/usr/syno/etc/usbtabが存在しない -enumusbdisksの結果も空
DSM 7ではもうレガシーでした。ネットに残っている解説記事は、たいていDSM 6時代のものです。
第3段階:カーネルレベルで切り離す
bash
echo 1 > /sys/block/usbX/device/delete
これでカーネルレベルの切り離しは成功。USBアイコンは消えました。
「勝った」
……と思ったら、File Stationに**usbshare1のフォルダだけが残っている**。しかも開こうとすると中身にアクセスできない。デバイスは切り離されたのに、共有フォルダの「抜け殻」だけが居座っている状態です。
第4段階:共有フォルダ削除は拒否される
じゃあ共有フォルダを消せばいい、とsynoshare --del。
返ってきたのはエラー0xB500。usbshareは通常の共有フォルダとは別扱いで、削除を拒否されます。
ここで完全に手詰まりになりました。深夜2時、5つのコマンドを積み上げても消えない抜け殻フォルダを眺めながら、「そもそもDSMのUIにある『取り出し』ボタンは、何をやってるんだ?」と思ったのが転機でした。
第5段階:正解はWebAPIだった
DSMのUIは、内部的にWebAPIを叩いています。そしてそのAPIは、CLIから直接呼べます。
bash
synowebapi --exec api=SYNO.Core.ExternalDevice.Storage.USB \
method=eject version=1 dev_id="usb3"
これ一撃でした。
アンマウント、ミラー解除、USBアイコン、usbshare表示——全部まとめて消えました。
積み上げた5段階の処理を全部捨てて、この1行に置き換えたのが最終形です。あの一晩は何だったのか。いや、遠回りしたからこそ、この教訓が身に沁みました。
🔍 Kay’s Check: DSMの内部コマンドで詰まったら、UIが呼んでいるWebAPI(
synowebapi)を疑え。 UIでできることは、必ずAPIとして存在します。これはUSB Copyに限らず、DSMをいじる全ての場面で使える汎用の教訓です。今回いちばんの収穫かもしれません。
その他のハマりポイント
迷宮以外にも、細かい落とし穴がいくつも。
1. タスクスケジューラの罠・第4弾
「1分毎」に設定したのに、自動では一切動かない。手動実行なら成功する。この症状の犯人は——「最終実行時間」がデフォルトの00:59のままだったことでした。
つまり毎日0時台の1時間しか動かないタスクになっていたわけです。23:59に修正して解決。繰り返し間隔と最終実行時間は必ずセットで確認してください。
2. bridgeマウント漏れで「偽フォルダ」が生まれる
compose.yamlにbridgeフォルダのマウントを追記し忘れていたため、Claudeがコンテナ内部に同名フォルダを勝手に新規作成していました。
依頼ファイルは正常に作られる。ログにも成功と出る。でもNAS側には永遠に届かない。 一見うまく動いているように見えるのが、いちばん厄介なタイプのバグでした。
3. CLAUDE.mdは追記しただけでは反映されない
CLAUDE.mdの読み込みはセッション開始時です。追記したら/clearしないと効きません。
完成した体験
長かった。では、最終形の使い心地をどうぞ。
カードリーダーを挿す ↓ ピッ(取り込み開始) ↓ 待つ(コーヒーを淹れる) ↓ ピッピッ(完了) ↓USBアイコンも共有フォルダ表示も自動で消滅 ↓ そのまま抜くだけ
事前登録、不要。フォーマット後のカードも、買ったばかりの新品カードも、挿せば動きます。DSMの画面を開く必要すらありません。
そして、iPhoneのClaudeに「USB Copyして」と話しかければ、同じ動作+件数報告つき。
Lightroom側は「フォルダを同期」を押すだけで、2026-07-21が新しいフォルダとしてカタログに載ります。
……本家、超えちゃいました。
Kay’s Voice: いや、純正USB Copyの方が機能は圧倒的に多いです。それは分かっています。でも、「僕の運用にだけ完璧にフィットする」という一点において、これに勝てるものはないんですよね。既製品を諦めて自作する理由って、たぶん全部そこに集約される気がします。
応用:メーカーごと・カメラごとに仕分けることもできる
ここまで読んで、勘のいいかたは気づいたかもしれません。このスクリプト、ファイルの拡張子を見て対象を拾っています(.nefとか.jpgとか)。ということは——拡張子でメーカーを見分けて、保存先を変えることもできるわけです。
RAWの拡張子は、メーカーごとにきっちり違います。
- ニコン:
.nef - ソニー:
.arw - キヤノン:
.cr3(新しめの機種。少し前の機種は.cr2) - 富士フイルム:
.raf - パナソニック:
.rw2/OMデジタル:.orf
つまり、こう頼めるということです。
「
.nefはCreatives/Nikon/へ、.arwはCreatives/Sony/へ、拡張子ごとに保存先を分けて」
複数マウントのシステムを持っていて機材もメーカー混在、というかたには効くと思います。カードを挿すだけで、メーカーごとに勝手に整理されるわけです。
さらにもう一段。多くのカメラは、ファイル名のプレフィックスを撮影者側で決められます。ニコンならDSC_を任意の3文字に変えられますし、機体ごとにBIRDとかWORKとか振っておけば——
「ファイル名が
BIRDで始まるものは野鳥/へ、WORKで始まるものは仕事/へ」
という用途別の自動仕分けまで到達します。カメラ2台持ちで「1台は野鳥専用、1台は仕事用」みたいな運用をしているかたなら、挿すだけで用途フォルダに散っていく世界が作れます。
🔍 Kay’s Check: ポイントは、判定に使う情報が最初からカードに書き込まれていることです。拡張子はメーカーが、ファイル名はカメラの設定が、撮影の瞬間に刻んでいる。こちらは何も登録しなくていい。DCIM検知でフォーマット問題を消したのと、まったく同じ発想の延長線上にあります。
僕は今のところニコン一台なので.nefだけで足りていますが、「機材が増えたらこう育てればいい」という道が最初から見えているのは、自作ならではの安心感です。
皆さんも、自分の保存ルールをAIに話してみてください
ここまで僕のフォルダ構造の話を延々としてきたのには、理由があります。
このスクリプトの本質は、僕の保存ルールを言葉にして、AIに渡しただけだからです。
Creatives/Lightroom/YYYY/YYYY-MM-DDという僕のルールは、皆さんには何の意味もありません。でも、皆さんにも皆さんのルールがあるはずです。カメラ本体ごとに分けている人、案件ごとに分けている人、月単位でざっくり派の人。
AIチャットの本当に便利なところは、自然言語でそれを伝えられることです。
「このメモリカードの写真を、撮影日を見て
Photos/2026/07月/の形式のフォルダに振り分けてコピーして。動画だけはVideosに分けて。同じファイルがあったらスキップして」
これで通じます。シェルスクリプトの文法も、findのオプションも、dateの書式指定も覚える必要がありません。自分の頭の中にある「いつものやり方」を、そのまま日本語で伝えるだけ。
これまで自動化は「スクリプトが書ける人の特権」でした。それが、「自分のルールを説明できる人の権利」に変わった。この変化、本当に大きいと思っています。
まとめ
- 純正USB Copyの弱点は、タスクがデバイス登録に紐づくこと。カードをフォーマットすると呼び出せなくなる
- 解決策は、デバイスIDではなく「DCIMフォルダの有無」で検知すること。フォーマット問題を根本から消せる
- 保存先は自分のLightroom運用ルール(
Creatives/Lightroom/YYYY/YYYY-MM-DD)をそのままスクリプト化 - トリガーは自動検知(1分毎タスク)とチャット指示(bridge経由の依頼ファイル方式)の2系統
- 冪等設計+サイズ不一致の自動修復+失敗時は完了ビープも排出もしない、で安全性を確保
- 自動排出の正解は**
synowebapiのeject API**。DSMの内部コマンドで詰まったら、UIが呼んでいるAPIを疑う echo 1 > /dev/ttyS1は電源断。絶対に間違えないこと
次にやりたいこと
すでに拡張候補がいくつも浮かんでいます。
- EXIF撮影日時での厳密な振り分け(コンテナにexiftoolは導入済み)
- 取り込み後のClaudeによる自動レポート(枚数・日付範囲・使用容量)
- 動画と静止画の自動振り分け
そして何より面白いのは、このスクリプトの改造自体を、NASに常駐しているClaudeに頼めるということです。作ったものが、作った環境の中で自分を育てていく。この自己増殖的な感じ、たまりません。
野鳥のカードをフォーマットするたびに小さくため息をついていた皆さん。あのため息、もう不要です。
そして、まだ自作していない皆さんへ。 難しいことは何もありません。必要なのは、自分がいつもやっている手順を、言葉にして説明することだけです。あなたの「いつものやり方」は、もう十分に仕様書として通用します。


コメント