サーバー上のフォルダを、Google Drive上にバックアップを取るためのスクリプト

## 概要
サーバー上に置いてあるフォルダを、zipで固めてGoogle Driveに保管しておくPythonのスクリプトです。定期的に実行されることを前提としていて、Google Driveの容量がいっぱいにならないよう古いバックアップファイルを削除する機能なども付けてあります。

[GitHubのリポジトリはこちらです。](https://github.com/KMimura/backup_script)

私はマインクラフトサーバーのセーブデータのバックアップを取るためにこのスクリプトを使っていますが、それ以外にも汎用的に使えると思い、Qiitaの記事にしてみました。ちなみに、AWSのEC2（OSはUbuntu）上で私はこのスクリプトを動かしてます。

動かすには、Google Drive APIを用いるために必要なモジュールをpipでインストールしてください。

```bash
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
```
## コード全体
コードの全体像は、以下の通りです。

```python
import pickle
import os.path
import datetime
import shutil
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.http import MediaFileUpload


# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/drive']
# バックアップを補完する世代数
GENERATIONS = 3
# バックアップデータのディレクトリ
DATA_DIR = '/home/minecraft/server/'
# バックアップしたいフォルダの名前
DATA_NAME = 'newworld'
# バックアップデータを保存する、Google DriveのフォルダID(urlに出てきます)
PARENT_ID = 'xxxxxx'

def main():
    '''
    メインの関数
    '''
    # 認証情報の取得
    creds = get_creds()
    # サービスの作成
    service = build('drive', 'v3', credentials=creds)
    # 既にアップロードされているファイルの確認
    existing_files = get_existing_files(service)
    if len(existing_files) >= (GENERATIONS - 1):
        #必要以上保管されていれば、古いものを削除する
        delete_unnecessary_files(service,existing_files)
    # アップロードするファイルをzipに固める
    file_to_upload = create_zip_file()
    # ファイルをアップロードする
    upload_file(service)

def get_creds():
    '''
    1. 認証情報の取得
    '''
    creds = None
    # 認証ファイルを開いてみる
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # 失敗したら、新しく作る
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)
    return creds

def get_existing_files(service):
    '''
    2. 既存のバックアップファイルの取得
    '''
    # Call the Drive v3 API
    query = "'" + PARENT_ID + "' in parents"
    results = service.files().list(fields="nextPageToken, files(id, name, createdTime)", q=query).execute()
    items = results.get('files', [])

    if not items:
        return {}
    else:
        return items

def delete_unnecessary_files(service,existing_files):
    '''
    3. もう保管する必要のないバックアップファイルを削除
    '''
    # 各ファイルの作成時間を取得し、datetimeオブジェクトに変換
    for f in existing_files:
        f['datetime_createdTime'] = datetime.datetime.strptime(f['createdTime'], '%Y-%m-%dT%H:%M:%S.%fZ')
    # 作成日でソート
    sorted_files = sorted(existing_files, key=lambda x: x['datetime_createdTime'])          
    delete_len = len(sorted_files) - (GENERATIONS - 1)
    for i in range(delete_len):
        service.files().delete(fileId=sorted_files[i]['id']).execute()

def create_zip_file():
    '''
    4. バックアップしたいフォルダをzipに固める
    '''
    shutil.make_archive(DATA_DIR + DATA_NAME, 'zip', root_dir=DATA_DIR + DATA_NAME)

def upload_file(service):
    '''
    5. ファイルをアップロード
    '''
    today_str = datetime.datetime.now().strftime("%D").replace("/","-")
    file_metadata = {'name': today_str + '.zip','parents':[PARENT_ID]}
    media = MediaFileUpload(DATA_DIR + DATA_NAME + '.zip', mimetype='application/zip')
    results = service.files().create(body=file_metadata,media_body=media,fields='id').execute()

if __name__ == '__main__':
    main()
```
## コードの解説
### 1. 認証情報の取得
認証情報の取得部分は、[Google Drive APIの公式サンプル](https://developers.google.com/drive/api/v3/quickstart/python)のモノをそのまま流用しています。

初回実行時には、ブラウザが開かれて認証の許可を求められます。認証のフローが完了すると認証情報が`token.pickle`に保存されるので、次回以降はブラウザでの認証は不要になります。

### 2. 既存のバックアップファイルの取得
Google Driveのフォルダ上に既にバックアップとして保存されているファイルを取ってきます。[Google Drive APIの公式サンプル](https://developers.google.com/drive/api/v3/quickstart/python)のモノに少し手を加えて、特定のフォルダ以下のファイルを取得するようにしています。

### 3. もう保管する必要のないバックアップファイルを削除
Google Driveの容量がいっぱいにならないように、不要なファイルは削除します。一つ前の手順で取得したGoogle Drive上のファイルデータを、アップロードされた時間順に並び替え、古いものから削除していきます。

###  4. バックアップフォルダをzipに固める
そのまんまです。shututilを使って行っています。

### 5. ファイルをアップロード
zipファイルをアップロードします。ファイルのメタデータをいじることで、特定のフォルダの下にアップロードするようにしています。
## ついでに
### サーバー起動時に自動で上記スクリプトを実行
私はサーバー起動時に上記のスクリプトが走るように設定しています。これを実現する方法はいくつかありますが、私はcronを使いました。具体的には、`crontab`で以下のような記述をしました。

```bash
@reboot python3 /home/minecraft/tools/backup.py
```

`@reboot`をつけることで、起動時に実行されます。