【Google Cloud Platformの使い方】GCP Storage にファイルの出し入れ(python3/Web API)

今日は、Google Cloud Storage にファイルをupload するところを復習したので、そのメモを書きます。

# 準備

データはBucket というラベルの単位で管理します。ここにファイルをホイホイ入れたり出したりします。

簡単に使う分にはCloud Storage の[クイックスタート](https://cloud.google.com/storage/docs/quickstart-console?hl=ja)をフォローするだけで十分です。

## bucket の作成

コマンドラインからも作れますが、console.cloud.google.com から行ったほうが便利です。何度も作るものではないと思うので。

コンソールのStorage のブラウザから、「バケット作成」にたどり着けます。下記の画面では、私が作った（塗りつぶした）bucket のほかに、最初から xxxx.artifacts.projectid.appspot.com という名前のbucket がありました。これは開くと分かりますが、Cloud Build で作成したdocker image が置かれていました。（今回は関係ありません）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294040/e0619ee6-5049-cdd0-b073-cdd0c35648e5.png)


## アクセス権の設定

どうやらbucket 単位でアクセス権を設定することができるようです。
全てのユーザに与えたり、特定にユーザにしたりできます。
個別のユーザだけでなく、allUsers, allAuthentificatedUsers が選べます。
おそらく[入門ガイド](https://cloud.google.com/storage/docs/how-to?hl=ja
)にある「機能の使用」や「アクセスの制御」をじっくり理解しなければならないのだと思いますが、今回は精読をスキップして、以下のようにデフォルトのサービスアカウントに権限を割り当てて、そのアカウントのキーを使用しました。

### サービスアカウントとキー

CREDENTIAL の扱い方については、いろいろな方法が用意されていますが、私はもともとこのプロジェクトにデフォルトで作成されているサービスアカウントを使うことにしました。
「ＩＡＭと管理」の「サービスアカウント」をみると、10000000000-compute@developer.gserviceaccount.com のようなCompute ENgine default service account がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294040/e97e7540-6735-cb5d-f000-1fc41559b714.png)

- ~~GCPで動かすときはこのアカウントが使われるらしい。ので、これに「ストレージオブジェクト管理者」のロールを与えます。さらに、作成したbucket にも権限を与えます。~~ (←これは違うかも。今は、キーファイルを使って運用しています。。。)
- ローカルで動かすときは、このサービスアカウントの鍵を作成し、これを環境変数GOOGLE_APPLICATION_CREDENTIALSに設定して利用します。

鍵は「サービスアカウント」のページの編集から鍵生成の画面に入れます。流れに従って鍵を生成すると myproject-0123456789012345.json のようなファイルが生成されるので、これを保存します。環境変数に設定します。

```
export GOOGLE_APPLICATION_CREDENTIALS=/...your_dir/myprojectid-0123456.json
```

### サービスアカウントにbucket へのアクセス権を付与

bucket へのアクセスするには、使用するサービスアカウントがその権限を持っているかで制御します。今は、自分が作ったbucket に使用するサービスアカウントを追加しました。

先ほどと同じストレージブラウザで対象となるbucket を選択し、「バケットの権限を編集」を起動します。「ユーザの追加」から先ほどのサービスアカウントを選択し、「ロールの追加」でCloud Storage の「ストレージオブジェクト管理者」を選択して保存しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294040/e6d280ad-d576-bf22-6dc9-289d3c71894e.png)

その他いろいろごちゃごちゃやったので、いまいちクリアにはなっていないので、もしかしたら少し違うかもしれませんが、私はこれで動いています。

