🌩️

Uppy でファイルアップロードする

2025/01/20に公開

Uppy とは

https://uppy.io/

Uppy はブラウザー向けファイルアップロードライブラリーで、シンプルかつ高機能な UI を簡単に統合できます。

  • 高機能なダッシュボード
    • ドラッグ & ドロップ
    • リモート URL、Web カメラ、マイク音声、など豊富な入力ソース
  • プラグインシステムによる拡張が可能
  • React, Vue, Svelte などの UI フレームワークに統合可能

https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js

Uploader の種類

  • Transloadit[1]
  • Tus[2]
  • AWS S3
  • XHR

SaaS の Transloadit を使うと、ファイルの変換や他社ストレージサービス(Google Drive, One Drive, Box...)からのアップロードなどの強力な機能が利用できます。(Free プランあり)
が、それを利用せずに別のプロトコルやセルフホストされたサーバーも利用できます。

今回は完全に無料かつローカルで試したいため、セルフホストサーバーを利用しての Tus プロトコルと AWS S3 互換ストレージへのダイレクトアップロードの 2 つの方法を試してみます。

デモ

デモプロジェクトを Github に用意したのでご覧ください

https://github.com/harumaxy/uppy-demo

ローカル環境のセットアップ

docker-compose.yml ファイルを以下のように書き、ローカル環境を用意します。
(環境変数に S3_ROOT_USERS3_ROOT_PASSWORD を設定しておいてください)

  • minio: S3 互換ストレージ
    • mc(minio クライアント)を起動時に実行し、事前にバケットを作成しておく
  • tusd: Tus プロトコルサーバーの公式実装
