この記事は木更津高専 Advent Calendar 2024参加記事です.
前の前の記事:今年もプロ研が学園祭Webサイト制作をした話 by naotiki
前の記事:祇園祭Webサイト2024のサーバ周りについて by NXVZBGBFBEN
次の記事:Vikeを試してみた by naotiki
はじめに
皆さんメリクリ。ないろきです。
先日私達で作成した、祇園祭webサイトでは画像などを保存するオブジェクトストレージとしてMinIOを使用しました。
その際に、MinioとHonoを組み合わせている記事が少なくとも軽く調べた範囲では見つからなかったので、大まかな経験とやり方を書いた記事をここに残しておこうと思います(まぁ愚直に実装しただけですが。)
祇園祭webに自体についての情報はnaotikiさんの今年もプロ研が学園祭Webサイト制作をした話を参照してください。
使ったjsライブラリ・フレームワーク
- Hono
- react(remix)
- sharp
- S3Client
- Zod
やったこと
とりあえずこの記事では適当なエンドポイントを作成し、jsonに含まれるbase64エンコードされた画像をMiniOに登録することを目指します。
DockerでMiniOの環境構築
試すためにはとにかくMiniOの環境が必要なのでサクッとDockerで作成しましょう
services:
minio:
image: minio/minio:RELEASE.2024-08-29T01-40-52Z
container_name: minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: root
MINIO_ROOT_PASSWORD: password
command: server /data --console-address :9001
volumes:
- "minio_bucket:/data"
minio_mc:
image: minio/mc:RELEASE.2024-08-26T10-49-58Z
depends_on:
- minio
entrypoint: >
/bin/sh -c "
until (mc alias set myminio http://minio:9000 root password) do echo '...waiting...' && sleep 1; done;
mc mb myminio/gionsai;
mc mb myminio/content;
mc anonymous set download myminio/gionsai;
mc anonymous set download myminio/content;
mc admin user add myminio hono_client firefire;
mc admin policy create myminio hono /policy/hono.json;
mc admin policy attach myminio hono --user hono_client;
echo '終了';
exit 0;
"
volumes:
- "./minio/policy:/policy"
volumes:
minio_bucket:
minio自体を立ち上げたあと、mcを立ち上げてgionsai
とcontent
バケットを作成し、権限情報を読ませてexitで終了しています。
ここのところは、mcコマンドを各自都合の良いように書いてください。
Honoのindex.tsを用意
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { env } from "~/lib/env";
import { minioRoute } from "./minio";
const app = new Hono().basePath("/api");
const route = app
.get("/", (c) => {
return c.text("Hello Hono!");
})
.route("/minio", minioRoute);
const port = Number(env.PORT) || 3000;
console.log(`Server is running on port ${port}`);
serve({
fetch: app.fetch,
port,
});
export type ApiType = typeof route;
必要最低限な状態のHonoのエンドポイントです。
.route("/minio", minioRoute);
この部分でminioのRouterを登録しています。
Honoにエンドポイントを追加
まずはコードの全体像です。
import { Buffer } from "node:buffer";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { zValidator } from "@hono/zod-validator";
//MinIOのExampleです
import { Hono } from "hono";
import sharp from "sharp";
import { z } from "zod";
import { env } from "~/lib/env";
const schema = z.object({
text: z.string(),
image: z.string(),
});
const minio = new S3Client({
region: "ap-northeast-1",
endpoint: env.MINIO_ENDPOINT || "",
forcePathStyle: true, // MinIO利用時には必要そう。
credentials: {
accessKeyId: env.MINIO_USER || "",
secretAccessKey: env.MINIO_PASSWORD || "",
},
});
const db = [];
export const minioRoute = new Hono().post(
"/",
zValidator("json", schema.pick({text:true,image:true})),
async (c) => {
try{
const data = c.req.valid("json");
const fileData = data.image.replace(/^data:\w+\/\w+;base64,/, "");
const decodedBuffer = Buffer.from(fileData, "base64");
const image_file = await sharp(decodedBuffer).toFormat("webp").toBuffer();
db.push("test")
await minio.send(new PutObjectCommand({
Bucket: "content",
Key: `${(db.length + 1).toString()}.webp`,
Body: image_file,
}),
);
return c.json({ message: "さくせす" }, 201);
}catch(err){console.log(err)
return c.json({message:"異常!"},500);
}
},
);
上から順番にかいつまんで説明していきます
const minio = new S3Client({
region: "ap-northeast-1",
endpoint: env.MINIO_ENDPOINT || "",
forcePathStyle: true, // MinIO利用時には必要そう。
credentials: {
accessKeyId: env.MINIO_USER || "",
secretAccessKey: env.MINIO_PASSWORD || "",
},
});
上記のコードでS3Clientを利用しMiniOと接続するためのインスタンスを作成してます。
export const minioRoute = new Hono().post(
"/",
zValidator("json", schema.pick({text:true,image:true})),
async (c) => {
try{
const data = c.req.valid("json");
const fileData = data.image.replace(/^data:\w+\/\w+;base64,/, "");
const decodedBuffer = Buffer.from(fileData, "base64");
const image_file = await sharp(decodedBuffer).toFormat("webp").toBuffer();
db.push("test")
await minio.send(new PutObjectCommand({
Bucket: "content",
Key: `${(db.length + 1).toString()}.webp`,
Body: image_file,
}),
);
return c.json({ message: "成功" }, 201);
}catch(err){console.log(err)
return c.json({message:"異常発生"},500);
}
},
);
Routerの部分です。
POSTで受け取ったjsonからZodを活用しつつkeyがimage
なvalueを取り出しbase64エンコードされた文字列を、Buffer形式に変えて、Sharp(ライブラリ)を使いwebpに変更し、minio.sendの部分でcountentバケットに適当なkeyでファイルを登録しています。
あくまでもこれは雑なサンプルなので適宜使いやすいように書き換えてください。
画像をHonoに投げるための簡易的なページの作成
import type React from "react";
import { useState } from "react";
export default function Page() {
const [file, setFile] = useState<string | null>(null);
const onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files?.[0]) {
const fr = new FileReader();
fr.onload = (e) => {
if (e.target) {
setFile(e.target.result);
alert("Fileのbase64化成功");
}
};
fr.readAsDataURL(files[0]);
}
};
const onClickSubmit = async () => {
if (!file) {
alert("Fileが未選択");
return;
}
try {
const res = await fetch("http://localhost:3000/minio", {
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ image: file, text: "text" }),
});
const json = await res.json();
console.log(json);
} catch (err) {
console.error(err);
}
};
return (
<>
<h1 className={"text-3xl text-center mt-4"}>MinioのExample</h1>
<input name="file" type="file" accept="image/*" onChange={onChangeFile} />
<button
className={
"bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
}
disabled={!file}
onClick={onClickSubmit}
>
Submit
</button>
</>
);
}
ファイルを登録するためのtailwindで最低限見た目を整えたreactのコードです。
最後に
私事にはなりますが、記事を書いたのが久しぶりで、また記事執筆に当てる時間があまりなかったので適当な出来の記事になってます。
ツッコミどころや質問があれば随時コメント下さい。
時間がある際にもっとわかりやすいように書き換えようと思ってます。
参考にした記事