xakki/laravel-file-uploader

Chunked file uploader package for Laravel 10+.

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

Language:JavaScript

pkg:composer/xakki/laravel-file-uploader

dev-main 2025-10-10 10:43 UTC

This package is auto-updated.

Last update: 2025-10-10 10:48:37 UTC


README

Fast, secure and convenient downloader of any files for Laravel 10+ (PHP 8.3+ ) with a modern JS widget.
Supports chunked upload (the chunk size is configured in the config, 1 MB by default), Drag&Drop, list of uploaded files (size/date), copying a public link, soft deletion to trash with TTL auto-cleanup, localization en/ru, flexible configuration and work with any disks (including s3/cloudfront).

Packagist Laravel PHP License CI Coverage

Features

โ€” ๐Ÿš€ Chunks: sending a file in parts, the chunk size is configurable (chunk_size), default is 1 MB.

  • ๐ŸŒi18n: en (default), ru. -*Service Provider: an autodiscaver, publishing assets/config/locales. โ€” ๐Ÿ“ฆ Any disks: default is files; there are ready-made recipes for s3/CloudFront (public/private).
  • ๐ŸŽจPop-up widget: for uploading files.
    • ๐Ÿ–ฑ๏ธDrag & Drop + file selection.
    • ๐Ÿ“‹File list: name, size, date, copy public link in one click, delete.
    • ๐ŸงนDeletion to the trash (soft-delete) + auto-cleaning by TTL (default is 30 days). โ€” ๐Ÿ” Access via middleware (default is web + auth) - changes in the config.

Content

Installation

composer require xakki/laravel-file-uploader

If the auto-finder is disabled, add the provider to config/app.php :

'providers' => [
    Xakki\LaravelFileUploader\Providers\FileUploaderServiceProvider::class,
],

Publish configs, assets, and translations.:

php artisan vendor:publish --tag=file-uploader-config
php artisan vendor:publish --tag=file-uploader-assets
php artisan vendor:publish --tag=file-uploader-translations

Make a public symlink (if not already created):

php artisan storage:link --relative

๐Ÿ’ก The default disk is public. Make sure that it is defined in config/filesystems.php .

Configuration

The file: `config/file-uploader.php ' (redefine if necessary).

About the size of the chunk: the client takes the chunk_size from the /init response, because the value change in the config is automatically picked up at the front.

Integration with S3 / CloudFront

Below are two proven scenarios.

Option A: S3 + CloudFront Public tank as CDN (simple public URLs)

.env:

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=eu-central-1
AWS_BUCKET=my-public-bucket
AWS_URL=https://dxxxxx.cloudfront.net
AWS_USE_PATH_STYLE_ENDPOINT=false

config/filesystems.php (fragment of the s3 driver):

's3' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'url' => env ('AWS_URL'), / / < -- cloudfront domain here
    'visibility' => 'public',
    'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
],

config/file-uploader.php:

'disk' => 's3',
'public_url_resolver' => null, / / Storage:: url () returns the CloudFront URL
``

> **Summary:** `Storage:: url (.path)` will build .l based on `aws_url' (CloudFront domain).

---

### Option B: S3 + CloudFront Private Tank with **signed links**

If the bucket is private and file access requires a signature, use one of two paths:

**B1. S3 pre-signed (temporary) URLs:**

* Create a temporary URL in the controller/service along with 'Storage:: url()`:

  ```php
  $url = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10));
  • Return it to the client (widget/listing).
      • Plus**: simple and regular. Minus: The URL will be s3 format, not CloudFront.

B2. CloudFront Signed URL (recommended if you need a CDN domain):

  1. Specify in ' config / file-uploader.php ` public URL resolver (string callable; it is convenient to put the class in a package/project):
