広告 プログラミング

【Python】PyDrive2を用いてVPS上のデータをGoogleDriveにバックアップする方法を解説

※本ページには、プロモーション(広告)が含まれています。

悩んでいる人

rcloneを用いてバックアップを取っていたが、定期的に認証情報を手動更新しないといけないため、使いづらい。

cronで使用したいため、一度の設定で半永久的に自動化するための方法を教えて欲しい。

こんなお悩みを解決します。

以前の記事で、rcloneを用いてGoogle Driveにバックアップする方法を解説しました。

一方、rcloneを用いる場合、定期的に認証情報の有効期限(リフレッシュトークン)を更新する必要があり、使いづらいものとなっていました。

この記事では、rcloneの代わりにPyDrive2というPythonライブラリを用いてrcloneと同様の作業を再現します。

記事を読み終えると、PyDrive2の使い方と自動的にバックアップを取る方法について知ることができますよ。

VPSサーバをまだ契約していない方

カスタマイズの自由度が高いサーバを求めている場合、仮想専用サーバ(VPS)を利用することをおすすめします。

また、まだVPSサーバを契約していない方は、以下の記事が参考になると思うので、ぜひご覧ください。

あわせて読みたい
【比較】おすすめのVPS 4選

続きを見る

OAuth2の設定

以下の記事を参考に、Google CloudにてOAuth2の認証情報を作成してください。

あわせて読みたい
【必見!】rcloneを使ってVPS上のデータをGoogleDriveへバックアップする方法を解説

続きを見る

また、以下を実施し、「OAuth2.0 クライアントID」のjsonファイルをダウンロードしてください。

ダウンロード後のjsonファイルは、client_secret.jsonとします。

Pythonの仮想環境構築

既存環境を汚さないようにするため、venvを用いた仮想環境を構築します。今回、使用するPythonのバージョンと関連ライブラリのバージョンは以下のようになります。

対象バージョン
Python3.10
PyDrive21.16.1
バージョン情報

venvのインストール

Python3.10の場合、venvを追加でインストールする必要があるため、以下のコマンドを実行します。

sudo apt update
sudo apt install python3.10-venv

また、python3.10-venvをインストールする際に以下のような画面が表示されることがあります。この場合、何も変更せずにOKを押下してください。

仮想環境の作成&関連ライブラリのインストール

仮想環境を作成し、上記で取り上げた関連ライブラリをインストールします。実行するコマンドは以下の通りです。

# 「venv」という仮想環境を作成
python3 -m venv venv
# 仮想環境の有効化
source venv/bin/activate
# PyDrive2のインストール
pip3 install PyDrive2

また、上記を実行後のディレクトリ構成は、以下のようになります。(関連するもののみを記載しています。)


${HOME}/
`-- venv/
    |-- bin/
    |-- include/
    |-- lib64/
    `-- pyvenv.cfg

PyDrive2によるバックアップの仕組み構築

rcloneによるバックアップの記事同様に、以下のステップでバックアップを行います。

  1. バックアップ用のディレクトリを作成する。
  2. 該当ファイル/ディレクトリをバックアップ用のディレクトリにコピーする。
  3. バックアップ用のディレクトリを圧縮する(アーカイブを作成する)。
  4. Google Driveのバックアップ先を確認し、一定期間以上経過したバックアップを削除する。
  5. 圧縮したファイルをGoogle Driveにバックアップする。
  6. ローカルで作成したバックアップ用のディレクトリと圧縮ファイルを削除する。

今回想定するバックアップ時のディレクトリ構成

今回のスクリプトを動かした際のバックアップ対象がイメージできるように、ディレクトリ構成もあわせて記載しておきます。


${HOME}/
|-- ddns-settings/
|-- cron-configs/
|   |-- gdrive_backup.py # 以降で示すスクリプト
|   |-- client_secret.json # Google Cloudからダウンロードしたjsonファイル
|   `-- settings.yaml
|-- mydns.log
|-- vpnaccess-wireguard-nginx/
|   |-- LICENSE
|   |-- README.md
|   |-- docker-compose.yml
|   |-- envs/
|   |-- nginx/
|   |-- wireguard/
|   `-- wrapper.sh
`-- venv/
    |-- bin/
    |-- include/
    |-- lib64/
    `-- pyvenv.cfg

上記のうち、今回は以下のファイル/ディレクトリをバックアップ対象とします。

  • ddns-settings
  • cron-configs
  • vpnaccess-wireguard-nginx

client_secret.jsonのコピー

