mcpField
W
web-files-file-upload-patterns

web-files-file-upload-patterns

github.com/agents-inc開発2676 回インストール2 日前に更新
リポジトリ

web-files-file-upload-patterns について

File Upload Patterns は、ブラウザーでのファイル選択、検証、アップロードの実装を支援するエージェントスキルです。ストレージへの直接アップロードや再開可能な方式を扱い、アクセシビリティとセキュリティにも配慮します。

主な機能

  1. 01クリック、キーボード、ドラッグで選択
  2. 02クライアント検証とマジックバイト確認
  3. 03XHR による進捗表示とキャンセル
  4. 04署名付き URL で直接アップロード
  5. 05チャンク分割、再試行、再開
  6. 06状態と進捗のアクセシブルな通知

活用シーン

  1. 01キーボード対応ドロップゾーンの構築
  2. 02進捗表示とキャンセル機能の追加
  3. 03オブジェクトストレージへの直接送信
  4. 04中断した大容量アップロードの再開

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agents-inc/skills --skill web-files-file-upload-patterns

インストール後、Claude Code で/web-files-file-upload-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

metadata.yaml
reference.md
examples/
accessibility.md
core.md
presigned-upload.md
preview.md
progress.md
resumable.md
validation.md

詳細

ファイルアップロードのパターン

クイックガイド: ドロップゾーンは、非表示のファイル入力を内包するキーボード操作可能なボタンであり、ドラッグ操作はその拡張機能です。ユーザーの利便性のため、クライアント側では拡張子、MIME タイプ、ファイル自体のマジックバイトの順に検証し、さらにサーバー側でも検証してください。クライアント側のチェックは、どれもセキュリティ対策にはならないためです。進捗の取得には XMLHttpRequest が必要です。fetch にはアップロード進捗イベントがありません。およそ 100MB を超える場合は、失敗時のやり直しを 1 チャンク分に抑えるため、ファイルをチャンクに分割してください。大きなファイルは、サーバーが発行する署名付き URL を使ってストレージに直接アップロードし、リクエストボディがプロキシされないようにします。

詳細リソース:

  • examples/core.md — ファイル入力、ドロップゾーン、ファイルリストの状態管理と描画、組み立て済みコンポーネント
  • examples/validation.md — ルールベースのバリデーター、マジックバイトの検出、寸法チェック、検証フック
  • examples/progress.md — 速度と残り時間を含む XHR の進捗、進捗バー、フォーマッター、並行アップロード
  • examples/preview.md — 選択したファイルのプレビューサムネイルとクリーンアップ
  • examples/presigned-upload.md — PUT と POST ポリシーによるアップロード、サーバーとの契約、マルチパートの各パート、フックとしての全体フロー
  • examples/resumable.md — リトライ対応のチャンクアップローダー、再読み込み後の再開、tus クライアント、tus サーバーとの契約
  • examples/accessibility.md — 選択と進捗の通知、ファイルダイアログを閉じた後のフォーカス復帰
  • reference.md — サイズに応じた方式の選択、有効期限の指針、検証順序、CORS、確認用チェックリスト

どの方法を選ぶか

ほぼすべては、アップロード先によって決まります。

  • ファイルを自分のエンドポイントに送信する場合 — FormData を使った 1 回の POST、XHR による進捗取得、サーバーで適用できるサイズ上限を使用します。examples/core.md と examples/progress.md で必要な内容はすべて揃います。
  • ファイルをオブジェクトストレージに送信する場合 — サーバーが署名付き URL を発行し、ブラウザーから直接アップロードすることで、アプリケーションをバイトデータが経由しないようにします。 examples/presigned-upload.md。
  • 失敗時の影響が大きいサイズのファイルの場合 — ファイルを分割して、同時実行数を制限しながら各パートをアップロードし、アップロード済みのパートを記録してリトライ時に再開できるようにします。 examples/resumable.md。

<critical_requirements>

アップロードコードを書く前に

ブラウザーだけでなく、サーバーでも検証してください。 クライアント側の検証は、何が拒否されるかをユーザーにすばやく知らせるためのものです。誰でも完全に回避できるため、安全性については何も保証しません。

種類が重要な場合は、ファイルの先頭のバイトを読み取ってください。 拡張子も MIME タイプも、どちらもファイルを作成した人が指定するものです。それらを信頼するチェックは、名前を変更した実行ファイルも通してしまいます。

作成したオブジェクト URL はすべて無効化してください。 プレビューは URL.revokeObjectURL() が実行されるまでファイル全体をメモリに保持するため、ユーザーが気を変えて 3 回ファイルを選び直すと、3 つのファイルがメモリに残ります。

ドロップゾーンにキーボードでアクセスできるようにしてください。 role="button"、tabIndex={0}、ファイルダイアログを開く Enter/Space ハンドラーを用意し、その上にドラッグ操作を追加します。モバイルではドラッグ操作ができないため、実際に使うのはクリック操作です。

