アルファテックブログ

【基礎からわかる】途中から再開できるファイルアップロード tus / tusd

カバー

目次

はじめに

あるプロジェクトで tus というプロトコルと tusd というサーバー実装が使われていました。ただ、当時の私は「ファイルアップロードで使われているもの」くらいの認識で、仕組みをちゃんと説明できる状態ではありませんでした。

上司から「せっかくだからブログにまとめてみたら」と言われたのをきっかけに、改めて調べ直した情報を記事にまとめました。

調べていく中で、「ファイルをアップロードする」と一口にいっても、やり方はたくさんあるとわかりました。Web フォームから送る方法、サーバー間で転送する方法、クラウドストレージに直接送る方法など、用途や環境によって選択肢がまったく違います。

その中で、プロジェクトで tus が採用されていた理由も見えてきました。動画やバックアップのような大きなファイルを扱う場面では、通信が途中で切れたときに最初からやり直しになるのが大きな課題になります。tus はこの「途中から再開できる」という部分に特化したプロトコルで、プロジェクトの要件に合っていたわけです。

この記事では、まずアップロード方式の選択肢を概観したうえで、tus の仕組みを掘り下げ、公式サーバー実装の tusd を使って実際に動かすところまでを一通りまとめます。最後には、大容量ファイルのアップロードを意識した簡単な Web システムを実際に組み立て、ZIP ファイルだけを受け付ける形でどう実装するかまで確認します。プログラマー 1〜2 年目の方にも読めるように、基礎から順に進めていきます。

1. アップロード方式にはどのようなものがあるか

「ファイルをサーバーへ送る」と一口にいっても、その方式はさまざまです。用途や環境によって選択肢がまったく違うため、まずは全体像を把握しておくと、アップロード方式を選びやすくなります。

ファイルアップロードに使われる代表的なプロトコルや仕組みを、用途別に 4 つのカテゴリに分けて整理すると次のようになります。

  • Web 系 ― ブラウザからサーバーへファイルを送る方式。HTTP multipart/form-data、WebSocket、WebDAV などが該当する
  • ファイル転送専用 ― サーバー間やシステム間でファイルをやり取りするためのプロトコル。FTP、SFTP、FTPS、SCP などが該当する
  • クラウド系 ― クラウドストレージの API を利用してファイルを送る方式。Amazon S3(以下、S3)マルチパートアップロード、Azure Blob Storage(Block Blob)などが該当する
  • 大容量・再開可能系 ― 大きなファイルの転送や、通信断からの再開に特化した方式。tus が該当する。S3 マルチパートアップロードも分割・並列送信や再送の仕組みを備えている

アップロード方式の分類

図1 アップロード方式の分類

このうち、図中で強調している HTTP multipart/form-dataSFTPS3 マルチパートアップロードtus の 4 つを、この記事では詳しく取り上げます。各カテゴリから実務で検討しやすいアップロード方式を 1 つずつ選んでおり、S3 マルチパートアップロードはクラウド系と大容量・再開可能系の両方にまたがりますが、ここではクラウド系の代表として扱います。

HTTP multipart/form-data

Web のフォームからファイルを送るときに使う、最も基本的なアップロード方式です12。フォームの送信ボタンを押すと、ファイルが一括でサーバーへ送られます。

  • 学習コストが低く、特別なライブラリも必要ない
  • 数 MB 程度の小さなファイルなら問題なく使える
  • 一括送信のため、大容量ファイルでは転送効率が落ちやすい
  • 通信が途中で切れると最初からやり直しになる
  • サーバー間でも使えるが、その用途向けの設計ではない
  • クラウドストレージに保存するには、別途転送処理の実装が必要になる

SFTP(SSH File Transfer Protocol)

SSH(サーバーへ安全に接続する仕組み)の上でファイルを転送するプロトコルです3。サーバーへのデプロイ(アプリや設定ファイルを配置して反映する作業)や、企業間でのファイル受け渡しなど、サーバー同士でのファイル転送によく使われます。

  • SSH ベースのため暗号化や認証の仕組みが成熟している
  • ブラウザからは直接使えない
  • 「途中の位置を指定して読み書きする」機能はあるが、送信済み位置の管理はクライアント側の実装に委ねられる
  • ツールやライブラリによって再開のしやすさが変わる
  • クラウドストレージに保存するには、別途転送処理の実装が必要になる

S3 マルチパートアップロード

S3 にファイルを送る際に使える分割アップロードの仕組みです4。ファイルをパートに分けて並列に送り、最後に結合します。Google Cloud Storage(Google が提供するクラウドストレージサービス。略称 GCS)や Azure Blob Storage(マイクロソフトが提供するクラウドストレージサービス)にも同様の仕組みがあります。

  • 分割・並列送信の仕組みがサービスに組み込まれており、大容量ファイルを効率よく転送できる
  • パート(分割単位)ごとに再送できるが、バイト単位の細かい再開はできない
  • クライアントからクラウドストレージの API を直接呼び出すため、中継サーバーが必要ない
  • 署名付き URL5(有効期限つきの一時的な URL)を使えばブラウザから直接アップロードもできるが、その URL を発行するサーバー側の処理が別途必要になる
  • AWS のアカウント作成やアクセス権限の設定、専用ライブラリの導入など、使い始めるまでの準備が多い
  • AWS の API 仕様に依存するため、他のクラウドサービスへ乗り換えにくくなる

tus

