File
とかBlob
とかBuffer
とか何それ?状態だったので整理
本記事ではFile
は画像ファイルを想定しています
1. クライアントPCからファイルを添付
<template>
<div>
<input type="file" accept="image/*" @change="fileSet" />
</div>
</template>
<script setup lang="ts">
const file = ref<File | null>(null);
const fileSet = (e: Event) => {
if (!(e.target instanceof HTMLInputElement)) return;
if (!e.target.files?.[0]) return;
file.value = e.target.files?.[0];
};
</script>
ここではFile
として取得可能
2. フロントからバックエンドにファイルを送信
フロント側
<script setup lang="ts">
const upload = async () => {
if (!file.value) return;
const formData = new FormData();
formData.append("file", file.value);
await $fetch("/api/upload", {
method: "POST",
body: formData
});
}
</script>
~/server/api/upload.ts
export default defineEventHandler(async (event) => {
const formData1: FormData = await readFormData(event);
const file: FormDataEntryValue | null = formData1.get('file');
// FormDataEntryValue: File | string
if (!(file instanceof File)) return;
const formData2: MultiPartData[] | undefined = await readMultipartFormData(event);
// MultiPartData: {
// data: Buffer;
// name?: string;
// filename?: string;
// type?: string;
// }
});
フロントからのformData
を受け取るにはreadFormData
とreadMultipartFormData
が使えるらしい
違いは未調査
3. server/apiから別のserver/apiにファイルを受け渡す
3-1. File
を再度FormData
にして投げる
うまくいかなかった
~/server/api/upload.ts
...
const newFormData = new FormData();
newFormData.append("file", file);
await $fetch("/api/hoge", {
method: "POST",
body: newFormData,
});
~/server/api/hoge.ts
export default defineEventHandler(async (event) => {
const formData = await readFormData(event);
const file = formData.get("file");
console.log(file);
// [nuxt] [request error] [unhandled] [500] Cannot read properties of undefined (reading 'get')
// at Object.handler (<projectroot>\server\api\s3\fileUpload.ts:7:1)
// at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
const forms = await readMultipartFormData(event);
console.log(forms);
// undefined
});
event
の中身を見たところデータがちゃんとセットできていなさそう(原因不明)
~/server/api/hoge.ts
console.log(event)
// H3Event {
// __is_event__: true,
// node:
// { req:
// IncomingMessage {
// __unenv__: undefined,
// _events: [Object: null prototype] {},
// _maxListeners: undefined,
// readableEncoding: null,
// readableEnded: true,
// readableFlowing: false,
// readableHighWaterMark: 0,
// readableLength: 0,
// readableObjectMode: false,
// readableAborted: false,
// readableDidRead: false,
// closed: false,
// errored: null,
// readable: false,
// destroyed: false,
// aborted: false,
// httpVersion: '1.1',
// httpVersionMajor: 1,
// httpVersionMinor: 1,
// complete: true,
// connection: [Socket],
// socket: [Socket],
// headers: [Object],
// trailers: {},
// method: 'POST',
// url: '/api/hoge',
// statusCode: 200,
// statusMessage: '',
// body: [FormData],
// originalUrl: '/api/hoge' },
// res:
// ServerResponse {
// __unenv__: true,
// _events: [Object: null prototype] {},
// _maxListeners: undefined,
// writable: true,
// writableEnded: false,
// writableFinished: false,
// writableHighWaterMark: 0,
// writableLength: 0,
// writableObjectMode: false,
// writableCorked: 0,
// closed: false,
// errored: null,
// writableNeedDrain: false,
// destroyed: false,
// _data: undefined,
// _encoding: 'utf-8',
// statusCode: 200,
// statusMessage: '',
// upgrading: false,
// chunkedEncoding: false,
// shouldKeepAlive: false,
// useChunkedEncodingByDefault: false,
// sendDate: false,
// finished: false,
// headersSent: false,
// strictContentLength: false,
// connection: null,
// socket: null,
// req: [IncomingMessage],
// _headers: {} } },
// web: undefined,
// context:
// { _nitro: { routeRules: {} },
// nitro: { errors: [] },
// matchedRoute: { path: '/api/hoge', handlers: [Object] },
// params: {} },
// _method: 'POST',
// _path: '/api/hoge',
// _headers: undefined,
// _requestBody: undefined,
// _handled: false,
// _onBeforeResponseCalled: undefined,
// _onAfterResponseCalled: undefined,
// fetch: [Function (anonymous)],
// '$fetch': [Function (anonymous)],
// waitUntil: [Function (anonymous)],
// captureError: [Function (anonymous)] }
3-2. File
をBuffer
にして投げる
~/server/api/upload.ts
...
const form = await readFormData(event);
const file = form.get("file");
if (!(file instanceof File)) throw new Error("400 Bad Request");
const arrBuf = await file.arrayBuffer();
const buf = Buffer.from(arrBuf);
const forms = await readMultipartFormData(event);
if (!forms || forms.length !== 1) throw new Error("400 Bad Request");
const buf2 = Buffer.from(forms[0].data);
await $fetch("/api/hoge", {
method: "POST",
body: { buf }, // { buf: buf2 }
});
~/server/api/hoge.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// {
// buf: {
// type: 'Buffer',
// data: [123, ...]
// }
// }
const buf = Buffer.from(body.buf.data);
console.log(buf);
// <Buffer ...
});
3-3. File
をBase64
エンコードして投げる
Buffer.toString("base64")
してbodyに入れるだけ
~/server/api/upload.ts
...
const base64str = buf.toString("base64");
await $fetch("/api/hoge", {
method: "POST",
body: { base64str },
});
base64は元ファイルを3bitごとに4bitのデータに変換する等の関係で
ファイルサイズが約1.37倍になるらしいので注意
4. 受け取ったAPI側でFile
に戻す
4-1. Buffer
をFile
に変換
const body = await readBody(event);
const buf = Buffer.from(body.buf.data);
const file = new File([buf], "test.txt", { type: body.buf.type });
4-2. base64
をFile
に変換
const body = await readBody(event);
const base64str = body.base64;
const buf = Buffer.from(base64str, "base64");
const file = new File([buf], "test.txt", { type: "image/jpg" });
※base64
に変換して渡すとtype
の情報が落ちるので注意
5. バックエンドからフロントにFile
を返す
例としてS3からGetObjectCommand
で取得した画像をフロント側に返すことを考える
GetObjectCommand
のres.Body
は以下の型
(IncomingMessage | Readable) & SdkStreamMixin
interface SdkStreamMixin {
transformToByteArray: () => Promise<Uint8Array>;
transformToString: (encoding?: string) => Promise<string>;
transformToWebStream: () => ReadableStream;
}
5-1. Blob
...
const command = new GetObjectCommand(input);
const res = await client.send(command);
if (!res.Body) throw new Error("404 Not Found");
if (!(res.Body instanceof IncomingMessage)) throw new Error("404 Not Found");
const type = res.Body.headers["content-type"];
const uint8arr = await res.Body.transformToByteArray();
const blob = new Blob([uint8arr], { type });
return blob;
<template>
<div>
<img v-if="imgSrc" :src="imgSrc" />
</div>
</template>
<script setup lang="ts">
const imgSrc = ref<string | null>(null)
const getImage = async () => {
const imgBlob = await $fetch("/api/getImage");
const url = window.URL.createObjectURL(imgBlob);
imgSrc.value = url;
}
</script>
5-2. File
Blob
にファイル名を追加するだけ
...
const type = res.Body.headers["content-type"];
const uint8arr = await res.Body.transformToByteArray();
const file = new File([uint8arr], 'test.png', { type });
return file;
5-3. base64
...
const type = res.Body.headers["content-type"];
const uint8arr = await res.Body.transformToByteArray();
const base64 = Buffer.from(uint8arr).toString("base64");
return { base64str, type };
<template>
<div>
<img v-if="imgSrc" :src="imgSrc" />
</div>
</template>
<script setup lang="ts">
const imgSrc = ref<string | null>(null)
const getImage = async () => {
const { base64str, type } = await $fetch("/api/getImage");
const url = `data:${type};base64,${base64str}`;
imgSrc.value = url;
}
</script>
おまけ S3にファイルをアップする
AWS SDKのPutObjectCommand
は、Bodyパラメータに以下を指定可能
string | Uint8Array | Buffer | Readable
Buffer
import { S3Client, PutObjectCommand, type PutObjectCommandInput } from "@aws-sdk/client-s3";
import { fromEnv } from "@aws-sdk/credential-providers";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const buf = Buffer.from(body.buf.data);
const client = new S3Client({
region: process.env["AWS_REGION"] ?? "",
credentials: fromEnv(),
});
const input: PutObjectCommandInput = {
Bucket: process.env["BUCKET_NAME"] ?? "",
Key: "test.jpg",
Body: buf,
};
const command = new PutObjectCommand(input);
await client.send(command);
});
Uint8Array
Buffer
があるなら
const arrBuf = new Uint8Array(buf);
const input: PutObjectCommandInput = {
Bucket: process.env["BUCKET_NAME"] ?? "",
Key: "test.jpg",
Body: arrBuf,
};
すれば良いだけ