リクエスト本体をプロキシするのではなく、サーバーから有効期限の短い署名付き URL を発行してください。 これにより、アップロードによってアプリケーションに負荷がかからず、ストレージの認証情報がブラウザーから参照されることもありません。

</critical_requirements>


自動検出: dropzone、dataTransfer.files、dragenter、dragleave、dragover、input type="file"、event.target.files、accept 属性、xhr.upload.addEventListener、lengthComputable、署名付き URL、uploadUrl、マルチパートアップロード、UploadPart、ETag、チャンク分割アップロード、file.slice、Content-Range、中断したアップロードの再開、tus、Tus-Resumable、Upload-Offset、マジックバイト、ファイルシグネチャ、FormData append file

対象:

  • クリック、キーボード、ドラッグによるファイル選択
  • 送信前の種類、サイズ、寸法の検証
  • 進捗、速度、残り時間の報告とキャンセル
  • サーバーが署名した URL を使ったストレージへの直接アップロード
  • 大きなファイルのチャンク分割と、中断したアップロードの再開
  • スクリーンリーダーへの選択、進捗、失敗の通知

別の場所で扱う内容:

  • バイトの到着後の受信、スキャン、保存
  • 送信前の画像のリサイズ、切り抜き、変換。このスキルが送信するのは、渡された File です
  • 保存されたオブジェクトの格納場所、配信方法、URL の形式
  • アップロード済みメディアのストリーミング再生

基本方針

アップロードは、混同されがちな 3 つの独立した課題から成り立っています。ファイルの選択、検証、そしてバイト列の転送です。それぞれを分けておくことで、どの部分も置き換え可能になります。

検証には、決して曲げてはならないルールがあります。クライアント側の検証はユーザー体験のための機能であり、制御手段となるのはサーバー側の検証だけです。 ブラウザーが把握しているファイルの情報、つまり名前、拡張子、type は、すべてファイル自体に由来します。マジックバイトを読み取れば基準は厳しくなりますが、本質は変わりません。依然として、クライアントが回避できるチェックです。

転送のスケーリングを左右するのは、ファイルの数ではなく、1 つのリクエストが開いたままになる時間です。短いリクエストなら、失敗しても全体を再試行できます。長いリクエストでは、接続が切れる可能性が積み重なり、全体を再試行するのが許容できなくなります。その時点で初めて、特定のバイト数に達したときではなく、チャンク分割の複雑さに見合う効果が得られるようになります。


基本パターン

パターン 1:ドロップゾーン

真偽値を追跡するのではなく、ドラッグイベントを数えてください。dragenter と dragleave は入れ子になった要素ごとに発生するため、ポインターが子要素を横切っただけで真偽値が切り替わってしまいます。

const dragCounterRef = useRef(0);

<div
  onDragEnter={() => { dragCounterRef.current++; setState("drag-over"); }}
  onDragLeave={() => {
    dragCounterRef.current--;
    if (dragCounterRef.current === 0) setState("idle");
  }}
  onDragOver={(e) => e.preventDefault()} // without this, drop never fires
  onDrop={handleDrop}
  onClick={() => inputRef.current?.click()}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
  }}
  role="button"
  tabIndex={disabled ? -1 : 0}
  aria-label="File upload area. Click or drag files to upload."
>
  <input ref={inputRef} type="file" hidden aria-hidden="true" tabIndex={-1} />
</div>

完全なコード:examples/core.md

パターン 2:ファイルリストの状態管理

ファイルごとに独自の状態を持つエントリーを 1 つ作成し、バッチ単位ではなくファイル単位で失敗を扱います。拒否されたファイルには理由が返され、UI に表示できます。

interface FileWithId {
  id: string;
  file: File;
  preview?: string;
  status: "pending" | "uploading" | "success" | "error";
  progress: number;
  error?: string;
}

// addFiles returns { added, rejected }, each rejection carrying its reason
// removeFile and clearFiles revoke any preview URL before dropping the entry

完全なコード:examples/core.md

パターン 3:XHR による進捗状況

fetch で取得時の進捗状況は報告されますが、アップロード時の進捗状況は報告されないため、アップロードの進捗状況には XMLHttpRequest を使います。速度の読み取り値が不安定にならないよう、直近数回のサンプルを平均します。

const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (event) => {
  if (!event.lengthComputable) return; // no total: show a spinner, not a bar
  const speed = rollingAverageSpeed(event.loaded, performance.now());
  setProgress({
    loaded: event.loaded,
    total: event.total,
    percentage: Math.round((event.loaded / event.total) * 100),
    speed,
    remainingTime: (event.total - event.loaded) / speed,
  });
});

キャンセルには xhr.abort() を使います。fetch のボディをストリーミングして測定できるのは、ネットワーク上で送信されたバイト数ではなく、ストリームに渡したバイト数です。そのため、代用にはなりません。

完全なコード:examples/progress.md

パターン 4:マジックバイトによる検出

