🔥

Hono Storageというuploadミドルウェアライブラリを作っている話

2023/12/07に公開

この記事は、Hono Advent Calendar 2023 の 7 日目の記事です。

https://qiita.com/advent-calendar/2023/hono

今回は私が 9 月から開発を進めている Hono Storage というライブラリについて紹介します。

開発の背景

Hono は Express の代替として注目を集めている Web フレームワークです。
多くのインフルエンサーが Hono を推していて、Express から Hono に移行しましょうというムーブが一部界隈で流行りつつあります。

そんなムーブの影響か Express からの移行を考えてるユーザーからたくさんの「Express にはこういうのがあるんだけど、Hono にはないの?」という質問が寄せられています。

最近だと Express の Helmet は Hono の Secure Headers Middleware という形で Watany さんによってもたらされました。

https://zenn.dev/watany/articles/c25bb4fef78c3d

そんな中、Hono の Issue や Discuss を眺めていると、Express の multer というミドルウェアが欲しいという声が多く寄せられていることに気づきました。

https://github.com/honojs/hono/issues/848

https://github.com/honojs/hono/issues/1069

https://github.com/orgs/honojs/discussions/1042

Hono 作者の yusukebe さんが今年の JS Conf で話されていたこちらでも述べられていましたが、WebStandard でないものの 1 つに「ファイルシステム」があります。

https://speakerdeck.com/yusukebe/the-power-of-web-standards

各ランタイムごとにファイルシステスへのアクセス方法が違う

  • Deno Deno.open()
  • Bun Bun.file()

そもそもファイルシステムがない

  • Cloudflare Workers
  • Vercel Edge
  • AWS Lambda

ファイルシステムではないストレージ

  • Cloudflare KV / R2
  • Vercel Blob
  • AWS S3

Hono は WebStandard に準拠することを 1 コンセプトとして掲げている以上、Hono 内ではこの差分を解決することが難しいはずです。

Hono には Hono/middleware という Hono の third-party ライブラリをまとめたリポジトリがあります。
このリポジトリ内に含まれているライブラリはすべてランタイム依存なく動作するものばかりです。[1]

アップロードミドルウェアはそれぞれランタイムの違いを吸収する必要があるため、Hono/middleware に含めるのは難しいと考え、Hono のサードパーティライブラリとして、ファイルシステムに依存しない upload ミドルウェアライブラリを作ることにしました。

コンセプト

Hono Storage を作るにおいて意識していることは以下の通りです。

  • 外部ストレージに依存しないインターフェース
    • 外部ストレージへのアクセス方法は内部的に吸収
    • 同じ API でアクセスできるようにする
  • Multer からの移行を容易にする
    • Multer の API を参考にする
    • Multer のオプションを参考にする
  • 拡張性を持たせる
    • 様々な外部ストレージがあり、これからも増えていく
    • それらのストレージに対応できるように拡張しやすい設計にする

仕様

12/7 時点での仕様は以下の通りです。

インターフェース

ストレージ定義

ストレージを定義するには、以下のように HonoStorage クラスをインスタンス化します。

その際に、ストレージの定義をパラメータとして渡します。

import { HonoStorage } from "@hono-storage/core";

const storage = new HonoStorage({
  storage: (c, files) => {
    // 例えはここでfor await ofでfs.writeFileする
  },
});

ミドルウェア

Multer にもあるような upload ミドルウェアは以下のように定義します。

const app = new Hono();

app.post("/upload", storage.single("hoge"), (c) => {
  // c.var.files.hoge から File オブジェクトを取得できる
});

storage.singlestorage に定義したストレージを使って、単一のファイルをアップロードするミドルウェアを作成します。引数には、ファイルのフィールド名を指定 (ここでは hoge) しています。

つまりこういうフォームを送信すると、storage に定義したストレージを使って、hoge というフィールド名のファイルをアップロードできます。

<form action="/upload" method="POST" enctype="multipart/form-data">
  <input type="file" name="hoge" required />
  <button type="submit">Upload</button>
</form>

multer は single, array, fields, none という 4 つのミドルウェアを提供しています。
対して Hono Storage は single, multiple, fields の 3 つのミドルウェアを提供します。array -> multiple です。

none はファイルをアップロードしない場合でも multipart/form-data をパースするためのミドルウェアです。
しかし、Hono 内部に c.req.parseBody というメソッドがあるので、Hono Storage では提供しません。

ファイルオブジェクト

素のファイルオブジェクトでは拡張子やファイル名などの情報が不足しているため、Hono Storage では Hono Storage File (HSF) というファイルオブジェクトを提供します。

const hsfFile = c.var.files.hoge;

hsfFile.originalname; // ファイル名
hsfFile.extension; // 拡張子

プラグインストレージ

現在、Hono Storage は以下のプラグインを提供します。

  • @hono-storage/node-disk
  • @hono-storage/s3
  • @hono-storage/memory

Cloudflare R2 が S3 互換 API を提供しているので、Cloudflare R2 にも対応できます。

使い方

Hono JSX と Node Disk Storage を使った例です。

import { serve } from "@hono/node-server";
import { HonoDiskStorage } from "@hono-storage/node-disk";
import { Hono } from "hono";

const app = new Hono();
const storage = new HonoDiskStorage({
  dest: "./uploads",
  filename: (_, file) => `${file.originalname}-${Date.now()}.${file.extension}`,
});

app.post("/upload", storage.single("hoge"), (c) => {
  console.log(c.var.files.hoge);
  return c.html(
    <>
      <h1>Uploaded</h1>
      <a href="/">Back</a>
    </>,
  );
});

app.get("/", (c) => {
  return c.html(
    <form action="/upload" method="POST" enctype="multipart/form-data">
      <input type="file" name="hoge" required />
      <button type="submit">Upload</button>
    </form>,
  );
});

serve(app);

これで、http://localhost:3000 にアクセスするとファイルをアップロードできます。
アップロードされたファイルは ./uploads に保存されます。

Hono JSX 便利ですね。

進捗

Hono Storage は multer からの移行容易性を重視しているため、なるべく挙動を multer と合わせるようにしたいという思いがあります。

https://zenn.dev/monica/scraps/27aa8700c71131

そのため、現在は multer の挙動を確認しながら Hono Storage の実装を近いものに修正していくという作業を進めています。

S3 Storage の署名付きファイルアップロードに対応や Deno Disk Storage の実装なども進めています。
(deno.land/x がモノレポと相性悪そうなのでどうやって公開するかは検討中です。いい案があれば教えてください)

展望

今後は Hono Storage をアップロードだけでなくストレージ全体を抽象化するものとして拡張していきたいと考えています。
いつか、アップロードだけでない様々なオペレーションをランタイムや外部ストレージなどを気にせず開発できるよう抽象化したいです。
呼ぶプラグインを変えるだけで移行完了みたいなのができたら最高ですよね。

最後に

私の個人的な考えですが、Hono Storage に関わらず、Hono コミュニティを盛り上げていくには周辺ツールやライブラリによるエコシステムの充実が必要だと考えています。既存の資産をいかに引き継げるかという視点を持ってこれからも Hono Storage を開発していきたいと思っています。

現在、Hono Storage は Under Development です。まだまだ開発途中であり、仕様も変わる可能性があります。
皆さんの使用感のレビューや、Issue があるととても心強いです。もし興味があればよろしくお願いします!

脚注
  1. のちに yusukebe さんへ確認したところ、third-party であればランタイム依存でも良いとのことでした。一応 Hono/middleware へ移譲することも検討しています。 ↩︎

GitHubで編集を提案

Discussion