12k
All articles

Uppy 6.0と再開可能なファイルアップロード

UppyとTusで再開可能なファイルアップロードを設定し、Uppy 6.0のS3署名やcoreパッケージの変更点と、バージョン5からの移行手順を確認します。

OpenReplay Team
OpenReplay Team
Uppy 6.0と再開可能なファイルアップロード

Uppyは、オープンソースのJavaScript製ファイルアップローダーです。@uppy/tusプラグインと組み合わせると、接続が切れたりタブが閉じられたりしても、大容量ファイルのアップロードを途中から再開できます。ユーザーがファイルを最初から送り直す必要はありません。

多くのチームがライブラリの導入を検討するきっかけは、たいてい同じようなバグ報告です。ホテルのWi-Fiで2 GBの動画を90%までアップロードしたところで接続が切れ、進捗がゼロに戻ってしまった、というものです。ファイルアップロード用ドロップゾーンの構築やReactでのカスタムファイルアップロードのガイドを参考にアップローダーを自作した場合も、こうした問題が出てきた段階で自作のアプローチでは対応しきれなくなります。

本記事では、再開可能なアップロードが重要な理由、Uppyの最小構成、Uppy 6.0での変更点、そして5.xからのアップグレードに必要な作業量について解説します。

重要なポイント

  • Tusは、HTTP上に構築された、アップロードを再開可能にするためのオープンなプロトコルです。接続が切れたりタブが閉じられたりしても進捗は失われず、サーバーが最後に受信したバイトからアップロードを続行します。
  • Uppy 6.0では、@uppy/aws-s3が3つの署名モード(getCredentials、signRequest、companionEndpoint)を軸に書き直されました。各設定では、このうちいずれか1つだけを使用します。
  • Uppy 6.0では、@uppy/utils、@uppy/store-default、@uppy/companion-client、@uppy/provider-viewsが、サブパスエクスポートとして@uppy/coreに同梱されるようになりました。
  • uppyメタパッケージまたはCDNバンドルを使用しているプロジェクトでは、6.0への移行に必要なコード変更はわずか、あるいはまったくありません。
  • セルフホストのCompanionを利用している場合は、クライアントを@uppy/core 6に移行する前に、CompanionをCompanion 7にアップグレードする必要があります。OAuthトークンがCompanionのWebSocket経由でやり取りされるようになったためです。また、Companion 7はExpress 5上で動作します。

自作のアップローダーではなくUppyを使う理由

単一のfetchまたはXMLHttpRequestによるPOSTで構築した自作のアップローダーでは、中断した箇所から処理を再開できません。接続が切れると、それまでに送信したバイトはすべて失われます。Tusはこの問題をプロトコルレベルで解決します。クライアントはまず、サーバーがすでにどこまで受信しているかを問い合わせ、残りの部分だけを送信します。セッションリプレイを使うと、この仕組みがないことによる損失がよくわかります。アップロードが失敗した後、ユーザーがファイル選択画面に戻り、同じ大容量ファイルをもう一度選び直す様子を確認できるからです。

ただし、Tusによる再開可能なアップロードには、対応するサーバー側の実装が必要です。@uppy/tusのドキュメントではtusdが推奨されています。Tusサーバーは、ファイルをローカルに保存することも、S3などの外部ストレージに保存することもできます。

DashboardとTusを使ったUppyの最小構成

3つのパッケージがあれば、再開可能なアップローダーを一通り構築できます。@uppy/coreが状態を管理し、@uppy/dashboardがUIを提供し、@uppy/tusが転送を担当します。

import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Tus from '@uppy/tus';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';

new Uppy()
	.use(Dashboard, { inline: true, target: 'body' })
	// Point this at your own tus server, e.g. a tusd instance
	.use(Tus, { endpoint: 'https://your-tus-server/files/' });

inline: trueを指定すると、Dashboardはモーダルとして開くのではなく、ターゲット要素の中に直接レンダリングされます。進捗表示のUIを自作したい場合は、アップロードプログレスバーの構築に関する記事で、その基盤となるイベントを解説しています。

Google Driveなどのリモートソースからのアップロードは、Uppyのサーバーコンポーネントである Companion を経由します。Companionはサーバー側でファイルを取得するため、データがユーザーのブラウザを通過することはありません。ここで注意すべき点が1つあります。@uppy/tusのchunkSize設定が適用されるのは、ユーザーのデバイスから送信されるファイルだけです。Companion経由のアップロードにはCompanion自身のchunkSize設定が適用されるため、リクエストごとのサイズを制限したい場合は両方を設定してください。

デフォルトでは、@uppy/tusは失敗したチャンクを所定の遅延間隔で段階的にリトライし、同時に実行されるアップロード数を制限します。サーバーがHTTP 429を返した場合、デフォルトのonShouldRetryロジックはキュー内のすべてのリクエストを一時停止し、retryDelaysに指定された遅延を順に適用しながら待機してから再試行します。

Uppy 6.0の新機能

