Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Chunk Upload Laravel Package

pion/laravel-chunk-upload

Laravel Chunk Upload simplifies large file uploads by splitting files into chunks. It supports resumable uploads, multiple JavaScript frontends such as Dropzone and FilePond, and integrates smoothly with Laravel storage, validation, and cleanup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require pion/laravel-chunk-upload
php artisan vendor:publish --provider="Pion\Laravel\ChunkUpload\Providers\ChunkUploadServiceProvider" --tag=config

Open config/chunk-upload.php first — it holds the disk/path used for in-flight chunks and the handler defaults. Read it before writing any code; the key names have drifted between releases, so trust the published file over blog posts.

Where to look first (in this order)

  1. config/chunk-upload.php — chunk storage disk, path, cleanup settings.
  2. src/Receiver/FileReceiver.php — the object you actually instantiate.
  3. src/Handler/ — one class per JS provider (DropZoneUploadHandler, jQueryFileUploadHandler, ResumableJSHandler, PUploadHandler, SingleUploadHandler). Skim the one matching your frontend; it tells you which request parameters the package reads.
  4. src/Handler/HandlerFactory.php — how the handler is auto-detected from the request.

Minimal working endpoint

// routes/web.php
Route::match(['get', 'post'], '/upload', [UploadController::class, 'upload'])->name('upload');
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException;
use Pion\Laravel\ChunkUpload\Handler\HandlerFactory;
use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;

class UploadController extends Controller
{
    public function upload(Request $request)
    {
        $receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));

        if (! $receiver->isUploaded()) {
            throw new UploadMissingFileException();
        }

        $fileReceived = $receiver->receive();

        // Intermediate chunk: report progress and bail out.
        if (! $fileReceived->isFinished()) {
            $handler = $fileReceived->handler();

            return response()->json([
                'done'       => $handler->getPercentageDone(),
                'status'     => true,
                '_chunkSize' => $handler->getChunkSize(),
            ]);
        }

        // Final chunk: the full file has been assembled on disk.
        $file = $fileReceived->getFile();

        $name = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)
              . '-' . uniqid()
              . '.' . $file->getClientOriginalExtension();

        $path = $file->storeAs('uploads', $name, 'local');

        $fileReceived->deleteChunkedFile(); // only AFTER the move succeeds

        return response()->json(['path' => $path, 'name' => $name]);
    }
}

First real use case: a 1–2 GB video upload to private storage from a DropZone widget. DropZone with chunking: true posts N requests to your endpoint; the first N‑1 return a percentage, the last one returns the stored path. Add X-CSRF-TOKEN to the DropZone headers or add the route to VerifyCsrfToken::$except.

Implementation Patterns

One endpoint, many frontends. HandlerFactory::classFromRequest($request) sniffs the payload (dzchunkindex, resumableChunkNumber, chunk/chunks, etc.) and picks the right handler. That means a single controller can serve DropZone today and resumable.js tomorrow without changing the server. If sniffing is ambiguous, pin it:

$receiver = new FileReceiver('file', $request, DropZoneUploadHandler::class);
// or: HandlerFactory::classFromName('dropzone')

Prefer pinning when you control the frontend — it turns a silent misdetection into an obvious failure.

Keep the request thin, offload the rest. The final chunk triggers an assembly + storeAs, which is disk-bound. Don't also generate thumbnails, probe video duration, or scan for viruses inline. Dispatch a job:

if ($fileReceived->isFinished()) {
    $path = $fileReceived->getFile()->storeAs('uploads', $name, 'local');
    $fileReceived->deleteChunkedFile();

    ProcessUpload::dispatch($path, $request->user()->id);

    return response()->json(['path' => $path, 'name' => $name]);
}

Validation before the last byte. Reject bad uploads early rather than after 800 MB of transfer:

$handler = $fileReceived->handler();

abort_if($handler->getTotalSize() > 2 * 1024 ** 3, 422, 'File too large');

$ext = strtolower($fileReceived->getFile()->getClientOriginalExtension());
abort_unless(in_array($ext, ['mp4', 'mov', 'mkv']), 422, 'Unsupported type');

Run this on the first chunk ($handler->isFirstChunk()) so the client stops immediately.

Tie the upload to a domain object. The package knows nothing about your models. Put a client-generated upload token in the request (resumable.js gives you resumableIdentifier; for DropZone send a custom params value), stash it in the session or a cache key, and read it back when isFinished() is true to attach the resulting path to a Media/Document row.

Resumability contract. For resumable.js, the route must answer GET on the same URL so the client can ask "do you already have chunk 7?". Register Route::match(['get','post'], ...) and let the handler respond — this is what makes reload-and-continue work. For DropZone, resuming means configuring chunking plus a stable uploadId; verify the behaviour rather than assuming it.

Testing. Fake the disk and drive chunks manually; it's the only reliable way to test the isFinished() branch:

Storage::fake('local');

foreach (range(1, 3) as $i) {
    $this->post('/upload', [
        'file'      => UploadedFile::fake()->create('clip.mp4', 100),
        'dzchunkindex' => $i - 1,
        'dztotalchunkcount' => 3,
    ]);
}

Assert the final file exists in Storage::disk('local') and that the chunk directory is empty. Note the fake uploaded file's contents repeat across chunks — fine for exercising the plumbing, not for asserting byte-exact assembly.

Gotchas and Tips

  • PHP and Nginx limits still apply per chunk. upload_max_filesize, post_max_size, client_max_body_size, and the proxy read timeout all constrain a single chunk, not the total. If chunks 413, the problem is server config, not the package. Chunk sizes of 5–10 MB are a sane default for large media.
  • Final assembly can exceed max_execution_time. Appending a 2 GB file to disk inside a web request is slow on network-backed storage. Either raise the timeout on that route only, or keep the chunk storage local and the destination disk separate.
  • Keep the chunk temp directory on the same volume as the final destination. Cross-device moves force a copy; on S3-backed disks you pay for a full round-trip. Don't point chunk storage at s3 unless you've measured it.
  • deleteChunkedFile() after the move, never before. It's the only copy of the assembled data until storeAs returns. Wrap the move in a try/catch and only delete on success — otherwise a failed move silently loses the upload.
  • Abandoned chunks accumulate. A user who closes the tab mid-upload leaves partial data on disk forever. Rely on the package's cleanup configuration if present in your version, but verify with ls on the chunks path; otherwise schedule your own Storage::disk('local')->deleteDirectory(...) for anything older than 24 h.
  • Filename collisions are yours to solve. The assembled file is just an UploadedFile-shaped object with the client's original name. Hash or UUID it — two users uploading video.mp4 will otherwise overwrite each other.
  • Handler misdetection looks like a data bug. Symptoms: progress stuck at 0%, "chunk index out of range", or the file never finishing. Log $request->all() behind a debug flag on first integration; the parameter names tell you instantly whether DropZone/resumable/jQuery was detected.
  • CSRF is the #1 integration failure. DropZone needs headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content }, or exclude the route. Same for pupload/resumable.
  • Don't storeAs twice. The move consumes/relocates the file; a second call on the same object fails. Compute the path once and reuse the string.
  • Pin the version. Config keys and handler names have changed across releases, and the library predates several Laravel storage API shifts. Pin the minor, publish the config, and don't blindly apply upgrade guides from a different major.
  • Extension points worth knowing: subclass AbstractHandler (or a concrete handler) to add custom progress fields in the JSON response, and wrap the whole controller body to add per-user quota checks / rate limiting — the package deliberately does no authorization, so auth middleware and a Gate check on the upload route are required, not optional.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo