やりたいこと

この Blog システムでは毎日データベースのダンプファイルを出力してバックアップを取っているのだが、あくまでサーバー内へのバックアップ出力なのである日突然サーバーがダウンして再起動できなくなってしまった場合サルベージできなくなってしまう。 バックアップファイルは外部ストレージに保存したいところだ。 今までは適当な期間をおいて Google Drive に手動でバックアップを移していたのだが、私は毎日 Blog に日記をつけているのでこれだとバックアップデータが十分に古い場合が出てくる。 毎日バックアップファイルを出力した時点で Google Drive に自動で転送したい。

PyDrive

Qiita に素晴らしい記事があったので大体の部分はこれでできた。 一部ハマった部分があったのでここに注釈として記しておく。

settings.yaml

公式サイトにも書いてあるが settings.yaml という設定ファイルを Python 実行ファイルと同じディレクトリに置く必要がある。 Google Developers Console から出力した client_secrets.json でも良いが、これだと認証情報が保存されない。毎回ブラウザベースでの認証が必要になってしまう。 具体的には YAML 内の以下の部分である:

save_credentials: True  # 認証情報を保存する
save_credentials_backend: file  # 何に保存するか. 今のところ 'file' しか指定できないらしい
save_credentials_file: credentials.json  # 認証情報保存ファイル名

上記の設定がされていることで初回アクセス時 (credentials.json がまだない時) はブラウザベースの認証画面がキックされ、認証が成功すると credentials.json として保存される。

また settings.yaml 内に OAuth の対象を絞り込むための設定があり、サンプルで以下のようになっている:

oauth_scope:
  - https://www.googleapis.com/auth/drive.file
  - https://www.googleapis.com/auth/drive.install

これだとファイルと作成のみの許可となっておりディレクトリなどの検索ができずにハマる (特にエラーなど出力されずヒットしないだけとなる)。 公開するアプリなどではない場合、特に権限を絞り込む必要がない場合は以下のように全権限にしておけば問題ない:

oauth_scope:
  - https://www.googleapis.com/auth/drive

ディレクトリに対し操作を行う場合必ず ID が必要

例えばローカルディレクトリ /path/to に対し操作を行う場合はシステムに対し一意となっているディレクトリパス /path/to の部分が分かっていれば良いのだが Google Drive の場合はそれだと駄目で必ずディレクトリに対する ID を指定する必要がある。 この ID はどうやって調べるのかというところだが、Web 版の Google Drive を使用している場合に対象となるディレクトリに移動して URL を見てみると https://drive.google.com/drive/folders/0B3GHspmhxAvDOXRCGTlSWjhrRWs のようになっている。 この 0B3GHspmhxAvDOXRCGTlSWjhrRWs がそのディレクトリの ID であり、これを使用して以下のように操作できる:

google_auth = GoogleAuth()
google_auth.CommandLineAuth()
drive = GoogleDrive(google_auth)

# 対象の ID のディレクトリ配下のファイル (ゴミ箱に入っていないもの) を全取得
file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format('0B3GHspmhxAvDOXRCGTlSWjhrRWs')}).GetList()

# Google Drive 上の対象 ID のディレクトリ内に hoge.txt を作成
file = drive.CreateFile({'title': 'hoge.txt', 'parents': [{'id': '0B3GHspmhxAvDOXRCGTlSWjhrRWs'}]})

この 'q': '(ID) in parents and trashed=false' というクエリが奇妙に映るが、これは Google Drive APIs (REST) の記法なので仕方ないところのようだ。

対象ディレクトリのファイルを全削除した上でローカルからコピーするサンプル

ということで Django のコマンドラインで動作するように以下のように実装してみた:

class Command(BaseCommand):
    help = 'Google Drive にバックアップファイルを転送する.'

    def add_arguments(self, parser):
        parser.add_argument('path', type=str, help='バックアップ元ファイルパス')
        parser.add_argument('parent_id', type=str, help='Google Drive 転送先ファイルパス')

    def handle(self, *args, **options):
        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理を開始します."))

        # Google Drive 認証を行う
        google_auth = GoogleAuth()
        google_auth.CommandLineAuth()
        drive = GoogleDrive(google_auth)

        # まず旧ファイルを削除
        file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format(options['parent_id'])}).GetList()
        for file in file_list:
            file.Delete()
        self.stdout.write(self.style.SUCCESS("{} 内の旧ファイルを削除しました.".format(options['parent_id'])))

        # backup フォルダ配下のファイルを軒並み転送
        for directory_path, directory_names, file_names in os.walk(options['path']):
            for file_name in file_names:
                file = drive.CreateFile({'title': file_name, 'parents': [{'id': options['parent_id']}]})
                file.SetContentFile(os.path.join(options['path'], file_name))
                file.Upload()
                self.stdout.write(self.style.SUCCESS("{} を転送しました.".format(file_name)))

        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理が完了しました."))

第一引数に転送元ローカルディレクトリを指定、第二引数に Google Drive の対象ディレクトリ ID を指定して以下のように実行できる:

manage.py sendtodrive backup/ 0B3GHFpmhx9vdUFFLVPE4EG9YNnM