express * typescript でfirebase storage へのアップロード機能をサクッと実装する

# この記事でわかること

- typescript で express を構築し、ほぼ最小限で fierbase storage へのアップロード機能を実装する方法
- ソースコード（最後に記載

# この記事でわからないこと

- firebase storage のルールを活用した例
- express や firebase の初期化
- firebase auth など firebase storage 以外の機能
- ローカルでアップロード機能を実現する方法
- firebase,typescript, express,multer などの細かい仕様

# 前提

- express と typescript の初期化が完了している
- firebase の初期化が完了している
- 筆者のわたしは express の実務経験はないため、express らしい書き方や誤っている部分などあればコメントください

# 実装

### index.ts

ほぼ基礎的な index の書き方だと思っています。
HTTP ログが出なかったのが辛かったので `morgan` というログ出力ミドルウェアをはさみました。

```index.ts
require("dotenv").config();
const express = require("express");
const app = express();
const morgan = require("morgan");
const PORT = process.env.PORT || 3000;

// middleware
app.use(express.json());
app.use(morgan("dev"));
app.use(express.urlencoded({ extended: true }));

// routes
app.use("/", require("./router.ts"));

app.listen(PORT);
```

### views

#### upload.ejs

めちゃくちゃシンプルなファイルアップロードの HTML。
すがすがしい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262578/4d486f0b-e6a1-a4a2-209e-dabf04fdc3b9.png)

```upload.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>test</title>
    <style>
      div {
        margin-bottom: 20px;
      }
    </style>
  </head>
  <body>
    <div>
      <form action="/upload" method="post" enctype="multipart/form-data">
        <div>
          <input type="file" name="file" />
        </div>
        <div>
          <input type="submit" value="送信する" />
        </div>
      </form>
    </div>
  </body>
</html>
```

### .env

firebase の関数は環境変数で初期化するように実装したので、
.env ファイルをホームに用意して対応する firebase のシークレットを入れておきます。

```.env
PORT=3000
FIREBASE_API_KEY=xxx
FIREBASE_AUTH_DOMAIN=xxx
FIREBASE_PROJECT_ID=xxx
FIREBASE_STORAGE_BUCKET=xxx
FIREBASE_MESSAGING_SENDER_ID=xxx
FIREBASE_APP_ID=xxx
```

### firebase.ts

初期化を行い、uploadFS という関数で firebase のアップロード関数を薄くラップします。

```　firebase.ts
import { initializeApp } from "firebase/app";
import { getStorage, ref, uploadBytes, UploadResult } from "firebase/storage";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
  measurementId: process.env.FIREBASE_MEASUREMENT_ID,
};
initializeApp(firebaseConfig);

export const uploadFS = (
  fileRef: string,
  fileBuf: Blob | Uint8Array | ArrayBuffer
): Promise<UploadResult> => {
  const storage = getStorage();
  const storageRef = ref(storage, fileRef);

  return uploadBytes(storageRef, fileBuf);
};

```

### router.ts

```router.ts
import { Request, Response } from "express";
import multer from "multer";
import { uploadFS } from "./firebase";
const router = require("express").Router();

router.get("/", (req: Request, res: Response) => {
  res.render("./upload.ejs");
});

router.post("/upload", multer().single("file"), (req: any, res: Response) => {
  const fileBuf = req.file.buffer;
  const fileRef = `${process.env.UPLOAD_BUCKET}/${req.file.originalname}`;
  uploadFS(fileRef, fileBuf)
    .then(() => {
      res.redirect("/success");
    })
    .catch((err: any) => {
      res.send(err);
    });
});

router.get("/success", (req: Request, res: Response) => {
  res.send("upload success");
});

module.exports = router;

```

multer を利用して File 形式で受け取るようにして、
先程作った関数に流し込みます。
このとき、バケットは環境変数で設定した `UPLOAD_BUCKET` のバケットにアップロードされます。

また firebase storage でルールがロックモード担っている場合はデフォルトですべてのユーザーの read,weite 権限が拒否されているため、修正する必要があります。

参照
https://firebase.google.com/docs/rules/basics?authuser=0&hl=ja#default_rules_locked_mode

便宜上、以下の画像のようにすべてのユーザーの write,read が可能なガバガバ設定にしました。（当然、実際のプロダクトでは推奨されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262578/725fc370-17df-129b-f6a0-14768b382d25.png)

# 実行

```
ts-node index.ts
```

1. ページを開く

```
open http://localhost:3000/
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262578/dd29ffcf-6999-02d1-b674-ab179de2dda7.png)

2.ファイルを選択して送信

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262578/220b2e3a-dad1-b96a-75d5-7871b8593f26.png)

`upload success` がでれば OK

3. firebase storage でファイルを確認

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3236323537382f66623432363634342d616462642d633030342d313137322d6632396361323764656135342e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262578/07fe4300-4f11-246f-fb76-3060ef3f04a8.png)

## トラブルシューティング

```bash
{"code":"storage/unauthorized","customData":{"serverResponse":""},"name":"FirebaseError","_baseMessage":"Firebase Storage: User does not have permission to access 'bucket-name/hoge.png'. (storage/unauthorized)"}
```

バケットの権限がないので、storage rule を修正する必要があります。詳細については routers.ts の章に記載

# ソースコード

https://github.com/KotaroYamazaki/express-ts-firebase-sample