puppeteerを使ってスクリーンキャプチャをFirebase Storageに保存する

#はじめに
Nodejs環境にて`FirebaseAdmin`と`puppeteer`を用いてWEBページのスクリーンショットを作成し、Firebase Storageに保存するまでの手順と注意点の覚書となります

#環境
- Nodejs

#使用するパッケージ
以下をnpmまたはyarnでインストール

- [firebase-admin](https://www.npmjs.com/package/firebase-admin) : Firebase Storageへのアップロードに使用
- [puppeteer](https://www.npmjs.com/package/puppeteer) : ヘッドレスChrome環境でスクリーンキャプチャを取得するために使用
- [uuid](https://www.npmjs.com/package/uuid) : 画像をダウンロードするためのアクセストークン付与に使用

#手順
## 1. 初期化処理
`firebaseAdmin `の初期化を行います（参考：[スタートガイド](https://firebase.google.com/docs/storage/admin/start?hl=ja)）

```ts:firebaseAdmin.ts
import * as firebaseAdmin from 'firebase-admin';
firebaseAdmin.initializeApp({
  credential: firebaseAdmin.credential.cert({
    privateKey: '**************',
    clientEmail: '**************',
    projectId: '**************',
  }),
  storageBucket: '<BUCKET_NAME>.appspot.com'
});
export default firebaseAdmin
```

## 2. スクリーンキャプチャの作成
`puppeteer`を用いてスクリーンキャプチャをメモリ上に作成します

```ts:capture.ts
import puppeteer from 'puppeteer'

const capture = async ()=>{
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('対象のURL', {
       waitUntil : ['load', 'networkidle2']
   });
   // メモリ上に作成
   const buffer = await page.screenshot();
   
   await browser.close();
}
```
ここでの注意点としては`page.goto`のoptionに`waitUntil : ['load', 'networkidle2']`を指定しています。この指定がなかった場合、ページによっては真っ白の状態（JS等による構築が未完）でキャプチャを作成してしまう場合があります。そのため、`networkidle2`を指定することで通信処理等がアイドルになっていることを完了の目安としています。こちらの詳細については[APIドキュメント](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagegotourl-options)が詳しいです。

## 3. Firebase Storageへのデータ登録
メモリ上に作成したスクリーンキャプチャをFirebase Storageに登録します。

```diff_tsx:capture.ts
import puppeteer from 'puppeteer'
+ import firebaseAdmin from './firebaseAdmin' // 1で初期化したもの
+ import { v4 } from 'uuid';

const capture = async ()=>{
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('対象のURL', {
       waitUntil : ['load', 'networkidle2']
   });
   const buffer = await page.screenshot();

+  const bucket = firebaseAdmin.storage().bucket('バケット名');
+  const savePath = 'Firebase Storageの保存先パス'
+  const bucketFile = bucket.file(savePath)
+  // firebaseStorageDownloadTokensはダウンロードURLを得るために必要　
+  const firebaseStorageDownloadTokens = v4()
+  await bucketFile.save(buffer, {
+     metadata: { 
+          contentType: 'image/png',
+          metadata: {
+              firebaseStorageDownloadTokens
+            }
+      },
+  const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/バケット名/o/${encodeURIComponent(savePath)}?alt=media&token=${firebaseStorageDownloadTokens}`;
+  await browser.close();
}
```
ここでの注意点ですが、

```ts
metadata: {
   firebaseStorageDownloadTokens
}
```
の記述がない場合、ファイルが正しくダウンロードできなくなります。Firebase condole上では次のように画像が延々とローディングが表示される状態となります
（参考：[Firebase Storage - Image preview is permenantly loading](https://stackoverflow.com/questions/60480323/firebase-storage-image-preview-is-permenantly-loading))

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200228/168c1edf-1ce3-dabf-6267-658fdd78eb9b.png)

また、`firebaseStorageDownloadTokens`は`uuid`形式の必要があるため、`uuid`のライブラリを使用してトークンを作成しています
（[Web版のfirebase](https://firebase.google.com/docs/storage/web/start?hl=ja)を用いてStorageにアップロードする場合には、このアクセストークンは自動的に付与されるようです）

# その他注意事項
`puppeteer`はChroniumをダウンロードする点や使用メモリを結構食う点、LambdaやCloud Functionなどの環境で実行する際には注意が必要になります

- [Running Puppeteer on AWS Lambda](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-on-aws-lambda)
    - ダウンロードされるChroniumがLambdaの50MBのサイズ制限を超えてしまうため、[chrome-aws-lambda ](https://github.com/alixaxel/chrome-aws-lambda)等を用いて制限を回避する必要があります
- [Running Puppeteer on Google Cloud Functions](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-on-google-cloud-functions)
    - node10以上を指定する。また別途、Firebase Cloud Functionから実行する際にはメモリ不足でエラーとなる可能性があるため、[割り当てメモリをデフォルトから変更](https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation)しておくことをおすすめします

# 最後に
`capture.ts`の全体のコードは以下のようになります

```ts:capture.ts
import puppeteer from 'puppeteer'
import { v4 } from 'uuid';
import firebaseAdmin from './firebaseAdmin'

const capture = async ()=>{
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   await page.goto('対象のURL', {
       waitUntil : ['load', 'networkidle2']
   });
   const buffer = await page.screenshot();
   const bucket = firebaseAdmin.storage().bucket('バケット名')
   const savePath = 'Firebase Storageの保存先パス'
   const bucketFile = bucket.file(savePath)
   const firebaseStorageDownloadTokens = v4()
   await bucketFile.save(buffer, {
       metadata: { 
           contentType: 'image/png',
           metadata: {
               firebaseStorageDownloadTokens
             }
       },
   })
   const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/バケット名/o/${encodeURIComponent(savePath)}?alt=media&token=${firebaseStorageDownloadTokens}`;
   await browser.close();
}
```