買ってみた・試してみた 読む時間 約10分

ラズパイで iPhone をバックアップ:USB に挿して丸ごと取り、NAS に送る(libimobiledevice)

ラズパイで iPhone をバックアップ:USB に挿して丸ごと取り、NAS に送る(libimobiledevice)

前の記事では、Windows の PC で iPhone をバックアップし、毎晩 NAS にコピーするようにしました。ただ、PC で取るには PC を起動してアプリを開く必要があります。

そこで今回は、家で動かしっぱなしのラズパイ(Raspberry Pi 3 Model B)に iPhone を USB で接続して、バックアップが取れるかを試しました。結果は、PC と同じ丸ごとのバックアップが 29 分で取れました。この記事では、その手順と、ラズパイならではのつまずきをまとめます。

この記事のポイント

  • ラズパイに libimobiledevice(iPhone とやり取りする Linux のツール)をインストール
  • iPhone を USB で接続して「信頼」し、SD カードに丸ごとバックアップ(8.5GB・約 35,000 ファイル・29 分)
  • そのバックアップを NAS に送る(29 分・ファイルの数が一致するのを確認)
  • 挿すだけで、声でパスコードを頼み、バックアップから NAS への送りまで済むようにした(2 回目からは 5 分ほど:バックアップ 2 分半・NAS 2 分)

使ったのは Debian 13(trixie)の Raspberry Pi OS、libimobiledevice 1.3.0 です。

1. ツールをインストールする

sudo apt update
sudo apt install libimobiledevice-utils usbmuxd

usbmuxd は、USB で接続した iPhone とやり取りするための常駐のプログラムです。libimobiledevice-utils に、バックアップの idevicebackup2 や、ペアリングの idevicepair が含まれています。

2. iPhone を接続して信頼する

idevicepair pair

iPhone に「このコンピュータを信頼しますか?」と出るので「信頼」を押し、パスコードを入力します。そのあと、もう一度 idevicepair pair を打つと「SUCCESS」と出ます。

つまずいた所:接続しても iPhone に何も出ない

最初は、接続しても iPhone に「信頼」の画面が出ませんでした。ラズパイの記録(dmesg)を見ると、USB のエラー(error -32)が出ていました。同じケーブルのまま、ラズパイの別の USB の差し口に挿し直すと、すぐに「信頼しますか?」が出ました。差し口との相性か、挿し方が浅かったようです。

3. バックアップを取る

mkdir -p ~/iphone-backup
idevicebackup2 backup --full ~/iphone-backup

始めると、iPhone にパスコードの入力を求められます。入力するまで先に進みません。これは Windows のときと同じです。

わたしは Windows のアプリで「ローカルバックアップを暗号化」をオンにしてあったので、ラズパイで取ったバックアップも暗号化されました(記録に「Backup will be encrypted.」と出ます)。暗号化の設定は iPhone の側に残るので、ラズパイでパスワードを入れ直す必要はありません。

Windows を使っていないときは、1 回目の前にラズパイで暗号化をオンにします。暗号化しないと、保存したパスワードやヘルスケアのデータがバックアップに残りません。

idevicebackup2 encryption on

結果

  • かかった時間:29 分(PC のときとほぼ同じ)
  • 大きさ:8.5GB・35,340 ファイル
  • 最後に「Backup Successful.」と出れば完了

2 回目からは –full を付けずに打つと、変更があった分だけを取ります。

つまずいた所:ラズパイが熱くなり、電気も足りなくなる

バックアップの途中で、ラズパイの監視(負荷が上がると声で知らせる仕組み)が「重くなっています」としゃべりました。調べると、毎日の DB のバックアップと重なって負荷が 5.7(4 コアなので少し詰まっている程度)、温度は 79.5℃ まで上がっていました。

さらに、電圧の記録(vcgencmd get_throttled)に、電圧が足りなくなったことがある記録が残っていました。iPhone をラズパイの USB で充電しながらバックアップしたので、電気が足りなくなったのだと思います。ラズパイは熱や電圧が危ないと自分で速度を落とすので、壊れはしませんが、時間がのびます。

バックアップ中のラズパイの負荷 5.73・温度 79.5 度・電圧の記録 0x70000
バックアップ中に見た、ラズパイの負荷・温度・電圧の記録

