Search by

thekaziomar / laravel-smart-upload

thekaziomar

A developer-first, zero-database file and chunk upload toolkit for Laravel with built-in image optimization, strict security, and database rollback safeguards.

Package info

github.com/kaziomarsh24/laravel-smart-upload

pkg:composer/thekaziomar/laravel-smart-upload

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.3 2026-10-29 10:49 UTC

This package is auto-updated.

Last update: 2026-10-29 10:56:07 UTC


README

Run Tests Latest Version on Packagist Total Downloads PHP Version Laravel Version License

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?

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, or 8.5+
  • Laravel Framework: 11.x, 12.x, or 13.x
  • PHP Extensions: fileinfo, gd (or imagick), 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:

  1. Zero Database Obligation: No migrations, no polymorphic tables, and no forced database writes. You store your paths where and how you want.
  2. Order-Independent Fluent Builder: You can chain ->toWebp()->scaleDown() or ->scaleDown()->toWebp(). The internal engine executes operations in optimal order only upon calling ->save().
  3. 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.
  4. 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.
  5. 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, UploadResult also implements ->paths() and ->urls() (returning 1-element arrays), while UploadCollection supports ->path and ->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 the HasUploadPurge trait on my Model? No! If your Eloquent model uses HasUploadPurge, 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 in storage/app/ (private) and Storage::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 fileinfo buffer.
  • Any executable MIME payload (application/x-php, text/x-php, application/x-executable, application/x-sh) is immediately rejected with a SecurityException.

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.png or avatar.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), and javascript: 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:

  1. A controller uploads an image to AWS S3.
  2. The controller executes database queries inside a DB::transaction().
  3. An unexpected SQL error occurs (e.g. duplicate key or constraint violation) and the transaction rolls back.
  4. 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)
Loading

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:

  1. Each chunk part is written to a temporary disk path.
  2. When the final chunk arrives, Smart Upload opens a low-memory PHP output stream (tempnam + stream_copy_to_stream) and concatenates the parts.
  3. 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!
Loading

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, HasUploadPurge automatically 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 $casts is standard Laravel best practice, it is NOT strictly mandatory for Smart Upload. Our purge engine is defensive: even if you forget $casts and your database column returns a raw JSON string (e.g. '["a.webp","b.webp"]'), HasUploadPurge automatically detects, parses, and cleans up the files without crashing.

Important

Security Best Practice for Purgeable Columns: Never populate attributes mapped in $purgeableFiles directly 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 of Upload::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?

  1. Direct DB queries & Mass-deletes via Query Builder:
    // ⚠️ Laravel DOES NOT fire Eloquent model events on direct SQL queries:
    DB::table('users')->where('id', 1)->delete();
    User::where('status', 'inactive')->delete();
    Solution: If mass-deleting models with files, iterate over the models so lifecycle hooks trigger:
    // Standard dataset:
    User::where('status', 'inactive')->get()->each->delete();
    
    // Large dataset (RAM-safe lazy streaming cursor):
    User::where('status', 'inactive')->cursor()->each->delete();
  2. Quiet operations (deleteQuietly() / updateQuietly()): Using Laravel's deleteQuietly() intentionally suppresses all model events, skipping the auto-purge.
  3. 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