Uppy 6.0の主な変更点は3つあります。@uppy/aws-s3がゼロから書き直されたこと、4つのサポートパッケージが@uppy/coreに統合されたこと、そして@uppy/instagramが削除されたことです。Uppy 6.0のリリース記事によると、メジャーバージョンが上がったのは@uppy/core、@uppy/companion、@uppy/aws-s3、@uppy/tus、@uppy/components、uppyの6パッケージです。

Uppy 5の旧@uppy/aws-s3プラグインには最大8つのコールバックがあり、必要なコールバックの組み合わせは構成によって変わりました。マルチパートアップロードか単一アップロードか、Companionを使うか使わないか、ブラウザで署名するかサーバーで署名するか、といった具合です。今回の書き直しでは、これらすべてが3つの署名モードに置き換えられ、そのうち1つだけを選択します。

モード署名を行う場所Companionの要否
getCredentialsブラウザ内(SigV4)。自前のエンドポイントから取得した一時的な認証情報を使用不要
signRequest独自の署名処理不要
companionEndpointCompanion必要
import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3';

new Uppy().use(AwsS3, {
	// Swap this for an R2, MinIO or Spaces endpoint
	s3Endpoint: 'https://my-bucket.s3.eu-west-1.amazonaws.com',
	region: 'eu-west-1',
	getCredentials: async () => fetch('/s3-credentials').then((r) => r.json()),
});

S3へのアップロードにCompanionは必須ではなくなりました。getCredentialsを使うと、ブラウザが各リクエストに自ら署名します。また、このプラグインはCloudflare R2、MinIO、DigitalOcean SpacesなどS3互換のあらゆるサービスで動作します。getCredentialsが返すべきフィールドは@uppy/aws-s3のリファレンスに記載されているため、上記の例では、/s3-credentialsエンドポイントがそのとおりの形式でデータを返す必要があります。リリース記事によると、今回の書き直しにより、リトライのバックオフ、認証情報の有効期限切れ、オフライン検知、マルチパートアップロードの一時停止・再開に関する長年のバグも解消されました。さらに、プラグインのCIテストはモックではなく、実際のMinIOサーバーに対して実行されるようになっています。

Uppy 5から6へのアップグレード方法

uppyメタパッケージまたはCDNバンドルを使用している場合、6.0へのアップグレードに必要なコード変更はわずか、あるいはまったくありません。それ以外の場合は、以下のリストに沿って作業を進めてください。詳細は公式の移行ガイドに記載されています。

  • 個別パッケージ: @uppy/utils、@uppy/store-default、@uppy/companion-client、@uppy/provider-viewsからのインポートを、@uppy/coreの対応するサブパスに変更します。旧パッケージはnpm上で非推奨(deprecated)になっています。
  • セルフホストのCompanion: まずCompanion 7にアップグレードし、その後でクライアントを@uppy/core 6に移行します。Companion 7は旧バージョンのUppyクライアントも引き続きサポートしますが、Uppy 6クライアントはCompanion 6以前では動作しません。Companion 7はExpress 5上に構築されており、Express 4アプリのミドルウェアとしては動作しないため、切り替える前にアプリをExpress 5に移行してください。また、companion.socket()の第2引数にcompanionOptionsが必須になりました。ホスト型Companionの利用者は、何も対応する必要はありません。
  • Tusのエラーハンドラー: responseに値が設定されるようになったため、upload-errorのコードを改めて確認してください。
- import { fetcher } from '@uppy/utils'
+ import { fetcher } from '@uppy/core/utils'

- import { RequestClient } from '@uppy/companion-client'
+ import { RequestClient } from '@uppy/core/companion-client'

CompanionClientProviderとCompanionClientSearchProviderも削除されました。代わりに@uppy/core/companion-clientからProviderをインポートしてください。パッケージ統合の目的は、特定の不具合を防ぐことにあります。それは、ロックファイルに新しい@uppy/coreと古い@uppy/utilsが共存し、1つの依存関係ツリーの中に同じパッケージのコピーが2つ存在してしまうという問題です。

Uppy 6.0以降、@uppy/tusはサーバーのステータスとレスポンスボディをupload-errorイベントとfile.responseに渡すようになりました。以前のバージョンでは、サーバーのレスポンスを含まずにエラーが発行されていました。

uppy.on('upload-error', (file, error, response) => {
	if (error.isNetworkError) return showOfflineNotice(); // placeholder helper
	console.log(response?.status);
	console.log(response?.body.xhr.responseText);
});

ただし、次の2つのケースでは、引き続きresponseがundefinedになります。サーバーからまったく応答がなかった場合(サーバーに到達できない場合など)と、Companion経由でアップロードされたリモートファイルの場合です。

Reactなどのフレームワークでの Uppy の利用

Uppyのフレームワーク用ラッパーを使うと、イベントを手作業で紐付けなくても、コンポーネントからUppyの状態をサブスクライブできます。Reactでは、@uppy/reactのuseUppyStateフックにUppyインスタンスとセレクターを渡すと、選択した状態の一部が変化したときにコンポーネントが再レンダリングされます。APIの全容はReact統合のドキュメントを参照してください。

