iPhoneのバックアップファイルのリストア

#はじめに

iPhone 内のファイルをパソコンで見たかったので、iTunes で iPhone をバックアップした時に作られるファイルを復元した。きっかけは、iPhone を移行する際、LINE のデータが消えたときのために、トークや写真などをPCから見れるようにしたかったため。自分は Windows を使っているが、Windows にはiPhoneの中身を直接参照するアプリがあったりする。ただ、あまり信用していないのと、iPhone 内に直接アクセスしたくないため、バックアップファイルのリストア方法を探した。

環境は以下の通り。

- Windows10
- iOS 13
- iTunes 12.20

#バックアップファイルの場所と構成
### 場所
iTunes で iPhone をバックアップすると、バックアップファイルが以下のフォルダに作成される。なお、 iTunesでバックアップするときに「暗号化しない」ようにする必要がある。

C:\Users\\[username]\Apple\MobileSync\Backup\XXXXX

「XXXXX」は環境ごとに異なると思われる文字列。複数のバックアップがあった場合、フォルダが複数ある。少し前のiOS（iTunes ?）はフォルダの場所が異なるため注意。

### 構成
上記の「XXXXX」フォルダ内に iPhone のバックアップデータが含まれている。主要なファイルは下記の通り。

- Manifest.db
SQLite形式のDBファイル。各ファイルの情報が記載されている。iPhone内でのファイルパスとバックアップフォルダ内でのファイル名が紐づけされている。

- データ
iPhoneの中に入っていた元のファイルが40文字のランダム（何らかのハッシュ値？）なファイル名に置き換えられて配置されている。各ファイルはファイル名の最初の2文字のフォルダに分散して格納されている。iTunesのオプションで暗号化してなければファイルの中身は元のままなので、エディタやビューワなどで開くとデータを見ることができる。


このほかにもいくつかの情報があるようだが、これ以上は解析していない。

# リストアするPythonスクリプト
すべてのバックアップファイルに対して、元のファイル名で出力する Python スクリプトを作成した。エラー処理などはちゃんとしていない。

- Windows の Python3.7で作成したため、Mac の場合はパスの区切り文字を変更する必要がある。ソースファイル中の '\\\\' を '/' に変更すればよいかと思うが、Windows以外では試していない。
- `src_basepath` を自分の環境でのパスに置き換えると実行できる。
- ファイルは `output` ディレクトリに作成される。

```python:restore.py
import os
import shutil
import sqlite3


# パスをディレクトリとファイル名に分ける
# （例）
# Input: '.\\output\\Library\\Logs\\test.log'
# Output: ('.\\output\\Library\\Logs\\', 'test.log')
def get_dst_filepath(relative_filepath):
    path_array = relative_filepath.split('\\')

    dst_path = ''
    for direcoty_name in path_array[0:-1]:
        dst_path += direcoty_name + '\\'

    filename = path_array[-1]

    return dst_path, filename


# パスの定義
src_basepath = r'C:\Users\username\Apple\MobileSync\Backup\00000000-0000000000000000'
dst_basepath = r'.\output'
db_filepath = src_basepath + '\Manifest.db'

# DB接続
connection = sqlite3.connect(db_filepath)
cursor = connection.cursor()
cursor.execute('SELECT fileID, relativePath, flags FROM Files')
result = cursor.fetchall()
connection.close()

### DBフィールドの説明 #######################
# fileID: バックアップディレクトリ内でのファイル名
# relativePath: 相対パス
# flags = 1: バックアップディレクトリ内に元ファイルが存在する
# flags = 2: バックアップディレクトリ内に元ファイルが存在しない
#############################################

# 各ファイルごとの処理
for row in result:
    src_filename = row[0]
    dst_relative_path = row[1].replace('/', '\\')
    src_exists = row[2] is 1

    src_filepath = src_basepath + '\\' + src_filename[0:2] + '\\' + src_filename
    dst_filepath, dst_filename = get_dst_filepath(dst_basepath + '\\' + dst_relative_path)

    # バックアップディレクトリ内に元ファイルがなければスキップ
    if not src_exists:
        continue

    print('{}\t{}\t{}'.format(src_filepath, dst_filepath, dst_filename))

    try:
        # 再帰的にディレクトリ作成
        os.makedirs(dst_filepath, exist_ok=True)
        # ファイルをコピー
        shutil.copy2(src_filepath, dst_filepath+'\\'+dst_filename)
    except :
        pass
```

# ファイルの中身など
参考にいくつかファイルのパスを記載する。

- 写真ファイル：Media\DCIM
- LINEデータ（トーク）：Library\Application Support\PrivateStore\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\Messages\Line.sqlite
- LINEデータ（写真などのメッセージ添付ファイル）：Library\Application Support\PrivateStore\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\Message Attachments\\


# 参考文献
5年くらい前に調べたので、参考にしたURLは覚えていません。なお、その時調べたファイルの場所やファイル構成は今と異なります。