Cloud Functionsなどfirebase-adminでStorageを追加や削除・存在確認をする

開発中の積読用の読書管理サービス「[積読ハウマッチ](https://tsundoku.site)」で、
新しく報告機能をリリースしました:tada:

画面はこんな感じ。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/%E7%A9%8D%E8%AA%AD%E3%83%8F%E3%82%A6%E3%83%9E%E3%83%83%E3%83%81?src=hash&amp;ref_src=twsrc%5Etfw">#積読ハウマッチ</a> で新しく追加した報告機能、<br>OGP画像の生成をしたりしてるので、<br>少し読み込みに時間が必要なときも...<br><br>ちょっとだけ待っていただけるとうれしいです!! <a href="https://t.co/8tLRvU9i4h">pic.twitter.com/8tLRvU9i4h</a></p>&mdash; 積読ハウマッチ📚きらぷか (@kira_puka) <a href="https://twitter.com/kira_puka/status/1163843304598859776?ref_src=twsrc%5Etfw">August 20, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ツイートにも書いてあるとおり、裏では、

1. OGP画像が生成しているかの存在確認
2. 生成してなければ、OGP画像の生成

をしています。

Firebase Functions上でこれらの処理をしているのですが、
firebase-adminを使った場合、Storageの扱いが違ったので、
備忘録的にまとめておきます。

(ImageMagicを使った画像生成とアップロードの記事は[こちら](https://www.memory-lovers.blog/entry/2019/06/26/194500))

#### firebase-adminではGoogle Cloud StorageのAPIが使われる

@hitsuji-hanetaさんの[記事](https://qiita.com/hitsuji-haneta/items/a2ee2eee961ec11d0ada)をみると、クライアント版とは違い、
firebase-adminを使うと、Google Cloud StorageのAPIが使わるよう。

なので、APIリファレンスとかも、
[@google-cloud/storage](https://cloud.google.com/nodejs/docs/reference/storage/2.5.x/)のほうを確認する必要がある。


#### いろいろやってるコード

コードとしては、こんな感じ。

1. ファイルの追加/アップロード: `upload()`
2. ダウンロードURLの取得: `download()`
3. ファイルの存在確認: `exists()`
4. ファイルの削除: `deleteFile()`

で上記の処理をそれぞれおこなっています。

firebase-adminを[ローカルでも実行できる](https://www.memory-lovers.blog/entry/2019/07/25/073000)ので、
今回はFunctionsではなく、そちらのコード例。

```javascript
const admin = require("firebase-admin");

// 配置したサービスアカウントの秘密鍵を取得
const serviceAccount = require("./key/XXXXX.json");

// firebase-adminを初期化
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  // storageBucketはローカルで実行するときのみ必要。
  // Functions上ではFunctions側で設定される
  storageBucket: "your-storage-bucket-name.appspot.com"
});

// storageのbucketのインスタンスを取得
const bucket = admin.storage().bucket();

/**
 * Cloud Storageへのアップロード
 *
 * @param {string} uploadFile アップロードするファイル
 * @param {string} uploadPath アップロード先のStorageのパス
 */
async function upload(uploadFile, uploadPath) {
  await bucket.upload(uploadFile, { destination: uploadPath });
}

/**
 * Cloud Storageへのダウンロード(StoregeのURL取得)
 * 簡易化のため、署名付きURLの取得ではなく、Storage上のURLを生成しています。
 * 
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 * @returns {string} ダウンロードURL
 */
async function download(uploadedPath) {
  const STORAGE_ROOT = "https://firebasestorage.googleapis.com/v0/b";
  const bucketName = bucket.name;
  const dlPath = encodeURIComponent(uploadedPath);
  const dlURL = `${STORAGE_ROOT}/${bucketName}/o/${dlPath}?alt=media`;

  return dlURL;
}

/**
 * Cloud Storage上のファイルの存在確認
 *
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 * @returns {boolean} ファイルの存在有無
 */
async function exists(uploadedPath) {
  const exists = await bucket.file(uploadedPath).exists();
  return exists[0];
}

/**
 * Cloud Storage上のファイルの削除
 *
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 */
async function deleteFile(uploadedPath) {
  await bucket.file(uploadedPath).delete();
}

// ****************************
// * MAIN
// ****************************
async function main() {
  console.log(`***** START MAIN`);

  // アップロード
  const uploadFile = "./ogp_top.png"; // アップロードしたいファイルのパス
  const uploadPath = "ogp/generate_ogp.png"; // アップロード先のStorage上のパス
  await upload(uploadFile, uploadPath);
  console.info(`${uploadFile} is uploaded into '${uploadPath}'.`);

  // ダウンロード
  const uploadedPath = uploadPath;
  const downloadURL = await download(uploadedPath);
  console.info(`downloadURL=${downloadURL}`);

  // ファイルの存在確認
  const isExists = await exists(uploadedPath);
  console.info(`isExists=${isExists}`);

  // ファイルの削除
  await deleteFile(uploadedPath);
  console.info(`${uploadedPath} is deleted.`);

  console.log(`***** END   MAIN`);
}

main().then();
```

ローカルで実行するときの注意点としては、`admin.initializeApp()`時に
`storageBucket`を設定しないといけないこと。
Functions上で実行する場合は、Functions側で設定してくれるので不要です。

```javascript
// firebase-adminを初期化
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  // storageBucketはローカルで実行するときのみ必要。
  // Functions上ではFunctions側で設定される
  storageBucket: "your-storage-bucket-name.appspot.com"
});
```

これで、ローカルで生成したファイルもアップロードできたりします(*´ω｀*)
もちろん、Cloud Functionsでも動くので、冒頭のツイート動画のように、
存在確認＆生成などもできたりヽ(=´▽`=)ﾉ

# おわりに

Nuxt.jsやFirebaseを使いながら、機能追加してるサービスです！
積読総額ランキングや人気の本などもあるので、よかったらあそんでもらえれば!!

積んでる本の総額がわかる読書管理サービス
『[積読ハウマッチ](https://tsundoku.site)』

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="25%"/>

要望・感想・アドバイスなどあれば、
公式アカウント([@MemoryLoverz](https://twitter.com/MemoryLoverz))や開発者([@kira_puka](https://twitter.com/kira_puka))まで〜♪

# 参考にしたサイト
- [Firebase Admin SDKからStorageにアップロードする - Qiita](https://qiita.com/hitsuji-haneta/items/a2ee2eee961ec11d0ada)
- [Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする - くらげになりたい。](https://www.memory-lovers.blog/entry/2019/06/26/194500)
- [ローカルPCからfirebase-adminを使ってFirestoreを操作する(管理ツール) - くらげになりたい。](https://www.memory-lovers.blog/entry/2019/07/25/073000)
- [@google-cloud/storage 2.5.0 » Class: Bucket  |  Node.js  |  Google Cloud](https://cloud.google.com/nodejs/docs/reference/storage/2.5.x/Bucket)
- [@google-cloud/storage 2.5.0 » Class: File  |  Node.js  |  Google Cloud](https://cloud.google.com/nodejs/docs/reference/storage/2.5.x/File)
- [File upload with filestreams and Firebase cloud functions + cloud storage - Stack Overflow](https://stackoverflow.com/questions/50745147/file-upload-with-filestreams-and-firebase-cloud-functions-cloud-storage)