'public_url_resolver' => \App\Support\FileUrlResolvers\CloudFrontSignedResolver::class.'@resolve',
  1. Implement `CloudFrontSignedResolver' (example):
<?php

namespace App\Support\FileUrlResolvers;

use Aws\CloudFront\UrlSigner;

class CloudFrontSignedResolver
{
    public function __construct(
        private readonly string $domain = 'https://dxxxxx.cloudfront.net',
        private readonly string $keyPairId = 'KXXXXXXXXXXXX',
        private readonly string $privateKeyPath = '/path/to/cloudfront_private_key.pem',
        private readonly int $ttlSeconds = 600, / / 10 minutes
    ) {}

    public function resolve(string $path): string
    {
// Normalizing the CloudFront URL
        $resourceUrl = rtrim($this->domain, '/').'/'.ltrim($path, '/');

        // Signing the URL
        $signer = new UrlSigner($this->keyPairId, file_get_contents($this->privateKeyPath));
        $expires = time() + $this->ttlSeconds;

        return $signer->getSignedUrl($resourceUrl, $expires);
    }
}

Important: use the string callable (Class@method') along with the closure โ€” this is compatible with php artisan config: cache'.

Widgets (JS)

Initialization

Connecting a script widget:

<script src="/vendor/file-uploader/file-upload.js" defer></script>

Insert the container and initialize the widget (for example, in layouts/app.blade.php ):

<div id="file-upload-widget"></div>
<script>
  window.FileUploadWidget?.init({
    endpointBase: '/file-upload',
    chunkSize: 1024 * 1024,
    listEnabled: true,
    allowDelete: true,
    locale: 'en',        // 'en' | 'ru'
    auth: 'csrf',        // 'csrf' | 'bearer' | 'none'
    token: null, / / bearer token for API
    styles: {/* custom CSS */
      toggle: { background: '#111827' },
      modal: { maxWidth: '380px' },
      dropzone: { borderColor: '#4f46e5' },
    },
    i18n: {/* custom Locale */
      title: 'Uploads',
      drop: 'Drop files here or click to browse',
      completed: 'Done!',
    }
  });
</script>

Events

  • file-uploader:success โ€” { file }
  • file-uploader:deleted โ€” { id }

Routes and API

Prefix:'config ('file-uploader.route_prefix), default is '/file-upload'. All routes are wrapped in middleware' from the config (default: web, auth`).

Redirecting chunks

POST /file-upload/chunks

Body - multipart/form-data with fields:

  • filechunk'-binary chunk (<<config ('file-uploader.chunk_size')).
  • `chunkIndex ' โ€” chunk number (0..N-1).
  • `totalChunks' โ€” total chunks.
  • uploadId' is a unique ID (for example, 'upload-${Datenow()}-${Math.random()}).
  • 'filesize', 'filename', 'mimetype' - metadata.

Response (200 JSON)

{
  "status": "ok",
  "completed": true,
  "file": {
    "id": "upload-...",
    "original_name": "report.pdf",
    "size": 7340032,
    "mime": "application/pdf",
    "url": "https://example.com/storage/uploads/report.pdf",
    "created_at": "2025-10-09T10:12:33Z"
  },
  "message": "File \"report.pdf\" uploaded successfully."
}

If `completed = false', the service will continue to wait for the remaining chunks.

List of files

GET /file-upload/files

Response (200 JSON)

{
  "status": "ok",
  "files": [
    {
      "id": "upload-...",
      "original_name": "report.pdf",
      "size": 7340032,
      "mime": "application/pdf",
      "url": "https://example.com/storage/uploads/report.pdf",
      "created_at": "2025-10-09T10:12:33Z"
    }
  ]
}

Delete and trash

Deletion (soft-delete)

DELETE /file-upload/files/{id}

Response (200 JSON)

{ "status": "ok", "message": "File moved to trash." }

Recovery

POST /file-upload/files/{id}/restore

Response (200 JSON)

{ "status": "ok", "message": "File restored." }

Emptying the trash (TTL)

php artisan file-uploader:cleanup

app/Console/Kernel.php:

$schedule->command('file-uploader:cleanup')->daily();

Via HTTP:DELETE /file-upload/trash/cleanup โ†’ `{"status": "ok", "count": }'.

