[Complete Practical Guide] Laravel File Upload & Delivery — Storage/S3, Presigned URLs, Image Optimization, PDFs/Video, Virus Scanning, Authorization, Caching, and Accessible Alternative Text
What you’ll learn in this article (key points)
- The basics of Laravel Storage (Flysystem) and how to design for local/S3/CDN
- Upload security (MIME validation, size limits, extension spoofing defenses, EXIF removal, virus scanning)
- Secure delivery with presigned URLs (expiration, Content-Disposition, when to use private vs public)
- Image variant generation (resize/thumbnails/WebP/AVIF),
srcset, lazy loading, and CLS prevention - PDF/Office delivery policy (HTML alternatives, summaries, tagged PDFs)
- Video delivery (how to think about HLS/DASH, captions/WebVTT, avoiding autoplay)
- Caching (ETag/Cache-Control) and CDN cost optimization
- Media accessibility (alt text, captions, subtitles, transcripts, and admin workflow)
Intended readers (who benefits?)
- Beginner–intermediate Laravel engineers: You want to handle images/files safely while protecting performance.
- Tech leads in SaaS/media/e-commerce: You want to standardize a media platform assuming S3/CDN.
- Editors/designers: You want to operationalize alt text and subtitles.
- QA/accessibility owners: You want to guarantee “understandable for everyone,” even for media.
Accessibility level: ★★★★★
Covers alt text, captions, subtitles, transcripts, screen-reader and keyboard operation, motion reduction, and non-color-dependent state signaling—both implementation and operations.
1. Introduction: File Features Sit Between “Convenient” and “Dangerous”
Uploading images, PDFs, and videos greatly improves UX. But it’s also an area prone to security incidents (extension spoofing, malware, data leaks) and performance problems (heavy images, transfer costs, cache inconsistency). Media also exposes accessibility gaps: without alt text or captions, information simply doesn’t reach some users.
Laravel enables an end-to-end approach—from Storage to authorization, signed URLs, queues, and HTTP responses. This guide organizes a practical “pattern” for a file platform that is safe, fast, and understandable to everyone.
2. Storage Design: Separate public and private
2.1 Core policy
- Default to private storage: anything requiring access control must be private
- Only “safe-to-publish variants” should be public
- Keep originals non-public; distribute via presigned URLs or through the app
On S3, it’s convenient to separate by bucket (or prefix), for example:
private/(non-public)public/(CDN delivery)
3. Storage Configuration: Prepare for S3 + CDN
FILESYSTEM_DISK=s3
AWS_DEFAULT_REGION=ap-northeast-1
AWS_BUCKET=example-bucket
AWS_URL=https://cdn.example.com
// Save (private)
$path = Storage::disk('s3')->putFile('private/uploads', $request->file('file'), 'private');
// Temporary distribution (10 minutes)
$url = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10), [
'ResponseContentDisposition' => 'inline',
]);
- Setting
AWS_URLto your CDN makesStorage::url()return CDN URLs. - For private distribution,
temporaryUrl()is convenient.
4. Upload Security: Pair Validation with Sanitization
4.1 Validation (MIME + size)
$request->validate([
'file' => ['required','file','mimetypes:image/jpeg,image/png,image/webp,application/pdf','max:8192'],
]);
- Validate MIME, not extension.
maxis in KB.
4.2 Defending against extension spoofing
- Use
mimetypesplus “real content” checks viafileinfo - Use random storage names after upload (don’t trust the original filename)
4.3 Removing EXIF (location metadata)
Leaving location data in photos can cause information leaks. It’s safer to strip EXIF during image processing (often done alongside resizing).
4.4 Virus scanning
- Enqueue scanning as a job immediately after upload (async)
- If malicious, quarantine and notify users with safe wording
- Don’t make externally accessible until scanning completes (best practice)
5. Manage Metadata in the DB: Don’t “just store files”
Storing only in the filesystem makes operations hard—manage metadata in the DB.
Example media table fields (conceptual):
diskpathmimesizevariants(thumbnails, etc.)meta(alt/caption/lang, etc.)owner_id/tenant_idvisibility(private/public)
This enables:
- authorization
- regeneration
- multilingual alt text
- auditing
6. Image Optimization: Generate variants in jobs
Originals are often large, so generate display-friendly variants:
- widths: 320/640/1280
- formats: AVIF/WebP/JPEG
- thumbnails for lists
- fix
width/heightto prevent CLS
Variant generation is slow if done synchronously—push it to queues.
dispatch(new GenerateImageVariants($mediaId))->onQueue('media');
7. Responsive Delivery: 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>
altis required (decorative images use emptyalt="")- Specifying width/height reduces CLS
loading="lazy"reduces transfer cost
8. Presigned URLs and Safe Downloads
8.1 Signed routes
If access control is required, the app should mediate distribution.
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: render in browser (images/PDF)attachment: download (sensitive files)
9. Caching: CDN + hashed filenames is the strongest combo
If public variants include a content hash in the filename:
- long-term caching (1 year)
- instant updates when content changes (URL changes)
Example: thumb.5f2a9c.webp
Cache-Control: public, max-age=31536000, immutable
For dynamic delivery, use ETag/304.
10. PDFs and Documents: Alternatives and Summaries Matter
- Don’t rely on PDF only; provide key information in HTML too
- Use tagged PDFs when possible (heading structure, alt text)
- For long PDFs, provide a summary and table of contents
- Include file type and size on download links (e.g., “PDF 2.3MB”)
Accessibility improves significantly when there is an alternative path for users who can’t open or read PDFs easily.
11. Video: Make captions (VTT) and transcripts standard
Because video carries lots of information, it won’t reach users without captions. Standardize at least:
<track kind="captions" src="...vtt" srclang="ja">- A transcript (text version of audio)
- Avoid autoplay; require user action
- Respect
prefers-reduced-motionand suppress hero videos where appropriate
12. Alt Text Operations: Quality Is Determined by Editorial Workflow
You can’t fill alt text with technology alone—use an operational pattern:
- Always show an alt input field at upload time
- Only allow empty alt when explicitly marked as “decorative”
- For text-in-image, put the same content in alt (if long, put it in body text instead)
- For multilingual sites, store per-language alt like
alt[ja]/alt[en]
13. Testing: Protect uploads and authorization
- Use Storage Fake to validate uploads
- Confirm signed URLs expire and are rejected
- Confirm other users cannot access (authorization)
Storage::fake('s3');
$res = $this->post('/media', [
'file' => UploadedFile::fake()->image('a.jpg', 800, 600),
]);
$res->assertCreated();
14. Common Pitfalls and Avoidance
- Putting originals in public
- Fix: keep originals private; publish variants only
- Trusting
acceptattributes- Fix: validate MIME and size on the server
- Leaving alt empty
- Fix: require input (except decorative)
- Heavy images and slow pages
- Fix: variants +
srcset+ long-term caching
- Fix: variants +
- Autoplay video with sound
- Fix: user-initiated playback + captions + transcripts
- Queue stops and variants stop generating
- Fix: Horizon monitoring + failed-job notifications
- Cache not working, costs rise
- Fix: hashed names +
immutable
- Fix: hashed names +
15. Checklist (Shareable)
Security
- [ ] Validate MIME + size; prevent extension spoofing
- [ ] Strip EXIF; virus scan (async)
- [ ] Keep originals private; distribute via presigned URLs
Performance
- [ ] Queue variant generation (resize/AVIF/WebP)
- [ ]
picture/srcset,width/height, lazy loading - [ ] CDN + hashed filenames + long-term caching
Operations
- [ ] Media metadata management (owner/tenant/variants/meta)
- [ ] Monitor queue/scheduler; notify on failures
- [ ] Deletion and lifecycle (clean old variants)
Accessibility
- [ ] Alt text operations (required; decorative is empty)
- [ ] Captions, subtitles (VTT), transcripts
- [ ] PDF alternatives (HTML/summary/size display)
Testing
- [ ] Validate uploads with
Storage::fake - [ ] Signed URL expiry and authorization
- [ ] Deny access by other users
16. Conclusion
Laravel’s file features become remarkably stable when designed consistently around Storage: store, distribute, optimize, authorize, and operate as a single system. Keep originals private and publish only derived variants. Enforce strict upload validation and sanitization, generate variants asynchronously with queues, and make delivery fast with CDN + long-term caching. At the same time, manage accessibility information such as alt text and captions as metadata, and secure quality via editorial workflow. Files are convenient—but they’re also incident-prone. That’s exactly why it pays to build the “pattern” early and grow safely from there.
Reference Links
- Laravel official
- Web / Accessibility
- AWS