途中で切れても続きから再開できるアップロードのルールを定めた、HTTP ベースのオープンプロトコルです6

  • バイト単位で再開位置を管理するため、中断と再開の精度が高い
  • HTTP ベースなので既存のインフラに乗せやすい
  • 特定のクラウドベンダーに縛られない
  • 特定のプログラミング言語に依存しない
  • ブラウザやモバイルからの利用を想定した設計で、サーバー間転送に使われることは少ない
  • インターネット技術の標準を策定する団体(IETF)で標準化が進行中だが、まだ正式な標準(RFC)にはなっていない(詳細は第 8 章

2. アップロード方式を比べてみる

第 1 章で挙げた 4 つのアップロード方式を、実際に選定するとき判断基準になりそうな観点で比較します。

何を比較するか

以下の 7 つの観点で、それぞれの特徴を確認します。

  • 導入の手軽さ: 学習コストや実装の手間がどれだけ小さいか
  • 中断・再開: 通信が切れたとき、途中から再開できるか
  • 大容量の転送速度: 大きなファイルを高速に送るための仕組みがあるか
  • サーバー間転送: サーバー同士でファイルをやり取りする用途に向いているか
  • ブラウザ利用: Web ブラウザから直接使えるか
  • クラウドストレージ連携: クラウドの保存先と中間処理なしに直接連携できるか
  • 仕様の標準化: オープンな仕様として公開・標準化されているか

比較表

 観点HTTP multipartSFTPS3 マルチパートtus 
 導入の手軽さ 
 中断・再開 
 大容量の転送速度 
 サーバー間転送 
 ブラウザ利用 
 クラウドストレージ連携 
 仕様の標準化 

◎ 特化 ○ 対応 △ 制約あり ✗ 非対応

比較してみると、万能なアップロード方式は存在しないことがわかります。ファイルが小さくて通信も安定しているなら HTTP multipart で十分ですし、保存先が S3 に決まっているなら S3 マルチパートが最も直接的です。

一方で、大きなファイルを扱い、通信が不安定になりうる環境で、かつブラウザやモバイルから送る必要があるという条件が重なると、tus の強みが際立ちます。冒頭で触れたプロジェクトでもまさにこの条件に該当しており、tus が選ばれた理由が腑に落ちました。

次章からは、tus に絞って仕組みを掘り下げていきます。

3. tus とは

ここからは、比較表で名前だけ出てきた tus の中身を見ます。まずは tus が何を解決するのかを押さえ、そのあとで具体的な仕組みへ進みます。

数百 MB から数 GB のファイルを送るとき、通信断や端末のスリープが起きると、従来のアップロードでは最初から送り直しになることがあります。HTTP にはダウンロードを途中から再開する仕組みがありますが、アップロード側には長い間それに相当する広く使える標準がなく、それぞれが独自の再開機構を作って相互運用しにくい状態になっていました。

tus は「ライブラリ」ではなく「プロトコル」

この問題に対して作られたのが tus です。tus は、再開可能アップロードのための通信ルールを定めた HTTP ベースのオープンプロトコル7です。

プロトコルとは、通信するときの約束事(ルール)のことです。たとえば、母国語が違う人同士でも「英語で話す」というプロトコル(ルール)を決めておけば、お互いに「Hello!!」でやり取りできます。逆に、片方がルールを無視して母国語で話しかけると、相手には通じません。

プロトコルは約束事(ルール)

図2 プロトコルは約束事(ルール)

コンピューター同士の通信もこれと同じです。クライアントとサーバーが共通のルールに従うことで、内部の実装が違っていても正しくやり取りできます。

tus がライブラリではなくプロトコルであることには、実用上の大きなメリットがあります。それは、クライアントとサーバーの実装を自由に選べるということです。

たとえば、あるアップロード機能が「特定のライブラリ」として提供されている場合を考えてみてください。そのライブラリが Java 向けに作られていれば、クライアントもサーバーも Java で書く必要があります。Python で書かれたサーバーに Java のクライアントライブラリからアップロードする、といった組み合わせは基本的にできません。使える言語や環境がライブラリの対応範囲に縛られてしまうわけです。

一方、tus はプロトコルです。「こういう HTTP リクエストを、こういう順番で送る」というルールだけを定めています。ルールさえ守っていれば、クライアントとサーバーは別々の言語で書かれていても正しく通信できます。

クライアントとサーバーの開発言語が違っていても、間に挟まるプロトコルが共通であれば問題なく動きます。具体的には、次のような組み合わせが可能です。

  • ブラウザ側は JavaScript の公式クライアント(tus-js-client)を使い、サーバー側は Go 製の tusd を動かす
  • Android アプリからは Java の公式クライアント(tus-java-client)でアップロードし、同じ tusd サーバーで受け取る
  • 既存のシステムに合わせてクライアントやサーバーを自作する

実際に tus の公式サイトには、JavaScript、Java、Go、Python、Ruby、.NET など多くの言語向けの実装が公開8されています。「サーバーは Java だけどクライアントは JavaScript」「今は Node.js のサーバーだけど将来 Go に移行するかもしれない」といった状況でも、tus プロトコルに準拠してさえいれば片方だけを差し替えられます。つまり、既存システムの言語やフレームワークを変えずに、アップロード部分だけを tus に置き換えて改善できるのが大きな利点です。

まずは全体像をつかむ

tus を使ったアップロードでは、登場人物を次の 2 つに分けて考えるとわかりやすいです。

  • クライアント(アップロード元): ブラウザ、モバイルアプリ、CLI ツールなど、ファイルを送る側を指す
  • サーバー(受信側): tus のルールに従ってデータを受け取る側を指す
クライアント(アップロード元)
│ POST / HEAD / PATCH
サーバー(受信側)
├─ ファイル保存先(ローカルディスク・クラウドストレージなど)
└─ 業務アプリ(認証、DB更新、後続処理)

ここで大切なのは、tus が決めているのはクライアントとサーバーの間の通信ルールだということです。保存先の種類や、アップロード完了後に何をするかまでは tus 自体の中心仕様ではありません。そこは、サーバー実装や業務アプリ側の役割になります。

基本の流れは POSTHEADPATCH

tus の代表的な流れは、次の 3 段階です。

 ステップメソッド役割 
 1POSTアップロード用リソースの作成 
 2HEAD現在の受信位置の確認(再開時) 
 3PATCH続きのデータの送信 

POST の結果として、サーバーはアップロード対象を識別する URL を返します。通信断なく進む場合は、この URL に対してそのまま PATCH でデータを送ります。

通信断が起きて再開する場合は、その URL に対して HEAD を送り、レスポンスの Upload-Offset ヘッダーで「何バイト目まで受信済みか」を確認してから続きを PATCH で送ります。

クライアント サーバー
| |
| POST /files/ | ← アップロード作成
|---------------------------------->|
| Location: /files/{id}
|<----------------------------------|
| |
| ~ 通信断が発生したとする ~ |
| |
| HEAD /files/{id} | ← 再開時: 受信済み位置を確認
|---------------------------------->|
| Upload-Offset: 10485760
|<----------------------------------|
| |
| PATCH /files/{id} | ← 続きから再送
| Upload-Offset: 10485760 |
| [残りのデータ] |
|---------------------------------->|
|<----------------------------------|

この流れのおかげで、通信断が起きても「最初から送り直す」のではなく、「サーバーが受け取れた位置から再開する」が可能になります。

tus 特有のヘッダー

ヘッダー名だけが並ぶとわかりにくいので、理解しやすいように、役割と「どのメソッドで使うか」をセットで説明していきます。すべてのヘッダーがすべてのメソッドで使われるわけではなく、メソッドやリクエスト/レスポンスの方向によって登場するヘッダーが異なります。

 ヘッダー役割 使用場面 
 Upload-Offset何バイト目まで送受信済みか PATCH のリクエストとレスポンス
HEAD のレスポンス
 
 Upload-Lengthファイル全体のサイズ POST のリクエスト
HEAD のレスポンス
 
 Upload-Metadataファイル名などの付加情報 POST のリクエスト
HEAD のレスポンス
 
 Tus-Resumabletus バージョンの宣言 ・全メソッドのリクエストとレスポンス
OPTIONS を除く)
 

Upload-Offset は特に重要です。クライアントは PATCH のリクエストで「ここからデータを送ります」という位置を宣言し、サーバーは HEADPATCH のレスポンスで「ここまで受け取りました」という位置を返します。この値がずれると 409 Conflict になるため、再開処理の中心となるヘッダーです。

tus の流れで使われる HTTP 標準ヘッダー

tus 特有のヘッダーに加え、HTTP の標準ヘッダーも tus の流れの中で使われます。

 ヘッダー役割 
 LocationPOST の応答で返され、作成されたアップロード専用の URL を示す 
 Content-TypePATCH で送る際に application/offset+octet-stream を指定する 

拡張モデルで機能を増やせる

tus プロトコルは、最小限のコアに加えて拡張仕様を組み合わせる設計です。コアだけでも再開可能アップロードは実現できますが、実際の運用では「作成と同時にデータを送りたい」「途中のデータ破損を検知したい」「古い未完了アップロードを期限切れにしたい」といった要件が出てきます。