TTL is controlled by `trash_ttl_days' (default 30 ).

Localization (i18n)

  • Server: locales from supported_locales' (en/ru), default is default_locale'.
  • Widget: by default, en; you can specify locale: 'ru and/or redefine the strings in 'i18n'.

PHP service

Xakki\LaravelFileUploader\Services\FileUpload

Responsible for:

  • Validation of size / 'mime' / 'extension' (config).
  • Receiving chunks to a temporary directory (storage/app/chunks/{uploadId}).
  • Assembling the final file and saving it to Storage::disk ()...).
  • Generation of a public or signed link (via public_url_resolver/Storage::url/temporaryUrl).
  • Transfer/restore files from the trash.
  • Clearing temporary chunks and trash (command/shadower).

Example:

use Xakki\LaravelFileUploader\Services\FileUpload;

/** @var FileUpload $uploader */
$uploader = app(FileUpload::class);
$result = $uploader->handleChunk([
    'fileChunk' => $request->file('fileChunk'),
    'chunkIndex' => $request->integer('chunkIndex'),
    'totalChunks' => $request->integer('totalChunks'),
    'fileSize' => $request->integer('fileSize'),
    'uploadId' => $request->input('uploadId'),
    'fileName' => $request->input('fileName'),
    'mimeType' => $request->input('mimeType'),
]);
// ['completed' => bool, 'file' => [...]]

Security

  • Type/extension/size validation.
  • Checking the actual MIME (whitelist).
  • CSRF (for `web') / Bearer (for API).
  • Access via middleware (authorized users by default).
  • CORS/Headers โ€” configure at the application level.
  • Regular cleaning of temporary/deleted data on a schedule.

Performance

  • chunk_size is configurable (1 MB by default). A larger chunk means fewer requests, but higher risks of retransmission; a smaller chunk is more resistant to network failures.
  • Parallel sending of chunks on the client is possible (turn it on with caution, given the limitations of the server).
  • For large files, consider post_max_size, upload_max_filesize, and reverse-proxy limits.

FAQ

Is it possible to download multiple files at the same time? yes. The widget supports queuing and (if desired) concurrency.

How do I change the disk/folder? config/file-uploader.php โ†’ disk / directory. For S3/CloudFront, see the integration section.

How do I get a CDN link? For a public CDN, specify AWS_URL (CloudFront domain) and use Storage::url()'. For a private CDNโ€” implement a public_url_resolver` with a CloudFront signature (example above).

How can I disable authorization? Change the middleware' (for example, ['web']`) or leave it empty โ€” only if it is safe to do so.

Troubleshooting

  • 415/422 โ€” check the MIME/extensions and `max_size'.
  • 404 on links โ€” check the storage:link and the disk/directory configuration.
  • CSRF โ€” pass _token or use Bearer.
  • Build failed โ€” make sure that all chunks are received (indexes are continuous) and the size is the same.

CI / QA / Coding Style

  • CI: GitHub Actions โ€” tests ('phpunit/pest'), static analysis (phpstan), linting (pint).
  • Coverage: Codecov.
  • Style: PSR-12, Laravel Pint.

Roadmap

  • Progress bar on file and shared.
  • Parallel loading of chunks + auto-resume.
  • Filters/search through the file list.
  • Additional locales.
  • Wrappers for Livewire/Vue.

Contributing

PR/Issue are welcome. Before shipping:

  • Cover new functionality with tests.
  • Follow the code style and SemVer.
  • Update CHANGELOG.md .

License

License Apache-2.0. See `LICENSE'.

Credits

  • Package: xakki/laravel-file-uploader
  • Namespace: Xakki\LaravelFileUploader
  • Author(s): @xakki