laravel-smart-upload maintained by thekaziomar
Laravel Smart Upload
Developer-First, Zero-Database File & Chunk Upload Engine for Laravel
Eliminates heavy database migrations and media tables, brings fluent order-independent method chaining, auto-converts images to WebP with responsive downscaling (
scaleDown) and center-cropping (fit), prevents orphaned "ghost files" on S3 during database rollbacks (DB::transaction), streams large video chunks with zero memory bloat, and auto-purges old files upon Eloquent model updates.
use App\Models\User;
use KaziOmar\SmartUpload\Facades\Upload;
// 1. Fluent One-Liner: Auto-WebP, downscaled, S3-routed, and replaces previous avatar on disk
$result = Upload::file($request->file('avatar'))
->replace($user->avatar)
->preset('avatar')
->save('avatars');
$user->update(['avatar' => $result->path]);
// 2. Large Video Resumable Chunk Upload: Stream-merges parts with zero RAM spike
$chunk = Upload::chunk($request);
if ($chunk->isComplete()) {
$video = $chunk->save('videos'); // Done!
}
Table of Contents
- Why Laravel Smart Upload?
- Requirements & Installation
- Quickstart in 10 Seconds
- Architectural Philosophy
- Layer 1: The Fluent Upload Engine (
Upload::)- Master Method Cheat Sheet & Decision Guide
- 1.1 Single Image Upload (WebP & Compression)
- 1.2 Smart Image Sizing (
scaleDownvsfit) - 1.3 Order-Independent Fluent Chaining
- 1.4 Batch Uploads (
Upload::files) &UploadCollection - 1.5 Zero-Boilerplate Request Uploads (
Upload::request) - 1.6 Raw Non-Image File Handling
- 1.7 Old File Replacement (
->replace()) - 1.8 Named Presets (
->preset()) - 1.9 Multi-Disk Routing (Local, S3, R2, Spaces)
- 1.10 Universal Media URL Formatter (
Upload::url()) - 1.11 Manual File Deletion (
Upload::delete())
- Layer 2: Strict Security & Defense-in-Depth Shield
- Layer 3: Atomic Database Rollback Shield (Ghost File Protection)
- Layer 4: Resumable Chunk Upload Engine (Large Files & Videos)
- Layer 5: Mobile & SPA Two-Step Upload Engine
- Layer 6: Eloquent Model Lifecycle Integration (
HasUploadPurge) - Layer 7: Automated Garbage Collection & Maintenance
- Driver & Cloud Storage Compatibility
- Configuration Reference
- Troubleshooting & FAQ
- Testing
- Changelog & Versioning
- Contributing & Security
- Credits & License
Why Laravel Smart Upload?
| Feature | Native Laravel store() |
Spatie MediaLibrary | Intervention Image | Laravel Smart Upload |
|---|---|---|---|---|
| Database Requirement | None | Requires Migrations & Tables | None | Zero Database Dependency |
| Fluent Chaining (DX) | Primitive | Model-Coupled | Primitive | Full Method Chaining |
| Automatic WebP Conversion | No | Manual Setup | Manual Coding | Built-in & Configurable |
Aspect-Ratio scaleDown() |
No | Requires Conversions | Manual Math | Built-in Intent |
Center-Crop fit() |
No | Requires Conversions | Manual Math | Built-in Intent |
| Old File Auto-Purge | Manual Storage::delete |
Semi-Automatic | No | Built-in ->replace() & Trait |
| Atomic DB Rollback Shield | No (Ghost files stay on S3) | No | No | Auto-Purge on DB Rollback |
| Resumable Chunk Engine | No | No | No | Built-in Stream Chunker |
| Mobile & Two-Step Upload | No | Complex | No | Built-in temp() / fromTemp() |
| MIME Magic-Bytes Security | Basic | Basic | None | Strict Guard by Default |
| SVG XSS Sanitizer | No | No | No | Built-in Threat Inspection |
Requirements & Installation
- PHP:
8.2,8.3,8.4, or8.5+ - Laravel Framework:
11.x,12.x, or13.x - PHP Extensions:
fileinfo,gd(orimagick),exif,sqlite(for testbench)
Install via Composer:
composer require thekaziomar/laravel-smart-upload
Publish the package configuration file:
php artisan vendor:publish --tag="smart-upload-config"
Quickstart in 10 Seconds
Approach A: The Fluent Facade (Direct Controller Upload)
use KaziOmar\SmartUpload\Facades\Upload;
public function store(Request $request)
{
// 1. Single file from request: Auto WebP, downscaled, safely named
$avatar = Upload::request($request, 'avatar')->preset('avatar')->save('avatars');
// 2. Or batch files with zero-boilerplate:
$photos = Upload::request($request, 'photos')->toWebp()->save('reviews')->paths();
return response()->json([
'avatar_url' => $avatar->url,
'photos' => $photos, // Array of paths or [] if none provided
]);
}
Approach B: Eloquent Model Auto-Purge (Zero-Boilerplate Cleanup)
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use KaziOmar\SmartUpload\Traits\HasUploadPurge;
class User extends Model
{
use HasUploadPurge;
// Files mapped here will be automatically deleted from disk/S3 when updated or deleted!
protected array $purgeableFiles = ['avatar', 'cover_photo'];
}
Approach C: Resumable Chunk Upload (Large Video Streams)
public function chunk(Request $request)
{
$chunk = Upload::chunk($request);
if ($chunk->isComplete()) {
$result = $chunk->save('courses/videos');
return response()->json(['status' => 'done', 'url' => $result->url]);
}
return response()->json(['status' => 'uploading', 'done' => $chunk->percentage()]);
}
Architectural Philosophy
Laravel Smart Upload was built on five core architectural tenets:
- Zero Database Obligation: No migrations, no polymorphic tables, and no forced database writes. You store your paths where and how you want.
- Order-Independent Fluent Builder: You can chain
->toWebp()->scaleDown()or->scaleDown()->toWebp(). The internal engine executes operations in optimal order only upon calling->save(). - Atomic File Safety: A file should never outlive the database transaction that authorized it. If an unhandled exception rolls back your SQL, your cloud storage rollbacks simultaneously.
- Low-Memory Streaming: High-megapixel camera photos and multi-gigabyte video files are processed using native PHP file pointers and streams (
fopen), preventing memory limit exhausts. - Secure by Default: We assume all user inputs are hostile until verified with magic-bytes, sanitized against traversal attacks, and stripped of tracking metadata.
Layer 1: The Fluent Upload Engine (Upload::)
Master Method Cheat Sheet & Decision Guide
Every method in the builder chain is order-independent and serves a specific architectural purpose. Use this quick reference to know exactly which method to pick, when it is unnecessary, and its alternative:
| Chained Method | When to Use It (Primary Use Case) | When NOT to Use It | Alternative / Counterpart |
|---|---|---|---|
->toWebp(?$q) |
Force convert any image to modern, lightweight WebP format. | Unnecessary if 'auto_webp' => true in config (it is already active by default). |
->keepOriginalFormat() |
->keepOriginalFormat() |
Retain original format (e.g. animated GIFs, lossless PNG brand logos, or legacy app compatibility). | Don't use when minimal bandwidth and fast loading speed are your priority. | Default Auto-WebP or ->toWebp() |
->quality(int $q) |
Fine-tune image compression ratio (1-100). E.g. 60 for thumbnails, 90 for crisp product photos. |
Unnecessary if default quality (85) in config is already optimal for you. |
->toWebp(quality: $q) or config |
->scaleDown($w, $h) |
Banners, article heroes, and gallery photos. Downscales preserving aspect ratio; never stretches smaller images. | Don't use when you need an exact square crop with zero letterboxing. | ->fit($width, $height) |
->fit($w, $h) |
Square profile avatars, thumbnails, or icons. Smartly center-crops to fill exact dimensions. | Don't use when cropping out image edges is unacceptable (e.g. panoramic landscapes). | ->scaleDown($w, $h) |
->preset($name) |
Reusable sizing/quality configs (e.g. avatar, banner) declared once in config/smart-upload.php. |
Unnecessary for one-off custom dimensions. | Manually chaining ->fit()->toWebp() |
->disk($disk) |
Route this specific upload to cloud storage (s3, r2, spaces) or private disk. |
Unnecessary if storing to the default disk configured in smart-upload.php. |
Set 'disk' => 's3' globally in config |
->replace($oldPath) |
Auto-delete previous file from disk upon successful save (e.g. updating avatar via DB::table()). |
NEVER needed if your model uses HasUploadPurge! (The trait auto-deletes old files upon $user->update()). |
Use HasUploadPurge trait on Model |
->temporary() |
Stage upload in isolated temp storage during multi-step registration or SPA wizards. | Don't use for standard, immediate form submissions. | Upload::temp($file) one-liner |
->save($directory) |
Terminal execution method. Runs the optimized pipeline, verifies security, writes to disk, and arms transaction shields. | Must be called at the very end of the chain. | — |
1.1 Single Image Upload (WebP & Compression)
Convert images to high-performance WebP formats while reducing bandwidth and storage costs:
$result = Upload::file($request->file('photo'))
->toWebp(quality: 80)
->save('gallery');
// $result is an immutable UploadResult DTO:
echo $result->path; // "gallery/my-photo_a8f9c1b2d3e4.webp"
echo $result->url; // "https://your-domain.com/storage/gallery/..."
echo $result->sizeFormatted; // "142 KB"
echo $result->mimeType; // "image/webp"
Retaining Original File Formats (->keepOriginalFormat())
By default, Smart Upload converts images to WebP for maximum compression and performance. If you need to keep the exact original format (e.g. animated GIFs, lossless PNG brand logos, or legacy app compatibility), simply chain ->keepOriginalFormat():
$result = Upload::file($request->file('logo'))
->keepOriginalFormat()
->scaleDown(800)
->save('logos');
// Saved as logos/brand-logo_x8f9c1b2.png (preserves original format while still applying sizing & security!)
1.2 Smart Image Sizing (scaleDown vs fit)
Avoid stretched, distorted, or blurry images by selecting the exact intent:
scaleDown(width, height): Ideal for banners, gallery photos, and content images. Ensures the image does not exceed dimensions while preserving aspect ratio. Never enlarges smaller images.fit(width, height): Ideal for square profile pictures, avatars, and thumbnails. Smartly crops and covers the exact dimensions from the center.
// Banner / Gallery Image:
Upload::file($file)->scaleDown(1920, 1080)->save('banners');
// Profile Picture:
Upload::file($file)->fit(300, 300)->save('avatars');
1.3 Order-Independent Fluent Chaining
The builder stores your configurations in state. The image is read, resized, and encoded in a mathematically optimal pipeline only when ->save() is called:
// Both snippets produce identical, optimal results:
Upload::file($file)->fit(200, 200)->toWebp(85)->disk('s3')->save('avatars');
Upload::file($file)->disk('s3')->toWebp(85)->fit(200, 200)->save('avatars');
1.4 Batch Uploads (Upload::files) & UploadCollection
Upload an array of files in a single line. The files() method strictly guarantees returning an UploadCollection regardless of file count (even if only 1 file is uploaded):
$collection = Upload::files($request->file('photos'))
->scaleDown(1200, 800)
->toWebp()
->save('products');
// UploadCollection helpers:
$paths = $collection->paths(); // ['products/1.webp', 'products/2.webp']
$urls = $collection->urls(); // ['https://...', 'https://...']
// Batch delete all files if needed:
$collection->delete();
[!TIP] Polymorphic DX Safety: To prevent crashes when expecting arrays or single paths,
UploadResultalso implements->paths()and->urls()(returning 1-element arrays), whileUploadCollectionsupports->pathand->url(returning the first item). Your code never breaks!
1.5 Zero-Boilerplate Request Uploads (Upload::request)
Eliminate controller if-else and hasFile() boilerplate. Automatically detects single or multiple files, validates integrity, and gracefully returns empty results if the user submitted no files:
public function store(Request $request)
{
// Upload whatever is in 'photos' (single, array, or empty) in 1 line:
$photos = Upload::request($request, 'photos')
->scaleDown(1200)
->toWebp(85)
->save('reviews')
->paths();
Review::create([
'comment' => $request->comment,
'photos' => $photos, // Safely saves array of paths, or [] if empty!
]);
}
1.6 Raw Non-Image File Handling
Non-image files (PDFs, ZIPs, DOCX, CSVs) bypass image processing and stream directly to disk:
$result = Upload::file($request->file('document'))->save('contracts');
// Uploads as contracts/my-document_x8f7d9a1.pdf
1.7 Old File Replacement (->replace())
Prevent storage disks from accumulating orphaned files when users update pictures:
// Automatically deletes $user->avatar from disk upon successful upload of the new avatar:
$result = Upload::file($request->file('avatar'))
->replace($user->avatar)
->preset('avatar')
->save('avatars');
$user->update(['avatar' => $result->path]);
[!NOTE] Do I need
->replace()if I use theHasUploadPurgetrait on my Model? No! If your Eloquent model usesHasUploadPurge, it handles old file deletion automatically upon$user->update().You only need
->replace()when:
- You aren't using the trait on the model.
- You are using direct query builder updates (
DB::table('users')->where(...)->update(...)).- You manage files outside models (e.g. application settings, configs).
- You prefer explicit manual replacement right in your controller.
1.8 Named Presets (->preset())
Define reusable sizing and formatting configurations in config/smart-upload.php:
// config/smart-upload.php
'presets' => [
'avatar' => ['fit' => [300, 300], 'webp' => true, 'quality' => 85],
'banner' => ['scale_down' => [1920, 600], 'webp' => true, 'quality' => 80],
],
Then invoke them cleanly anywhere in your application:
Upload::file($file)->preset('avatar')->save('avatars');
1.9 Multi-Disk Routing (Local, S3, R2, Spaces)
Seamlessly route files across multiple cloud storage drivers:
Upload::file($file)->disk('s3')->save('private-docs');
Upload::file($file)->disk('r2')->save('cdn-assets');
[!NOTE] Private Local vs Public Storage: If your filesystem disk is
local(FILESYSTEM_DISK=local), files are stored instorage/app/(private) andStorage::url()is not publicly accessible via web browsers. For publicly accessible assets, use'disk' => 'public'or cloud drivers (s3,r2).
1.10 Universal Media URL Formatter (Upload::url() & upload_url())
Effortlessly convert any stored database path, plain filename, or external link into a fully qualified URL across Local and Cloud (S3, R2, Spaces) storage without hardcoding asset('storage/...'):
use KaziOmar\SmartUpload\Facades\Upload;
// 1. Storage relative path (works on Local, S3, Cloudflare R2 automatically)
$url = Upload::url($user->avatar);
// Local: https://example.com/storage/avatars/pic.webp
// S3: https://bucket.s3.amazonaws.com/avatars/pic.webp
// 2. External links preserved untouched (CDN, Google OAuth, Unsplash)
$url = Upload::url('https://lh3.googleusercontent.com/avatar.jpg');
// https://lh3.googleusercontent.com/avatar.jpg
// 3. Auto-prefix default directory when path is just a plain filename
$url = Upload::url('logo.png', defaultDir: 'stores/logos');
// https://example.com/storage/stores/logos/logo.png
// 4. Fallback placeholder for null/empty database columns
$url = Upload::url($user->avatar, fallback: 'https://placehold.co/150x150.png');
// 5. Global helper function anywhere in Blade views or resources:
echo upload_url($user->avatar);
1.11 Manual File Deletion (Upload::delete())
When you want to explicitly delete a file from disk without relying on model events:
use KaziOmar\SmartUpload\Facades\Upload;
// 1. Delete by relative path:
Upload::delete('avatars/old_pic.webp');
// 2. Delete by full public URL (automatically parses path):
Upload::delete('https://yourdomain.com/storage/avatars/old_pic.webp');
// 3. Delete from custom storage disk (e.g. S3):
Upload::delete('contracts/doc.pdf', disk: 's3');
// 4. Directly from an UploadResult or UploadCollection DTO:
$result->delete(); // Deletes this specific file
$collection->delete(); // Batch deletes all uploaded files
Layer 2: Strict Security & Defense-in-Depth Shield
Web applications face continuous threats from malicious file uploads. Laravel Smart Upload enforces defense-in-depth security on every upload:
2.1 Real MIME Magic-Bytes Inspection
Attackers often rename malicious scripts (e.g., backdoor.php to photo.jpg) to bypass client-side validation.
- Smart Upload reads the true file header bytes using PHP's
fileinfobuffer. - Any executable MIME payload (
application/x-php,text/x-php,application/x-executable,application/x-sh) is immediately rejected with aSecurityException.
2.2 Anti-Path Traversal & Null-Byte Protection
- Filenames containing
../,..\, or null-bytes (\0) designed to overwrite system files or escape target directories are caught and blocked before touching disk.
2.3 Multi-Extension Spoofing Guard (.php.jpg)
- Files with double extensions (such as
invoice.php.pngoravatar.sh.jpg) designed to trigger web server execution bugs are automatically detected and blocked.
2.4 SVG XSS Sanitization
- SVG files are vector XML documents capable of executing arbitrary JavaScript. Smart Upload parses SVG files and blocks embedded
<script>tags, inline event handlers (onload,onerror), andjavascript:URIs.
2.5 EXIF & Geolocation Metadata Stripping
- Camera and smartphone photos contain EXIF headers exposing user GPS latitude/longitude, device serial numbers, and capture timestamps. All image encoding operations strip private EXIF tags by default.
2.6 Cryptographic Entropy & Safe Slug Naming
- Files are renamed using sanitized URL slugs concatenated with a 12-character high-entropy random string, eliminating collision and guessable filename vulnerabilities.
Layer 3: Atomic Database Rollback Shield (Ghost File Protection)
3.1 The Ghost File Problem in Cloud Storage
A common flaw in Laravel applications:
- A controller uploads an image to AWS S3.
- The controller executes database queries inside a
DB::transaction(). - An unexpected SQL error occurs (e.g. duplicate key or constraint violation) and the transaction rolls back.
- The result: The database record was not created, but the file remains on S3 forever as an orphaned "ghost file", costing money and cluttering buckets.
3.2 Automatic Transaction Level Detection
Smart Upload automatically detects if an upload is executing inside an active database transaction (DB::transactionLevel() > 0).
3.3 Transaction Rollback Workflow
When active, Smart Upload listens to Laravel's TransactionRolledBack event. If the database transaction fails or throws an exception, Smart Upload automatically issues an immediate delete request to remove the file from storage!
sequenceDiagram
autonumber
actor Client
participant Controller
participant SmartUpload as Smart Upload Engine
participant S3 as Storage / S3
participant DB as Database (Transaction)
Client->>Controller: Submit Form with File
Controller->>DB: Begin DB::transaction()
Controller->>SmartUpload: Upload::file()->save('invoices')
SmartUpload->>S3: Stream write file to S3
SmartUpload->>DB: Register Rollback Listener (Shield)
Controller->>DB: Insert Invoice Record
DB-->>Controller: SQL Exception (Rollback!)
DB->>SmartUpload: Fire TransactionRolledBack Event
SmartUpload->>S3: Auto-Delete Ghost File from S3!
Controller-->>Client: 500 / 422 Error (No Orphaned Files)
Layer 4: Resumable Chunk Upload Engine (Large Files & Videos)
Large files and video uploads often fail due to PHP upload_max_filesize, gateway timeouts, or unstable mobile network connections.
4.1 Low-Memory Sequential Stream Merging
Rather than buffering multi-gigabyte files in RAM, Smart Upload handles chunks sequentially:
- Each chunk part is written to a temporary disk path.
- When the final chunk arrives, Smart Upload opens a low-memory PHP output stream (
tempnam+stream_copy_to_stream) and concatenates the parts. - The assembled file is validated against the Security Sanitizer, streamed to its final disk destination, and the temporary chunk folder is purged.
4.2 Client-Agnostic Parameter Support (Dropzone, Resumable.js, Uppy)
Smart Upload auto-detects parameters from all leading frontend chunk upload libraries:
- Identifier:
dzuuid,uuid,resumableIdentifier,qquuid,upload_id. - Chunk Index:
dzchunkindex,chunk,resumableChunkNumber. - Total Chunks:
dztotalchunkcount,total_chunks,resumableTotalChunks.
public function handleChunk(Request $request)
{
$chunk = Upload::chunk($request);
if ($chunk->isComplete()) {
$result = $chunk->save('videos');
return response()->json([
'status' => 'completed',
'url' => $result->url,
'path' => $result->path,
]);
}
return response()->json([
'status' => 'uploading',
'progress' => $chunk->percentage(), // e.g. 75.0%
]);
}
4.3 Incomplete Upload Safeguards
Calling ->save() on an incomplete chunk upload throws a ChunkException, preventing corrupt or truncated files from being written.
Layer 5: Mobile & SPA Two-Step Upload Engine
5.1 Direct Multipart vs Two-Step Uploads
- Direct Upload (
Upload::file()): Best for standard forms where user fills fields and submits the form in one HTTP request. - Two-Step Upload (
Upload::temp()+Upload::fromTemp()): Best for mobile apps (Flutter, React Native) and SPAs (React, Vue, Livewire) where an image is selected and immediately uploaded to show an instant progress bar and preview before final form submission.
5.2 Livewire, React, Flutter & React Native Workflows
sequenceDiagram
actor User as Mobile / SPA User
participant App as Flutter / React Native
participant API as Laravel Backend
participant Storage as Local / S3 Storage
User->>App: Pick Image from Camera
App->>API: POST /api/upload-temp
API->>Storage: Upload::temp($file)
Storage-->>API: Store in temp/ with UUID
API-->>App: Return { temp_path: "smart-upload/temp/uuid.jpg" }
App-->>User: Show Instant Preview
User->>App: Click "Complete Registration"
App->>API: POST /api/register { name, email, temp_path }
API->>Storage: Upload::fromTemp($temp_path)->toWebp()->save('avatars')
Storage-->>API: Move to permanent storage & WebP
API-->>App: Registration Complete!
5.3 Temporary Storage (Upload::temp())
// POST /api/upload-temp
public function uploadTemp(Request $request)
{
$temp = Upload::temp($request->file('photo'));
return response()->json(['temp_path' => $temp->path]);
}
5.4 Promoting Temporary Uploads (Upload::fromTemp())
// POST /api/create-product
public function store(Request $request)
{
$result = Upload::fromTemp($request->temp_path)
->scaleDown(1200, 800)
->toWebp()
->save('products');
Product::create([
'title' => $request->title,
'image' => $result->path,
]);
}
Layer 6: Eloquent Model Lifecycle Integration (HasUploadPurge)
6.1 Declarative Attribute Purging ($purgeableFiles)
Add the HasUploadPurge trait to any Eloquent model and declare the file columns. Supports single string columns as well as JSON / Array columns (e.g. $casts = ['photos' => 'array']):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use KaziOmar\SmartUpload\Traits\HasUploadPurge;
class Review extends Model
{
use HasUploadPurge;
protected $casts = [
'photos' => 'array',
];
// Handles both string columns and array/json columns automatically:
protected array $purgeableFiles = ['photos'];
}
[!TIP] Smart Array Diffing: When updating an array/JSON column,
HasUploadPurgeautomatically computes the difference. If a review has 3 photos and you remove 1, only the removed photo is purged from storage while the remaining photos are preserved!
[!NOTE] Is
$casts = ['photos' => 'array']mandatory for multiple files? While declaring$castsis standard Laravel best practice, it is NOT strictly mandatory for Smart Upload. Our purge engine is defensive: even if you forget$castsand your database column returns a raw JSON string (e.g.'["a.webp","b.webp"]'),HasUploadPurgeautomatically detects, parses, and cleans up the files without crashing.
[!IMPORTANT] Security Best Practice for Purgeable Columns: Never populate attributes mapped in
$purgeableFilesdirectly from unvalidated raw user inputs (e.g.$user->update($request->all())). An attacker could supply another user's file path to trigger unauthorized deletion of that file. Always set purgeable attributes using the verified result ofUpload::file(),Upload::request(), or strictly validate ownership before updating.
6.2 Auto-Purge on Model Update
When an attribute listed in $purgeableFiles is modified:
$user->update(['avatar' => 'avatars/new_photo.webp']);
// The previous avatar file on disk is automatically deleted!
6.3 Auto-Purge on Model Deletion
When the model is deleted, all referenced files are automatically removed from storage:
$user->delete();
// Both avatar and cover_image are automatically cleaned up!
6.4 SoftDeletes Compatibility
If your model uses Laravel's SoftDeletes trait:
- Standard
$model->delete()retains the files on storage so restored models retain their images. - Calling
$model->forceDelete()permanently purges the files from storage.
6.5 Trait Limitations & Query Builder Caveats
Like all Eloquent model traits in Laravel, HasUploadPurge listens to model lifecycle events (updated, deleted, forceDeleted).
[!WARNING] When does the trait NOT automatically trigger?
- Direct DB queries & Mass-deletes via Query Builder:
Solution: If mass-deleting models with files, iterate over the models so lifecycle hooks trigger:// ⚠️ Laravel DOES NOT fire Eloquent model events on direct SQL queries: DB::table('users')->where('id', 1)->delete(); User::where('status', 'inactive')->delete();// Standard dataset: User::where('status', 'inactive')->get()->each->delete(); // Large dataset (RAM-safe lazy streaming cursor): User::where('status', 'inactive')->cursor()->each->delete();- Quiet operations (
deleteQuietly()/updateQuietly()): Using Laravel'sdeleteQuietly()intentionally suppresses all model events, skipping the auto-purge.- Fallback: If you ever update or delete files outside Eloquent events, use
Upload::delete($path)or->replace($oldPath)manually.
Layer 7: Automated Garbage Collection & Maintenance
7.1 The Artisan Prune Command
Users often drop internet connections mid-upload or abandon forms after uploading temporary pictures. The prune command purges expired temporary files and abandoned chunk directories:
php artisan smart-upload:prune --hours=24
7.2 Automatic Daily Scheduler Integration
You don't need to manually modify routes/console.php. Smart Upload automatically registers the prune command with Laravel's scheduler upon boot (configurable via 'auto_schedule_prune' => true).
Driver & Cloud Storage Compatibility
Laravel Smart Upload operates across all major filesystem drivers supported by Laravel Flysystem:
| Driver | Storage Target | Tested Compatibility |
|---|---|---|
local / public |
Local Server Storage (storage/app/public) |
✅ 100% |
s3 |
Amazon Web Services (AWS S3) | ✅ 100% |
r2 |
Cloudflare R2 (S3-compatible) | ✅ 100% |
spaces |
DigitalOcean Spaces | ✅ 100% |
minio |
Self-hosted MinIO Object Storage | ✅ 100% |
gcs |
Google Cloud Storage | ✅ 100% |
Configuration Reference
return [
// Default disk (falls back to FILESYSTEM_DISK or 'public')
'disk' => env('SMART_UPLOAD_DISK', env('FILESYSTEM_DISK', 'public')),
// Global automatic WebP conversion for all images
'auto_webp' => env('SMART_UPLOAD_AUTO_WEBP', true),
// Default image compression quality (1-100)
'quality' => (int) env('SMART_UPLOAD_QUALITY', 85),
// Global scaleDown limits (prevents oversized camera uploads)
'max_width' => env('SMART_UPLOAD_MAX_WIDTH', 1920),
'max_height' => env('SMART_UPLOAD_MAX_HEIGHT', 1080),
// Ghost file protection on database transaction rollback
'transaction_safe' => env('SMART_UPLOAD_TRANSACTION_SAFE', true),
// Temporary & chunk upload settings
'temporary' => [
'disk' => env('SMART_UPLOAD_TEMP_DISK', 'local'),
'directory' => env('SMART_UPLOAD_TEMP_DIR', 'smart-upload/temp'),
'chunk_directory' => env('SMART_UPLOAD_CHUNK_DIR', 'smart-upload/chunks'),
'expiration_hours' => (int) env('SMART_UPLOAD_TEMP_EXPIRES', 24),
],
// Automatically schedules daily pruning
'auto_schedule_prune' => env('SMART_UPLOAD_AUTO_SCHEDULE_PRUNE', true),
// Disallowed executable script extensions
'disallowed_extensions' => [
'php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'sh', 'bat', 'cgi',
'pl', 'py', 'jar', 'jsp', 'asp', 'aspx', 'vbs', 'com'
],
// Named presets
'presets' => [
'avatar' => ['fit' => [300, 300], 'webp' => true, 'quality' => 85],
'thumbnail' => ['fit' => [150, 150], 'webp' => true, 'quality' => 80],
'banner' => ['scale_down' => [1920, 600], 'webp' => true, 'quality' => 85],
'content' => ['scale_down' => [1200, 800], 'webp' => true, 'quality' => 85],
],
];
Troubleshooting & FAQ
Q: How does Smart Upload differ from Spatie MediaLibrary?
A: Spatie MediaLibrary is an Eloquent-bound architecture requiring database migrations, a dedicated media table, and model collections. Smart Upload is a standalone, zero-database fluent utility. You get automatic WebP conversions, chunk uploads, and safety checks without migrations.
Q: Does WebP conversion work with transparent PNGs?
A: Yes. Modern WebP formats support full alpha-channel transparency. Transparent PNGs converted via Smart Upload retain their transparency with significant file size reduction.
Q: What happens if an image is smaller than scaleDown() dimensions?
A: scaleDown() will never upsize or stretch smaller images. A 400x300 image passed to scaleDown(1920, 1080) remains 400x300.
Q: How do I disable WebP conversion for a single upload?
A: Chain ->keepOriginalFormat():
Upload::file($file)->keepOriginalFormat()->save('icons');
Testing
Run the automated test suite using PHPUnit:
composer test
# or
vendor/bin/phpunit
The test suite covers:
- Magic-bytes MIME verification and extension spoofing detection.
- Path traversal, null-byte injection, and SVG XSS attacks.
- Aspect ratio downscaling and center-crop fitting via Intervention v3.
- Order-independence across all fluent methods.
- Database transaction rollback auto-purging.
- Resumable chunk assembly and file integrity.
- Eloquent model updating, deleting, and SoftDeletes handling.
- Artisan pruning and garbage collection.
Changelog & Versioning
All notable changes are documented in CHANGELOG.md.
Current Status: v1.0.0 (Unreleased / Pre-release)
This package is currently in pre-release stage pending community review.
Contributing & Security
Please review CONTRIBUTING.md for contribution guidelines. If you discover any security vulnerabilities, please review SECURITY.md and report them privately via email to softeng.kaziomar@gmail.com.
Credits & License
- Author: Kazi Omar Faruk
- Email:
softeng.kaziomar@gmail.com - License: The MIT License (MIT). Please see License File for details.