そこで tus では、必要な機能を拡張として追加できるようになっています。まずはどのような拡張があるのかを一覧で見ておくと、後で仕様を追いやすくなります。

 拡張(tus プロトコル仕様)できること 
 CreationPOST でアップロードを作成 
 Creation With Upload作成と初回データ送信を同時実行 
 Concatenation分割したアップロードを後で結合 
 TerminationDELETE でアップロードを破棄 
 Checksum受信データの破損を検知 
 Expiration未完了アップロードに有効期限を設ける 

ここで注意したいのは、拡張の有無はサーバー実装ごとに異なることです。これはあくまでプロトコル仕様上の定義であり、実際にどの実装がどの拡張に対応しているかは別途確認が必要です。

また、認証や認可は tus 自体の中心仕様には含まれていません。Authorization ヘッダーなど、通常の HTTP の仕組みを組み合わせて使います。つまり tus は「アップロード再開のルール」を担当し、アクセス制御や業務ルールはアプリケーション側が担います。仕組みが見えたところで、次章ではそのルールを実際に担う代表実装 tusd を見ていきます。

4. tusd とは ― 公式サーバー実装とエコシステム

前章では tus を「通信ルール」として見ました。この章では、そのルールを実際に動かす実装群を整理します。タイトルにある tusd9 は、tus の 公式リファレンスサーバー実装 です。

公式実装

tus.io では、複数の公式実装が案内8されています。

 区分実装主な用途 
 サーバーtusdGo 製の公式リファレンスサーバー 
 サーバーtus-node-serverNode.js 系アプリへの統合 
 サーバーtusdotnet.NET アプリへの統合 
 クライアントtus-js-clientブラウザ / Node.js 
 クライアントtus-java-clientJava アプリケーション 
 クライアントtus-py-clientPython アプリケーション 
 クライアントtus-android-clientAndroid 
 クライアントTUSKitiOS 

ブラウザから使うなら tus-js-client10、Java 側からクライアントとして送るなら tus-java-client11 がわかりやすい出発点です。Python からは tus-py-client12 が使えます。

tusd が扱いやすい理由

tusd は公式リファレンス実装であり、次のような特徴があります。

  • コマンドライン操作だけで起動できる
  • ローカルディスクのほか、 S3・Google Cloud Storage・Azure Blob Storage といったクラウドストレージサービスへ保存できる
  • フック(アップロードの開始・完了などのタイミングで外部アプリへ通知・連携する仕組み)を HTTP・gRPC(高速な通信方式の 1 つ)・スクリプトなど複数の方法で使える
  • アップロード件数やエラー数などの統計情報を外部の監視ツールで取得できる

このため、検証段階では「すぐに試せる」、本番では「既存システムとつなぎやすい」というバランスが取れています。プロトコルだけでは手触りがつかみにくいときも、tusd があることで実際の構成をイメージしやすくなります。

ここまでで「tus は何か」「tusd は何か」がそろいました。次章では、実際に tusd を起動して、POSTHEADPATCH の流れやフックの動作を確認します。

5. 実際に tusd を動かしてみる

この章では、tusd を使って実際にファイルをアップロードしてみます。手を動かしながら、前章で見た POSTHEADPATCH の流れや、フックを使った制御の雰囲気を確認します。

まずは最小構成で動かし、そのあとに失敗例やフック連携も試します。ここを一度体験しておくと、次章で触れる「運用で気をつける点」もイメージしやすくなります。

5.1 事前準備

この章では、Windows 環境で tusd を動かした例を扱います。
まずはフックなしで POSTHEADPATCH の流れを確認し、そのあとに tusd の強みの 1 つであるフックを最小構成で試します。

今回使用した環境は次のとおりです。

  • tusd v2.9.1
  • Node.js v22.22.2

tusd の入手

GitHub リリースページ から Windows 向けの v2.9.1 のファイルをダウンロードして展開します。

展開したフォルダーに tusd.exe が入っていれば準備完了です。5.2 ではそのフォルダーで tusd.exe を起動します。

コマンドプロンプトで curl を使う

この記事のコマンド例は、Windows のコマンドプロンプトで確認した内容です。HTTP リクエストの送信には、Windows に含まれている curl を使います。

この章でよく使うオプションだけ先に確認しておきます。

 オプション意味 
 -X POST などHTTP メソッドを指定する 
 -H "ヘッダー名: 値"リクエストヘッダーを追加する 
 --data-binary "..."ボディに送るデータを指定する 
 -iレスポンスヘッダーも表示する 

5.2 Hello world をアップロードしてみる

ここでは「Hello world」(11 バイト)を前半と後半に分けて送り、途中経過を HEAD で確認します。
この節で確認したいのは、サーバーがどこまで受け取ったかを確認して、その位置から続きを送れるという tus の基本動作です。

まず、tusd.exe を起動します。

tusd を起動する
tusd.exe

次のように表示されれば起動成功です。

Terminal window
You can now upload files to: http://[::]:8080/files/

このターミナルはそのままにして、別のコマンドプロンプトを開いて以降のコマンドを実行してください。

ステップ 1: アップロードを作成する(POST)

リクエスト
curl -X POST http://localhost:8080/files/ -H "Tus-Resumable: 1.0.0" -H "Upload-Length: 11" -i

Tus-Resumable: 1.0.0 は tus のバージョンを宣言するヘッダーです。tus のルールとしてすべてのリクエストに付けることが求められており、以降のステップでも同様に付けています。
Upload-Length: 11 は「これから合計 11 バイトのデータを送ります」という宣言です。

レスポンス
HTTP/1.1 201 Created
Location: http://localhost:8080/files/552b47219f9e9553ea0db209fff42851
Tus-Resumable: 1.0.0
X-Content-Type-Options: nosniff
Date: Mon, 30 Mar 2026 12:00:00 GMT
Content-Length: 0

201 Created が返り、Location にこのアップロード専用の URL が発行されました。

レスポンスには以下のような HTTP 共通ヘッダーも含まれていますが、tus 固有のものではないため、以降のレスポンス例では省略します。

  • X-Content-Type-Options: ブラウザに内容の推測をさせないためのセキュリティヘッダーにあたる
  • Date: レスポンスを返した日時を示す
  • Content-Length: レスポンスボディのバイト数を示す

以降は LocationUpload-OffsetUpload-Length など、tus の流れを追うのに必要なヘッダーだけを載せます。

Location の末尾のハッシュ(552b47219f9e9553ea0db209fff42851 の部分)はアップロードごとに異なります。
以降のステップでは、ご自身の環境で返ってきた Location の URL に読み替えてください。

POST 後にサーバー側で何が起きているか

この時点で data\ ディレクトリを見ると、アップロード ID と同じ名前のファイルが 2 つ作成されています。

POST 直後の data フォルダーの中身

図3 POST 直後の data フォルダーの中身
  • 552b47219f9e9553ea0db209fff42851 ― データ本体が入る。まだ送っていないので 0 バイトになっている
  • 552b47219f9e9553ea0db209fff42851.info ― 管理情報を記録した JSON ファイルにあたる。中身は次のとおり
{
"ID": "552b47219f9e9553ea0db209fff42851",
"Size": 11,
"SizeIsDeferred": false,
"Offset": 0,
"MetaData": {},
"IsPartial": false,
"IsFinal": false,
"PartialUploads": null,
"Storage": {
"InfoPath": "data\\552b47219f9e9553ea0db209fff42851.info",
"Path": "data\\552b47219f9e9553ea0db209fff42851",
"Type": "filestore"
}
}

