【実務完全ガイド】Laravelのファイルアップロード&配信――Storage/S3、署名付きURL、画像最適化、PDF/動画、ウイルススキャン、権限、キャッシュ、アクセシブルな代替テキスト
この記事で学べること(要点)
- Laravel Storage(Flysystem)の基本と、ローカル/S3/CDNの設計
- アップロードのセキュリティ(MIME検証、サイズ制限、拡張子詐称対策、EXIF除去、ウイルススキャン)
- 署名付きURLでの安全な配布(期限、Content-Disposition、private/publicの使い分け)
- 画像の派生生成(リサイズ/サムネイル/WebP/AVIF)、
srcset、遅延読み込み、CLS対策 - PDF/Officeの配布ポリシー(HTML代替、要約、タグ付きPDF)
- 動画配信(HLS/DASHの考え方、字幕/WebVTT、オートプレイ抑制)
- キャッシュ(ETag/Cache-Control)とCDN、コスト最適化
- メディアのアクセシビリティ(代替テキスト、キャプション、字幕、トランスクリプト、管理画面フロー)
想定読者(だれが得をする?)
- Laravel 初〜中級エンジニア:画像やファイルを安全に扱い、パフォーマンスも担保したい方
- SaaS/メディア/ECのテックリード:S3/CDN前提のメディア基盤を標準化したい方
- 編集者/デザイナー:代替テキストや字幕を運用として回したい方
- QA/アクセシビリティ担当:メディアでも「誰でも理解できる」を保証したい方
アクセシビリティレベル:★★★★★
代替テキスト、キャプション、字幕、トランスクリプト、読み上げ・キーボード操作、動きの抑制、色に依存しない状態表示まで、実装と運用の両面で網羅します。
1. はじめに:ファイル機能は“便利”と“危険”が隣り合わせです
画像やPDF、動画のアップロードは、ユーザー体験を大きく上げます。一方で、セキュリティ事故(拡張子詐称、マルウェア、情報漏えい)や、性能問題(重い画像、転送コスト、キャッシュ不整合)を招きやすい領域でもあります。さらに、メディアはアクセシビリティの差が出やすく、代替テキストや字幕が無いだけで情報が伝わらなくなることがあります。
Laravelは Storage を軸に、権限、署名URL、キュー、HTTPレスポンスまで一貫して実装できます。この記事では「安全・速い・だれでも理解できる」ファイル基盤を、実務の型として整理します。
2. 保存先の設計:publicとprivateを分ける
2.1 基本方針
- private保存が基本:アクセス制御が必要なファイルは必ずprivate
- public配信は「公開してよい派生ファイル」だけ
- 原本は非公開で保存し、配布は署名URLやアプリ経由で行う
S3なら、バケット(またはプレフィックス)で
private/(非公開)public/(CDN配信)
のように分けると管理が楽です。
3. Storage設定:S3+CDNを前提に整える
FILESYSTEM_DISK=s3
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=example-bucket
AWS_URL=https://cdn.example.com
// 保存(private)
$path = Storage::disk('s3')->putFile('private/uploads', $request->file('file'), 'private');
// 一時的に配布(10分)
$url = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10), [
'ResponseContentDisposition' => 'inline',
]);
AWS_URLをCDNにすると、Storage::url()がCDN経由になります。- private配布は
temporaryUrl()が便利です。
4. アップロードのセキュリティ:検証と無害化をセットで
4.1 バリデーション(MIME+サイズ)
$request->validate([
'file' => ['required','file','mimetypes:image/jpeg,image/png,image/webp,application/pdf','max:8192'],
]);
- 拡張子ではなく MIME を検証します。
maxはKB単位です。
4.2 拡張子詐称対策
mimetypesとfileinfoによる実体検査- アップロード後の保存名はランダムに(元のファイル名を信頼しない)
4.3 EXIF(位置情報)除去
写真の位置情報が残ると情報漏えいにつながります。画像処理(リサイズ時)に合わせてEXIFを除去すると安全です。
4.4 ウイルススキャン
- アップロード直後にジョブへ投げて非同期スキャン
- 陽性なら隔離し、ユーザーに安全な文面で通知
- “未スキャン”状態では外部公開しない設計が安心です
5. DBでのメタ管理:ファイルは「ただ保存」しない
ファイルはストレージに置くだけでは運用が難しいので、DBでメタを管理します。
例:media テーブル(概念)
diskpathmimesizevariants(サムネイル等)meta(alt/caption/lang など)owner_id/tenant_idvisibility(private/public)
これがあると、
- 権限
- 再生成
- 多言語 alt
- 監査
がやりやすくなります。
6. 画像最適化:派生生成(variant)をジョブで作る
原本は大きいことが多いので、表示用の派生を作ります。
- 幅別:320/640/1280
- フォーマット:AVIF/WebP/JPEG
- サムネイル:一覧用
width/heightを固定してCLSを防ぐ
派生生成は同期でやると遅いので、キューへ。
dispatch(new GenerateImageVariants($mediaId))->onQueue('media');
7. レスポンシブ配信:picture と srcset
<picture>
<source type="image/avif" srcset="{{ $v['avif'] }}">
<source type="image/webp" srcset="{{ $v['webp'] }}">
<img
src="{{ $v['w640'] }}"
srcset="{{ $v['w320'] }} 320w, {{ $v['w640'] }} 640w, {{ $v['w1280'] }} 1280w"
sizes="(max-width: 640px) 90vw, 640px"
alt="{{ $media->meta['alt'] ?? '' }}"
width="640" height="400"
loading="lazy" decoding="async"
>
</picture>
altは必須(装飾は空alt="")- 幅/高さ指定でCLSを抑制
loading="lazy"で転送量削減
8. 署名付きURLと安全なダウンロード
8.1 署名付きルート
アクセス制御が必要なら、アプリが配布を仲介します。
Route::get('/files/{media}', [FileController::class,'show'])
->middleware(['auth','signed'])
->name('files.show');
$url = URL::temporarySignedRoute('files.show', now()->addMinutes(10), ['media'=>$media->id]);
8.2 Content-Disposition
inline:ブラウザ表示(画像/PDF)attachment:ダウンロード(機密ファイル)
9. キャッシュ:CDN+ハッシュ名が最強
公開派生ファイルは、内容ハッシュをファイル名に入れると、
- 長期キャッシュ(1年)
- 更新時はURLが変わるので即反映
が実現できます。
例:thumb.5f2a9c.webp
Cache-Control: public, max-age=31536000, immutable
動的配布が必要な場合は ETag/304 を使います。
10. PDF/ドキュメント:代替と要約が大切です
- PDFだけにせず、重要情報はHTMLでも提供
- 可能ならタグ付きPDF(見出し構造、代替テキスト)
- 長いPDFには要約と目次
- ダウンロードリンクにはファイル種別とサイズを併記(例:PDF 2.3MB)
アクセシビリティ上、「PDFを開けない/読みにくい人」への代替があると安心です。
11. 動画:字幕(VTT)とトランスクリプトを標準に
動画は情報量が多い分、字幕がないと伝わりません。最低限、次を標準にします。
<track kind="captions" src="...vtt" srclang="ja">- 音声内容の文字起こし(トランスクリプト)
- オートプレイを避け、ユーザー操作で再生
prefers-reduced-motionを尊重し、ヒーロー動画を抑制
12. 代替テキスト運用:編集フローで品質が決まる
技術だけでは alt は埋まりません。運用の型が必要です。
- 画像アップロード時に alt 入力欄を必ず表示
- 装飾画像は「装飾」と指定したときだけ空 alt を許可
- テキスト画像は、同じ内容を alt に入れる(長文なら本文へ)
- 多言語なら
alt[ja]/alt[en]のように言語別保存
13. テスト:アップロードと権限を守る
- StorageのFakeでアップロードを検証
- 署名付きURLが期限切れで弾かれること
- 別ユーザーが見られないこと(認可)
Storage::fake('s3');
$res = $this->post('/media', [
'file' => UploadedFile::fake()->image('a.jpg', 800, 600),
]);
$res->assertCreated();
14. よくある落とし穴と回避策
- publicに原本を置く
- 回避:原本はprivate、公開は派生のみ
accept属性だけで安心する- 回避:サーバでMIME検証、サイズ制限
- alt が空のまま
- 回避:入力必須化(装飾指定時のみ例外)
- 画像が重くて遅い
- 回避:派生生成+
srcset+長期キャッシュ
- 回避:派生生成+
- 動画が音付き自動再生
- 回避:ユーザー起点+字幕+トランスクリプト
- キュー停止で派生生成が止まる
- 回避:Horizon監視、失敗ジョブ通知
- キャッシュが効かずコスト増
- 回避:ハッシュ名+immutable
15. チェックリスト(配布用)
安全
- [ ] MIME+サイズ検証、拡張子詐称対策
- [ ] EXIF除去、ウイルススキャン(非同期)
- [ ] 原本はprivate、配布は署名URL
性能
- [ ] 派生生成(リサイズ/AVIF/WebP)をキュー化
- [ ]
picture/srcset、width/height、lazy load - [ ] CDN+ハッシュ名+長期キャッシュ
運用
- [ ] mediaメタ管理(owner/tenant/variants/meta)
- [ ] キュー/スケジューラ監視、失敗通知
- [ ] 削除・ライフサイクル(古い派生の整理)
アクセシビリティ
- [ ] alt運用(必須、装飾は空)
- [ ] キャプション、字幕(VTT)、トランスクリプト
- [ ] PDFの代替(HTML/要約/サイズ表記)
テスト
- [ ] Storage::fakeでアップロード検証
- [ ] 署名URLの期限・認可
- [ ] 別ユーザー参照の拒否
16. まとめ
Laravelのファイル機能は、Storageを中心に「保存」「配布」「最適化」「権限」「運用」を一貫して設計すると、驚くほど安定します。原本はprivateに置き、派生だけを公開。アップロードはMIME検証と無害化を徹底し、派生生成はキューで非同期に。CDNと長期キャッシュで速くしつつ、altや字幕などアクセシビリティの情報をメタとして管理し、編集フローで品質を担保します。ファイルは便利な反面、事故が起きやすい領域です。だからこそ、最初に型を作って、安心して育てていきましょう。