TeraTermを使っている人は、VPSサーバにSSH接続している状態で、ダウンロードした「client_secret.json」を画面上にドラッグ&ドロップしてください。

以下のような画面が表示されるので、コピー先を指定し、OKを押下します。

settings.yamlの作成

以下の内容のsettings.yamlを作成してください。

client_config_file: client_secret.json

save_credentials: True
save_credentials_backend: file
save_credentials_file: saved_credentials.json

get_refresh_token: True

※saved_credentials.jsonは、初回実行時に自動生成されます。

gdrive_backup.pyの作成

「プログラムの全体像」で取り上げているスクリプトの内容を反映して下さい。また、下記のコード96行目の「HOMEDIR」において、default値は環境に合わせて修正してください。

プログラムの全体像

作成したプログラムの全体像は以下のようになります。

import argparse
import os
import shutil
import sys
import logging
import logging.config
from datetime import datetime, timedelta
from glob import glob
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive

class CmdWrapper:
    def __init__(self, log_config, args, timelimit):
        gauth = GoogleAuth(settings_file=args.config)
        gauth.CommandLineAuth()
        logging.config.dictConfig(log_config)
        self.logger = logging.getLogger('backup')
        self.drive = GoogleDrive(gauth)
        self.is_dryrun = args.dry_run
        self.remote_path_id = args.remote_path_id
        self.log_message = '[dry-run] ' if self.is_dryrun else ''

        # logging
        self.__loginfo(f'=== Parameters ===')
        self.__loginfo(f' target: {args.target}')
        self.__loginfo(f' output: {args.output}')
        self.__loginfo(f' limit:  {timelimit.strftime("%Y/%m/%d")}')
        self.__loginfo(f'==================')

    def __loginfo(self, message):
        self.logger.info(f'{self.log_message}{message}')

    def mkdir(self, dirname):
        self.__loginfo(f'mkdir -p {dirname}')

        if not self.is_dryrun:
            os.makedirs(dirname, exist_ok=True)

    def copy(self, srcdir, dstdir):
        targets = [
            name.rstrip('/') for name in glob(f'{srcdir}/*[!.log]')
            if all([name.find(ignore_file) < 0 for ignore_file in [os.path.basename(dstdir), 'venv']])
        ]

        for dirname in targets:
            self.__loginfo(f'copy {dirname} to {dstdir}')

            if not self.is_dryrun:
                if os.path.isdir(dirname):
                    basedir = os.path.basename(dirname)
                    shutil.copytree(dirname, os.path.join(dstdir, basedir))
                else:
                    shutil.copy2(dirname, dstdir)

    def archive(self, dstdir):
        self.__loginfo(f'zip -r {dstdir}.zip {dstdir}')
        file_path = shutil.make_archive(dstdir, format='zip', root_dir=dstdir, dry_run=self.is_dryrun)

        return file_path

    def delete_remote_files(self, timelimit, max_results):
        for file_list in self.drive.ListFile({'q': f"'{args.remote_path_id}' in parents and trashed=false", 'maxResults': max_results}):
            for gdrive_file in file_list:
                created_time = datetime.strptime(gdrive_file['createdDate'].split('T')[0], '%Y-%m-%d')

                # Check threshold
                if created_time < timelimit:
                    # Delete remote file
                    filename = gdrive_file['title']
                    self.__loginfo(f'delete remote file({filename})')

                    if not self.is_dryrun:
                        gdrive_file.Trash()

    def upload(self, local_file_path):
        self.__loginfo(f'upload {local_file_path} to remote directory')

        if not self.is_dryrun:
            gdrive_file = self.drive.CreateFile({
                'title': os.path.basename(local_file_path),
                'parents': [{'id': self.remote_path_id}]
            })
            gdrive_file.SetContentFile(local_file_path)
            gdrive_file.Upload()

    def delete_local_data(self, local_path):
        self.__loginfo(f'delete local file or directory({local_path})')

        if not self.is_dryrun:
            if os.path.isdir(local_path):
                shutil.rmtree(local_path)
            else:
                os.remove(local_path)

