PythonでGoogleDriveへアクセス

# はじめに
Flaskから個人が持つGoogle Driveへアクセスするツールの話。

やりたかったことは、素性を知っている人たちの各々が撮影した写真をGoogle Driveへ集めたかったです。Google Driveのアプリをインストールしてない人もいるかもしれないし、アプリをインストールしてても、他人がシェアしてるGoogle Driveを、アプリ内で開く方法がわからなかったです。なので、Google DriveへアップロードするWebツールを作りました。

技術的には、誰かが作ったGoogle Diveのライブラリがいくつかあるけど、数年前前のだったり、メリットがわからなかったので、Google純正のライブラリを使いたくなりました。
またググって出てくるブログや掲示板が古かったり、複数の選択肢がある状態で、わからない頭で読んでも区別できなかったりで、苦労しました。

結果のソースはこちら

https://github.com/yo16/upload_to_google_drive

# 概要
■正解
1. GCPで、サービスアカウントを作り、Google Driveへのアクセス権を付与
2. Google Driveのサイトで、アクセスしたいフォルダへのアクセス権を、サービスアカウントに付与
3. Pythonからアクセスする

■誤解答
今回採用しなかった認証の「誤解答」として、下記があります。認証のことがあまりわかってないままやってたので、ここに時間がかかってしまいました。
- APIキーを使った認証
- OAuth2.0を使った認証

■別解
「別解答」として GOOGLE WORKSPACE があります。これは組織でGoogleアカウントを持っている場合に使うもので、無料枠もありそうだけど、個人のスポット開発には適さないので対象外です。
- GOOGLE WORKSPACE（旧 Google Apps for Business、G Suite）

「正解」「誤解答」「別解」をうまく選り分けて、正解を見つける必要がありました。


# 手順詳細
※ 見た目等がたまに変わるので、あえてざっくり説明です。

## 1. GCPでサービスアカウントを作り、Google Driveへのアクセス権を付与

1. GCPの左にあるプロダクト群の「APIとサービス」
1. (サービスアカウントを作る)
    1. 左のメニューから「認証情報」
    1. 「認証情報を作成＞サービスアカウント」
        → 長いメールアドレスのようなものが発行される
        → これがサービスアカウントの、アカウント名
    1. キーから、「鍵を追加」
        1. タイプをJSONで作成
            → JSONファイルがダウンロードされる
            → 秘密鍵なのでとても大事。Githubとか公開されるところに置かないように厳重注意。
1. (Google Drive用のAPIを有効化)
    1. 左のメニューから「ライブラリ」
    1. Google Drive APIを検索
    1. 有効化する

サービスアカウントとは、何かのサービスを行うためのロボットのためのアカウントのようなものです。そのロボットに、適切なアカウントを与えることで、サービスごとにそれに適したアカウントを設定できるメリットがあります。
たとえば、管理者1人で10個のシステムをお守りするとして、管理者が全権限を持ってそのアカウントを全システムで使うと、特定のシステムが思いもよらぬアクセスや更新ができちゃって大変なことになることを防ぐ、みたいな。

またJSONファイルの内容を他人に知られて悪用されると、GCPにアクセスされて知らない間にお金がかかったり、仮想端末等を使って犯罪に利用されるかもしれません。何度も書きますが、GitHubの対象に含まれないように注意してください。万が一間違えてあげてしまったら、サービスアカウントの削除、GitHubのレポジトリ削除等を急いでやってください。修正でなく削除。GCPプロジェクトの削除、GCPアカウントの削除も視野に入れて。


## 2. Google Driveのサイトで、アクセスしたいフォルダへのアクセス権を、サービスアカウントに付与

1. Google Driveでフォルダを開く
1. 右上の i のアイコン（詳細）を押して、「アクセスを管理」
1. ユーザーやグループを追加の入力欄に、サービスアカウント名を入力
1. 編集者の権限を与える


## 3. Pythonからアクセスする