docker-compose.yml
services:
  s3:
    image: minio/minio:RELEASE.2024-12-18T13-15-44Z
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: ${S3_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${S3_ROOT_PASSWORD}
    volumes:
      - .tmp/minio/data:/export
    command: server /export --console-address ":9001"

  mc:
    depends_on:
      - s3
    image: minio/mc:RELEASE.2024-11-21T17-21-54Z
    entrypoint: []
    environment:
      S3_ROOT_USER: ${S3_ROOT_USER}
      S3_ROOT_PASSWORD: ${S3_ROOT_PASSWORD}
    command: >
      /bin/sh -c "
      until (mc config host add s3 http://s3:9000 $S3_ROOT_USER $S3_ROOT_PASSWORD) do sleep 1; done;
      mc mb s3/my-bucket;
      exit 0;
      "

  tus:
    image: tusproject/tusd:sha-76aeb6b
    volumes:
      - .tmp/tusd:/data
    ports:
      - "8080:8080"
    environment:
      AWS_REGION: "ap-northeast-1" # S3互換ストレージにアップロードする際に必須。(ローカル環境では値は何でもOK)
      AWS_ACCESS_KEY_ID_FILE: /run/secrets/aws-access-key-id
      AWS_SECRET_ACCESS_KEY_FILE: /run/secrets/aws-secret-access-key
    secrets:
      - aws-access-key-id
      - aws-secret-access-key
    command: -verbose -s3-bucket my-bucket -s3-endpoint http://s3:9000

secrets:
  aws-access-key-id:
    environment: S3_ROOT_USER
  aws-secret-access-key:
    environment: S3_ROOT_PASSWORD

tusd サーバー経由でアップロードする

bun create vite@latest client
cd client
bun add @uppy/core @uppy/dashboard @uppy/webcam @uppy/tus
src/main.ts
import Uppy from "@uppy/core";
import Dashboard from "@uppy/dashboard";
import Webcam from "@uppy/webcam";
import Tus from "@uppy/tus";

import "@uppy/core/dist/style.min.css";
import "@uppy/dashboard/dist/style.min.css";

const tusdEndpoint = "http://localhost:8080/files";

const uppy = new Uppy()
	.use(Dashboard, {
		inline: true,
		target: "#app",
	})
	.use(Webcam)
	.use(Tus, {
		endpoint: tusdEndpoint,
	});

uppy.on("complete", (result) => {
	console.log("Upload result:", result.successful);
});

uppy.on("upload-success", async (file, _res) => {
	console.log("Upload success:", file);
});

@uppy/core が UI 実装であり、プラグインによりアップロード実装を追加できます。

@uppy/tus プラグインを使うことで、 Tus プロトコルサーバーを介してファイルをアップロードすることが出来ます。
バックエンドのストレージ製品を GCP、Azure などへ変更しても、クライアント - サーバー間が Tus プロトコルさえ使っていればフロントの実装を修正する必要はありません。

Uppy クライアントのアップロード完了イベントにコールバックを登録するか、または tusd サーバーでも Hook を利用することで既存の Web アプリケーションとの統合が可能です。(ファイル情報を DB に登録するなど)
https://tus.github.io/tusd/advanced-topics/hooks/#list-of-available-hooks

署名付きURLで S3 に直接アップロードする

Tus プロトコル以外にも、署名付き URL による直接アップロードも可能です。
tusd サーバーは不要になりますが、代わりに URL を発行するサーバーを実装する必要があります

サーバー実装

自前のサーバーを使う場合、以下の API 実装が必要になります。

GET    /uppy/sts  # (任意)一時的な認証情報の取得
POST   /uppy/s3   # アップロード用の署名済みURLを発行

# マルチパートアップロードする場合は、以下のAPI実装も必要 (ファイルサイズが大きい場合など)
POST   /uppy/s3-multipart                       # マルチパートアップロードを作成する
GET    /uppy/s3-multipart/:uploadId             # パーツ一覧を取得
GET    /uppy/s3-multipart/:uploadId/:partNumber # パートごとに、アップロード用の署名済みURLの発行
POST   /uppy/s3-multipart/:uploadId/complete    # アップロード完了
DELETE /uppy/s3-multipart/:uploadId             # アップロード中断

サーバー実装のコードを貼ると長いので、デモプロジェクトを参照してください。(ElysiaJS で実装しました)

クライアント実装

@uppy/aws-s3 プラグインを追加します。
あと、ElisyaJS で実装した API を End-to-End で型付けされた RPC で呼び出したいので、同じ workspace 内の server パッケージと、 @elysiajs/eden も追加します。

package.json
{
  "name": "uppy-demo",
  "private": true,
+ "workspaces": [
+   "client",
+   "server"
+ ]
}
client/package.json
{
  "name": "client",
  "dependencies": {
    ...
+   "@elysiajs/eden": "^1.2.0",
+   "@uppy/aws-s3": "^4.1.3",
+   "server": "workspace:*"
  }
  ...
}

cd client
bun install

クライアント実装は以下のような感じで、Uppy.use(AwsS3, options) のコールバック内で S3 アップロードに必要な情報をサーバーから取得するコードを記述します。

import Uppy from "@uppy/core";
import Dashboard from "@uppy/dashboard";
import Webcam from "@uppy/webcam";
+import AwsS3 from "@uppy/aws-s3";

+import { edenTreaty } from "@elysiajs/eden";
+import type { Server } from "server";

import "@uppy/core/dist/style.min.css";
import "@uppy/dashboard/dist/style.min.css";

+ const server = edenTreaty<Server>("http://localhost:8000");

const uppy = new Uppy()
  .use(Dashboard, {
    inline: true,
    target: "#app",
  })
  .use(Webcam)
+  .use(AwsS3, {
+    endpoint: "",
+    async getUploadParameters(file, _options) {
+      const res = await server.uppy.s3.get({
+        $query: {
+          name: file.name ?? "unknown",
+          type: file.type,
+          size: file.size ?? 0,
+        },
+      });
+      if (res.error) throw new Error(res.error.message);
+      return res.data;
+    },
+
+    shouldUseMultipart(file) {
+      // 10 MB 以上ならマルチパートアップロードする
+      return (file.size ?? 0) >= 10 * 1024 ** 2;
+    },
+
+    async createMultipartUpload(file) {
+      const res = await server.uppy["s3-multipart"].post({
+        name: file.name ?? "unknown",
+        type: file.type,
+        size: file.size ?? 0,
+      });
+      if (res.error) throw new Error(res.error.message);
+      return res.data;
+    },
+
+    async listParts(_file, { uploadId, key }) {
+      const res = await server.uppy["s3-multipart"][uploadId].post({
+        key,
+      });
+      if (res.error) throw new Error(res.error.message);
+      return res.data;
+    },
+
+    async signPart(_file, { key, uploadId, partNumber }) {
+      const res = await server.uppy["s3-multipart"][uploadId][partNumber].post({
+        key,
+      });
+      if (res.error) throw new Error(res.error.message);
+      return res.data;
+    },
+
+    async abortMultipartUpload(_file, { key, uploadId }) {
+      const res = await server.uppy["s3-multipart"][uploadId].delete({
+        key,
+      });
+      if (res.error) throw new Error(res.error.message);
+      return;
+    },
+
+    async completeMultipartUpload(_file, { key, uploadId, parts }) {
+      const res = await server.uppy["s3-multipart"][uploadId].complete.post({
+        key,
+        parts,
+      });
+      if (res.error) throw new Error(res.error.message);
+      return { location: res.data.location };
+    },
+  });

uppy.on("complete", (result) => {
  console.log("Upload result:", result.successful);
});

uppy.on("upload-success", async (file, _response) => {
+  await server.uppy["upload-success"].post({
+    result: {
+      name: file?.meta.name ?? "",
+      type: file?.meta.type ?? "",
+      size: String(file?.size ?? 0),
+    },
+  });
  console.log("Upload success:", file);
});

比較

tusd(Tus プロトコル)

  • メリット
    • 既製品のサーバー実装があるため、手間が省ける
    • アップロード中断時の再開機能がある
      • 大容量ファイルや、不安定なネットワークでのアップロードに有利
    • メモリー効率が高い設計
  • デメリット
    • tusd サーバーのセルフホスティングが必要(管理の手間が増える)

S3 の署名付きURLへ直接アップロード

  • メリット
    • 中継サーバーを介さないため、アップロードが速い(と思われる)
    • 署名付き URL 発行のサーバー実装は、既存の Web アプリケーションに同居できる
    • マルチパート対応で、大容量ファイルも効率的にアップロードできる
  • デメリット
    • 多少の自前コードが必要

参考URL

脚注
  1. Transloaditは Uppy を開発した企業で、ファイルアップロード・変換・配信を提供する SaaS を運営している ↩︎

  2. 同じく Transloadit 社が開発した、効率的で再開可能なファイルアップロードプロトコル ↩︎

GitHubで編集を提案
manabo Tech Blog

Discussion