if __name__ == '__main__':
    HOMEDIR = os.getenv('HOME', default='/home/yuruto') # 環境に合わせてdefault値を変更すること
    parser = argparse.ArgumentParser(
        prog='python3 gdrive_backup.py',
        description='Backup VPS files to Google-Drive',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument('-t', '--target', required=True, type=str, help='target directory')
    parser.add_argument('-o', '--output', type=str, help='output directory')
    parser.add_argument('-r', '--remote-path-id', type=str, default='root', help='remote path')
    parser.add_argument('-p', '--dry-run', default=False, action='store_true', help='dry-run option')
    parser.add_argument('-d', '--delete-after-days', default=7, type=int, help='number of days elapsed at time of deletion')
    parser.add_argument('-c', '--config', type=str, default='settings.yaml', help='Automatic authentication configuration file when this script accesses to Google Drive.')
    args = parser.parse_args()

    # log config
    log_config = {
        'version': 1,
        'disable_existing_loggers': False,
        'formatters': {
            'info': {
                'format': '[%(asctime)s %(levelname)s] %(name)s %(message)s',
                'datefmt': '%Y/%m/%d %H:%M:%S',
            },
        },
        'handlers': {
            'timeRotate': {
                'class': 'logging.handlers.TimedRotatingFileHandler',
                'formatter': 'info',
                'filename': f'{HOMEDIR}/pydrive2.log',
                'when': 'W6', # Sunday
                'backupCount': 1
            },
            'console': {
                'class': 'logging.StreamHandler',
                'formatter': 'info',
            },
        },
        'loggers': {
            'backup': {
                'level': 'INFO',
                'handlers': ['timeRotate', 'console']
            }
        }
    }

    # Check arguments
    if not os.path.exists(args.target):
        print(f'Invalid target directory(target: {args.target})')
        sys.exit(1)
    if not os.path.exists(args.config):
        print(f'Does not exist configuration file(config: {args.config})')
        sys.exit(1)
    # Check output directory
    if args.output is None:
        today = datetime.now().strftime('%Y%m%d')
        args.output = os.path.join(HOMEDIR, f'backup{today}')
    elif not os.path.exists(args.output):
        args.output = os.path.join(HOMEDIR, os.path.basename(args.output))
    # Calculate  timelimit
    timelimit = datetime.now() - timedelta(days=args.delete_after_days)

    # ================
    # = main routine =
    # ================
    wrapper = CmdWrapper(log_config, args, timelimit)

    # Step1: Create output directory if it does not exist.
    if not os.path.exists(args.output):
        wrapper.mkdir(args.output)
    # Step2: Copy data to output directory.
    wrapper.copy(args.target, args.output)
    # Step3: Compress output directory.
    tar_file_path = wrapper.archive(args.output)
    # Step4: Search for old files and delete them if they exceed the threshold.
    wrapper.delete_remote_files(timelimit, args.delete_after_days * 2)
    # Step5: Copy backup file to remote path
    wrapper.upload(tar_file_path)
    # Step6: Delete temporary file and directory
    wrapper.delete_local_data(args.output)
    wrapper.delete_local_data(tar_file_path)

以降では、上記のステップと照らし合わせつつ、コードの解説を行います。

引数処理

rcloneの時と同様に、引数で動的に動きを変更できるようにしています。指定できる引数は以下のようになります。

引数内容デフォルト値
-t, --targetバックアップ対象のディレクトリ。設定必須のパラメータNone
-o, --outputバックアップ先のディレクトリ。指定されていない場合はbackup[日付]という名称になる。
また、指定されたディレクトリが存在しない場合はホームディレクトリ直下にディレクトリが作成される。
None
-r, --remote-path-idバックアップであるGoogle DriveのディレクトリIDを指定する。(詳細は後述)root
-p, --dry-run実行時の処理内容を出力するが、実際には実行しない。動作確認時に使用する。False
-d, --delete-after-days削除対象とする日数。実行日時からカウントし、指定された日数よりも前のファイルをGoogle Drive上から削除する。7
-c, --configGoogle Driveに接続する際のOAuth2の設定情報を記載したyamlファイル。settings.yaml
引数一覧

これは、スクリプトの以下の部分で処理しています。

    HOMEDIR = os.getenv('HOME', default='/home/yuruto') # 環境に合わせてdefault値を変更すること
    parser = argparse.ArgumentParser(
        prog='python3 gdrive_backup.py',
        description='Backup VPS files to Google-Drive',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument('-t', '--target', required=True, type=str, help='target directory')
    parser.add_argument('-o', '--output', type=str, help='output directory')
    parser.add_argument('-r', '--remote-path-id', type=str, default='root', help='remote path')
    parser.add_argument('-p', '--dry-run', default=False, action='store_true', help='dry-run option')
    parser.add_argument('-d', '--delete-after-days', default=7, type=int, help='number of days elapsed at time of deletion')
    parser.add_argument('-c', '--config', type=str, default='settings.yaml', help='Automatic authentication configuration file when this script accesses to Google Drive.')
    args = parser.parse_args()
    # ============
    # === 中略 ===
    # ============
    # Check arguments
    if not os.path.exists(args.target):
        print(f'Invalid target directory(target: {args.target})')
        sys.exit(1)
    if not os.path.exists(args.config):
        print(f'Does not exist configuration file(config: {args.config})')
        sys.exit(1)
    # Check output directory
    if args.output is None:
        today = datetime.now().strftime('%Y%m%d')
        args.output = os.path.join(HOMEDIR, f'backup{today}')
    elif not os.path.exists(args.output):
        args.output = os.path.join(HOMEDIR, os.path.basename(args.output))
    # Calculate  timelimit
    timelimit = datetime.now() - timedelta(days=args.delete_after_days)

バックアップ処理

引数処理以降は、別途定義したクラスのメソッドを呼び出しているだけとなるため、定義したクラスを中心に説明していきます。

バックアップ用のディレクトリ作成

ディレクトリが存在しない場合のみ、該当するディレクトリを作成するようにしています。

def mkdir(self, dirname):
        self.__loginfo(f'mkdir -p {dirname}')

        if not self.is_dryrun:
            os.makedirs(dirname, exist_ok=True)
    # Step1: Create output directory if it does not exist.
    if not os.path.exists(args.output):
        wrapper.mkdir(args.output)

該当ファイル/ディレクトリのコピー

該当するファイル/ディレクトリをコピーします。この時、*.log、バックアップ先のディレクトリ、venvはコピーしないようにします。

def copy(self, srcdir, dstdir):
        # glob(f'{srcdir}/*[!.log]'): *.logファイルを除いたファイル/ディレクトリ一覧を取得
        # all([...]) : バックアップ先のフォルダとvenvをバックアップ対象から除外
        targets = [
            name.rstrip('/') for name in glob(f'{srcdir}/*[!.log]')
            if all([name.find(ignore_file) < 0 for ignore_file in [os.path.basename(dstdir), 'venv']])
        ]

        for dirname in targets:
            self.__loginfo(f'copy {dirname} to {dstdir}')

            if not self.is_dryrun:
                if os.path.isdir(dirname):
                    basedir = os.path.basename(dirname)
                    shutil.copytree(dirname, os.path.join(dstdir, basedir))
                else:
                    shutil.copy2(dirname, dstdir)
    # Step2: Copy data to output directory.
    wrapper.copy(args.target, args.output)

アーカイブ作成

今回は、zip形式で圧縮します。

def archive(self, dstdir):
        self.__loginfo(f'zip -r {dstdir}.zip {dstdir}')
        file_path = shutil.make_archive(dstdir, format='zip', root_dir=dstdir, dry_run=self.is_dryrun)

        return file_path
    # Step3: Compress output directory.
    tar_file_path = wrapper.archive(args.output)

一定期間以上経過したバックアップの削除

指定されたGoogle Driveのディレクトリ以下にあるファイルを取得し、作成から一定期間以上経過したファイルを削除します。

この時、Google Drive上のディレクトリIDを指定する必要があり、ディレクトリIDは、Webブラウザ上の以下から確認できます。

これを実行時引数として指定します。今回の場合、以下のようになります。(関連する部分のみ明記)

python3 cron-configs/gdrive_backup.py -r 1GjGJuymChjo1dLNJBov4qNt0ELRD9Fya

また、実際の処理としては以下のようになります。

def delete_remote_files(self, timelimit, max_results):
        for file_list in self.drive.ListFile({'q': f"'{args.remote_path_id}' in parents and trashed=false", 'maxResults': max_results}):
            for gdrive_file in file_list:
                # 作成日時を取得
                created_time = datetime.strptime(gdrive_file['createdDate'].split('T')[0], '%Y-%m-%d')

                # Check threshold
                if created_time < timelimit:
                    # Delete remote file
                    filename = gdrive_file['title']
                    self.__loginfo(f'delete remote file({filename})')

                    if not self.is_dryrun:
                        # 完全には削除せず、ゴミ箱に移動する
                        gdrive_file.Trash()
    # Step4: Search for old files and delete them if they exceed the threshold.
    wrapper.delete_remote_files(timelimit, args.delete_after_days * 2)

今回は、作成日時を取得して判定していますが、他の情報を取得したい場合は、以下のリンクにアクセスしAPI仕様を確認してください。

REST Resource: files(ホーム > Google Workspace > Google Drive > 参照)

Google Driveにバックアップ

圧縮ファイル(zip形式)をGoogle Driveにアップロードします。

def upload(self, local_file_path):
        self.__loginfo(f'upload {local_file_path} to remote directory')

        if not self.is_dryrun:
            gdrive_file = self.drive.CreateFile({
                'title': os.path.basename(local_file_path),
                'parents': [{'id': self.remote_path_id}]
            })
            gdrive_file.SetContentFile(local_file_path)
            gdrive_file.Upload()
    # Step5: Copy backup file to remote path
    wrapper.upload(tar_file_path)

ディレクトリと圧縮ファイルの削除

最後に、不要になったディレクトリと圧縮ファイルを削除します。

def delete_local_data(self, local_path):
        self.__loginfo(f'delete local file or directory({local_path})')

        if not self.is_dryrun:
            if os.path.isdir(local_path):
                shutil.rmtree(local_path)
            else:
                os.remove(local_path)
    # Step6: Delete temporary file and directory
    wrapper.delete_local_data(args.output)
    wrapper.delete_local_data(tar_file_path)

使い方

認証情報の設定の都合上、最初の1回は手動で実行する必要があるため、手元の環境で実行します。

cron-configsのディレクトリに移動し、以下のコマンドを実行します。このとき、-rオプションは環境にあわせて変更してください。

${HOME}/venv/bin/python3 gdrive_backup.py -t ${HOME} -r 1GjGJuymChjo1dLNJBov4qNt0ELRD9Fya

実行後に以下のような画面が表示されるため、rcloneの記事同様に認証を実施してください。

【再掲】認証のやり方

表示されたURLにアクセス後、ログインを求められるので該当するアカウントでログインしてください。

「続行」を押下します。

アクセス権を付与するリソースについて説明されるため、内容を確認後、「続行」を押下してください。

最後に認証コードが表示されるため、コピーします。

Go to the following link in your browser:

    https://accounts.google.com/o/oauth2/auth...

Enter verification code: ***** # 先程コピーした認証コードを入力

認証処理後、ログが表示されると思います。自身の環境にあった情報が表示されているかを確認してください。

また、ホームディレクトリ直下に「pydrive2.log」というファイルも保存されていると思うので、あわせて確認してください。

[1999/12/31 23:55:03 INFO] backup === Parameters ===
[1999/12/31 23:55:03 INFO] backup  target: /home/yuruto
[1999/12/31 23:55:03 INFO] backup  output: /home/yuruto/backup19991231
[1999/12/31 23:55:03 INFO] backup  limit:  1999/12/24
[1999/12/31 23:55:03 INFO] backup ==================
[1999/12/31 23:55:04 INFO] backup mkdir -p /home/yuruto/backup19991231
[1999/12/31 23:55:05 INFO] backup copy /home/yuruto/vpnaccess-wireguard-nginx to /home/yuruto/backup19991231
[1999/12/31 23:55:15 INFO] backup copy /home/yuruto/ddns-settings to /home/yuruto/backup19991231
[1999/12/31 23:55:16 INFO] backup copy /home/yuruto/cron-configs to /home/yuruto/backup19991231
[1999/12/31 23:55:20 INFO] backup zip -r /home/yuruto/backup19991231.zip /home/yuruto/backup19991231
[1999/12/31 23:55:23 INFO] backup upload /home/yuruto/backup19991231.zip to remote directory
[1999/12/31 23:55:58 INFO] backup delete local file or directory(/home/yuruto/backup19991231)
[1999/12/31 23:55:59 INFO] backup delete local file or directory(/home/yuruto/backup19991231.zip)

cronの設定

最後に、今回のプログラムをcronに登録しておきます。

毎日3:00に実行されるように設定する場合、以下のようになります。

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""
HOME=/home/yuruto
0 3 * * *  cd ${HOME}/cron-configs && ${HOME}/venv/bin/python3 gdrive_backup.py -t ${HOME} -r 1GjGJuymChjo1dLNJBov4qNt0ELRD9Fya

まとめ

今回は、PyDrive2を用いてVPS上のファイルを定期的にGoogle Driveにバックアップする方法について解説しました。

rcloneでは認証情報の定期更新が必要になりますが、今回のケースではその点も自動的に実施できるため、より使いやすくなったと思います。

私自身も、しばらく様子を見つつ使っていこうと思います。

VPSサーバをまだ契約していない方

カスタマイズの自由度が高いサーバを求めている場合、仮想専用サーバ(VPS)を利用することをおすすめします。

また、まだVPSサーバを契約していない方は、以下の記事が参考になると思うので、ぜひご覧ください。

あわせて読みたい
【比較】おすすめのVPS 4選

続きを見る

スポンサードリンク

-プログラミング
-, , ,