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?

Nuxt3のフロント/バックエンドでファイル送受信

Last updated at Posted at 2024-10-04

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を受け取るにはreadFormDatareadMultipartFormDataが使えるらしい
違いは未調査

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. FileBufferにして投げる

~/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. FileBase64エンコードして投げる

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. BufferFileに変換

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. base64Fileに変換

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で取得した画像をフロント側に返すことを考える
GetObjectCommandres.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,
};

すれば良いだけ

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?