【JS応用】再開可能なファイルアップロード

再開可能なファイルアップロード:UXと堅牢性を両立させる技術的アプローチ

現代のWebアプリケーションにおいて、大容量ファイルのアップロードは避けて通れない機能の一つです。動画配信プラットフォーム、クラウドストレージ、あるいは業務用のログ分析ツールなど、数GB単位のデータを扱う場面は増加しています。しかし、標準的な「HTTP POSTによるマルチパートアップロード」には、ネットワークの瞬断やブラウザのクラッシュに対して非常に脆弱であるという弱点があります。

この記事では、再開可能なファイルアップロード(Resumable File Upload)を実現するための技術的要件、プロトコル、そして実務におけるベストプラクティスを網羅的に解説します。

再開可能なアップロードの基本原理:チャンク分割とステート管理

再開可能なアップロードの核心は、「ファイルを小さな断片(チャンク)に分割し、それぞれを独立したリクエストとして送信する」ことにあります。これにより、特定のチャンクの送信に失敗しても、その部分だけを再送すれば済み、ファイル全体を最初からアップロードし直す必要がなくなります。

このプロセスを支える主要なコンポーネントは以下の3点です。

1. ファイルの分割(File Slicing):JavaScriptのFile API(Blob.slice())を使用して、ファイルを固定サイズのバイナリ断片に分割します。
2. ステータスの追跡:どのチャンクがサーバーに到達したか、あるいはどれが未完了かを管理します。
3. チェックポイントの共有:クライアントとサーバー間で「次に送るべきオフセット(バイト位置)」を合意し、通信が途絶した後にその位置から再開します。

TUSプロトコル:標準化された解決策

ゼロから再開可能なアップロードの仕組みを設計することも可能ですが、実務では「TUS(tus.io)」プロトコルの利用を強く推奨します。TUSは、HTTP上に構築されたオープンなプロトコルであり、信頼性の高いアップロードを実現するための仕様が詳細に定義されています。

TUSの主な特徴は以下の通りです。
・リクエストの再試行を前提とした設計
・アップロードの状態をサーバー側で保持可能
・異なる言語やフレームワークでの実装が豊富

実務的な実装における技術的詳細

再開可能なアップロードを実装する際、フロントエンド側で注意すべき点は、単なるネットワークエラーのハンドリングだけではありません。

まず、チャンクサイズの決定が重要です。小さすぎるとHTTPリクエストのオーバーヘッドが増大し、大きすぎると再送時のコストが上がります。一般的には5MBから10MB程度が推奨されます。また、ブラウザのメモリ消費量を考慮し、FileReaderやStream APIを用いて非同期的に読み込む設計が求められます。

次に、サーバー側の「シーク」処理です。サーバーはクライアントから送られてきたチャンクを一時的なストア(S3のマルチパートアップロードや、ローカルの一時ディレクトリ)に書き込みます。クライアントが再開を要求した際、サーバーは既に保持しているデータのサイズを返し、クライアントはその続きからデータを送信するという連携が必要です。

サンプルコード:Blob.sliceを用いた実装の概念

以下は、ブラウザ側でファイルをチャンクに分割し、逐次送信する基本的なロジックの例です。


async function uploadFile(file) {
  const CHUNK_SIZE = 1024 * 1024 * 5; // 5MB
  let offset = 0;

  while (offset < file.size) {
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    const success = await sendChunk(chunk, offset);

    if (success) {
      offset += chunk.size;
      console.log(`Progress: ${((offset / file.size) * 100).toFixed(2)}%`);
    } else {
      console.error("Upload failed. Retrying in 3 seconds...");
      await new Promise(resolve => setTimeout(resolve, 3000));
      // ここで再試行ロジックを呼び出す
    }
  }
}

async function sendChunk(chunk, offset) {
  try {
    const response = await fetch('/upload-endpoint', {
      method: 'POST',
      headers: {
        'Content-Range': `bytes ${offset}-${offset + chunk.size - 1}/${file.size}`,
        'Content-Type': 'application/octet-stream'
      },
      body: chunk
    });
    return response.ok;
  } catch (e) {
    return false;
  }
}

実務エンジニアのための設計アドバイス

実務でこの機能を導入する場合、以下のポイントに留意してください。

1. 認証とセキュリティ
再開可能なアップロードは、リクエストが複数回に分かれるため、各リクエストが同一のユーザーによって行われていることを検証する必要があります。JWT(JSON Web Token)などの署名付きトークンを各リクエストのヘッダーに含めるのが一般的です。

2. サーバー側のクリーンアップ
中断されたアップロードが放置されると、サーバーのストレージが圧迫されます。一定期間更新がない一時ファイルを削除するバッチジョブ(CronやLambdaなど)を必ず実装してください。

3. 並列アップロードの検討
ネットワーク帯域が十分に広い場合、複数のチャンクを並列でアップロードすることで速度を向上させることができます。ただし、サーバー側のリソース消費と順序制御の複雑さが増すため、まずは直列のアップロードで安定性を確保してから導入することをお勧めします。

4. ユーザー体験(UX)の向上
単に「再開可能である」だけでなく、アップロードの中断・一時停止・再開ボタンをUIに配置してください。これにより、ユーザーは自分の意図で通信をコントロールでき、不必要なデータ通信を避けることが可能になります。

まとめ

再開可能なファイルアップロードは、単なる機能追加ではなく、ユーザーに対する「信頼」の提供です。ネットワーク環境が完璧ではないモバイルデバイスや不安定なWi-Fi環境において、大容量データを確実に送信できる仕組みは、アプリケーションの品質を大きく引き上げます。

TUSプロトコルのような既存の規格を活用しつつ、フロントエンドのFile APIとサーバー側のステート管理を適切に組み合わせることで、堅牢なアップロード基盤を構築することが可能です。まずは小規模なチャンク分割から始め、徐々にエラーハンドリングと再試行戦略を磨き上げることで、プロダクションレベルの堅牢性を実現してください。

技術の進歩により、ブラウザ側での処理能力は向上していますが、ネットワークの物理的な制約を解決するエンジニアリングの姿勢こそが、フロントエンド・スペシャリストとして最も重要視されるべき要素です。

コメント

タイトルとURLをコピーしました