次からの対策

  • iPhone は、ある程度充電してから接続する
  • できれば電源つきの USB ハブを通す
  • ほかの重い処理(DB のバックアップなど)と時間をずらす

4. NAS に送る

SD カードだけだと、SD カードが壊れたときに一緒になくなるので、NAS にも送りました。ファイルが 35,000 個あるので、1 個ずつ送るより、tar で 1 本にまとめて ssh で流すほうが速く済みます。

前もって、ラズパイから NAS に ssh の鍵(パスワードなし)でログインできるようにし、~/.ssh/config に NAS の名前を付けておきます。NAS には tar と rsync が要ります(5 で rsync を使います)。

tar -C ~ -cf - iphone-backup | ssh <NAS の名前> "mkdir -p <NAS の置き場所> && tar -C <NAS の置き場所> -xf -"

29 分で送り終え、SD カードと NAS のファイルの数がどちらも 35,338 件で一致しました(「ファイル」だけを数えた数)。

5. 挿すだけで取れるようにする

毎回コマンドを打つのは手間なので、iPhone をラズパイに挿すだけで、バックアップから NAS への送りまで済むようにしました。

決まった時間に自動で取る形にしなかったのは、バックアップを始めるたびに iPhone にパスコードの入力が出るからです。夜中に始めても、パスコードを入力するまで止まったままになります。そこで「挿したら始めて、声でパスコードを頼む」形にしました。

動きの流れ

  1. cron で 1 分ごとに、iPhone が接続されているかを見る(idevice_id -l)
  2. 接続されていたら、声で「パスコードを入れてください」と知らせる
  3. iPhone でパスコードを入力すると、変更があった分だけバックアップする
  4. 完了したら、変更があった分だけ NAS に送り(rsync)、声で「完了しました」と知らせる
  5. 失敗したときは、声と LINE で知らせる(LINE は notify.py を使うとき)。挿したままなら次は始めず、一度抜いて挿し直すと取り直す

1 日 1 回までにしてあるので、充電のためだけに挿しても、2 回目は取りません。信頼していない iPhone を挿したときも何もしません。パスコードを入力せずに 2 時間たつと、止めて失敗にします(それでも止まらないときは、その 1 分後に強く止めます)。

実際の声を聞いてみる

今回のために VOICEVOX で作った 3 つの声です。ラズパイのスピーカーから流れているのと同じファイルです。▶ を押すと再生します(音が出ます)。

挿したとき:「アイフォンのバックアップを始めます。アイフォンに、パスコードを入れてください。」

完了したとき:「アイフォンのバックアップが、完了しました。」

失敗したとき:「アイフォンのバックアップが、失敗しました。調べてください。」

音声:VOICEVOX:里石ユカ(つぼみ)

声は、PC で VOICEVOX を起動しておき、次のスクリプトで作りました(Node.js 18 以上)。できた WAV をラズパイの ~/notify/sounds/(下の、声だけの say なら ~/sounds/)に置きます。話者の番号(126)は VOICEVOX のバージョンで変わることがあるので、http://127.0.0.1:50021/speakers で確認してください。

