Cloud Storage for Firebaseを使ってサーバーでアップロードしてブラウザで受け取るまでの奮闘記

# 概要

Cloud Storage for Firebaseをサーバーから更新しようとしたんですが、またしてもGCPのドキュメントで苦戦。
どのような経緯でブラウザで値を取得できるに至ったかの話になります。


# 読者対象

- 将来的にFunctionsやGCP以外のサーバーからCloud Storageを操作しようとしている方。


# Road to 解決

## またしてもGCPの大海に投げ出される

Cloud Storageを利用したいと思ったので、まずはサーバーからアップロードするため公式ドキュメントを見てみる。
Storage->管理->スタートガイドがサーバーからの更新に関するドキュメント。
（Firestoreの場合、新しい製品のためサーバーもクライアントも同じページに書かれていたが、Storageは古いためかページ構成が異なることに注意）
https://firebase.google.com/docs/storage/admin/start?hl=ja

１ページしかないから嫌な予感しかしない((((；ﾟДﾟ))))ガクブル
”デフォルトのバケットを使用する”に従って、bucket取得までは難なく完了。

>Google Cloud Storage クライアント ライブラリ
Firebase Admin SDK は Google Cloud Storage クライアント ライブラリを介して Cloud Storage にアクセスします。Admin SDK から返されるバケット参照は、これらのライブラリで定義されたオブジェクトです。バケット参照をファイルのアップロードやダウンロードなどで使用する方法については、Google Cloud Storage クライアント ライブラリのドキュメントと API リファレンスをご覧ください。

リンク先に飛んでみると、クイックスタートとして課金を有効にしろだの、APIを有効にしろだのイミフな言葉が並ぶ。
結論としてはFirebaseが裏でGCPを既に設定していて、その辺はスキップしてOK。
必要なのはAdminSDKがGCPのラッパーであるため、どのAPIをどう使えばいいかという話になる。
目次見るとuploadがあるので、これを利用すればいいことはすぐに理解。
https://cloud.google.com/nodejs/docs/reference/storage/2.3.x/Bucket#upload

>```javascript:公式ドキュメントサンプル
>// Imports the Google Cloud client library
>const {Storage} = require('@google-cloud/storage');
>
>// Creates a client
>const storage = new Storage();
>
>/**
> * TODO(developer): Uncomment the following lines before running the sample.
> */
>// const bucketName = 'Name of a bucket, e.g. my-bucket';
>// const filename = 'Local file to upload, e.g. >./local/path/to/file.txt';
>
>// Uploads a local file to the bucket
>await storage.bucket(bucketName).upload(filename, {
>  // Support for HTTP requests made with `Accept-Encoding: gzip`
>  gzip: true,
>  metadata: {
>    // Enable long-lived HTTP caching headers
>    // Use only if the contents of the file will never change
>    // (If the contents will change, use cacheControl: 'no->cache')
>    cacheControl: 'public, max-age=31536000',
>  },
>});
>
>console.log(`${filename} uploaded to ${bucketName}.`);
```

公式ドキュメントの通りに実行すれば、問題なくアップロードは可能。
Firebaseの``` var bucket = admin.storage().bucket() ``` がGCPの ```storage.bucket(bucketName)  ``` であることに気が付く必要があるが。
（私は最初よくわからず、Firebaseのbucketの型の中身を見てから理解した）


## 鞭の次は飴

GCPへの丸投げはFirestoreで経験済みなので、驚きや戸惑いはあったもののクリア。
次はブラウザ側でダウンロードしたいので、今度はクライアント側の公式ドキュメントを見る。
https://firebase.google.com/docs/storage/web/download-files?hl=ja#download_data_via_url

>```javascript:公式ドキュメントサンプル
>storageRef.child('images/stars.jpg').getDownloadURL().then(function(url) {
>  // `url` is the download URL for 'images/stars.jpg'
>
>  // This can be downloaded directly:
>  var xhr = new XMLHttpRequest();
>  xhr.responseType = 'blob';
>  xhr.onload = function(event) {
>    var blob = xhr.response;
>  };
>  xhr.open('GET', url);
>  xhr.send();
>
>  // Or inserted into an <img> element:
>  var img = document.getElementById('myimg');
>  img.src = url;
>}).catch(function(error) {
>  // Handle any errors
>});
>```

サンプルがまさかのXMLHttpRequestか・・・orz
FetchAPIでtryしてみる。
エラーの際の挙動は後に回したらシンプルだ。