先頭の 12 バイトを読み取り、既知のシグネチャと照合します。ファイル全体を読み込んではいけません。大きなファイルではタブがフリーズします。

const FILE_SIGNATURES = [
  { mime: "image/jpeg", extension: "jpg", signature: [0xff, 0xd8, 0xff] },
  { mime: "image/png", extension: "png", signature: [0x89, 0x50, 0x4e, 0x47] },
  {
    mime: "application/pdf",
    extension: "pdf",
    signature: [0x25, 0x50, 0x44, 0x46],
  },
  {
    mime: "application/zip",
    extension: "zip",
    signature: [0x50, 0x4b, 0x03, 0x04],
  },
];

const buffer = await file.slice(0, 12).arrayBuffer();
const bytes = new Uint8Array(buffer);

Office ドキュメントは ZIP アーカイブなので、ZIP に一致した場合はさらに確認が必要です。先頭の 1 キロバイトに含まれる word/、xl/、ppt/ のいずれかで、どの形式かを特定できます。

完全なコード:examples/validation.md

パターン 5:署名付きアップロード

4 つのステップで完了し、アプリケーションがバイトデータを保持することはありません。

  1. クライアントが名前、種類、サイズを送信して、サーバーに URL をリクエストします。
  2. サーバーがリクエストを認可し、名前をサニタイズしてキーを作成し、有効期限の短い URL に署名します。
  3. クライアントがその URL にファイルを PUT します。
  4. クライアントがキーをサーバーに伝え、サーバーが記録します。
const { uploadUrl, key } = await requestPresignedUrl(file);

const xhr = new XMLHttpRequest();
xhr.open("PUT", uploadUrl);
xhr.setRequestHeader("Content-Type", file.type);
xhr.upload.addEventListener("progress", reportProgress);
xhr.send(file);

PUT の代わりに POST ポリシー URL を使うと、ストレージサービス側でサイズとコンテンツタイプを適用できます。ただし、FormData のフィールド順序が重要になるため、ファイルは最後に追加します。

完全なコード:examples/presigned-upload.md

パターン 6:チャンク分割と再開

ファイルを分割し、同時実行数を制限して各チャンクをアップロードします。失敗したチャンクは最初からやり直すのではなく、指数バックオフで再試行します。

const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;

const start = chunkIndex * chunkSize;
const chunk = file.slice(start, Math.min(start + chunkSize, file.size));

完了したチャンクのインデックスは、name-size-lastModified をキーにして永続化します。そうすれば、再読み込み時に最初からではなく途中から再開できます。1 日後にそのレコードを期限切れにします。独自のプロトコルではなく相互運用可能なプロトコルを使う場合、tus では作成に POST を使い、オフセットの取得に HEAD を使い、追記に PATCH を使います。

完全なコード:examples/resumable.md


<red_flags>

要注意事項

実行時に問題が発生します:

  • dragover に event.preventDefault() がないと、drop が発生せず、ブラウザーがファイルに移動します。dragover と drop の両方でデフォルト動作を防止してください
  • ドラッグ状態を真偽値で管理すると、ポインターが子要素を横切る際にちらつきます。ref で dragenter と dragleave をカウントしてください
  • 選択後もファイル入力の値が設定されたままだと、同じファイルを 2 回選択しても change イベントが発生しません。files を読み取った後、event.target.value = "" を代入してください
  • 進捗状況が必要な箇所で fetch を使うと、アップロードの進捗イベントがなく、ストリームの進捗状況も誤った値になります。XMLHttpRequest を使ってください
  • 種類の判定に file.text() や readAsDataURL() を使うと、ファイル全体がメモリに読み込まれます。先頭の 12 バイトだけをスライスしてください
  • レンダー関数内でオブジェクト URL を作成すると、レンダーごとに新しい URL が作られ、どれも解放されません。エフェクト内で作成し、クリーンアップ時に解放してください
  • ストレージバケットに CORS の設定がないと、直接アップロードはプリフライトで失敗します。オリジンとメソッドを許可し、マルチパート用に ETag を公開してください

予想外の動作:

  • 署名付き URL はベアラートークンです。保持している人は誰でも、有効期限が切れるまでその操作を実行できます
  • 拡張子、MIME タイプ、File.type はすべてクライアント側から提供され、偽装可能です
  • モバイルブラウザーにはドラッグ&ドロップがないため、そこで使えるのはクリック操作だけです
  • 長さが不明なリクエストでは lengthComputable は false になり、true になるまではパーセンテージに意味がありません
  • 完了も中止もされていないマルチパートアップロードでは、パーツに課金される一方、表示されません。パーツを期限切れにするライフサイクルルールを設定してください
  • Safari のドラッグイベントは Chromium と十分に異なるため、別途テストする価値があります
  • 写真には EXIF の向き情報が含まれており、ブラウザーは表示時に適用しますが、Canvas 処理では適用されません

</red_flags>

コメント