solid3d / laravel-tus-s3
Laravel TUS S3 — Tus 1.0 server backed by S3 multipart uploads
v1.1.0
2026-08-20 22:26 UTC
Requires
- php: ^8.5
- illuminate/contracts: ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.14
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
README
Laravel Tus 1.0 server backed by S3 multipart uploads. Designed for stateless web pods: no PVC, no shared filesystem, no temporary local upload files.
Requires PHP 8.5+ and Laravel 13.
Protocol subset
| Method | Purpose |
|---|---|
OPTIONS |
Capability discovery |
POST |
Create upload (creation) |
HEAD |
Authoritative Upload-Offset / length / expiry |
PATCH |
Upload next chunk |
DELETE |
Abort (termination) |
Extensions: creation, expiration, checksum, termination.
Not implemented: concatenation, creation-with-upload.
Architecture
HTTP (TusUploadController)
→ TusUploadStore (DurableTusUploadStore)
→ PostgreSQL (tus_uploads: offset, parts, multipart id, expiry)
→ MultipartUploader
→ S3MultipartUploader (production)
→ LocalMultipartUploader (local / Storage::fake)
- Object keys are always generated server-side under
tus.temporary_prefix(defaulttus/tmp/{ulid}). - The Laravel disk
rootis applied by Flysystem /S3KeyResolver— clients cannot choose bucket or key. - PATCH takes a short row lock, uploads the part outside the transaction, then commits ETag/offset atomically.
- If S3 succeeds but the DB update fails, the next PATCH reconciles via
ListParts.
Configuration
| Env | Default | Notes |
|---|---|---|
TUS_STORAGE_DISK |
FILESYSTEM_DISK / s3 |
Disk for temporary objects |
TUS_TEMPORARY_PREFIX |
tus/tmp |
Relative to disk root |
TUS_UPLOAD_EXPIRATION |
60 |
Minutes |
TUS_PATH |
tus |
Route prefix |
TUS_MIN_PART_SIZE |
5242880 |
S3 non-final part minimum (5 MiB) |
TUS_MAX_PART_BYTES |
5242880 |
Bounds checksum buffering; keep Uppy chunkSize ≤ this |
Publish config (optional):
php artisan vendor:publish --tag=tus-config
Uppy
.use(Tus, { endpoint: '/tus', chunkSize: 5_242_880, // >= 5 MiB for S3 multipart })
Operations
php artisan tus:prune # abort expired multipart uploads; delete stale rows
Schedule hourly. Safe to run repeatedly.
Required S3 permissions
s3:CreateMultipartUploads3:UploadParts3:CompleteMultipartUploads3:AbortMultipartUploads3:ListMultipartUploadPartss3:DeleteObjects3:GetObject(finalization streams the temp object)
Scope keys to {disk-root}/tus/tmp/*.
Events
Solid3d\LaravelTusS3\Events\FileUploadCreated($tusFile)Solid3d\LaravelTusS3\Events\FileUploadFinished($tusFile)
TusFile exposes id, path (relative object key), disk, and metadata. Completed
uploads can be consumed without handling streams:
public function handle(FileUploadFinished $event): void { $fingerprint = $event->tusFile->fingerprint(maximumBytes: 1_073_741_824); $stored = $event->tusFile->moveTo( disk: 's3', path: "library/{$fingerprint->sha256}.bin", ); // $stored points to the durable object; the temporary object is gone. }
fingerprint()returns the SHA-256 and size in one bounded pass.moveTo()uses a storage-native move on the same disk and safely streams between different disks.delete()removes a completed temporary upload when the application reuses an existing object.