0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

木更津高専Advent Calendar 2024

Day 19

Minio×Hono×Reactをやろうとした経験談を書いておこうと思ふ。

Last updated at Posted at 2024-12-18

この記事は木更津高専 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で作成しましょう

docker-compose.yaml
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を立ち上げてgionsaicontentバケットを作成し、権限情報を読ませてexitで終了しています。
ここのところは、mcコマンドを各自都合の良いように書いてください。

Honoのindex.tsを用意

index.js
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にエンドポイントを追加

まずはコードの全体像です。

minio.ts
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のコードです。

最後に

私事にはなりますが、記事を書いたのが久しぶりで、また記事執筆に当てる時間があまりなかったので適当な出来の記事になってます。
ツッコミどころや質問があれば随時コメント下さい。
時間がある際にもっとわかりやすいように書き換えようと思ってます。

参考にした記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?