POST で渡した Upload-Length(Size)や Upload-Metadata(MetaData)などが記録されています。Offset はまだデータを送っていないので 0 です。

この .info ファイルは POST 時に 1 回だけ作成され、PATCH では更新されません。tusd は受信済みバイト数をデータ本体ファイルの実サイズから算出しているためです。

ステップ 2: 前半「Hello」を送る(PATCH)

リクエスト
curl -X PATCH http://localhost:8080/files/552b47219f9e9553ea0db209fff42851 -H "Tus-Resumable: 1.0.0" -H "Upload-Offset: 0" -H "Content-Type: application/offset+octet-stream" --data-binary "Hello" -i
レスポンス
HTTP/1.1 204 No Content
Tus-Resumable: 1.0.0
Upload-Offset: 5

204 No Content は「リクエストは成功したが、返すべきボディがない」という HTTP のステータスコードです。PATCH はデータを送りつける操作なので、成功してもサーバーから返すべき内容はありません。
「受け取った」という事実はステータスコード 204 で、「どこまで受け取ったか」は Upload-Offset ヘッダーで伝えます。
Upload-Offset: 5 で、サーバーが 5 バイト目まで受信済みであることがわかります。

ここで指定している Content-Type: application/offset+octet-stream は tus の仕様で定められた専用の値です。
通常の HTTP では application/jsonimage/png のようにデータの種類を示しますが、tus ではこの値を指定することで「Upload-Offset の位置から続くバイナリデータである」ことを表します。指定しないと 400 Bad Request で拒否されます。

PATCH 後にサーバー側で何が起きているか

PATCH を送ったあと、データファイルの中身を確認してみます。

PATCH 後のデータファイルの中身

図4 PATCH 後のデータファイルの中身

データファイルに「Hello」の 5 バイトが書き込まれていることがわかります。

ステップ 3: 受信位置を確認する(HEAD)

リクエスト
curl -X HEAD http://localhost:8080/files/552b47219f9e9553ea0db209fff42851 -H "Tus-Resumable: 1.0.0" -i
レスポンス
HTTP/1.1 200 OK
Tus-Resumable: 1.0.0
Upload-Length: 11
Upload-Offset: 5

11 バイト中 5 バイトまで受信済みであることが確認できます。
この HEAD による確認が、tus の「再開可能」を支える仕組みです。通信が途中で切れた場合でも、サーバーがどこまで受け取ったかを問い合わせて、その続きから送り直せます。

ステップ 4: 後半「 world」を送る(PATCH)

リクエスト
curl -X PATCH http://localhost:8080/files/552b47219f9e9553ea0db209fff42851 -H "Tus-Resumable: 1.0.0" -H "Upload-Offset: 5" -H "Content-Type: application/offset+octet-stream" --data-binary " world" -i

Upload-Offset: 5 は「ステップ 3 の HEAD で確認した受信済み位置(5 バイト目)から続きを送ります」という宣言です。
この値がサーバー側の受信済み位置と一致しているので、データは 5 バイト目の直後から正しくつながります。

レスポンス
HTTP/1.1 204 No Content
Tus-Resumable: 1.0.0
Upload-Offset: 11

「 world」(スペース含めて 6 バイト)が送られ、5 + 6 = 11 で全体の送信が完了しました。

もしここで Upload-Offset3 を指定するとどうなるでしょうか。サーバーは 5 バイト目まで受信済みなのに 3 バイト目からデータを受け入れると、既に書き込まれた部分が上書きされてファイルが壊れてしまいます。tus ではこのようなずれを防ぐため、サーバー側の受信位置と一致しない Upload-Offset は拒否されます。

間違った Offset を送った場合のレスポンス
HTTP/1.1 409 Conflict
Content-Type: text/plain; charset=utf-8
Tus-Resumable: 1.0.0
ERR_MISMATCHED_OFFSET: mismatched offset

409 Conflict が返り、データは書き込まれません。正しく再開するには、HEAD で受信位置を確認してからその値を Upload-Offset に指定します。この整合性チェックがあるおかげで、通信の途中でデータが重複したり欠落したりすることを防げます。

ステップ 5: 完了を確認する(HEAD)

リクエスト
curl -X HEAD http://localhost:8080/files/552b47219f9e9553ea0db209fff42851 -H "Tus-Resumable: 1.0.0" -i
レスポンス
HTTP/1.1 200 OK
Tus-Resumable: 1.0.0
Upload-Length: 11
Upload-Offset: 11

Upload-OffsetUpload-Length が一致しています。アップロードが完了していることを確認できました。

最後に、データファイルの中身を確認します。

アップロード完了後のデータファイルの中身

図5 アップロード完了後のデータファイルの中身

ステップ 2 の「Hello」とステップ 4 の「 world」がつながり、「Hello world」として保存されていることがわかります。
ここまでで、POST で作成したアップロードに対して、HEAD で位置を確認し、PATCH で続きを送る流れを一通り確認できました。

流れのまとめ

  sequenceDiagram
    participant C as クライアント
    participant S as tusd サーバー

    C->>S: POST「11バイトのファイルを送ります」
    S-->>C: 201 Created(URL発行)

    C->>S: PATCH「0バイト目から Hello を送ります」
    S-->>C: 204 No Content(Upload-Offset: 5)

    C->>S: HEAD「今どこまで受信した?」
    S-->>C: 200 OK(5 / 11 バイト)

    C->>S: PATCH「5バイト目から world を送ります」
    S-->>C: 204 No Content(Upload-Offset: 11)

    C->>S: HEAD「全部届いた?」
    S-->>C: 200 OK(11 / 11 バイト ― 完了)
図6 Hello world アップロードの流れ

5.3 フックを使ってアップロードを制御する

ここまでの例では、tusd はファイルを受け取って保存するだけでした。しかし実際のアプリケーションでは、「アップロード前にチェックしたい」「完了後に通知したい」といった要件が出てきます。

tusd の フック を使うと、これが実現できます。

フックとは何か

フックとは、「あるイベントが起きたときに、自動で別の処理を実行する仕組み」です。

  flowchart LR
    subgraph without["フックなし"]
      direction LR
      a1["開始"] --> a2["チェック"]:::skip --> a3["受信"] --> a4["保存"] --> a5["通知"]:::skip --> a6["終了"]
    end
    classDef skip fill:#f5f5f5,stroke:#ccc,stroke-dasharray: 5 5,color:#bbb
  flowchart LR
    subgraph with["フックあり"]
      direction LR
      b1["開始"] --> b2["チェック"] --> b3["受信"] --> b4["保存"] --> b5["通知"] --> b6["終了"]
    end
図7 フックなし/ありの処理の流れ

フックなしの場合、tusd はファイルを受け取って保存するだけです。チェックと通知のステップは点線で表しており、実行されません。
フックを使うと、これらのステップが有効になり、受信の前後に任意の処理を差し込めるようになります。差し込める処理はこの 2 つに限りません。具体的なタイミングは次の表で確認します。

フックのタイミング

tusd では、以下のタイミングでフックを実行できます。

 フック名タイミング制御可否 活用例 
 pre-createアップロード作成前ブロック ファイル種別や容量の事前チェック 
 post-createアップロード作成後通知のみ アップロード情報の DB 登録 
 post-receiveデータ転送中に
