Cloud FunctionsでPDFを作成 => Firebase Storageにアップロード => 署名付きURLを返す方法

仕事でCloud functionsを用いてPDF作成機能を実装することになったので、その時のメモです。

やりたかったこと
1. Cloud functionsにてonCallでデータを受け取る
2. 受け取ったデータを元にPDFを作成する
3. 作成したPDFをfirebase storageに保存する
4. 保存したファイルのダウンロードリンクを返す

では、いきましょう。

## 全体のコード
```typescript
module.exports = functions
  .runWith(Constant.runtimeOptions)
  .region("asia-northeast1")
  .https.onCall(async (data, context) => {
    if (!context.auth) {
      console.error("認証されていないユーザーからのアクセスです。");
      throw new functions.https.HttpsError(
        "failed-precondition",
        "認証されていないユーザーからのアクセスです。"
      );
    }

    const noData = " -";
    let sample1 = data.sample1;
    let sample2 = data.sample2;
    
    try {
      // A4サイズのPDFでマージンを50にする
      const doc = new pdf({ size: "A4", margin: 50 });

      // データをメモリで受け取るStreamを初期化
      const memoryStream = new MemoryStream(null, {
        readable: false,
      });

      // docにmemoryStreamを設定
      doc.pipe(memoryStream);

      // PDFで用いるフォントを読み込む
      doc.font("fonts/ipaexm.ttf");

      // y軸
      let currentYAxis = 0;

      // タイトル
      currentYAxis += 80;
      doc.fontSize(18).text("詳　細", 0, currentYAxis, {
        width: 600,
        align: "center",
      });

      // サブタイトル
      currentYAxis += 70;
      doc.fontSize(14).text("サブタイトル", 50, currentYAxis);

      // サンプル１
      if (typeof sample1 !== "string") {
        sample1 = noData;
      }
      currentYAxis += 30;
      buildTitleValueTextWithLine({
        doc: doc,
        title: "サンプル１",
        value: sample1,
        yAxis: currentYAxis,
        LineFromXAxis: 140,
        lineToXAxis: 250,
      });

      // サンプル２
      if (typeof sample2 !== "string") {
        sample2 = noData;
      }
      currentYAxis += 30;
      buildTitleValueTextWithLine({
        doc: doc,
        title: "サンプル２",
        value: sample2,
        yAxis: currentYAxis,
        LineFromXAxis: 140,
        lineToXAxis: 250,
      });

      // 書き込み完了
      doc.end();

      let isCreatePdfEnded = false;
      let url: any = "";
      doc.on("end", async () => {
        const fileName = `sample.pdf`;
        
        // メモリーに書き込んだデータをバッファーにいれる
        const buffer = Buffer.concat(memoryStream.queue);

        // firebase storageへの参照を作成
        const file = admin
          .storage()
          .bucket("bucket_name")
          .file(`directory/${fileName}`);

        // fileの保存
        await file.save(buffer, {
          metadata: { contentType: "image/pdf" },
        });

        // 認証用トークン付きのダウンロードURLを取得
        url = await file.getSignedUrl({
          action: "read",
          expires: moment().add(2, "minutes").toDate(),
        });

        // PDF作成完了フラグをtrueに
        isCreatePdfEnded = true;
      });

      // PDF作成完了フラグがtrueになるまで1秒ずつ待ち続ける
      while (!isCreatePdfEnded) {
        await sleepByPromise(1);
      }

      // urlは配列で返ってくるため、stringとして返す
      return {
        url: url[0],
      };
    } catch (e) {
      console.error(e);
      throw new functions.https.HttpsError(
        "internal",
        "PDF出力中にエラーが発生しました。"
      );
    }
});

function buildTitleValueTextWithLine({
  doc,
  title,
  value,
  yAxis,
  LineFromXAxis,
  lineToXAxis,
}: {
  doc: any;
  title: string;
  value: string;
  yAxis: number;
  LineFromXAxis: number;
  lineToXAxis: number;
}) {
  // x軸が50で、y軸がyAxisの場所からフォント10pxの文字をかく
  doc.fontSize(10).text(title, 50, yAxis);

  // x軸が140で、y軸がyAxisの場所からフォント10pxの文字をかく
  doc.fontSize(10).text(value, 140, yAxis);

  // x軸がLineFromXAxisからlineToXAxis、y軸がyAxis + 15の範囲に下線を引く
  doc
    .moveTo(LineFromXAxis, yAxis + 15)
    .lineTo(lineToXAxis, yAxis + 15)
    .stroke();
}

export const sleepByPromise = function (sec) {
    return new Promise(resolve => setTimeout(resolve, sec * 1000));
}
```

## 用いているライブラリ
- [moment.js](https://momentjs.com/)
- [memorystream](https://www.npmjs.com/package/memorystream)
- [pdfkit](https://pdfkit.org/)