goetas/multipart-upload-bundle
A Symfony bundle for handling chunked, multipart file uploads, letting you stream large files to Amazon S3 using its multipart upload API while integrating with OneupUploaderBundle for resumable, reliable uploads.
First, the honest framing: goetas/multipart-upload-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no config/multipart-upload.php to publish, and no facade. It ships Symfony DI service definitions, a routing import (@GoetasMultipartUploadBundle/Resources/config/routing.xml), and at least one Symfony FormType — none of which Laravel can consume. So "installing it into a Laravel app" is not the play.
The play is to treat it as two artifacts:
goetas/multipart-upload — the underlying, framework-agnostic PHP library. This is the only piece worth composer require-ing in a Laravel project. It gives you the chunk-upload primitives: an uploaded-file abstraction, a file-token concept, and validators (file identity + byte-range checks). No Symfony coupling.Minimal path for a Laravel developer:
composer require goetas/multipart-upload
Skim the bundle's controller and form type on GitHub (it's ~a handful of files) — that's your spec for the request contract: fields carrying a token, a byte range (start/end), the total size, and the chunk body. Read the library's Validator classes first; they're small and self-contained and tell you exactly what invariants the bundle enforces.
First use case: a large (hundreds of MB to several GB) video or archive upload where you need resume-on-failure, from a JS client like resumable.js, Uppy, or tus. Note the 21 stars / 0 dependents signals: this is a niche, single-maintainer project. Read it, port it, don't depend on it blindly.
Decision gate before writing code: if your chunks ultimately land in S3, skip this entirely and use S3's native multipart upload through Flysystem — you get the same chunking semantics with far less code and no local temp-file coordination.
1. Mirror the wire contract, not the Symfony classes. Reimplement the bundle's controller logic as a plain Laravel controller. The contract is essentially:
POST /uploads/{token} multipart/form-data or raw body
token : stable identifier for this logical upload
start : inclusive byte offset of this chunk
end : exclusive byte offset
total : total expected size (optional, sent on first chunk)
file : the chunk bytes
Make each chunk request idempotent — the client will retry after a network drop. Rewriting the same byte range must be harmless.
2. Use the library's validators directly, no container.
use Goetas\MultipartUpload\...\FileValidator;
use Goetas\MultipartUpload\...\RangeValidator;
// instantiate with your own config; these are POJOs, not services
$validator->validate($uploadedFile); // throws on mismatch
Wrap the throw in a try/catch and map to 422 — Laravel has no equivalent of Symfony's validation exception handling for these.
3. Signed, user-scoped tokens. The bundle assumes the token identifies the file; it does not assume authorization. In Laravel, derive the token and sign it:
$token = hash_hmac('sha256', $userId.'|'.$filename.'|'.$size, config('app.key'));
Store a row in uploads (user_id, token, expected_size, received_bytes, status, expires_at). Authenticate the chunk request against that row in a route middleware or a Gate. Never let the client's filename reach the filesystem path.
4. Streaming write, not buffering. Don't $request->file('file')->store(). Open the target with fopen($path, 'c+b'), fseek($fh, $start), then stream_copy_to_stream($request->getContent(true), $fh). For raw-body uploads use getContent(true) to get a stream — it avoids loading the chunk into memory.
5. Lock per token, not per request. Concurrent chunks to the same file will interleave writes. Use Cache::lock("upload:{$token}", 30) (Redis) or flock($fh, LOCK_EX). Update received_bytes inside the lock.
6. Finalize atomically. On the chunk that completes the range, verify total size (and preferably a client-supplied SHA-256), then rename() the temp file into its final location and flip the DB status. rename is atomic on the same filesystem — don't copy+unlink.
7. Clean up on a schedule. Register php artisan uploads:prune in routes/console.php to delete temp files and DB rows past expires_at. The bundle leaves this to you too; orphaned chunks are the #1 operational problem with any chunked-upload implementation.
8. Testing. UploadedFile::fake() doesn't exercise streaming paths. Use $this->call('POST', $uri, [], [], [], [...], $rawBytes) and assert on file contents and offsets. Test out-of-order chunks, duplicate chunks, and a truncated final chunk explicitly.
['token' => 'required|string', 'start' => 'required|integer|min:0', ...] is the whole job.upload_max_filesize and post_max_size cap each chunk, not the total. The entire point of chunking is to stay under them — size your client chunks at ~5–10 MB and document it. Also check max_file_uploads if you ever send multiple chunks at once.client_max_body_size (default 1 MB) returns a 413 before PHP ever runs. This is the single most common "the package is broken" report for chunked uploads.php://input is one-shot. Reading it twice returns empty. Pick one path — $request->file() for multipart, $request->getContent(true) for raw bodies — and stick to it for the whole endpoint.Storage::disk('s3') with the native multipart API and skip this library entirely.start is beyond received_bytes + allowed_gap.OPTIONS on the upload route doesn't hit auth middleware and that Content-Type/Range headers are allowed.app/Support/ over depending on it. You'll own the code either way; at least then you can fix it.(token, start, end, bytes_received) on every chunk; when an upload fails, the gap in the offsets tells you instantly whether it's a client bug, a proxy truncation, or a lock timeout.How can I help you explore Laravel packages today?