定期的に実行
通知のみ 進捗率の通知・ログ記録 
 pre-finish全データ受信後、
レスポンス送信前
ブロック 完了時のカスタムレスポンス 
 post-finish全データ受信後、
レスポンス送信後
通知のみ ファイルの後処理・完了通知 
 pre-terminateアップロード削除前ブロック 削除可否の判定 
 post-terminateアップロード削除後通知のみ DB からのレコード削除・ログ記録 

「ブロック」のフックは、tusd が処理を続行する前にフックの応答を待ちます。フック側で RejectUpload: true を返せば、その操作を拒否できます。一方「通知のみ」のフックは、tusd がクライアントへのレスポンスと並行して実行するため、処理を止めることはできません。

今回試すこと

今回は pre-create を試します。filename がなければアップロード作成を拒否する、というシンプルなチェックです。

フックサーバーのコードを用意する

以下の内容を、作業用フォルダー(tusd を展開した場所など)に hook_server.js という名前で保存しました。ここでは pre-create フックの処理部分だけを抜粋します。

// pre-create: filename がなければ拒否する
if (hookType === "pre-create") {
if (!upload.MetaData || !upload.MetaData.filename) {
console.log("❌ 拒否: filename が指定されていません");
hookResponse = {
RejectUpload: true,
HTTPResponse: {
StatusCode: 400,
Body: "filename は必須です",
},
};
} else {
console.log(
`✅ 承認: filename = ${upload.MetaData.filename}`
);
}
}

tusd はフックイベントが発生すると、イベントの種類(hookType)やアップロード情報(upload)を JSON で送ってきます。このコードでは pre-create(アップロード作成前)のときだけ filename の有無をチェックし、なければ RejectUpload: true で拒否しています。

起動する

フックを使うときは、次の 3 つを別々に動かします。

