Uppy でファイルアップロードする
Uppy とは
Uppy はブラウザー向けファイルアップロードライブラリーで、シンプルかつ高機能な UI を簡単に統合できます。
- 高機能なダッシュボード
- ドラッグ & ドロップ
- リモート URL、Web カメラ、マイク音声、など豊富な入力ソース
- プラグインシステムによる拡張が可能
- React, Vue, Svelte などの UI フレームワークに統合可能
Uploader の種類
SaaS の Transloadit を使うと、ファイルの変換や他社ストレージサービス(Google Drive, One Drive, Box...)からのアップロードなどの強力な機能が利用できます。(Free プランあり)
が、それを利用せずに別のプロトコルやセルフホストされたサーバーも利用できます。
今回は完全に無料かつローカルで試したいため、セルフホストサーバーを利用しての Tus プロトコルと AWS S3 互換ストレージへのダイレクトアップロードの 2 つの方法を試してみます。
デモ
デモプロジェクトを Github に用意したのでご覧ください
ローカル環境のセットアップ
docker-compose.yml
ファイルを以下のように書き、ローカル環境を用意します。
(環境変数に S3_ROOT_USER
、S3_ROOT_PASSWORD
を設定しておいてください)
-
minio: S3 互換ストレージ
- mc(minio クライアント)を起動時に実行し、事前にバケットを作成しておく
- tusd: Tus プロトコルサーバーの公式実装
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
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 に登録するなど)
署名付き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
も追加します。
{
"name": "uppy-demo",
"private": true,
+ "workspaces": [
+ "client",
+ "server"
+ ]
}
{
"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
- https://uppy.io/
- https://tus.io/
- https://www.youtube.com/watch?v=EWKiWwBRcwY
- https://techracho.bpsinc.jp/ykuki0805/2024_12_19/147420
-
Transloaditは Uppy を開発した企業で、ファイルアップロード・変換・配信を提供する SaaS を運営している ↩︎
-
同じく Transloadit 社が開発した、効率的で再開可能なファイルアップロードプロトコル ↩︎
Discussion