GoogleDriveに格納しているファイルを一括ダウンロードするPythonスクリプト

## 背景：
最近はFTPでのファイル転送を使うより、GoogleDrive(GSuite)を使ってのファイルのやりとりをしたいとの要望が増えた気がします。
先日はたくさんの画像ファイルをGoogleDrive経由で受け取り、それを加工して弊社サーバーに上げるという作業を行った時に、GoogleDriveに上がっているファイルを一括ダウンロードするPythonスクリプトを用意したのでご紹介します。

## 設定①：サービスアカウントの準備
GoogleDriveで利用するサービスアカウントキーをGCPから作成します。
サービスアカウントの作成は以下の記事に詳細があります。

https://support.google.com/a/answer/7378726?hl=ja

ステップ4.3で作成したサービスアカウントで `${入力したアカウント名}@project-${プロジェクトID}.iam.gserviceaccount.com` がメールアドレスのような形で取得できます。これは次の設定②で利用します。

ステップ4.8にて秘密鍵JSONを取得することができるので、それをダウンロードして保持しておきます。

## 設定②：サービスアカウントをGoogleDriveの共有設定に登録する

GoogleDriveのフォルダ名をクリックすると、「共有」というリンクが表示されるのでクリックします。
![スクリーンショット 2021-12-01 18.17.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40127/e9281763-2e82-4fbe-08a2-b49844ccb722.png)

共有するユーザー一覧のところに、設定①で作成したサービスアカウントを登録します。
そうすることで、このサービスアカウントを経由してGoogleDriveのファイルを参照することが可能になります。
![スクリーンショット_2021-12-01_18_03_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/40127/f95f19be-b463-d1c7-e8cf-4f661f9805f1.png)

## スクリプト実装
設定①で作成したJSONファイルをPythonから参照できるところに保存しておく。
今回は、'lib/credentials/google-drive-credentials.json'に保存しました。

本記事への投稿用に簡略化していますが以下のスクリプトを実行するとファイルが一括DLできます。

```
import os

from django.conf import settings
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
from httplib2 import Http
from oauth2client.service_account import ServiceAccountCredentials

SCOPES = ['https://www.googleapis.com/auth/drive']


def main() -> None:
    credentials = ServiceAccountCredentials.from_json_keyfile_name(
        credentials_file_path(), SCOPES
    )
    http_auth = credentials.authorize(Http())
    service = build('drive', 'v3', http=http_auth)
    parent = '1xDiN68Ocz1aXaABCDEFGHIJK'  # 先程共有したフォルダのIDでブラウザのURLから取得する
    while True:
        response = service.files().list(
            q=f"'{parent}' in parents" if parent else None,
            pageSize=100).execute()
        items = response.get('files', [])

        if not items:  # itemsが存在しない場合中断
            print(f'No files. {parent}')
            break

        for item in items:
            request = service.files().get_media(fileId=item.get('id'))
            file_path = f"to/save/path/{item.get('name')}"
            with open(file_path, 'wb', buffering=0) as fh:
                downloader = MediaIoBaseDownload(fh, request)
                done = False
                while done is False:
                    status, done = downloader.next_chunk()
                    print(f'Downloading... {file_path}, {status.total_size / 1024 / 1024}MB')


def credentials_file_path() -> str:
    """秘密鍵を保存しているファイルパス"""
    path = os.path.join(
        settings.BASE_DIR,
        'lib',
        'credentials',
        'google-drive-credentials.json'  # 設定①で取得したJSONファイルを格納した場所
    )
    return path

main()
```


## 改善① フォルダ内のファイルが100件以上の場合はnextPageTokenを利用する
response.get('nextPageToken')で2ページ目・3ページ目・・・と次の100件が取得できるようになります。

```
page_token = None  # Noneの場合はページャーで初期値
while True:
    response = service.files().list(
        q=f"'{parent}' in parents" if parent else None,
        pageToken=page_token,
        pageSize=100).execute()
    items = response.get('files', [])
    page_token = response.get('nextPageToken')  # 対象件数が100件以上あれば、このpage_tokenを用いて次のページを取得する
    if not items:  # itemsが存在しない場合中断
        print(f'No files. {parent}')
        break
```

## 改善②： このままだとTeamDriveという機能を使っている組織とのファイル共有ができない。

TeamDriveでGoogleDriveの権限を制御している場合は、ファイルリストを取得する時に includeItemsFromAllDrives=True というパラメータを付与する必要があります。そうしないとファイルリストが取得できませんでした。

```
response = self.service.files().list(
    q=f"'{parent}' in parents" if parent else None,
    includeItemsFromAllDrives=True, # TeamDriveも含めて参照することができる
    pageToken=page_token,
    pageSize=100).execute()
```

このあたりのパラメータは最近も変更されているので、公式ドキュメントを参照することをオススメします。

https://developers.google.com/resources/api-libraries/documentation/drive/v3/csharp/latest/classGoogle_1_1Apis_1_1Drive_1_1v3_1_1FilesResource_1_1ListRequest.html

## 改善③：共有フォルダの中に更にフォルダがある場合は再帰的に取得する。

```
def walk(parent:str = None):
  ....
  items = response.get('files', [])
  ....
  for item in items
　　  　　if item.get('mimeType') == 'application/vnd.google-apps.folder': # itemがフォルダであることを判定
 　　  　　　　 # 再帰的に取得する
  　　  　　　　return walk(item.get('id'))
 　　 　　else:
  　　 　　　　 ....# ファイルをダウンロードする
```

まとめ：
・GoogleDriveからファイルを一括ダウンロードする設定とPythonスクリプトをご紹介。
・GoogleDriveの認証に今回はサービスアカウントを使ってみました。前はOAuthでやったことあったのですが、サービスアカウントの方が楽に参照できる範囲を絞れたりしてとても良いです😁

良いクリスマスを！