```javascript
async function getJson() {
    const pathReference = storage.ref('foo.json');
    const url = await pathReference.getDownloadURL();
    const response = await fetch(url);
    const json = await response.json();
    return json;
}
```

json()でawaitしないといけないの知らなくて少し時間を使ったが(;^_^A、Firebaseドキュメントは楽でいい！


## クライマックスは盛り上げないといけない義務感いらない

人生で後何度見かけるんだろうか・・・こんにちはCORSさんm(__)m

```text:console
No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

それではStorageのCORS設定はどうすればいいのか、公式ドキュメントを探す。
すると意外にもダウンロードページすぐ下の項目にあった・・・

>CORS の構成
ブラウザで直接データをダウンロードするには、Cloud Storage バケットに対してクロスオリジン アクセス（CORS）を構成する必要があります。この操作は gsutil コマンドライン ツールで行えます。ツールはここからインストールできます。

・・・GUIでサクッと設定するか、コードで自動化したい・・・探すか。
ググるとStorageの公式ドキュメントにCORSのページがあったが、gsutil,XML API,JSON APIと深みにはまっていくくく。
https://cloud.google.com/storage/docs/cross-origin?hl=ja#server-side-support
一つ一つリンク見て行ってみると、ようやくまともなページにたどり着く！
https://cloud.google.com/storage/docs/configuring-cors?hl=ja
>バケットの CORS 構成をプログラムで設定または取得するには、Cloud Storage のクライアント ライブラリのいずれかを使用します。詳細は、次のリファレンス コンテンツをご覧ください。
>
>Node.js: Bucket

嫌な予感がしながらも開いてみたが、やっぱりアップロードの時にみたAPIリファレンス( ´Д｀)=3 フゥ
しかもCORSなんて単語一切なし、どこに何を設定すればいいか教えてください(´Д⊂ヽ
あきらめてREST APIでやろうかとも考えたが、Authorizationが必要なのでこれはこれで厄介。
patchで入れるだけなんだがと公式のREST APIを眺める。

```json:公式ドキュメントのREATAPIでの設定値
{
  "location": "us-east-1",
  "storageClass": "regional",
  "cors": [
      {
          "maxAgeSeconds": "3600",
          "method": [
             "GET"
             "HEAD"
             "DELETE"
          ],
          "origin": [
             "http://example.appspot.com"
          ],
          "responseHeader":[
            "Content-Type"
         ]
      }
  ]
}
```

storageClassのAPIはnodeでもあるのに・・・( ﾟдﾟ)ハッ!ということはオブジェクトのどこかに格納されている！
ということで巨大なbucketオブジェクトをチェックしていくと、metadataというプロパティに色々いる！！！
これに設定するのはsetMetadata()でpatch APIを参照のこととある。

https://cloud.google.com/nodejs/docs/reference/storage/2.3.x/Bucket?hl=ja#setMetadata
https://cloud.google.com/storage/docs/json_api/v1/buckets/patch?hl=ja

サンプル見る限り変更したいデータのみの指定でOKそうなので、corsだけ追加してみることに。

```javascript
const setCors = async () => {
    await bucket.setMetadata({
        "cors": [
            {
                "maxAgeSeconds": "3600",
                "method": [
                   "GET",
                   "HEAD"
                ],
                "origin": [
                   "http://localhost:3000",
                   "http://localhost:5000",
                ],
            }
        ]
    });
    console.log("", (await bucket.get())[0]);
}
```

どうせダメなんだろうなと思いつつ、APIコール後すぐにブラウザで値とってみたらあっさり取得OK＼(^o^)／
賢人なら1分、無知なら1時間とはこういうことなんだろうなと(´Д⊂グスン



# まとめ

またしてもGCPにもてあそばれた話でした。
素直にgsutil使っていれば簡単だったろうに、従わなかったのだから自己責任ですね( ´Д｀)=3 フゥ
ただ、高けりゃ高い壁のほうが登ったとき気持ちいいと聞きますが、Firestoreからstorageに移行して、JSのバンドルサイズとキャッシュスピードの面でメリットが大きかったのは収穫でした。
オフライン対応を考えるとFirestore一択でしたが、リアルタイム同期が不要で小さなファイル扱うならstorageとIndexedDBでもありに思えてきました。
Firestoreとstorageの比較はまたいずれかの機会に。