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).
— 🚀 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.
- Installation
- [Configuration] (#configuration)
- Integration with S3 / CloudFront
- [Widget (JS)] (#widget-js)
- Routes and API
- Delete and Trash
- [Localization (i18n)] (#localization-i18n)
- [PHP Service] (#php service)
- [Security] (#security)
- [Performance] (#performance)
- FAQ
- Troubleshooting
- CI / QA / Coding Style
- Roadmap
- Contributing
- License
- Credits
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 inconfig/filesystems.php
.
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.
Below are two proven scenarios.
.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):
- 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',
- 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'.
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>
file-uploader:success
—{ file }
file-uploader:deleted
—{ id }
Prefix:'config ('file-uploader.route_prefix
)
, default is '/file-upload'. All routes are wrapped inmiddleware' from the config (default:
web,
auth`).
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.
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 /file-upload/files/{id}
Response (200 JSON)
{ "status": "ok", "message": "File moved to trash." }
POST /file-upload/files/{id}/restore
Response (200 JSON)
{ "status": "ok", "message": "File restored." }
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 ).
- 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'.
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' => [...]]
- 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.
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.
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.
- 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: GitHub Actions — tests ('phpunit
/
pest'), static analysis (phpstan
), linting (pint
). - Coverage: Codecov.
- Style: PSR-12, Laravel Pint.
- Progress bar on file and shared.
- Parallel loading of chunks + auto-resume.
- Filters/search through the file list.
- Additional locales.
- Wrappers for Livewire/Vue.
PR/Issue are welcome. Before shipping:
- Cover new functionality with tests.
- Follow the code style and SemVer.
- Update
CHANGELOG.md
.
License Apache-2.0. See `LICENSE'.
- Package: xakki/laravel-file-uploader
- Namespace:
Xakki\LaravelFileUploader
- Author(s): @xakki