- [Drive API v3 Reference &nbsp;|&nbsp; Google Developers](https://developers.google.com/drive/api/v3/reference)

認証のところ

```Python:get_authenticated_service_with_service_account()
from google.oauth2 import service_account
from googleapiclient.discovery import build

def get_authenticated_service_with_service_account():
    service_account_key = {
        'type': 'service_account',
        'project_id': os.environ.get('PROJ_ID', ''),
        'private_key_id': os.environ.get('PRIVATE_KEY_ID', ''),
        'private_key': os.environ.get('PRIVATE_KEY', '').replace('\\n','\n'),
        'client_email': os.environ.get('CLIENT_EMAIL', ''),
        'client_id': os.environ.get('CLIENT_ID', ''),
        'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
        'token_uri': 'https://oauth2.googleapis.com/token',
        'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
        'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/google-drive-api-hiu2022spr%40analog-height-345100.iam.gserviceaccount.com'
    }
    credentials = service_account.Credentials.from_service_account_info(service_account_key)
    scoped_credentials = credentials.with_scopes(SCOPES)
    return build(API_SERVICE_NAME, API_VERSION, credentials=scoped_credentials)
```
`service_account_key` は、サービスアカウントを作った時にダウンロードした、JSONファイルの内容です。その内容は、ソースに書いてはいけない
ソースに書いてはいけない、GitHubに上げてはいけない内容は、全部環境変数に設定してそこを利用するようにしています。
※ `from_service_account_info()` の代わりに、JSONファイルから作る `from_service_account_file()` もあります。

この認証情報を使って、filesオブジェクトを取得します。
```Python:
obj_drive = get_authenticated_service_with_service_account()
obj_files = obj_drive.files()
```

filesオブジェクトを取得するところまでくれば、あとはいろいろなブログとかでいけますが、私のソースの一部を説明すると、、、
```Python:
for p in photos:
    #print(p.filename)
    file_path = f'{TMP_DIR}/{p.filename}'
    p.save(file_path)
    
    file_metadata = {
        'name': p.filename,
        'parents': [sub_folder_id]
    }
    media = MediaFileUpload(
        file_path,
        mimetype=mimetypes.guess_type(file_path)[0],
        resumable=True
    )
    file = obj_files.create(
        body=file_metadata,
        media_body=media,
        fields='id'
    ).execute()
```
photosは、[werkzeug.datastructures.FileStorage](https://werkzeug.palletsprojects.com/en/2.0.x/datastructures/#werkzeug.datastructures.FileStorage)のリストです。ファイルをいったんローカルに保存`save()`し、それをGoogle Driveへアップロードしてます。
（※ バイナリ変数のままアップロードしたい。そのうちやる。）

最終的に、`create()` でアップロードされるんですが、専用の `MediaFileUpload` インスタンスを作ったりちょっと煩雑ですね。でも調べればすぐ出てくる情報だし、一度やればわかるので、ほかのライブラリを使う必要はないと思っています。

全体的に、Google Driveにオブジェクトを生成するとIDが得られる、そのIDめがけてへアップロードする、という流れのようです。なおIDで管理されているので、ファイル名が被っても別のファイルになります。


# 誤解答（APIキー、OAuth2.0）について

GCPの認証のところの話です。

## 1. APIキー
APIキーは、Google Driveでは使えないようです。ログインせずに誰でも使えるサービス用の簡単なアクセス制御用なので。使う場面は例えば Google Map とかでは使えるようです。

とはいえ、Google Driveのフォルダを「リンクを知っている人は編集可能」な状態でシェアすれば使えるかなーと思ってみたりしたのですが、ダメでした。

なおGoogle DriveのAPIの認証のところで、OAuth2かサービスアカウントが使えますよーとしっかり丁寧に書いてあるんですが、それは後からわかった話ということで。。

## 2. OAuth2
OAuth2は、使用するユーザーが、例えば「このサービスをyo16が使いますよ」と名乗ってその人なりの権限で操作する、というものです。つまり広くログインせず使うサービスの場合は適さないです。

技術的には、OAuth2認証してGoogle Driveへのアクセスをする実装をすると、認証が必要な場面になると Googleのどのアカウントでこのサービスを使いますか？ という確認ダイアログが出ました。


# あとがき
ググった情報が古くて使えなかったり、知識不足で区別できなかったりして、私は苦労しましたが、この情報が誰かの助けになれば。