import { useState } from 'react';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import { useUppyState } from '@uppy/react';

function UploadStatus() {
	const [uppy] = useState(() =>
		new Uppy().use(Tus, { endpoint: 'https://your-tus-server/files/' }),
	);
	const files = useUppyState(uppy, (state) => state.files);
	const progress = useUppyState(uppy, (state) => state.totalProgress);
	return <p>{Object.keys(files).length} files, {progress}% uploaded</p>;
}

useStateの遅延初期化を使うことで、Uppyインスタンスはレンダリングのたびではなく、コンポーネントごとに一度だけ生成されます。Uppyには、Vue、Svelte、Angular向けのラッパーも用意されています。

Uppyを使うべきでないケース

プロフィール画像のような小さなファイルを1つアップロードするだけなら、UppyもTusも必要ありません。自前のエンドポイントへの通常のフォームPOSTで十分であり、Tusを導入すると、運用しなければならないサーバー側の統合が増えるだけです。将来、通常のHTTPエンドポイントのままUppyのUIを使いたくなった場合は、@uppy/xhr-uploadで対応できます。このプラグインは、標準的なマルチパートフォームによるアップロードとPUTリクエストのどちらでも送信できます。受信側の実装については、NodeでのMulterによるファイルアップロードのガイドで解説しています。

まとめ

再開可能なアップロードこそが、Uppyを導入する価値のある理由です。Uppy 6.0では、設定ごとに署名モードが1つだけのS3プラグインと、バージョンを揃え続ける必要があった5つのパッケージに代わる単一のコアパッケージにより、運用がよりシンプルになりました。まずは@uppy/core、@uppy/dashboard、@uppy/tusをインストールし、Tusプラグインの接続先をtusdインスタンスに設定したうえで、ネットワークを帯域制限した状態で大容量ファイルをアップロードしてみてください。すでに5.xを使用している場合は、既存のコードを壊す可能性があるCompanionとupload-errorの変更から対応を始めましょう。

よくある質問

@uppy/aws-s3のgetCredentials関数は何を返す必要がありますか?

credentialsプロパティを持つオブジェクトを返します。このプロパティには、キャメルケースのaccessKeyId、secretAccessKey、sessionTokenからなる一時的な認証情報と、任意のexpirationを含めます。さらに、署名対象のregionも返します。AWS STSはこれらのキーをパスカルケースで返すため、事前に変換してください。Uppyは、取得した結果をプラグインが存在する間キャッシュします。ExpiredTokenまたはInvalidAccessKeyIdエラーが発生した場合は、新しい認証情報を取得して1回だけリトライします。これらの認証情報はブラウザが保持することになるため、付与するポリシーの権限は厳密に絞り込んでください。

Uppy 6.0で削除された@uppy/aws-s3のオプションはどれですか?また、shouldUseMultipartは引き続き使えますか?

shouldUseMultipartは、getChunkSize、allowedMetaFields、limitとともに、@uppy/aws-s3 6.0でも引き続き使用できます。削除されたオプションは、endpoint、headers、cookiesRule、getTemporarySecurityCredentials、getUploadParameters、signPart、createMultipartUpload、listParts、abortMultipartUpload、completeMultipartUpload、uploadPartBytes、retryDelaysです。既存のCompanion構成は、endpointをcompanionEndpointに名前変更するだけで移行できます。headersとcookiesRuleには直接の代替手段がありません。同一オリジンのCookieは引き続きCompanionに送信されますが、Bearerトークンやクロスサイトのクッキーを使う場合は、Companionの前段に認証用プロキシを置くなど、別の構成が必要です。

Tusによる再開可能なアップロードと@uppy/golden-retrieverの違いは何ですか?

Tusは、サーバーがすでに受信したデータを保持し、転送を継続できるようにする仕組みです。一方、Golden Retrieverは、クラッシュやタブを閉じた後に、ブラウザ側のファイル選択状態とUppyの状態を復元します。Uppy 6.0のGolden Retrieverは、デフォルトでメタデータ、状態、および10 MiBまでのローカルファイルをIndexedDBに保存し、メタデータについてはlocalStorageにフォールバックします。それより大きなファイルには、オプションのService Workerストアが必要です。このストアはタブを閉じてもデータが保持されますが、ブラウザを再起動すると失われます。

Uppy 6クライアントは旧バージョンのCompanionサーバーに接続できますか?

できません。OAuthトークンがwindow.openerではなくCompanionのWebSocket接続経由でやり取りされるようになったため、Uppy 6クライアントにはCompanion 7が必要です。Companion 7は、6.0より前のUppyクライアント向けのフォールバックとしてpostMessageを引き続きサポートしているため、先にCompanion 7をデプロイしてから、クライアントをアップグレードしてください。サーバー側では、companion.app()に渡しているものと同じオプションオブジェクトをcompanion.socket()にも渡します。ホスト型Companionの利用者は、変更の必要はありません。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.