laravel-uploads maintained by ghostcompiler
Laravel Uploads
A Laravel package for upload storage, secure file URLs, model-based URL fields, inline preview support, cleanup tools, and browser-focused image optimization.
Overview
Laravel Uploads is built to keep file handling simple inside Laravel apps.
It gives you:
- one upload API with an optional custom folder path
- facade usage with
Uploads::upload(...) - helper usage with
GhostCompiler()->upload(...) - storage through Laravel
Storage - default file storage inside
LaravelUploads - database tracking for uploaded files
- database tracking for generated links
- model integration through one trait
- clean URL fields like
avatar - browser preview support
- forced download support with
?download=1 - image optimization with AVIF, WEBP, and original-file fallback
- optional aspect-ratio-safe resizing
- expired link cleanup command
Default Storage
By default, files are stored under:
storage/app/private/LaravelUploads
If you pass a custom path like:
Uploads::upload('demo/image', $request->file('avatar'));
the file is stored under:
storage/app/private/LaravelUploads/demo/image
Installation
Install from Packagist
composer require ghostcompiler/laravel-uploads
Install from a local package path
If you are developing this package locally and want to use it inside a Laravel app without publishing it to Packagist, add a path repository to your Laravel project's composer.json:
{
"repositories": [
{
"type": "path",
"url": "/absolute/path/to/laravel-uploads"
}
],
"require": {
"ghostcompiler/laravel-uploads": "*"
}
}
Then run:
composer update ghostcompiler/laravel-uploads
Package Install Command
Publish config and package migrations:
php artisan ghost:laravel-uploads
If the files already exist, the command asks before overwriting them.
Overwrite without prompts:
php artisan ghost:laravel-uploads --force
Run migrations:
php artisan migrate
Database Tables
The package manages two tables:
laravel_uploads_uploads
Stores:
- storage disk
- visibility metadata
- file path
- original file name
- mime type
- extension
- size
- metadata
laravel_uploads_links
Stores:
- upload reference
- generated token
- expiry time
- last accessed time
Model Setup
Your own model still needs upload ID columns like avatar_id, resume_id, or document_id.
Example migration:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->unsignedBigInteger('avatar_id')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('avatar_id');
});
}
};
Add the trait to your model:
use GhostCompiler\LaravelUploads\Concerns\LaravelUploads;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use LaravelUploads;
protected $uploadable = [
'avatar_id' => [
'name' => 'avatar',
'type' => 'public',
'id' => 'hide',
'expiry' => 60,
],
];
}
Usage
Upload with the facade
use GhostCompiler\LaravelUploads\Facades\Uploads;
$upload = Uploads::upload($request->file('avatar'));
Upload into a custom folder
use GhostCompiler\LaravelUploads\Facades\Uploads;
$upload = Uploads::upload('demo/image', $request->file('avatar'));
Upload with the helper
$upload = GhostCompiler()->upload($request->file('avatar'));
Upload with the helper and a path
$upload = GhostCompiler()->upload('demo/image', $request->file('avatar'));
Save the upload ID on a model
$user->avatar_id = $upload->id;
$user->save();
Read the file URL from the model
$user->avatar;
Remove a file
Uploads::remove($user->avatar_id);
Or:
GhostCompiler()->remove($user->avatar_id);
Example controller
use App\Models\User;
use GhostCompiler\LaravelUploads\Facades\Uploads;
use Illuminate\Http\Request;
class ApiController
{
public function uploadAvatar(Request $request)
{
$user = auth()->user();
if ($request->hasFile('avatar')) {
$upload = Uploads::upload('superadmin.com', $request->file('avatar'));
$user->avatar_id = $upload->id;
$user->save();
}
return response()->json([
'user' => $user,
]);
}
}
Response Shape
If avatar_id is mapped like:
'avatar_id' => [
'name' => 'avatar',
]
then:
avatar_idstays in the databaseavatarbecomes the returned URL field
Example API response:
{
"id": "019d810d-1499-7192-9f99-4a67c5ad350b",
"name": "Ghost Compiler",
"email": "ghost@example.com",
"avatar": "https://your-app.test/_laravel-uploads/file/your-token"
}
Preview and Download Behavior
Previewable files open directly in the browser. Other files download automatically.
Preview currently supports:
image/avifimage/jpegimage/pngimage/gifimage/webpimage/svg+xmlapplication/pdftext/plain
Example preview URL:
https://your-app.test/_laravel-uploads/file/your-token
Force download:
https://your-app.test/_laravel-uploads/file/your-token?download=1
Image Optimization
The package can optimize uploaded images globally.
When enabled:
- supported images try AVIF first
- if AVIF is unavailable, the package falls back to WEBP
- if neither conversion path works, the original file is stored
- resizing keeps the original aspect ratio
- images are never upscaled
- browser delivery becomes lighter and faster
Supported input image types:
image/jpegimage/pngimage/webp
Important note
If this config is disabled, no image conversion happens:
'image_optimization' => [
'enabled' => false,
]
To actually optimize images, enable it:
'image_optimization' => [
'enabled' => true,
'quality' => 75,
'convert_to_avif' => true,
'max_width' => 1600,
'max_height' => null,
]
In that example:
- width is capped at
1600 - height is calculated automatically from the original aspect ratio
Cleanup Command
Delete expired generated links:
php artisan ghost:laravel-uploads-clean
Preview how many expired links would be removed:
php artisan ghost:laravel-uploads-clean --dry-run
Config Guide
Published config file:
return [
'disk' => 'local',
'base_path' => 'LaravelUploads',
'defaults' => [
'type' => 'private',
'id' => 'hide',
'expiry' => 60,
],
'image_optimization' => [
'enabled' => false,
'quality' => 75,
'convert_to_avif' => true,
'max_width' => null,
'max_height' => null,
],
'preview_mime_types' => [
'image/avif',
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/svg+xml',
'application/pdf',
'text/plain',
],
'delete_files_with_model' => false,
'route' => [
'prefix' => '_laravel-uploads',
'name' => 'laravel-uploads.show',
'middleware' => ['web'],
],
];
Config key reference
disk
Laravel disk used for storing package files.
base_path
Base folder inside the selected Laravel disk.
defaults.type
Default upload visibility metadata used by the package.
defaults.id
Controls whether the raw upload ID field should remain visible.
defaults.expiry
Default link expiry in minutes.
image_optimization.enabled
Enable or disable global image optimization.
image_optimization.quality
Compression quality from 1 to 100.
image_optimization.convert_to_avif
If enabled, supported images try AVIF first and automatically fall back to WEBP.
image_optimization.max_width
Optional maximum width for optimized images. If set by itself, height is calculated automatically from the original aspect ratio.
image_optimization.max_height
Optional maximum height for optimized images. If set by itself, width is calculated automatically from the original aspect ratio.
preview_mime_types
List of mime types that should open inline in the browser instead of downloading.
delete_files_with_model
If enabled, deleting the model also deletes the stored file and related upload record.
route.prefix
URL prefix for generated file links.
route.name
Laravel route name used internally by the package.
route.middleware
Middleware applied to generated file serving routes.
Local Development
Use this package inside a local Laravel app
- Add a Composer path repository in the Laravel app.
- Require
ghostcompiler/laravel-uploads. - Run:
composer update ghostcompiler/laravel-uploads
php artisan package:discover
php artisan ghost:laravel-uploads
php artisan migrate
Pull latest package changes into the Laravel app
When you make changes in this package repo and want your Laravel app to use them:
composer update ghostcompiler/laravel-uploads
php artisan package:discover
If you changed helper autoloading or package metadata, this is also useful:
composer dump-autoload
If you changed the published config or migration stubs:
php artisan ghost:laravel-uploads
Overwrite existing published files without prompts:
php artisan ghost:laravel-uploads --force
Recommended pull / push workflow
Inside the package repo:
git pull
Make your changes, then:
git add .
git commit -m "Update Laravel Uploads"
git push
Inside the Laravel app using the local path repository:
composer update ghostcompiler/laravel-uploads
php artisan package:discover
Testing
This package now includes a PHPUnit + Testbench scaffold.
Install dev dependencies
Inside the package repo:
composer install
Run tests
composer test
Current test coverage
- resize dimension calculation
- aspect-ratio preservation
- no upscaling behavior
- expired link cleanup command
- dry-run cleanup reporting
Suggested manual testing in a Laravel app
Use a real Laravel test project and verify:
- upload works with
Uploads::upload(...) - upload works with
GhostCompiler()->upload(...) - custom folder uploads work
- model serialization returns URL fields correctly
- preview URL opens supported file types
?download=1forces download- AVIF conversion works when supported
- WEBP fallback works when AVIF is unavailable
- resize limits preserve the original aspect ratio
- cleanup command removes only expired links
Notes
Uploads::upload($file)stores files in the configuredbase_pathUploads::upload('demo/image', $file)stores files insidebase_path/demo/imageGhostCompiler()->upload($file)uses the same Laravel Uploads service directly- generated URLs are tracked in the database
- image optimization only applies to supported images
- AVIF is tried first
- WEBP is used as the main fallback format
- resizing keeps the original aspect ratio
- GD is used first and
Imagickis used as a fallback encoder when available
Contribution Checklist
Before opening a pull request, please check:
- code is clean and readable
- syntax checks pass
- tests pass
- new config options are documented
- README is updated when behavior changes
- no unrelated files are included