import { writeFileSync } from 'node:fs';
const S = 126;
const V = {
  'iphone_start.wav': 'アイフォンのバックアップを始めます。アイフォンに、パスコードを入れてください。',
  'iphone_done.wav': 'アイフォンのバックアップが、完了しました。',
  'iphone_fail.wav': 'アイフォンのバックアップが、失敗しました。調べてください。',
};
for (const [f, t] of Object.entries(V)) {
  const q = await (await fetch(`http://127.0.0.1:50021/audio_query?speaker=${S}&text=${encodeURIComponent(t)}`, { method: 'POST' })).json();
  const w = Buffer.from(await (await fetch(`http://127.0.0.1:50021/synthesis?speaker=${S}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(q) })).arrayBuffer());
  writeFileSync(f, w); console.log(f, w.length);
}
scp iphone_*.wav <ラズパイの名前>:~/notify/sounds/

声が鳴らないときは、ラズパイで aplay ~/notify/sounds/iphone_start.wav を打って、スピーカーから出るかを確認します(cron から鳴らすには、ユーザーが audio のグループに属している必要があります)。

cron には 1 行足すだけです。root の権限は使わず、ふだんのユーザーで動きます。

* * * * * /bin/bash /home/<ユーザー名>/bin/iphone_backup.sh >/dev/null 2>&1

スクリプトの全文は次のとおりです。mkdir -p ~/bin でフォルダーを作り、~/bin/iphone_backup.sh に置いて、chmod 755 ~/bin/iphone_backup.sh で動かせるようにします。

#!/bin/bash
# iPhone をラズパイに挿したら、1 日 1 回バックアップを取って NAS に送る
# cron で 1 分ごとに動かす。つながっていない・信頼していない・今日はもう取った・前の回が動いている、のときは何もしないで終わる。
#   始め:声「パスコードを入れてください」→ iPhone でパスコードを入れると進む
#   終わり:声「完了しました」→ 変更があった分だけ NAS に送る(rsync)
#   失敗:声と LINE で知らせる(notify.ini の iphone_fail)。挿したままなら次は始めない。一度抜いて挿し直すと取り直す
#   パスコードを入力せずに放っておいたときは、2 時間で止めて失敗にする
# 手で動かすとき:bash ~/bin/iphone_backup.sh --force(今日もう取っていても取る)
set -u
DIR="$HOME/iphone-backup"
STAMP="$HOME/.iphone-backup-last"
FAILED="$HOME/.iphone-backup-failed"   # 失敗した印。iPhone が抜かれたら消す
LOG="$HOME/iphone-backup.log"
NAS_HOST="<NAS の名前>"         # ~/.ssh/config に書いた NAS の名前
NAS_DIR="<NAS の置き場所>"        # NAS の中のバックアップの置き場所
NOTIFY="/usr/bin/python3 $HOME/notify/notify.py"
# 声は順番待ちに入れたらすぐ鳴らす(1 分ごとの cron を待たない・夜の静かにする時間は playq.py が守る)
say() { $NOTIFY "$@" >/dev/null 2>&1; /usr/bin/python3 "$HOME/notify/playq.py" >/dev/null 2>&1 & }
exec 9>/tmp/iphone-backup.lock
flock -n 9 || exit 0
udid=$(idevice_id -l 2>/dev/null | head -1)
# つながっていない。失敗の印があれば消す(抜いたので、次に挿したら取り直す)
if [ -z "$udid" ]; then
  rm -f "$FAILED"
  exit 0
fi
# 失敗したあと、挿したままのあいだは始めない(1 分ごとに「失敗しました」が続かないように)
[ -f "$FAILED" ] && [ "${1:-}" != "--force" ] && exit 0
today=$(date +%F)
if [ "${1:-}" != "--force" ] && [ "$(cat "$STAMP" 2>/dev/null)" = "$today" ]; then
  exit 0
fi
# 信頼していない iPhone(初めて挿したとき)は取らない。iPhone の「信頼」を押すと、次の 1 分で始まる
idevicepair -u "$udid" validate >/dev/null 2>&1 || exit 0
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "始める $udid"
say iphone_start "iPhone のバックアップを始めます。パスコードを入れてください"
mkdir -p "$DIR"
start=$(date +%s)
out=$(timeout -k 1m 2h idevicebackup2 -u "$udid" backup "$DIR" 2>&1)
rc=$?
echo "$out" | tail -5 >> "$LOG"
if [ $rc -ne 0 ] || ! echo "$out" | grep -q "Backup Successful"; then
  log "失敗 番号 $rc"
  echo "$udid $(date '+%F %T')" > "$FAILED"
  say iphone_fail "iPhone のバックアップが失敗しました(番号 $rc)。一度抜いて挿し直すと取り直します"
  exit 1
fi
echo "$today" > "$STAMP"
log "完了 $(( $(date +%s) - start )) 秒 $(du -sh "$DIR" | cut -f1)"
# NAS へ(変更があった分だけ・消えた古いファイルも合わせる)
if rsync -a --delete "$DIR/" "$NAS_HOST:$NAS_DIR/iphone-backup/" >> "$LOG" 2>&1; then
  log "NAS へ送った"
  say iphone_done "iPhone のバックアップが完了しました"
else
  log "NAS へ送れなかった"
  say iphone_fail "iPhone のバックアップは取れましたが、NAS へ送れませんでした"
  exit 1
fi

この記事を参考に試してみる場合に書きかえる所

  • <…> の所:自分の環境に合わせて書きかえる(NAS_HOST は ~/.ssh/config に書いた NAS の名前・鍵でパスワードなしにログインできるようにしておく。NAS_DIR は NAS の中の置き場所)
  • say:わたしの環境では、通知の仕組み(notify.py と、声を順番に鳴らす playq.py)を呼んでいます。仕組みはメールを声で知らせる記事に書きました。声だけでよければ、下の 1 行に置きかえれば動きます
say() { aplay -q "$HOME/sounds/$1.wav" >/dev/null 2>&1 & }

どの知らせを声にするか、LINE にも送るかは、通知の設定ファイル(notify.ini)で決めています。失敗だけは気づかないと困るので、声と LINE の両方にしました。

[routes]
iphone_start = voice
iphone_done = voice
iphone_fail = voice, line
[voice]
iphone_start = iphone_start.wav
iphone_done = iphone_done.wav
iphone_fail = iphone_fail.wav

挿して試した結果(2 回目・差分)

  • 16:31 挿して 1 分以内に見つけて開始 → 声「パスコードを入れてください」
  • パスコードを入力して 2 分半で完了(変更があったのは 163 ファイル。1 回目の 29 分にくらべて、ずっと速い)
  • NAS へ送るのに 2 分 → 声「完了しました」
  • ラズパイと NAS のファイルの数は、どちらも 35,337 件で一致。温度は 57.5℃ で、今回は熱くならなかった

つまずいた所:声がパスコードのあとに流れた

最初は、声を「順番待ち」に置くだけにしていました。順番待ちは 1 分ごとにまとめて鳴らす作りなので、iPhone にパスコードの画面が出て、入力し終わってから「パスコードを入れてください」が流れてしまいました。順番待ちに置いたあと、すぐに鳴らす処理を呼ぶように直して、挿した直後に流れるようにしました(夜の静かにする時間は、今までどおり鳴らしません)。

つまずいた所:失敗すると 1 分ごとに「失敗しました」が続く作りだった

記事を見直していて気づきました。最初の作りでは、失敗すると「今日の印」を付けずに終わるので、挿したままなら次の 1 分でまた始まり、声と LINE の「失敗しました」が 1 分ごとに続くところでした。失敗したら目印のファイルを置き、iPhone が抜かれるまで次を始めないように直しました。パスコードを入力せずに放っておいたときに待ち続けないよう、timeout -k 1m 2h も付けています。

はじめは timeout 2h だけにしていましたが、パスコードの画面が出ないまま iPhone のケーブルを抜いたとき、idevicebackup2 が止まらずに CPU を使い続け、ラズパイが 18 時間ほど 70℃ 台のままになりました。2 時間の「止めて」の合図(TERM)では止まらなかったため、-k 1m を足して、その 1 分後に強く止める(KILL)ようにしました。前の回が終わらないと次の日のバックアップも始まらないので、ここは付けておくのがおすすめです。

元に戻すとき

idevicebackup2 restore --system --settings ~/iphone-backup

戻す前に、iPhone の設定で「探す」(iPhone を探す)をオフにします。オンのままだと始まりません。暗号化のパスワードを聞かれるので入力します。

SD カードのバックアップがなくなっていたら、NAS から同じ形でラズパイに戻してから打ちます。PC の Apple デバイスのアプリでも戻せます。そのときは、~/iphone-backup の中の、UDID(iPhone の番号)の名前のフォルダーを、PC の MobileSync\Backup の下に置きます。

やってみて

ラズパイでも、PC と同じ丸ごとのバックアップが取れました。PC を起動しなくても、ラズパイに挿すだけで済み、2 回目からは 5 分ほどで終わるのが便利です。

毎回 iPhone のパスコードを入力する必要があるので、寝ているあいだに全部自動で、とはいきません。それでも「挿して、声が聞こえたらパスコードを入力する」だけなので、充電のついでに 1 日 1 回取れるようになりました。

この記事を書いた人

フリースタイル

Web の仕事 25 年以上。WordPress の保守・引っ越しと、PHP の業務の仕組みづくり。 プロフィール →

まずは、お気軽にご相談ください。

相談と見積りは無料です。急ぎのときは、フォームで「急ぎ」を選んでください。