ターミナル1: フックサーバー(Node.js
ターミナル2: tusd を起動
ターミナル3: curl でアップロード

ターミナル 1 でフックサーバーを起動します。

Terminal window
node hook_server.js
Terminal window
フックサーバー起動中: http://localhost:8000/check

ターミナル 2 でターミナルを開き、tusd をフック付きで起動します。

Terminal window
tusd.exe -hooks-http http://localhost:8000/check
Terminal window
Using 'http://localhost:8000/check' as the endpoint for hooks
Enabled hook events: pre-create, post-create, post-receive, post-terminate, post-finish
You can now upload files to: http://[::]:8080/files/

-hooks-http は、「フック用の HTTP サーバーはここにある」と tusd に教えるオプションです。

テスト①: filename なしで送る(拒否される)

ターミナル 3 で以下を実行します。

リクエスト
curl -X POST http://localhost:8080/files/ -H "Tus-Resumable: 1.0.0" -H "Upload-Length: 11" -i
レスポンス
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Tus-Resumable: 1.0.0
filename は必須です

フックサーバー(ターミナル 1)にも以下が表示されます。

Terminal window
❌ 拒否: filename が指定されていません

filename を指定しなかったため、フックサーバーが拒否し、tusd がクライアントに 400 を返しました。

テスト②: filename ありで送る(成功する)

filename ありで送る
curl -X POST http://localhost:8080/files/ -H "Tus-Resumable: 1.0.0" -H "Upload-Length: 11" -H "Upload-Metadata: filename aGVsbG8udHh0" -i
レスポンス
HTTP/1.1 201 Created
Location: http://localhost:8080/files/{id}
Tus-Resumable: 1.0.0

フックサーバーにも以下が表示されます。

Terminal window
✅ 承認: filename = hello.txt

Upload-Metadata の値は tus の仕様によって Base64 エンコードして送ります。
aGVsbG8udHh0hello.txt を Base64 エンコードした値です。

ここまでで、tus の基本動作とフックによる制御を最小構成で確認できました。次章では、tus-js-client・リバースプロキシ・フックを組み合わせた、より実践的な構成を試します。

6. tus-js-client・リバースプロキシ・フックを組み合わせて検証する

第 5 章では curl を使って tus の基本動作を確認しました。ここからは、もう少し実践的な構成を試します。

実際のプロジェクトでは、クライアントライブラリ・リバースプロキシ・業務ロジックを組み合わせてシステムを構成します。そこで今回は、ZIP ファイルだけを受け付けるアップロードシステムを簡単に組み立て、次の 3 点を確認します。

  • Web 画面から指定の保存先へファイルをアップロードできる
  • フックで ZIP ファイルのみを受け付ける制御ができる
  • 送信を一時停止しても、途中から再開できる

検証環境は次のとおりです。

  • tusd v2.9.1
  • Node.js v22.22.2
  • Nginx 1.28.3

6.1 全体の構成

今回は、役割を 4 つのコンポーネントに分けて検証しました。

検証環境の全体構成

図8 検証環境の全体構成

クライアントからのリクエストは Nginx を経由して tusd へ届き、tusd はイベントごとにフックサーバーへ通知を送ります。各コンポーネントの詳細は 6.26.4 で説明します。

6.2 Web 画面側の実装

今回作成した画面は次のような見た目です。ファイルの選択、アップロードの開始・一時停止・再開をボタンで操作でき、進捗やファイル情報がリアルタイムに確認できます。

tus アップロード検証画面のスクリーンショット

図9 検証用 Web 画面

以降では、この画面を構成する HTML と JavaScript の実装を見ていきます。

ブラウザから tus プロトコルでアップロードするには、HTML に次の 1 行を追加します。

tus-js-client を読み込む
<script src="https://cdn.jsdelivr.net/npm/tus-js-client@latest/dist/tus.min.js"></script>

これは tus-js-client(tus プロトコルの公式 JavaScript クライアントライブラリ)を CDN から読み込むタグです。第 5 章では curl で POSTHEADPATCH を 1 つずつ手動で実行しましたが、tus-js-client はこの一連の流れを自動で処理します。通信断時のオフセット確認やリトライもライブラリ側で行うため、アプリケーション側でプロトコルの詳細を実装する必要がありません。

このスクリプトを読み込むと、グローバルに tus オブジェクトが使えるようになります。アップロード処理の中心になるのが tus.Upload です。

tus.Upload の基本的な使い方
// file: <input type="file"> で選択された File オブジェクト
var upload = new tus.Upload(file, {
endpoint: "http://localhost:1080/files/", // tusd(Nginx 経由)の URL
retryDelays: [0, 3000, 5000, 10000], // リトライ間隔(ミリ秒)
metadata: {
filename: file.name, // Upload-Metadata として送信される
filetype: file.type,
},
onProgress: function (bytesUploaded, bytesTotal) {
// プログレスバーの更新などに使う
var pct = ((bytesUploaded / bytesTotal) * 100).toFixed(1);
console.log(pct + "%");
},
onSuccess: function () {
console.log("完了: " + upload.url);
},
onError: function (error) {
console.log("エラー: " + error);
},
});
upload.start(); // アップロード開始(POST → PATCH を自動実行)

tus.Upload の第 1 引数にはファイル選択で取得した File オブジェクトを、第 2 引数にはオプションを渡します。主なオプションの役割は次のとおりです。

 オプション 役割 
 endpoint アップロード先の URL(Nginx 経由の場合はそのアドレスを指定する) 
 retryDelays 通信エラー時のリトライ間隔
(ミリ秒単位の配列、要素数が最大リトライ回数)
 
 metadata ファイル名や種別などの付加情報
(tus の Upload-Metadata ヘッダーとして送信される)
 

一時停止と再開は abort()start() の組み合わせで実現します。abort() で進行中のリクエストを中止し、再度 start() を呼ぶと、ライブラリが HEAD で受信済み位置を確認してから続きの PATCH を送ります。第 5 章で手動で行った再開処理と同じ流れです。

詳細は tus-js-client GitHub リポジトリ10を参照してください。

6.3 Nginx の設定

Nginx をリバースプロキシとして tusd の前段に置きました。検証で使った設定は次のとおりです。

Nginx でリクエストを tusd へ転送する
location /files/ {
proxy_pass http://127.0.0.1:8080/files/; # tusd へ転送
proxy_http_version 1.1;
proxy_request_buffering off; # クライアント→Nginx のバッファリングを無効化(最重要)
proxy_buffering off; # Nginx→クライアントのバッファリングを無効化
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; # 元のホスト情報を転送
proxy_set_header X-Forwarded-Proto $scheme; # 元のプロトコル(http/https)を転送
proxy_set_header X-Forwarded-Port $server_port;
}

tusd と組み合わせるうえで押さえるべき要点は次の 2 つです。

  • proxy_request_buffering off: Nginx がデータを途中でため込まず、そのまま tusd へ流す設定。tus は「どこまで届いたか」をリアルタイムに把握する必要があるため、この設定がないと再開処理が正しく動かない

この点は、第 7 章の「7.1 リバースプロキシの設定」で本番向けの詳細を改めて説明します。

6.4 フックサーバーの実装

今回は pre-create フックで、ZIP ファイル以外のアップロードを拒否する設定にしました。

pre-create フックで zip のみ許可する(要点)
// pre-create ハンドラー
if (hookType === "pre-create") {
const filename = sanitizeFilename(meta.filename); // zip 以外は例外を投げる
sendJson(res, 200, { ChangeFileInfo: { MetaData: { filename } } });
return;
}
// sanitizeFilename が例外を投げた場合は catch ブロックで rejectUpload を呼ぶ
function sanitizeFilename(name) {
const filename = path.basename(name || "");
if (!filename.toLowerCase().endsWith(".zip")) {
throw new Error("zip ファイルのみ許可します");
}
return filename;
}
function rejectUpload(res, message, statusCode = 415) {
sendJson(res, 200, {
RejectUpload: true,
HTTPResponse: { StatusCode: statusCode, Body: JSON.stringify({ message }) },
});
}

5.3 で試したように、pre-create フックはアップロード作成前にサーバー側で可否を判断できる仕組みです。

6.5 起動と動作確認

フックサーバーと tusd はそれぞれ別ターミナルで起動します。Nginx は exe を実行するだけです。

起動コマンド

ターミナル 1: フックサーバーを起動します。

Terminal window
node hook_server.js

ターミナル 2: tusd をフック付きで起動します。

Terminal window
tusd.exe -hooks-http http://127.0.0.1:9001/check -hooks-enabled-events pre-create -upload-dir ./data -behind-proxy

5.3 で使った -hooks-http に加え、以下のオプションを指定しています。

  • -hooks-enabled-events: フックを実行するイベントを限定する。
    • ここでは pre-create だけを有効にしている
  • -upload-dir: アップロードされたファイルの保存先ディレクトリを指定する
  • -behind-proxy: リバースプロキシの背後で動作することを tusd に伝える

3 つすべてが起動したら、ブラウザから http://localhost:1080 にアクセスします。

ZIP 以外のファイルを送ってみる

まず、PNG ファイルなど ZIP 以外のファイルを選択してアップロードしてみます。フックサーバーが pre-create で拒否するため、アップロードは開始されません。

ZIP 以外のファイルを送ったときのエラー画面

図10 ZIP 以外のファイルを送ったときのエラー画面

Nginx の access.log にも、POST が拒否されたことが記録されています。

Nginx access.log(ZIP 以外を送った場合)
127.0.0.1 - - [03/Apr/2026:14:58:22 +0900] "POST /files/ HTTP/1.1" 415 51 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"

フックサーバーのターミナルにも拒否ログが出力されます。6.4 で実装した pre-create フックが機能していることを確認できました。

一時停止して再開してみる

次に、256MB の ZIP ファイルを選択してアップロードを開始します。進捗が途中まで進んだところで「一時停止」ボタンを押し、通信を止めます。

一時停止中のアップロード画面

図11 一時停止中のアップロード画面

そのあと「再開」ボタンを押すと、進捗が途中から再開します。

Nginx の access.log を見ると、再開時の流れが確認できます。

Nginx access.log(抜粋)
127.0.0.1 - - [03/Apr/2026:15:13:29 +0900] "PATCH /files/a74dad98fb583dc436b4fef2dfb229d8 HTTP/1.1" 204 0 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
127.0.0.1 - - [03/Apr/2026:15:13:29 +0900] "PATCH /files/a74dad98fb583dc436b4fef2dfb229d8 HTTP/1.1" 499 0 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
127.0.0.1 - - [03/Apr/2026:15:14:01 +0900] "HEAD /files/a74dad98fb583dc436b4fef2dfb229d8 HTTP/1.1" 200 0 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
127.0.0.1 - - [03/Apr/2026:15:14:01 +0900] "PATCH /files/a74dad98fb583dc436b4fef2dfb229d8 HTTP/1.1" 204 0 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
127.0.0.1 - - [03/Apr/2026:15:14:01 +0900] "PATCH /files/a74dad98fb583dc436b4fef2dfb229d8 HTTP/1.1" 204 0 "http://localhost:1080/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"

強調した 3 行目が再開のポイントです。一時停止で進行中の PATCH が中断され(2 行目の 499)、再開ボタンを押すとまず HEAD で受信済み位置を確認し、そこから PATCH で続きを送っています。第 5 章で手動で行った再開処理と同じ流れが、tus-js-client によって自動的に実行されていることがわかります。

アップロード結果の確認

アップロードが完了すると、画面に完了状態が表示されます。

アップロード完了後の Web 画面

図12 アップロード完了後の Web 画面

-upload-dir ./data で指定したフォルダーを確認すると、アップロードした ZIP ファイルが保存されていました。

data フォルダーにファイルが保存されている様子

図13 data フォルダーの中身(アップロード完了後)

ZIP 以外の拒否、一時停止からの再開、保存先への保存まで一通り確認できました。次章では、この検証を踏まえて本番運用で気をつけたい点を整理します。

7. 本番運用で気をつけたいポイント

前章では、クライアントライブラリ・リバースプロキシ・フックを組み合わせて一通りの動作を確認しました。ここからは「本番運用で気をつけるべきことは何か」を、第 6 章の検証を踏まえて整理します。

7.1 リバースプロキシの設定

6.3 では Nginx をリバースプロキシ(利用者からの通信をいったん受けて、内側のアプリへ渡す中継サーバー)として tusd の前段に置きました。このとき入れた設定には、それぞれ tus ならではの理由13があります。

バッファリングを無効にする: Nginx はデフォルトで、受信した HTTP リクエストをボディも含めてすべて読み込んでから裏側のサーバーへ転送します。通常のフォーム送信であればこれで問題ありませんが、tus の再開可能アップロードはデータを受信しながら処理・保存することで中断後の再開を実現しています。バッファリングが有効だと、データが Nginx に貯まるだけで tusd に届かないため、再開の前提が崩れてしまいます。そのため proxy_request_buffering off が必要になります。

-behind-proxy を付けて起動する: リバースプロキシを挟む場合、tusd の起動時にこのオプションを付ける必要があります。これを忘れると、tusd が返すアップロード URL のホスト名が Nginx のアドレスではなく tusd 自身のアドレスになってしまい、クライアントからアクセスできなくなります。検証時には見落としやすいポイントです。

7.2 セキュリティ

6.4 のフックでは ZIP の拡張子チェックだけを実装し、認証は入れていませんでした。つまり、あの構成ではアップロード URL さえ知っていれば誰でもファイルを送れる状態です。tus 自体には認証の仕組みがないため、本番環境では別途対策を組み込む必要があります。

たとえば、次のような点が挙がってきます。

  • pre-create フックで認証を確認する: 5.3 で紹介した pre-create フック(アップロード作成前にサーバー側で任意の処理を実行できる仕組み)を使い、ログイン済みのユーザーかどうかを確認してからアップロードを許可する
  • Upload-Metadata の値をそのまま信用しない: ファイル名や種別はクライアント側で自由に書き換えられるため、サーバー側でバリデーションしてから使用する
  • チェックサムでファイルの整合性を確認する: tus ではファイルを分割して送るため、途中で内容が壊れたり差し替えられたりするリスクがある。post-finish フックでアップロード完了後にチェックサム(ハッシュ値)を照合し、クライアントが送ったつもりのファイルとサーバーに届いたファイルが一致しているかを確認してから後続処理に進めるとよい

7.3 未完了アップロードの掃除

6.5 で一時停止を試したとき、data/ フォルダーに途中まで書き込まれたファイルが残っていました。今回はすぐに再開したので問題ありませんでしたが、本番環境ではこうした中途半端なデータが蓄積していくことになります。

再開可能アップロードの性質上、途中でブラウザを閉じたり回線が切れたりして、完了しないまま残るデータは避けられません。放置するとストレージ容量を圧迫するだけでなく、「どこまでが有効なアップロードか」を運用側で判断しにくくなります。

tus のプロトコル仕様には Expiration 拡張14が定義されており、サーバーが未完了アップロードに有効期限を設けてクライアントに通知する仕組みが規定されています。ただし、これはあくまで通信上のルールであり、tusd はこの拡張を実装していません。そのため、未完了データの掃除は別途仕組みを用意する必要があります。

具体的には、次のような点をあらかじめ決めておくとスムーズです。

  • 期限をどれくらいにするか: スマートフォン回線や大容量動画のように中断しやすい用途では長め、社内 LAN(社内ネットワーク)での帳票アップロードのように短時間で終わる用途では短めが向いている
  • 削除の方法をどうするか: クラウドストレージの自動削除機能を使う、定期バッチで古いファイルを削除する、フックで管理するなど、環境に合った方法を選ぶ
  • 監視や定期整理をどうするか: 古い未完了データを定期的に確認・整理する運用も検討する

この記事では、「再開機能と未完了データの掃除はセットで考えるもの」という点を押さえておけば十分です。

7.4 実運用で遭遇した問題

6.5 の再開テストでは、tus-js-client が HEADPATCH の流れを自動で処理してくれるため、特に問題なく再開できました。ただ、別のプロジェクトで tus を使った際に、いくつか注意が必要な点が見つかったので共有します。

再開時に 409 エラーが出る

ネットワークが一時的に切断されてセッションが切れた後、同じ HTTP クライアントをそのまま使ってアップロードを再開しようとしたところ、409 Conflict エラーが返ってきました。

原因を調査したところ、セッション切れのあとに HTTP クライアントが送る Upload-Offset0 にリセットされていたことが分かりました。サーバー側ではすでに途中まで受信が済んでいるため、「送られてきた Offset と受信済みの位置が一致しない」と判断して PATCH を拒否していた形です。

Termination 拡張を有効にしたらフックが動かない

Termination 拡張(クライアントからアップロードを削除できる機能)に関する問題です。そのプロジェクトでは、当初 -disable-termination を付けて Termination 拡張を無効化していました。後からこのオプションを外して有効化したところ、アップロードを削除しても post-terminate フックが呼ばれないという事象が発生しました。

こちらは、起動時の -hooks-enabled-events オプションに post-terminate を追加したら動くようになりました。tusd のフックはデフォルトで有効なものとそうでないものがあり、Termination 拡張の有効化とフックの有効化は別の設定でした。

フォルダーごと複数ファイルをアップロードしたい場合

もう一つ、実装を進める中で対応方針を検討する必要があったのが、フォルダーごと複数ファイルをアップロードしたいケースです。

tus プロトコルは本質的に 1 回のアップロード = 1 つのファイル(バイナリストリーム) を扱う設計になっています。フォルダーやディレクトリツリーという概念はプロトコル仕様に含まれていません。つまり、フォルダー配下の複数ファイルをまとめてアップロードしたい場合、クライアント側の制御とサーバー側のフックを組み合わせて独自に実現する必要がありました。

8. 今後の動き ― IETF 標準化と tus のこれから

最後に、tus を取り巻く今後の動きを整理しておきます。今の技術選定だけでなく、標準化やバージョンアップの方向性を知っておくと、将来の判断がしやすくなります。

8.1 IETF での標準化の動き

HTTP での再開可能アップロードを標準化する動きが、IETF(インターネット技術の国際標準化団体)の httpbis ワーキンググループで進んでいます。ドラフトのタイトルは 「Resumable Uploads for HTTP」 で、2026年3月に中国・深圳(シンセン)で開催された IETF 125 でも議論がおこなわれ、同月に第 11 版(draft-ietf-httpbis-resumable-upload-11)が公開されています15。最終的には「Proposed Standard」(標準化提案)として RFC 化することを目指していますが、現状はまだ「Internet-Draft が存在する」という初期段階にとどまっており、正式な RFC(インターネット技術の公式仕様書)にはなっていません。

このドラフトと tus v1 の関係を正確に把握しておくことは重要です。ドラフトの Acknowledgments(謝辞)セクションには、次のように記載されています。

The tus v1 protocol (https://tus.io/) is a specification for a resumable file upload protocol over HTTP. It inspired the early design of this protocol.16

つまり、tus v1 がこの仕様の初期設計に影響を与えたことは明言されていますが、tus v1 をそのまま RFC 化するものではなく、httpbis ワーキンググループが策定する独立した仕様です。

tus v1 と IETF ドラフトの主な違いを表にまとめます。

 項目 tus v1IETF ドラフト 
 作成・取消 拡張機能(Creation / Termination)コア仕権に統合 
 識別ヘッダー Tus-Resumable: 1.0.0(全リクエストに必須)廃止 
 追加ヘッダー Upload-Complete
Upload-Limit
 
 自動切替 不可(事前に POST が必要)104 レスポンスで自動切替 
 メディア型 application/offset+octet-stream(IANA 未登録)application/partial-upload(IANA 正式登録) 
 Checksum 拡張機能(Upload-Checksum含まない(RFC 9530 で代替) 
 Concat 拡張機能(並列アップロード用)対象外(tus v2 拡張で提供予定) 

これらの変更に共通するのは、tus 独自の仕組みを減らし、既存の HTTP 標準にできるだけ乗せるという設計方針です。tus v1 のコアは HEADPATCH だけで、実際に使うにはほぼ必ず Creation 拡張が必要でした。IETF ドラフトではこれをコアに統合し、仕様単体で完結するようにしています。

Tus-Resumable ヘッダーが廃止された理由は、再開可能アップロードが HTTP 標準の一部になるためです。tus v1 は独自プロトコルなので「このリクエストは tus 対応です」と示す識別ヘッダーが必要でしたが、HTTP 標準仕様として定義されるなら不要になります。

新しく追加された 2 つのヘッダーにも明確な役割があります。
Upload-Complete は送信がこれで最後かどうかを示すヘッダーで、ファイルサイズが事前に分からないストリーミングのようなケースでも完了を明示できます。
Upload-Limit はサーバーが受け入れ可能なサイズ上限をクライアントに通知するヘッダーで、大きすぎるファイルを送ってからエラーになるという無駄を防げます。

もう一つの大きな変化が 104 ステータスコードの導入です。tus v1 では空の POST で専用リソースを作ってからデータを送る 2 段階が必須でした。Creation-with-upload 拡張機能を使えば POST にデータを含めることもできましたが、あくまでオプションであり、クライアント側が tus を意識した実装でなければ使えませんでした。
IETF ドラフトではこの仕組みがコア仕様に統合され、クライアントが最初からデータを含む通常の POST を送り、サーバーが途中で 104 を返すことで「中断しても再開できる状態」に自動で移行します。リソース作成の往復を待たずにデータ送信を始められるため、パフォーマンスやスループットの面でも有利です。
クライアントから見ると普通のアップロードと同じ手順で始められ、サーバー側の対応だけで透過的に再開可能アップロードへアップグレードできるのが利点です。

tus v1 の拡張機能のうち、Checksum は IETF ドラフトには含まれていませんが、データの整合性を検証するヘッダーを定めた RFC 9530 で代替できます。一方、Concatenation(並列転送)や Expiration(有効期限)は HTTP の守備範囲を超えるため標準化の対象外となりました。これらの機能は、将来 tus v2 の拡張として提供される方針が示されています。

次回の IETF 126 は2026年7月にオーストリア・ウィーンで開催予定です。httpbis セッションでこのドラフトがどう進むのか、気になる方はチェックしてみてはいかがでしょうか。

8.2 tus v2 と tusd v2 の方向性

IETF での標準化と並行して、tus プロジェクト自体も次のバージョンに向けた方針を示しています。

tus v2 の基本的な考え方は、コアとなるアップロードプロトコルを IETF 標準仕様に合わせるというものです17。tus v1 の拡張のうち Creation や Termination の機能は IETF ドラフトのコアに統合され、Checksum は RFC 9530 で代替されます。一方、Concatenation や Expiration のように HTTP 標準でカバーされない機能は、tus v2 独自の拡張として引き続き提供される方針です。ただし、これらの拡張仕様の開発は IETF での標準化後に着手する方針で、2026年4月時点ではまだ始まっていません。

tusd v2 はすでにリリースされており、以下のような改善が含まれています18

  • フックシステムの刷新: 双方向通信に対応し、フックからアップロードの拒否・進行中アップロードの停止・アップロード ID のカスタマイズが可能になった
  • ネットワーク耐性の向上: 接続が切れた際のロック解放の仕組みが改善され、中断後の再開がよりスムーズになった
  • IETF プロトコルの実験的サポート: -enable-experimental-protocol フラグで IETF ドラフト仕様を試せるようになった
  • S3 並列パートアップロード: S3 バックエンド利用時のアップロードパフォーマンスが向上した

なお、tusd v2 では tus v1 の HTTP インターフェースに破壊的変更はなく、既存のクライアントはそのまま動作します。

8.3 今の時点でどう見ておくか

ここまでを踏まえると、現時点での判断材料は次のように整理できます。

  • IETF ドラフトはまだ RFC 化されておらず、実運用に使える正式な標準仕様ではない
  • tusd v2 は tus v1 との互換性を維持しており、既存の構成はそのまま使える
  • 公式ライブラリは将来的に tus v1 と v2 の両方をサポートする方針が示されている

これらを踏まえると、当面は tus v1 + tusd の構成で実用上の問題はなく、IETF での標準化が進んだ段階で移行を検討しても遅くはなさそうです。ドラフトの更新状況は IETF Datatracker19 で追えるので、定期的にチェックしておくとよいでしょう。

ここまでで、調査の出発点から今後の見通しまで一通りつながりました。最後に、この記事の要点をまとめます。

おわりに

この記事では、再開可能アップロードを実現する tus と、その公式リファレンスサーバー実装 tusd を整理しました。

ポイントをまとめると、次のとおりです。

 テーマ 要点 
 tus とは ライブラリではなく HTTP ベースのプロトコルで、大容量ファイルの中断・再開に対応した仕組み 
 基本動作と再開 POSTHEAD(再開位置の確認)→ PATCH の流れで中断したところからアップロードを再開できる設計 
 拡張モデル Termination・Checksum・Expiration など、必要な機能を選んで追加できる設計 
 tusd の役割 tus の公式リファレンスサーバーで、フックで業務ロジックと連携し、クラウドストレージへの保存にも対応 
 本番運用 プロキシのバッファリング無効化、認証、未完了データの掃除が重要 

もし「大きなファイルのアップロードを途中から再開したい」「アップロード機能を既存のシステムに後から組み込みたい」と感じているなら、まずは tusd をローカルで立て、tus-js-client などのクライアントライブラリで小規模に試すのがおすすめです。仕組みを一度体験すると、HTTP アップロードとの違いがはっきり見えてきます。

Footnotes

  1. RFC 2046: MIME Part Two: Media Types

  2. RFC 7578: Returning Values from Forms: multipart/form-data

  3. SSH File Transfer Protocol draft-02(SFTP バージョン 3 仕様)

  4. AWS ドキュメント: S3 マルチパートアップロードの概要

  5. AWS ドキュメント: 署名付き URL を使用したオブジェクトのアップロード

  6. tus.io 公式サイト

  7. tus protocol

  8. tus implementations 2

  9. tusd GitHub リポジトリ

  10. tus-js-client GitHub リポジトリ 2

  11. tus-java-client GitHub リポジトリ

  12. tus-py-client GitHub リポジトリ

  13. tusd ドキュメント: Configuration

  14. tus protocol: Expiration 拡張

  15. Resumable Uploads for HTTP (draft-ietf-httpbis-resumable-upload-11)

  16. Resumable Uploads for HTTP (draft-ietf-httpbis-resumable-upload-11) — Acknowledgments

  17. tus.io ブログ: tus 2.0 and beyond(2022年2月24日)

  18. tus.io ブログ: tusd v2.0(2023年9月20日)

  19. IETF Datatracker: Resumable Uploads for HTTP

記事の執筆にあたっては情報の正確性に努めておりますが、掲載されている文章やソースコード、設定ファイル等の内容について、完全な正確性や安全性を保証するものではありません。活用される際は、必ず公式ドキュメント等をご自身で確認のうえご判断ください。


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。