MWZ

MINDWAREZONE

How to Upload Files in Chunks with FilePond and Laravel

What is FilePond?

FilePond is a modern JavaScript library that simplifies file uploads by providing drag-and-drop functionality, previews, validation, image editing, and support for large chunked uploads.

It supports chunked uploads, making it possible to upload large files in smaller pieces and resume interrupted uploads.

It is highly customizable, allowing developers to modify its appearance and behavior through options and plugins.

It integrates with popular frameworks, including Laravel, React, Vue, Angular, and plain JavaScript applications.

Filepond CDN

<link href="https://unpkg.com/filepond/dist/filepond.min.css" rel="stylesheet" />
<script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
            

Make a blade file upload.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600" rel="stylesheet" />

        <!-- Styles / Scripts -->
        @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
            @vite(['resources/css/app.css', 'resources/js/app.js'])
        @endif

        <link href="https://unpkg.com/filepond/dist/filepond.min.css" rel="stylesheet" />
        <script src="https://unpkg.com/filepond/dist/filepond.min.js"></script>
    </head>
    <body ">
        <div>
            <input type="file" class="filepond" name="file" />
        </div>

        <script>
            const csrf = document.querySelector('meta[name="csrf-token"]').content;

            FilePond.create(document.querySelector('input[type="file"]'), {
                chunkUploads: true,
                chunkSize: 5 * 1024 * 1024,

                allowMultiple: false,

                server: {
                    process: {
                        url: '/upload',
                        method: 'POST',
                        headers: {
                            'X-CSRF-TOKEN': csrf
                        }
                    },

                    patch: {
                        url: '/upload/',
                        method: 'PATCH',
                        headers: {
                            'X-CSRF-TOKEN': csrf,
                            'Accept': 'application/json'
                        }
                    },

                    revert: {
                        url: '/revert',
                        method: 'DELETE',
                        headers: {
                            'X-CSRF-TOKEN': csrf
                        }
                    }
                }
            });
            </script>
    </body>
</html>

            

Create a controller using an Artisan command.

php artisan make:controller UploadController
            
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class UploadController extends Controller
{
    public function process(Request $request)
    {
        $id = (string) Str::uuid();

        Storage::disk('public')->makeDirectory("chunks/$id");

        return response($id, 200)
            ->header('Content-Type', 'text/plain');
    }

    public function uploadChunk(Request $request, $id)
    {
        $path = storage_path("app/public/chunks/$id");

        if (!file_exists($path)) {
            mkdir($path, 0777, true);
        }

        $offset = $request->header('Upload-Offset', 0);
        $length = $request->header('Upload-Length');

        //  FilePond sends RAW binary body here
        $data = $request->getContent();

        if ($data === null || $data === '') {
            return response()->json([
                'error' => 'Empty chunk received'
            ], 400);
        }

        file_put_contents("$path/$offset", $data);

        // check progress
        $uploaded = collect(glob("$path/*"))
            ->sum(fn($f) => filesize($f));

        if ($length && $uploaded >= (int)$length) {
            $this->mergeChunks($id);
        }

        return response()->noContent();
    }

    private function mergeChunks($id)
    {
        $chunkPath = storage_path("app/public/chunks/$id");
        $finalDir = storage_path("app/public/videos");

        if (!file_exists($finalDir)) {
            mkdir($finalDir, 0777, true);
        }

        
        $fileName = $id . '.mp4';
        $finalPath = "$finalDir/$fileName";

        $files = glob("$chunkPath/*");
        sort($files, SORT_NATURAL);

        $out = fopen($finalPath, 'ab');

        foreach ($files as $file) {
            $in = fopen($file, 'rb');
            stream_copy_to_stream($in, $out);
            fclose($in);
        }

        fclose($out);

        array_map('unlink', $files);
        rmdir($chunkPath);
    }

    public function revert(Request $request)
    {
        $uuid = $request->getContent();

        Storage::disk('public')->deleteDirectory("chunks/$uuid");

        return response()->noContent();
    }
}
            

Create a route in routes/web.php.  

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\UploadController;

Route::get('/', function () {
    return view('upload');
});

Route::post('/upload', [UploadController::class, 'process']);
Route::patch('/upload/{id}', [UploadController::class, 'uploadChunk']);
Route::delete('/revert', [UploadController::class, 'revert']);
            

Run the Application Using Artisan

php artisan serve
            

This will start the Laravel development server, usually accessible at:

http://127.0.0.1:8000

Upload an Image or Video

Uploaded Images or Videos