[サーバー間での本番環境アプリケーションの認証の設定](https://cloud.google.com/docs/authentication/production?hl=ja#windows)を参考にしました。


# python によるファイルの出し入れの実装

google-cloud-storage を pip でインストールします。
https://pypi.org/project/google-cloud-storage/
ここでは全てpython3 で実装と動作確認を行っています。

## Storage にアクセスするClient 

pythonの実装では、まず、Storage へのClient を作成します。
CREDENTIAL の設定ができていれば、以下でclient が作成されるはずです。直接にkeyファイルを指定するには``` storage.Client.from_service_account_json()```も使えますが、最近は上記のように環境変数を利用するほうがサーバとローカルとで同じ関数で書けるので使わなくなりました。

```python
import google.cloud.storage as storage
from os import environ
def test_bucket():
    print("GOOGLE_APPLICATION_CREDENTIALS={}".format(environ.get("GOOGLE_APPLICATION_CREDENTIALS")))
    client = storage.Client()
    for bucket in client.list_buckets():
        print(bucket)
```

今一つbucket を作っていたので、以下のようになります。

```
GOOGLE_APPLICATION_CREDENTIALS=myserveraccountkey.json
<Bucket: asia.artifacts.my-project.appspot.com>
<Bucket: my-bucket>
```

### file のupload

あとは、例文にあるように実装するだけです。が、[APIの解説](https://googleapis.dev/python/storage/latest/blobs.html)ではupload_from_file の引数がfile_obj ですが、私が動かしている範囲ではfile のpath を引数にしないとダメなようです。また、この状態だと、同じファイル名だと上書きを行っています。

```python
import google.cloud.storage as storage
from os import path

def upload_file(local_fpath:str, bucket_name:str, remote_fpath:str):
    client = storage.Client()

    bucket = client.lookup_bucket(bucket_name)
    assert isinstance(bucket, storage.bucket.Bucket)

    bucket = client.get_bucket(bucket_name)

    if not path.isfile(local_fpath):
        return ""
    blob = bucket.blob(remote_fpath)
    if blob is not None:
        blob.upload_from_filename(local_fpath)
        url = "https://console.cloud.google.com/storage/browser/{}/{}".format(bucket_name, remote_fpath)
        return url
    return ""
```



### file の download 

同様にしてdownload もできます。ここでもdownload_to_fileの引数はファイルのパスで動いています。

```python
import google.cloud.storage as storage
def download_file(bucket_name:str, remote_fpath:str, local_fpath:str):
    client = storage.Client()
    bucket = client.lookup_bucket(bucket_name)
    blob = bucket.blob(remote_fpath)
    if blob is not None:
        blob.download_to_filename(local_fpath)
```

最低限のことはできるようになりました。

# WEB APIの利用

実はpython API で実装している中身はREST APIを使って通信をしているらしいです。今回は、ファイルの存在だけ確認しました。使い方は、[APIリファレンスのobject/list](https://cloud.google.com/storage/docs/json_api/v1/objects/list)に書かれています。 

```
GET https://storage.googleapis.com/storage/v1/b/<bucket-name>/o
```
## 解説ページでパラメータの動作確認ができる

この解説ページのある"try it"で簡単に試すことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294040/f411f335-737c-1090-2d99-e08860a238f0.png)

ここでは簡単に動きました。Storage のbucket の所定のprefix (フォルダ)にあるはずのファイル名が返ってきました。

## OAUth 2.0 Playground (開発者向けの動確ページ)

ですが、実際にターミナルで動かすにはまた一苦労です。OAuth のaccess token が必要でした。

OAuthを利用したリクエストは、[OAUth 2.0 Playground](https://developers.google.com/oauthplayground/)で試すことができます。token を作成し、http request を試すことができます。token 発行までの流れはここの手順に従っていけば良く、gcloud auth login と同じようにアカウントが認証します。

https://developers.google.com/oauthplayground/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294040/f0e67243-7533-05a4-0259-c5522e583953.png)

## WEB APIで確認

ここで実行したリクエストをcurl でも動作することを確認しました。my-bucket-name というbucket にあるtest/dir01 以下にあるファイル名を取得します。access token は先に取得したもので、ya29.で始まる文字列でした。

```
curl 'https://storage.googleapis.com/storage/v1/b/my-bucket-name/o?prefix=test%2Fdir01' \
    --header 'Authorization: Bearer ya29.accesstokenislooooooooooooooooong' \
    --header 'Accept: application/json' \
    --compressed

{
  "kind": "storage#objects",
  "items": [
    {
      "kind": "storage#object",
      "id": "mqtt-log-test/test/dir01/my-filename/1234567890123456",
      "selfLink": "https://www.googleapis.com/storage/v1/b/my-bucket-name/o/test%2Fdir01%2Fmy_filename",
      "mediaLink": "https://storage.googleapis.com/download/storage/v1/b/my-bucket-name/o/test%2Fdir01%2Fmy_filename?generation=1588578408260925&alt=media",
      "name": "test/dir01/my-filename",
      "bucket": "my-bucket-name",
      "generation": "1234567890123456",
      "metageneration": "1",
      "contentType": "application/x-tar",
      "storageClass": "STANDARD",
      "size": "192852",
      "md5Hash": "vLES9qETsDyglngtaBsrdw==",
      "crc32c": "XxXxXx==",
      "etag": "CL3K493bmekCEAE=",
      "timeCreated": "2020-05-04T07:46:48.260Z",
      "updated": "2020-05-04T07:46:48.260Z",
      "timeStorageClassUpdated": "2020-05-04T07:46:48.260Z"
    }
  ]
}
```

# 便利なgsutil 

実は急いでダウンロードしたときはgsutil を使っています。早くて便利だ。

```
gsutil -m cp gs://bucket_name/......./* .
```

# 所感

とりあえずファイルの出し入れはできるようになりました。また、権限の問題も最低限のところはクリアできた感じです。自分はpython の実装も十分に楽ですが、REST APIの方がもっと楽だという話も聞きます。

ぱらぱら見ていると、
- データを暗号化できる
- ユーザごとにアクセス権や暗号化キーを設定できる
など、ＡＰＩドキュメントや解説を見ていても、いろいろ使い方あるようです。

- [Cloud Storage 向けの Cloud IAM リファレンス](https://cloud.google.com/storage/docs/access-control/iam-reference?hl=ja)
- [python API refrence](https://googleapis.dev/python/storage/latest/client.html)
- [REST API](https://cloud.google.com/storage/docs/json_api/v1)

ふー。
(2020/05/04)

# 追記
- (2020/05/05) 実際にbucket にファイルをupload するとき、"dir01/dir02/file"ならＯＫですが、"dir01/dir02//file" はＮＧでした。
- (2020/05/06) サービスアカウントにあった長い数字はPROJECT NUMBER らしい。これはシェルからは ```gcloud projects describe ${PROJECT_ID} --format='get(projectNumber)'``` で取得できる。
- (2020/05/06) 権限のところ、勘違いがあったかもしれないので、取り消し線を入れました。
- (20200615) gsutil を追加