PHPackages                             netipar/laravel-chunky - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. netipar/laravel-chunky

ActiveLibrary[File &amp; Storage](/categories/file-storage)

netipar/laravel-chunky
======================

Chunk-based file upload package for Laravel with event-driven architecture, resume support, batch upload, and framework-agnostic frontend clients for Vue 3, React, Alpine.js, and Livewire.

v0.22.5(1mo ago)01.2k↑1470%[3 PRs](https://github.com/NETipar/laravel-chunky/pulls)MITPHPPHP ^8.2CI passing

Since Mar 8Pushed 5d agoCompare

[ Source](https://github.com/NETipar/laravel-chunky)[ Packagist](https://packagist.org/packages/netipar/laravel-chunky)[ Docs](https://github.com/NETipar/laravel-chunky)[ RSS](/packages/netipar-laravel-chunky/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (22)Versions (40)Used By (0)

  ![laravel-chunky](art/banner.svg)Chunky for Laravel
==================

[](#chunky-for-laravel)

[![Latest Version on Packagist](https://camo.githubusercontent.com/f3713b5fcf138a14f3b712841e611d572b06058b1ebd83eae8d3a073405fe896/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6574697061722f6c61726176656c2d6368756e6b792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/netipar/laravel-chunky)[![Tests](https://camo.githubusercontent.com/d41f574160853c2bc2406288bf89456493936ca360c168dd92914803d684472a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f4e4554697061722f6c61726176656c2d6368756e6b792f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/NETipar/laravel-chunky/actions?query=workflow%3ATests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/6de5c7920b3dca0138f4d2c87960e0b4b7938270997b70d2da1cd87014d78dad/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e6574697061722f6c61726176656c2d6368756e6b792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/netipar/laravel-chunky)

Chunk-based file upload package for Laravel with event-driven architecture, resume support, and framework-agnostic frontend clients for **Vue 3**, **React**, **Alpine.js**, and **Livewire**. Upload large files reliably over unstable connections.

Table of contents
-----------------

[](#table-of-contents)

- [Quick example](#quick-example)
- [Requirements](#requirements)
- [5-minute quickstart](#5-minute-quickstart)
- [Production deployment checklist](#production-deployment-checklist)
- [Installation](#installation) — [Backend](#backend) · [Frontend](#frontend) · [Livewire](#livewire)
- [Frontend packages](#frontend-packages)
- [Usage](#usage)
- [Batch upload (multiple files)](#batch-upload-multiple-files)
- [Authentication &amp; authorization](#authentication--authorization)
- [Context setup](#quick-context-setup)
- [Listening to events](#listening-to-events)
- [Broadcasting (Laravel Echo)](#broadcasting-laravel-echo)
- [Using the facade](#using-the-facade)
- [Configuration](#configuration) → full reference in [`docs/configuration.md`](docs/configuration.md)
- [Error handling](#error-handling)

Additional documentation:

- [`docs/configuration.md`](docs/configuration.md) — full config reference + deployment recipes
- [UPGRADE.md](UPGRADE.md) — minor-release migration notes (the package is in `0.x`)
- [SECURITY.md](SECURITY.md) — supported versions and reporting policy
- [CHANGELOG.md](CHANGELOG.md) — release history

Quick Example
-------------

[](#quick-example)

```
// Backend: Listen for completed uploads
// EventServiceProvider
protected $listen = [
    \NETipar\Chunky\Events\UploadCompleted::class => [
        \App\Listeners\ProcessUploadedFile::class,
    ],
];
```

```

import { useChunkUpload } from '@netipar/chunky-vue3';

const { upload, progress, isUploading, pause, resume } = useChunkUpload();

function onFileChange(event) {
    upload(event.target.files[0]);
}

    Pause

```

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11, 12 or 13

5-Minute Quickstart
-------------------

[](#5-minute-quickstart)

```
# 1. Install backend
composer require netipar/laravel-chunky
php artisan vendor:publish --tag=chunky-config
php artisan migrate

# 2. Install frontend (Vue 3 example)
npm install @netipar/chunky-vue3
```

```

import { useChunkUpload } from '@netipar/chunky-vue3';

const { upload, progress, isUploading, isComplete, error } = useChunkUpload();

function onFileChange(event) {
    const file = event.target.files[0];
    if (file) upload(file);
}

    Done!
    Error: {{ error }}

```

```
# 4. Test it
php artisan serve
# Open the page, upload a 10MB file. The chunks land in
# storage/app/chunky/temp/{uploadId}/ during the transfer; once
# complete, the AssembleFileJob writes the final file to
# storage/app/chunky/uploads/{uploadId}/{fileName}.
```

Production Deployment Checklist
-------------------------------

[](#production-deployment-checklist)

- **Auth middleware** on `chunky.routes.middleware` (e.g. `['api', 'auth:sanctum']`)
- **Queue worker** running for `AssembleFileJob` (don't run on `sync`)
- **Cache driver** that supports `Cache::lock()` if using `chunky.lock_driver = 'cache'` (Redis / Memcached / DB / DynamoDB; **not** `array` or `file`)
- **`CHUNKY_BROADCASTING=true`** if real-time UI updates are needed (requires Echo + a WebSocket server)
- **`chunky.staging_directory`** set to a path with enough free space if accepting uploads larger than `/tmp` (cloud-disk targets buffer the full file locally before upload)
- **`chunky.metadata.max_keys`** and **`chunky.max_files_per_batch`** tuned for your DOS profile
- **`chunky.metrics.*`** wired to Datadog / Prometheus / your observability stack
- **`chunky:cleanup`** scheduled daily (auto-scheduled if `auto_cleanup = true`)
- **Custom `Authorizer`** bound if the default ownership check (auth user\_id == upload user\_id) doesn't fit your access model
- **`routes/channels.php`** auto-registered (default) or hand-written if you set `chunky.broadcasting.register_channels = false`

Installation
------------

[](#installation)

### Backend

[](#backend)

```
composer require netipar/laravel-chunky
```

Publish the config file:

```
php artisan vendor:publish --tag=chunky-config
```

Run the migrations (for database tracker):

```
php artisan migrate
```

### Frontend

[](#frontend)

Install the package for your framework:

```
# Vue 3
npm install @netipar/chunky-vue3

# React
npm install @netipar/chunky-react

# Alpine.js (standalone, without Livewire)
npm install @netipar/chunky-alpine

# Core only (framework-agnostic)
npm install @netipar/chunky-core
```

> The `@netipar/chunky-core` package is automatically installed as a dependency of all framework packages.

### Livewire

[](#livewire)

No npm package needed. The Livewire component uses Alpine.js under the hood and is included in the Composer package. Just add the component to your Blade template:

```

```

Frontend Packages
-----------------

[](#frontend-packages)

PackageFrameworkPeer Dependencies`@netipar/chunky-core`None (vanilla JS/TS)-`@netipar/chunky-vue3`Vue 3.4+`vue``@netipar/chunky-react`React 18+ / 19+`react``@netipar/chunky-alpine`Alpine.js 3+-Usage
-----

[](#usage)

### How It Works

[](#how-it-works)

1. **Frontend** initiates an upload with file metadata
2. **Backend** returns an `upload_id`, `chunk_size`, and `total_chunks`
3. **Frontend** slices the file and uploads chunks in parallel with SHA-256 checksums
4. **Backend** stores each chunk, verifies integrity, tracks progress
5. When all chunks arrive, an `AssembleFileJob` merges them on the queue
6. **Events** fire at each step -- hook in your own listeners

### CSRF Protection

[](#csrf-protection)

The frontend client automatically reads the `XSRF-TOKEN` cookie (set by Laravel) and sends it as the `X-XSRF-TOKEN` header. No manual CSRF setup is needed in most Laravel applications.

If you need a custom token header, use `setDefaults()`:

```
import { setDefaults } from '@netipar/chunky-core';

setDefaults({ headers: { 'X-CSRF-TOKEN': 'your-token' } });
```

### Config Isolation

[](#config-isolation)

For multiple upload scopes on the same page:

```
import { ChunkUploader, createDefaults } from '@netipar/chunky-core';

const scope = createDefaults({ headers: { 'X-Custom': 'value' } });
const uploader = new ChunkUploader({ context: 'docs' }, scope);
```

### API Endpoints

[](#api-endpoints)

The package registers seven routes (configurable prefix/middleware):

MethodEndpointPurpose`POST``/api/chunky/upload`Initiate upload`POST``/api/chunky/upload/{uploadId}/chunks`Upload a chunk`GET``/api/chunky/upload/{uploadId}`Get upload status`DELETE``/api/chunky/upload/{uploadId}`Cancel upload`POST``/api/chunky/batch`Initiate batch`POST``/api/chunky/batch/{batchId}/upload`Add file to batch`GET``/api/chunky/batch/{batchId}`Get batch status#### HTTP status codes

[](#http-status-codes)

CodeWhen`201 Created`Upload or batch initiated`200 OK`Chunk accepted, status fetched`204 No Content`Cancel succeeded`404 Not Found`Upload/batch doesn't exist — or the caller isn't its owner (intentional, prevents probe attacks)`409 Conflict`Late chunk against a cancelled / completed / failed / assembling upload`410 Gone`Upload has expired`422 Unprocessable Entity`Validation error (missing field, invalid file\_name, batch in terminal state, etc.)`503 Service Unavailable`Upload temporarily contended on the lock; client may safely retry (idempotent)### Vue 3

[](#vue-3)

```

import { useChunkUpload } from '@netipar/chunky-vue3';

const {
    progress, isUploading, isPaused, isComplete, error,
    uploadId, uploadedChunks, totalChunks, currentFile,
    upload, pause, resume, cancel, retry,
    onProgress, onChunkUploaded, onComplete, onError,
} = useChunkUpload({
    maxConcurrent: 3,
    autoRetry: true,
    maxRetries: 3,
    withCredentials: true,
});

function onFileChange(event: Event) {
    const input = event.target as HTMLInputElement;
    if (input.files?.[0]) {
        upload(input.files[0]);
    }
}

```

### React

[](#react)

```
import { useChunkUpload } from '@netipar/chunky-react';

function FileUpload() {
    const {
        progress, isUploading, isPaused, isComplete, error,
        upload, pause, resume, cancel, retry,
    } = useChunkUpload({ maxConcurrent: 3 });

    const handleChange = (e: React.ChangeEvent) => {
        const file = e.target.files?.[0];
        if (file) upload(file);
    };

    return (

            {isUploading && }
            {isUploading && (

                    {isPaused ? 'Resume' : 'Pause'}

            )}
            {error && {error}}

    );
}
```

### Alpine.js

[](#alpinejs)

```

import { registerChunkUpload } from '@netipar/chunky-alpine';
import Alpine from 'alpinejs';

registerChunkUpload(Alpine);
Alpine.start();

            Cancel

            Retry

```

### Livewire

[](#livewire-1)

```
{{-- Basic usage --}}

{{-- With context for validation --}}

{{-- With custom slot content --}}

```

Listen for the upload completion in your Livewire parent component:

```
#[On('chunky-upload-completed')]
public function handleUpload(array $data): void
{
    // Default payload: $data['uploadId'], $data['fileName'], $data['fileSize']
    //
    // Set `chunky.broadcasting.expose_internal_paths = true` in
    // config/chunky.php to additionally receive $data['finalPath'] and
    // $data['disk']. By default they're stripped to avoid leaking
    // server-internal paths to the browser.
}
```

### Core (Framework-agnostic)

[](#core-framework-agnostic)

```
import { ChunkUploader } from '@netipar/chunky-core';

const uploader = new ChunkUploader({
    maxConcurrent: 3,
    autoRetry: true,
    maxRetries: 3,
    context: 'documents',
});

uploader.on('progress', (event) => {
    console.log(`${event.percentage}%`);
});

uploader.on('complete', (result) => {
    console.log('Done:', result.uploadId);
});

uploader.on('error', (error) => {
    console.error('Failed:', error.message);
});

await uploader.upload(file, { folder: 'reports' });

// Controls
uploader.pause();
uploader.resume();
uploader.cancel();
uploader.retry();

// Cleanup when done
uploader.destroy();
```

Batch Upload (Multiple Files)
-----------------------------

[](#batch-upload-multiple-files)

Upload multiple files as a batch and get a single event when all files are done.

### Vue 3

[](#vue-3-1)

```

import { useBatchUpload } from '@netipar/chunky-vue3';

const {
    progress, isUploading, isComplete, completedFiles, totalFiles,
    failedFiles, currentFileName, error,
    upload, cancel, pause, resume,
    onFileComplete, onComplete, onFileError,
} = useBatchUpload({ maxConcurrentFiles: 2, context: 'documents' });

function onFilesChange(event: Event) {
    const input = event.target as HTMLInputElement;
    if (input.files?.length) {
        upload(Array.from(input.files));
    }
}

        {{ completedFiles }}/{{ totalFiles }} files
        Uploading: {{ currentFileName }}

    All files uploaded!

```

### React

[](#react-1)

```
import { useBatchUpload } from '@netipar/chunky-react';

function MultiFileUpload() {
    const {
        progress, isUploading, isComplete, completedFiles, totalFiles,
        upload, cancel,
    } = useBatchUpload({ maxConcurrentFiles: 2 });

    const handleChange = (e: React.ChangeEvent) => {
        const files = e.target.files;
        if (files?.length) upload(Array.from(files));
    };

    return (

            {isUploading && }
            {isUploading && {completedFiles}/{totalFiles} files}
            {isComplete && All files uploaded!}

    );
}
```

### Alpine.js

[](#alpinejs-1)

```

```

### Core (Framework-agnostic)

[](#core-framework-agnostic-1)

```
import { BatchUploader } from '@netipar/chunky-core';

const batch = new BatchUploader({ maxConcurrentFiles: 2, context: 'documents' });

batch.on('fileComplete', (result) => console.log('File done:', result.fileName));
batch.on('complete', (result) => console.log(`Batch done: ${result.completedFiles}/${result.totalFiles}`));

await batch.upload(files);
batch.destroy();
```

### How Batch Works

[](#how-batch-works)

1. Frontend calls `POST /api/chunky/batch` with `total_files` count
2. Backend creates a batch record and returns `batch_id`
3. For each file, frontend calls `POST /api/chunky/batch/{batchId}/upload` to initiate
4. Chunks are uploaded normally via `POST /api/chunky/upload/{uploadId}/chunks`
5. When each file's assembly completes, the batch counter increments atomically
6. When all files are done, `BatchCompleted` (or `BatchPartiallyCompleted`) event fires

Every upload creates a batch — even a single file becomes a batch of 1. This ensures consistent behavior: every upload gets a `batchId` and fires `BatchCompleted`. `useBatchUpload` is the single entry point for all uploads.

**Failure policy**: Lenient -- if a file fails, other files continue. The batch ends with `PartiallyCompleted` status.

### Sequential Batches with `enqueue()`

[](#sequential-batches-with-enqueue)

`upload()` throws if a batch is already in progress on the same `BatchUploader` instance. For UIs that accept files faster than they can upload (multi-paste, drag-while-uploading), use `enqueue()` instead:

```
import { BatchUploader } from '@netipar/chunky-core';

const uploader = new BatchUploader({ maxConcurrentFiles: 2 });

// First call: behaves like upload()
await uploader.enqueue([file1]);

// While the first batch is still running, queue more:
const second = uploader.enqueue([file2, file3]);
const third = uploader.enqueue([file4]);

// Each enqueue() returns its own promise. The queued batches run
// strictly serially (one at a time), so you get a consistent
// progress signal across all of them.
await Promise.all([second, third]);
```

If you `cancel()` or `destroy()` the uploader before a queued batch starts, its promise rejects with a clear error message. The Vue 3 / React / Alpine wrappers all expose `enqueue` as a sibling of `upload`.

Authentication &amp; Authorization
----------------------------------

[](#authentication--authorization)

### Authentication

[](#authentication)

By default, upload endpoints use only the `api` middleware. To protect them with authentication, update `routes.middleware` in `config/chunky.php`:

```
'routes' => [
    'prefix' => 'api/chunky',
    'middleware' => ['api', 'auth:sanctum'],
],
```

This applies to all routes (initiate, upload chunk, cancel, status, batch). No custom request or controller override is needed.

### Authorization (per-upload, per-batch)

[](#authorization-per-upload-per-batch)

When auth is active, the package automatically enforces ownership: an authenticated caller can only access uploads / batches they created (the `user_id` captured at initiation time). Non-owners see a `404` (not `403`) so upload IDs can't be probed.

The check is delegated to a swappable `Authorizer` interface:

```
namespace NETipar\Chunky\Authorization;

interface Authorizer
{
    public function canAccessUpload(?Authenticatable $user, UploadMetadata $upload): bool;
    public function canAccessBatch(?Authenticatable $user, BatchMetadata $batch): bool;
}
```

The default `DefaultAuthorizer` does plain ownership: `auth()->id() === upload->userId`, with anonymous uploads (no `user_id`) accessible to everyone (backward compat).

#### Custom Authorizer (admin overrides, team access, …)

[](#custom-authorizer-admin-overrides-team-access-)

Bind your own implementation in `AppServiceProvider::register()`:

```
use NETipar\Chunky\Authorization\Authorizer;
use NETipar\Chunky\Authorization\DefaultAuthorizer;

$this->app->singleton(Authorizer::class, function ($app) {
    return new class extends DefaultAuthorizer
    {
        public function canAccessUpload(?Authenticatable $user, UploadMetadata $upload): bool
        {
            // Admins access everything
            if ($user?->is_admin) {
                return true;
            }

            // Teammates share access
            if ($upload->userId !== null) {
                $owner = User::find($upload->userId);
                if ($owner && $user?->team_id === $owner->team_id) {
                    return true;
                }
            }

            return parent::canAccessUpload($user, $upload);
        }
    };
});
```

The same `Authorizer` is used by the broadcast channel auth callbacks (`routes/channels.php`, auto-registered when `broadcasting.enabled = true`) — HTTP and WebSocket access stay in sync.

### `user_id` is portable

[](#user_id-is-portable)

The `chunked_uploads.user_id` and `chunky_batches.user_id` columns are `string` type since v0.14, so any user-id shape works out of the box: auto-increment integers, UUIDs, ULIDs, or arbitrary strings. The package never does arithmetic on `user_id`, only string equality comparisons.

Quick Context Setup
-------------------

[](#quick-context-setup)

For the most common case -- validate and move the file to a directory:

```
use NETipar\Chunky\Facades\Chunky;

Chunky::simple('documents', 'uploads/documents', [
    'max_size' => 50 * 1024 * 1024, // 50MB
    'mimes' => ['application/pdf', 'image/jpeg', 'image/png'],
]);
```

This registers a context that validates the file and moves it from the temp directory to `uploads/documents/{fileName}` after assembly. No event listener needed.

Context-based Validation &amp; Save Callbacks
---------------------------------------------

[](#context-based-validation--save-callbacks)

Contexts define per-upload validation rules and save handlers. You can use class-based contexts (recommended) or inline closures.

### Class-based Contexts (Recommended)

[](#class-based-contexts-recommended)

Create a context class:

```
namespace App\Chunky;

use NETipar\Chunky\ChunkyContext;
use NETipar\Chunky\Data\UploadMetadata;

class ProfileAvatarContext extends ChunkyContext
{
    public function name(): string
    {
        return 'profile_avatar';
    }

    public function rules(): array
    {
        return [
            'file_size' => ['max:5242880'], // 5MB
            'mime_type' => ['in:image/jpeg,image/png,image/webp'],
        ];
    }

    public function save(UploadMetadata $metadata): void
    {
        auth()->user()
            ->addMediaFromDisk($metadata->finalPath, $metadata->disk)
            ->toMediaCollection('avatar');
    }
}
```

Register via config (`config/chunky.php`):

```
'contexts' => [
    App\Chunky\ProfileAvatarContext::class,
    App\Chunky\DocumentContext::class,
],
```

Or register manually in your `AppServiceProvider`:

```
use NETipar\Chunky\Facades\Chunky;

public function boot(): void
{
    Chunky::register(ProfileAvatarContext::class);
}
```

### Inline Closures

[](#inline-closures)

For simple cases, you can register contexts inline:

```
use NETipar\Chunky\Facades\Chunky;

public function boot(): void
{
    Chunky::context(
        'documents',
        rules: fn () => [
            'file_size' => ['max:104857600'], // 100MB
            'mime_type' => ['in:application/pdf,application/zip'],
        ],
    );
}
```

### Using Contexts from Frontend

[](#using-contexts-from-frontend)

```
// Vue 3
const { upload } = useChunkUpload({ context: 'profile_avatar' });

// React
const { upload } = useChunkUpload({ context: 'profile_avatar' });

// Alpine.js
//
```

Listening to Events
-------------------

[](#listening-to-events)

Register listeners in your `EventServiceProvider`:

```
use NETipar\Chunky\Events\UploadCompleted;
use NETipar\Chunky\Events\ChunkUploaded;
use NETipar\Chunky\Events\FileAssembled;

protected $listen = [
    UploadCompleted::class => [
        \App\Listeners\ProcessUploadedFile::class,
        \App\Listeners\NotifyUserAboutUpload::class,
    ],
    ChunkUploaded::class => [
        \App\Listeners\TrackUploadProgress::class,
    ],
];
```

Example listener:

```
namespace App\Listeners;

use NETipar\Chunky\Events\UploadCompleted;
use Illuminate\Support\Facades\Storage;

class ProcessUploadedFile
{
    public function handle(UploadCompleted $event): void
    {
        // Full UploadMetadata DTO available via $event->upload
        $upload = $event->upload;

        Storage::disk($upload->disk)->move(
            $upload->finalPath,
            "documents/{$upload->uploadId}.zip",
        );

        // Shorthand properties also available for convenience:
        // $event->uploadId, $event->finalPath, $event->disk, $event->metadata
    }
}
```

### Available Events

[](#available-events)

EventPayloadWhenBroadcasts?`UploadInitiated`uploadId, fileName, fileSize, totalChunksUpload initialized—`ChunkUploaded`uploadId, chunkIndex, totalChunks, progress%After each successful chunk—`ChunkUploadFailed`uploadId, chunkIndex, exceptionOn chunk error—`FileAssembled`uploadId, finalPath, disk, fileName, fileSizeAfter file assembly—`UploadCompleted`upload (UploadMetadata)Full upload complete✅`UploadFailed`upload (UploadMetadata), reasonSave callback failed or assembly job exhausted retries✅`BatchInitiated`batchId, totalFilesBatch created—`BatchCompleted`batchId, totalFilesAll batch files completed✅`BatchPartiallyCompleted`batchId, completedFiles, failedFiles, totalFilesBatch done with failures✅Broadcasting (Laravel Echo)
---------------------------

[](#broadcasting-laravel-echo)

Get real-time notifications when uploads or batches complete. Broadcasting is **disabled by default** -- enable it in your `.env`:

```
CHUNKY_BROADCASTING=true

```

Four events are broadcastable: `UploadCompleted`, `UploadFailed`, `BatchCompleted`, and `BatchPartiallyCompleted`. They use private channels — when `chunky.broadcasting.register_channels = true` (default), the package auto-registers `Broadcast::channel()` callbacks that delegate to the bound `Authorizer`, so the same ownership rules apply on HTTP and WebSocket.

If you set `register_channels = false`, register them manually in your `routes/channels.php`:

```
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chunky.uploads.{uploadId}', function ($user, $uploadId) {
    // Verify the user owns this upload
    return true;
});

Broadcast::channel('chunky.batches.{batchId}', function ($user, $batchId) {
    return true;
});
```

### Vue 3

[](#vue-3-2)

```

import { useChunkUpload, useUploadEcho } from '@netipar/chunky-vue3';

const echo = inject('echo');
const { upload, uploadId } = useChunkUpload();

useUploadEcho(echo, uploadId, (data) => {
    console.log('Upload ready:', data.fileName);
});

```

### React

[](#react-2)

```
import { useChunkUpload, useUploadEcho } from '@netipar/chunky-react';

function FileUpload({ echo }) {
    const { upload, uploadId } = useChunkUpload();

    useUploadEcho(echo, uploadId, (data) => {
        console.log('Upload ready:', data.fileName);
    });

    // ...
}
```

### Batch Echo

[](#batch-echo)

```
// Vue 3
import { useBatchUpload, useBatchEcho } from '@netipar/chunky-vue3';

const { upload, batchId } = useBatchUpload();

useBatchEcho(echo, batchId, {
    onComplete: (data) => console.log(`All ${data.totalFiles} files ready`),
    onPartiallyCompleted: (data) => console.log(`${data.failedFiles} files failed`),
});
```

### Core (Framework-agnostic)

[](#core-framework-agnostic-2)

```
import { listenForUploadComplete, listenForBatchComplete } from '@netipar/chunky-core';

const unsubscribe = listenForUploadComplete(echo, uploadId, (data) => {
    console.log('Ready:', data.fileName);
});

// Cleanup when done
unsubscribe();
```

### User Channel

[](#user-channel)

Instead of subscribing per-upload or per-batch, listen on the **user channel** to receive all upload events — even after page reload:

```
// routes/channels.php
Broadcast::channel('chunky.user.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});
```

```

import { useUserEcho } from '@netipar/chunky-vue3';

const echo = inject('echo');
const userId = ref(auth.user.id);

useUserEcho(echo, userId, {
    onUploadComplete: (data) => console.log('File ready:', data.fileName),
    onBatchComplete: (data) => console.log(`All ${data.totalFiles} files done`),
    onBatchPartiallyCompleted: (data) => console.log(`${data.failedFiles} failed`),
});

```

```
// React
import { useUserEcho } from '@netipar/chunky-react';

useUserEcho(echo, auth.user.id, {
    onUploadComplete: (data) => console.log('File ready:', data.fileName),
});
```

The user channel requires authenticated routes (`auth:sanctum` middleware) and `user_id` is automatically captured from `auth()->id()` during upload initiation.

Using the Facade
----------------

[](#using-the-facade)

```
use NETipar\Chunky\Facades\Chunky;

// Register contexts
Chunky::register(ProfileAvatarContext::class);
Chunky::context('documents', rules: fn () => [...], save: fn ($metadata) => ...);

// Programmatic initiation (returns InitiateResult DTO)
$result = Chunky::initiate('large-file.zip', 524288000, 'application/zip');
// $result->uploadId, $result->chunkSize, $result->totalChunks

// Query upload status (returns UploadMetadata DTO)
$status = Chunky::status($uploadId);
// $status->progress(), $status->fileName, $status->status, etc.

// Batch upload (returns BatchMetadata DTO)
$batch = Chunky::initiateBatch(totalFiles: 5, context: 'documents');
// $batch->batchId, $batch->totalFiles, $batch->status

// Add file to batch (returns InitiateResult DTO with batchId)
$file = Chunky::initiateInBatch($batch->batchId, 'photo.jpg', 5242880);
// $file->uploadId, $file->batchId

// Query batch status (returns BatchMetadata DTO)
$batch = Chunky::getBatchStatus($batchId);
// $batch->completedFiles, $batch->failedFiles, $batch->isFinished()
```

Configuration
-------------

[](#configuration)

The full configuration reference lives in [`docs/configuration.md`](docs/configuration.md) — every key, default, and the common deployment recipes (large videos, S3, authenticated routes, per-chunk progress broadcast). The TL;DR `.env`:

```
CHUNKY_TRACKER=database          # database | filesystem
CHUNKY_DISK=local                # any Laravel filesystem disk
CHUNKY_CHUNK_SIZE=1048576        # 1MB
CHUNKY_BROADCASTING=false        # opt-in WebSocket broadcasting
CHUNKY_LOCK_DRIVER=flock         # flock | cache (cache for cloud disks)
CHUNKY_STAGING_DIRECTORY=        # null = sys_get_temp_dir()
CHUNKY_CACHE_PREFIX=chunky:v1:   # versioned cache-key prefix

```

Publish the config to customise:

```
php artisan vendor:publish --tag=chunky-config
```

The config is grouped into 10 sections (since v0.18): `storage`, `chunks`, `lifecycle`, `limits`, `metadata`, `locking`, `idempotency`, `cache`, `authorization`, `broadcasting`. Older flat keys were renamed — see [UPGRADE.md](UPGRADE.md) for the full migration table.

Tracking Drivers
----------------

[](#tracking-drivers)

### Database (default)

[](#database-default)

Uses the `chunked_uploads` table. Best for production -- queryable, reliable, supports status tracking.

```
CHUNKY_TRACKER=database

```

### Filesystem

[](#filesystem)

Uses JSON metadata files on disk. Zero database dependency -- useful for simple setups.

```
CHUNKY_TRACKER=filesystem

```

Error Handling
--------------

[](#error-handling)

```
use NETipar\Chunky\Exceptions\ChunkyException;
use NETipar\Chunky\Exceptions\ChunkIntegrityException;
use NETipar\Chunky\Exceptions\UploadExpiredException;

try {
    $manager->uploadChunk($uploadId, $chunkIndex, $file);
} catch (ChunkIntegrityException $e) {
    // SHA-256 checksum mismatch
} catch (UploadExpiredException $e) {
    // Upload has expired (past 24h default)
} catch (ChunkyException $e) {
    // Base exception (catches all above)
}
```

Examples
--------

[](#examples)

- [English examples](examples/en/)
- [Magyar peldak](examples/hu/)

Testing
-------

[](#testing)

```
composer test
```

Credits
-------

[](#credits)

- [NETipar](https://netipar.hu)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance96

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 94.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

35

Last Release

42d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/13118776?v=4)[NETipar](/maintainers/NETipar)[@NETipar](https://github.com/NETipar)

---

Top Contributors

[![hegedustibor](https://avatars.githubusercontent.com/u/6483104?v=4)](https://github.com/hegedustibor "hegedustibor (94 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (5 commits)")

---

Tags

laravels3filelivewireuploadchunkreactechoBroadcastingfile-uploadalpinevueresumablelarge filesbatch-upload

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/netipar-laravel-chunky/health.svg)

```
[![Health](https://phpackages.com/badges/netipar-laravel-chunky/health.svg)](https://phpackages.com/packages/netipar-laravel-chunky)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9742.3M121](/packages/roots-acorn)[unisharp/laravel-filemanager

A file upload/editor intended for use with Laravel 5 to 10 and CKEditor / TinyMCE

2.1k3.4M81](/packages/unisharp-laravel-filemanager)[api-platform/laravel

API Platform support for Laravel

59156.3k11](/packages/api-platform-laravel)[flat3/lodata

OData v4.01 Producer for Laravel

99346.1k](/packages/flat3-lodata)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
