blalmal10a / free-upload
Filament v4/v5 file upload plugin with custom XHR uploads, PXVT encoding, and proxy decoding
Fund package maintenance!
Requires
- php: ^8.2
- ext-gd: *
- filament/filament: ^4.0 || ^5.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-arch: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
- rector/rector: ^2.0
- spatie/laravel-ray: ^1.26
README
A Filament v4/v5 file upload form component that uploads files directly to an external image hosting API over XMLHttpRequest (with real upload progress), stores plain URL strings in your model, and serves those files back through the plugin's own PHP proxy (fetch from the image host, PXVT-decode on demand).
- Filament v4 and v5 — one package major, same component API on both.
- Images (
image/*) are uploaded raw and served straight from the image host. - Non-image files are PXVT-encoded client-side into a PNG, uploaded, then decoded server-side back into the original bytes (MIME type + filename preserved).
- No axios — plain
XMLHttpRequestwithxhr.upload.onprogress. - URL state — the stored value is a string URL; no state casts, no storage disk needed.
Installation
composer require blalmal10a/free-upload
Publish the config file:
php artisan vendor:publish --tag="free-upload-config"
Configuration
All options are environment-driven:
| Key | Env | Default | Purpose |
|---|---|---|---|
upload_endpoint |
FREEUPLOAD_UPLOAD_ENDPOINT |
null → plugin route freeupload.upload |
user-defined upload endpoint (URL, path, or route name) |
proxy_base_url |
FREEUPLOAD_PROXY_BASE_URL |
null → the app's own routes |
base URL of the proxy that serves stored files |
image_path |
FREEUPLOAD_IMAGE_PATH |
images |
path segment for raw image streaming (no decode) |
files_path |
FREEUPLOAD_FILES_PATH |
files |
path segment for PXVT-decoded files |
upload_host |
FREEUPLOAD_UPLOAD_HOST |
https://freeimage.host/api/1/upload |
upstream hosting API |
api_key |
FREEUPLOAD_API_KEY |
'' |
upstream API key |
max_size_kb |
FREEUPLOAD_MAX_SIZE_KB |
32768 |
upload validation cap |
max_encoded_file_mb |
FREEUPLOAD_MAX_ENCODED_FILE_MB |
30 |
client-side cap for encoded non-image uploads |
image_host |
FREEUPLOAD_IMAGE_HOST |
https://iili.io |
image host the proxy fetches files from |
proxy_timeout |
FREEUPLOAD_PROXY_TIMEOUT |
10 |
upstream fetch timeout (seconds) |
register_routes |
FREEUPLOAD_REGISTER_ROUTES |
true |
register the plugin's upload and file routes |
route_prefix |
FREEUPLOAD_ROUTE_PREFIX |
freeupload |
route prefix |
route_middleware |
FREEUPLOAD_ROUTE_MIDDLEWARE |
['web', 'auth'] |
route middleware |
Set FREEUPLOAD_API_KEY in your .env file:
FREEUPLOAD_API_KEY=your-freeimage-host-key
Upload endpoint
upload_endpoint accepts:
- a full URL (
https://example.com/upload), - a path (
some/pathor/some/path), - a named route (
some.route.name— resolved via Laravel's route helper, falling back to the plugin'sfreeupload.uploadroute when the name doesn't exist).
When unset, the plugin's own POST /freeupload/upload route (named freeupload.upload) is used.
Serving files (the proxy)
The plugin registers two GET routes under the route prefix (when register_routes is enabled):
GET /{prefix}/{image_path}/{id}/{filename}— fetches the image fromimage_hostand streams it as-is (Cache-Control: public, max-age=86400).GET /{prefix}/{files_path}/{id}/{filename}— fetches the encoded PNG, PXVT-decodes it, and streams the original file (Content-Disposition: inline,Cache-Control: no-store).
Stored URLs point at these routes by default (proxy_base_url unset), e.g. https://your-app.com/freeupload/images/abc123.png/photo.png. Set FREEUPLOAD_PROXY_BASE_URL to serve them from an external proxy instead; the same /images and /files path structure is used on the external base.
Usage
use Blalmal10a\FreeUpload\Forms\Components\FreeUpload; FreeUpload::make('file') ->multiple() ->acceptedFileTypes(['image/*', 'application/pdf'])
The component extends Filament's FileUpload, so all regular options work (->image(), ->maxSize(), ->reorderable(), ...). The uploaded value is a URL string (or an array of URL strings when ->multiple()).
The upload endpoint is configured globally via env/config (FREEUPLOAD_UPLOAD_ENDPOINT / upload_endpoint) — it is deliberately not overridable per component, so every form field uploads to the same endpoint.
Additional per-component options:
FreeUpload::make('file') ->maxEncodedFileMb(10) // override the encoded-upload size cap per component
How uploads work
- The user picks a file. If it is a non-image file and fits within
max_encoded_file_mb, the browser PXVT-encodes it into a PNG on a canvas and uploads that; images are uploaded raw. - The upload is sent via
XMLHttpRequestwith progress reporting to the component's endpoint (by default the plugin's ownPOST /freeupload/uploadroute, validated and forwarded to yourupload_host). - The endpoint responds with
{"url": "..."}— a URL served by the proxy ({base}/{image_path}/{id}/{filename}for images,{base}/{files_path}/{id}/{filename}for encoded files). - The URL is written back into the Livewire state and rendered into the component on re-visit; the proxy serves the file and decodes PXVT payloads on demand.
Note
Stored URL strings are client-controllable values. Treat them as untrusted when the field is editable by non-admin users.
Publishing & customizing the component
You can publish the FreeUpload component stub into your application and take full ownership of it:
php artisan freeupload:publish-component
This writes the component to app/Forms/Components/FreeUpload.php with the namespace App\Forms\Components (override with --namespace=):
php artisan freeupload:publish-component --namespace="MyApp\Forms\Components"
The raw stub can also be copied via php artisan vendor:publish --tag="free-upload-component" (the {{ namespace }} placeholder then needs to be replaced manually).
To use your copy instead of the package's:
use App\Forms\Components\FreeUpload; FreeUpload::make('file') ->multiple() ->acceptedFileTypes(['image/*', 'application/pdf']);
The published copy is a snapshot — upgrades to the package won't touch it. The component keeps using the package's config keys and routes unless you override them, so a published copy only changes how the component renders and uploads.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
License
The MIT License (MIT). Please see License File for more information.