laravel-media-vault maintained by mohamedsamy902
Laravel Media Vault
A highly scalable, production-ready file and media management package for Laravel. It provides unified management for centralized uploads and custom polymorphic model fields, seamlessly handling URL downloads, image processing, massive datasets with robust memory safety, and chunked resumable uploads.

Table of Contents
- Features
- Why Media Vault?
- Requirements
- Installation
- Configuration
- Usage Examples
- Response Format
- Database Model
- Dashboard & Media Manager UI
- Cloud Storage Setup
- Console Commands
- ⚠️ Danger Zone / Known Limitations
- Testing
- Advanced Security & Performance Features
- Security
- Contributing
- Changelog
- License
1. Features
- Upload Handlers: Direct File Upload, Multiple (Batch) Uploads, Chunked (Resumable) Uploads, and Uploads from URL.
- Image Processing: On-the-fly resizing, format conversion (e.g.
webp), optimization, and watermarking via Intervention Image v3. - Storage Integration: Out-of-the-box support for Local disks, Amazon S3, Google Cloud Storage, and CDN URL rewriting.
- Multi-Source Media: Auto-discovery of custom Eloquent models via the
HasMediaFieldstrait alongside the central polymorphic repository (HasUploads). - Database Tracking: Comprehensive file tracking with usage state, polymorphic ownership, and soft deletion.
- Dashboard SPA: Built-in zero-dependency SPA UI (Vanilla JS + PJAX + CSS Tokens) to manage files, view statistics, and handle orphans directly without npm builds.
- Bulk Deletion Safety Net: 2-step verification (Preview → Token → Execute) for mass deletions to prevent catastrophic data loss.
- Security & Quotas: Strict SSRF URL validation, user-level storage quotas, strict MIME binary validation, and SVG entity expansion sanitization.
2. Why Media Vault?
Unlike traditional Laravel media management packages (e.g., Spatie Media Library or standard upload helpers) that load entire files into memory leading to Out-Of-Memory (OOM) crashes on large files:
- ⚡ Zero-OOM Streaming Engine: Streams remote and chunked uploads directly to storage with minimal memory footprint (< 10MB) even when handling multi-gigabyte files.
- 📱 Multi-Platform Integration Ready: Built-in execution patterns for Traditional Blade Forms, JS Client (with automatic state resumption), and Mobile REST APIs (Flutter, React Native, Swift, Kotlin).
- 🖥️ Zero-Build SPA Media Manager: Features a standalone SPA Dashboard (Vanilla JS PJAX + CSS Tokens + SweetAlert2) providing zero-reload navigation, live search, and orphan recovery out-of-the-box without npm/Vite compilation.
- 🛡️ Production Safety Net: Includes magic-byte binary verification, SSRF protection, ClamAV antivirus scanning, user storage quotas, and a 2-stage cryptographically signed preview token for mass deletions.itization.
3. Requirements
| Requirement | Version | Notes |
|---|---|---|
| PHP | ^8.2 |
|
| Laravel | >=10.0 |
|
| Extensions | ext-gd or ext-imagick |
Required for image processing |
4. Installation
1. Install via Composer:
composer require mohamedsamy902/laravel-media-vault
2. Publish Configuration:
php artisan vendor:publish --tag="media-vault-config"
3. Run Migrations (Optional, requires database.enabled = true):
php artisan migrate
5. Configuration
This package is highly customizable through config/media-vault.php. Below is a comprehensive list of every configuration key available.
Storage (storage)
| Key | Type | Default | Description |
|---|---|---|---|
storage.disk |
string | 'public' |
The default Laravel storage disk to use. |
storage.path |
string | 'uploads' |
The base directory inside the selected disk. |
storage.default_folder |
string | 'default' |
The default subfolder used if none is specified during upload. |
storage.cdn.enabled |
boolean | false |
Whether to replace the storage URL domain with a CDN domain. |
storage.cdn.url |
string | '' |
The base URL of your CDN. |
Validation (validation)
Contains standard Laravel validation strings mapped to file types (image, video, audio, document, other, custom_fields). Used internally to validate incoming files based on rules like sizes and MIME types.
URL Upload / Download (url_download & url_upload)
| Key | Type | Default | Description |
|---|---|---|---|
url_download.enabled |
boolean | true |
Allow uploading files via a remote URL. |
url_download.chunked |
boolean | true |
True = load into memory; False = stream to disk directly. |
url_download.chunk_size |
int | 5242880 |
Size of streaming chunks (5MB). |
url_download.allowed_mimes |
array | [...] |
Whitelisted extensions for URL downloads grouped by type. |
url_upload.allowed_domains |
array | [] |
Allowed domains for SSRF protection (empty = allow all public). |
url_upload.timeout_seconds |
int | 10 |
Hard timeout for the download HTTP request (overrides legacy url_download.timeout). |
url_upload.max_size_bytes |
int | 52428800 |
Max file size allowed from a URL (50MB default) (overrides legacy url_download.max_size). |
Image Processing (image_driver & processing)
| Key | Type | Default | Description |
|---|---|---|---|
image_driver |
string | 'gd' |
'gd' or 'imagick' (Requires corresponding PHP extension). |
processing.image.enabled |
boolean | true |
Enable/disable all image processing features. |
processing.image.resize |
array | [...] |
Contains width, height, maintain_aspect_ratio, and upsize. |
processing.image.watermark |
array | [...] |
Contains enabled, path, position, opacity, x_offset, y_offset. |
processing.image.filters |
array | [] |
Allowed filters: brightness, contrast, greyscale, blur. |
processing.image.convert_to |
string|null | 'webp' |
Global format conversion target (e.g. webp, jpg). |
processing.image.quality |
int | 85 |
Compression quality (1-100). |
processing.image.optimize |
boolean | false |
Optional integration for Spatie image optimizer. |
processing.video.enabled |
boolean | false |
Enable video processing features. |
processing.video.convert_to |
string | 'mp4' |
Target video format. |
processing.video.bitrate |
string | '1000k' |
Target video bitrate. |
processing.video.resolution |
string | '1280x720' |
Target video resolution. |
Thumbnails (thumbnails)
| Key | Type | Default | Description |
|---|---|---|---|
thumbnails.enabled |
boolean | true |
Automatically generate thumbnails during image upload. |
thumbnails.sizes |
array | [...] |
Associative array of sizes (e.g. small => ['width' => 150, 'crop' => true]). |
thumbnails.for_videos |
boolean | false |
Generate thumbnails for video files (requires FFmpeg). |
thumbnails.seconds |
int | 5 |
Second to capture for video thumbnails. |
Quota Management (quota)
| Key | Type | Default | Description |
|---|---|---|---|
quota.enabled |
boolean | false |
Enable storage quotas per user. |
quota.max_size_per_user |
int | 1073741824 |
1 GB default max size. |
quota.key_column |
string | 'user_id' |
DB column to identify owner. Change to tenant_id for multi-tenant. |
quota.warning_threshold |
float | 0.9 |
Fraction (e.g. 0.9 = 90%) to trigger QuotaWarning event. |
quota.check_method |
string | 'database' |
Method for checking quota (database or session). |
Multi-Source & DB Tracking (media_models, database)
| Key | Type | Default | Description |
|---|---|---|---|
media_models |
array | [] |
List of Custom Eloquent models using HasMediaFields. |
max_orphan_scan_limit |
int | 100000 |
Cap on files scanned per run to prevent memory exhaustion. |
database.enabled |
boolean | true |
Track uploads in the file_uploads table. |
database.model |
string | FileUpload::class |
Class reference for the database model. |
database.table |
string | 'file_uploads' |
Database table name. |
database.prune_after |
int|null | 30 |
Days to retain soft-deleted/unused records before physical prune. |
Security & Chunks (security, chunked)
| Key | Type | Default | Description |
|---|---|---|---|
security.bulk_delete_warning_threshold |
int | 100 |
Deletions exceeding this require an extra warning. |
security.strict_mime_validation |
boolean | true |
Validate file binary magic bytes against declared MIME type. |
security.rate_limit.enabled |
boolean | false |
Enable upload rate limiting. |
security.rate_limit.max_uploads |
int | 60 |
Max uploads per time window. |
security.rate_limit.per_minutes |
int | 1 |
Time window in minutes. |
security.virus_scan.enabled |
boolean | false |
Enable ClamAV virus scanning. |
security.virus_scan.driver |
string | 'clamav' |
Virus scanning driver. |
security.virus_scan.path |
string | '/usr/bin/clamscan' |
Path to the ClamAV executable. |
chunked.session_ttl_hours |
int | 24 |
Hours to keep pending resumable upload sessions before expiration. |
Advanced Features (temp_url, compression, logging)
| Key | Type | Default | Description |
|---|---|---|---|
temp_url.route_prefix |
string | 'media-vault-urls' |
Prefix for temporary signed URL routes. |
temp_url.middleware |
array | [] |
Middleware for temporary signed URLs. |
compression.enabled |
boolean | false |
Enable file compression (e.g., zip) before storage. |
compression.types |
array | [...] |
File extensions eligible for compression. |
compression.quality |
int | 80 |
Compression quality level. |
logging.enabled |
boolean | true |
Enable internal operation logging. |
logging.level |
string | 'info' |
The log level to use. |
UI Dashboard (ui)
| Key | Type | Default | Description |
|---|---|---|---|
ui.route_prefix |
string | 'media-vault' |
Base URL for the Media Manager Dashboard. |
ui.middleware |
array | ['web'] |
CRITICAL: Add 'auth' to protect the dashboard! |
6. Usage Examples
Single & Multiple File Upload via HTTP Request
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
use Illuminate\Http\Request;
public function store(Request $request) {
// Single file
$result = MediaVault::upload($request, [
'field_name' => 'avatar',
'folder_name' => 'avatars',
'convert_to' => 'webp',
'quality' => 90
]);
// Batch files (using name="files[]" in HTML)
$results = MediaVault::upload($request, [
'field_name' => 'files' // Automatically processes all array items
]);
}
Direct Upload with UploadedFile
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
// Assuming $file is an Illuminate\Http\UploadedFile instance
$result = MediaVault::upload($file, [
'disk' => 's3',
'folder_name' => 'documents',
]);
Upload from Remote URL
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
// Single URL
$result = MediaVault::uploadFromUrl('https://example.com/image.jpg', [
'folder_name' => 'downloads',
'disk' => 's3'
]);
// Multiple URLs
$results = MediaVault::uploadFromUrl([
'https://example.com/file1.pdf',
'https://example.com/file2.pdf'
], [
'folder_name' => 'downloads'
]);
Upload Methods & Integration Scenarios
The package supports three distinct integration approaches depending on your application frontend architecture:
Option A: Traditional Blade HTML Form (No JS / Non-Chunked)
For standard Laravel applications submitting traditional HTML forms directly to the backend:
Blade Template (resources/views/upload.blade.php):
<form action="/upload" method="POST" enctype="multipart/form-data">
@csrf
<input type="file" name="avatar" required>
<button type="submit">Upload File</button>
</form>
Laravel Controller:
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
public function store(Request $request) {
$request->validate(['avatar' => 'required|file|max:10240']);
$result = MediaVault::upload($request, [
'field_name' => 'avatar',
'folder_name' => 'avatars',
'convert_to' => 'webp',
]);
return back()->with('success', 'File uploaded: ' . $result->url);
}
Option B: Resumable Chunked Uploads (Blade + JS Client)
For web applications requiring client-side progress bars and auto-resumption across page refreshes or network drops:
Blade Template (resources/views/resumable-upload.blade.php):
<link rel="stylesheet" href="{{ asset('vendor/media-vault/media-vault.css') }}">
<!-- Auto-rendered container for interrupted upload prompts -->
<div id="afu-resume-container"></div>
<input type="file" id="afu-fileInput" multiple>
<button onclick="startUpload()">Start Upload</button>
<script src="{{ asset('vendor/media-vault/media-vault.js') }}"></script>
<script>
function startUpload() {
window.afuUploadFile({
inputId: 'afu-fileInput',
uploadUrl: '/media-vault/upload',
onProgress: function(percent, chunk, total, file) {
console.log(`Progress (${file.name}): ${percent}% (Chunk ${chunk}/${total})`);
},
onSuccess: function(file, sessionId) {
console.log(`Upload completed for ${file.name}`);
},
onError: function(err, file) {
console.error(`Upload error: ${err.message}`);
}
});
}
</script>
Frontend JS Client Capabilities (media-vault.js)
- Per-File LocalStorage Fingerprint: Each file is uniquely hashed by
afu_upload_${file.name}_${file.size}_${file.lastModified}. - Incremental Persistence: Updates
localStorageafter every successful chunk and purges state when complete. - Concurrent Multi-File Transfers: Supports uploading multiple files in parallel (
afuUploadFiles([file1, file2])). - Auto-Detection on Page Load: Automatically queries the backend for pending sessions on
DOMContentLoadedand renders a Resume Card to re-send missing chunks only.
Option C: Mobile Applications & REST APIs (Flutter, React Native, Swift, Kotlin)
For mobile applications or single-page applications (SPAs) connecting via REST endpoints:
1. Send Chunks Sequentially
Upload binary chunk data to POST /media-vault/upload (or your custom API endpoint):
POST /media-vault/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----FormBoundary
------FormBoundary
Content-Disposition: form-data; name="file"; filename="blob"
Content-Type: application/octet-stream
<Binary Chunk Data>
------FormBoundary
Content-Disposition: form-data; name="chunkNumber"
1
------FormBoundary
Content-Disposition: form-data; name="totalChunks"
5
------FormBoundary
Content-Disposition: form-data; name="originalName"
video.mp4
------FormBoundary--
Response (First Chunk):
{
"status": true,
"sessionId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"done": 20,
"missing": [1, 2, 3, 4]
}
Mobile App stores sessionId in local storage (SharedPreferences / AsyncStorage / Hive).
2. Query Session Ground Truth on Re-Open / Resume
If the mobile app crashes or network disconnects, query the session status to get the exact missing chunk indices:
GET /media-vault/sessions/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/status HTTP/1.1
Accept: application/json
Response:
{
"status": true,
"session_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"original_name": "video.mp4",
"total_chunks": 5,
"uploaded_count": 2,
"progress_percentage": 40,
"missing_chunks": [2, 3, 4],
"is_complete": false,
"expires_at": "2026-08-10T12:00:00Z"
}
3. Send Missing Chunks & Complete Assembly
The mobile app sends only the indices listed in missing_chunks along with the sessionId. When the final chunk arrives, the server returns the completed UploadResult JSON object.
Database Storage Strategies: Central Table vs Model Columns
Laravel Media Vault provides two distinct architectural approaches for linking uploaded media to your Eloquent models. Choose the strategy that fits your schema requirements:
| Feature / Aspect | Central Table (HasUploads) |
Separate Model Columns (HasMediaFields) |
|---|---|---|
| Database Storage | Central file_uploads table (Polymorphic morphMany). |
Direct columns on the model's own table (e.g. products.cover_image). |
| Required Migration | Package default file_uploads table (php artisan migrate). |
Columns on your model's table (string for single, json for multiple). |
| Best Used For | Dynamic attachments, user avatars, documents, soft-deleted media tracking. | Standard entity attributes (e.g., product cover, category banner, brand logo). |
| Dashboard & Scanner | Tracked automatically in Media Library dashboard. | Auto-discovered via php artisan media-vault:discover-models. |
| Thumbnails & Fallbacks | Full metadata support with automatic size fallback. | Dynamic filename suffix resolution with original image fallback. |
Strategy 1: Central Database Tracking (HasUploads)
Use HasUploads when you want files stored as polymorphic relationships in the central file_uploads database table.
1. Migration Setup:
Ensure package migrations are executed (database.enabled = true in config):
php artisan migrate
2. Model Definition:
use Illuminate\Database\Eloquent\Model;
use MohamedSamy902\LaravelMediaVault\Traits\HasUploads;
class User extends Model {
use HasUploads;
}
3. Uploading & Association:
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
// Upload file
$result = MediaVault::upload($request, ['field_name' => 'avatar']);
// Link to model polymorphically
$user = User::find(1);
$user->uploads()->create([
'path' => $result->path,
'disk' => $result->disk,
'original_name' => $result->original_name,
'mime_type' => $result->mime_type,
'size' => $result->size,
'type' => 'image',
'metadata' => ['thumbnails' => $result->toArray()['thumbnail_urls'] ?? []],
]);
4. Retrieving URLs & Thumbnails:
// Retrieve latest original image URL
$originalUrl = $user->getMediaUrl('image', 'original');
// Retrieve specific thumbnail size (e.g., 'small', 'medium', 'large')
$mediumUrl = $user->getMediaUrl('image', 'medium');
// Retrieve map of all available thumbnails: ['small' => '...', 'medium' => '...']
$allThumbnails = $user->getThumbnails();
Strategy 2: Separate Model Columns (HasMediaFields)
Use HasMediaFields when you want file paths stored directly inside columns of your model's database table, avoiding extra rows in the central file_uploads table while preserving dashboard scanning and URL resolution.
1. Migration Setup:
Add string or json columns to your model's database 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::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('cover_image')->nullable(); // Single file path (string)
$table->json('gallery')->nullable(); // Multiple file paths (JSON array)
$table->timestamps();
});
}
};
2. Model Definition:
use Illuminate\Database\Eloquent\Model;
use MohamedSamy902\LaravelMediaVault\Traits\HasMediaFields;
class Product extends Model {
use HasMediaFields;
// Declare columns and their storage disks
protected array $mediaFields = [
'cover_image' => ['multiple' => false, 'disk' => 'public'],
'gallery' => ['multiple' => true, 'disk' => 's3'],
];
}
Run php artisan media-vault:discover-models to index custom fields for the dashboard scanner.
3. Uploading & Saving:
// Upload single file
$coverResult = MediaVault::upload($request, ['field_name' => 'cover_image']);
// Save path directly to model column
$product = Product::find(1);
$product->update([
'cover_image' => $coverResult->path,
]);
// Upload multiple gallery files
$galleryResults = MediaVault::upload($request, ['field_name' => 'gallery']);
$paths = array_map(fn($item) => $item->path, $galleryResults);
$product->update([
'gallery' => $paths, // Auto-serialized as JSON
]);
4. Retrieving URLs & Thumbnails:
// Retrieve original image URL from column
$coverUrl = $product->getMediaUrl('cover_image');
// Retrieve specific thumbnail size from column file
$smallCoverUrl = $product->getMediaUrl('cover_image', 'small');
// Retrieve URL of first image from JSON gallery array
$galleryFirstUrl = $product->getMediaUrl('gallery');
Image Resolution & Thumbnail Behavior (Enabled vs Disabled)
The getMediaUrl($fieldOrType, $size) method is fail-safe and works seamlessly whether thumbnails are enabled or disabled in config/media-vault.php:
| Configuration State | Requested Size Parameter | Returned URL & Behavior |
|---|---|---|
thumbnails.enabled = true |
'original' |
Returns the full-resolution original file URL. |
thumbnails.enabled = true |
'small', 'medium', 'large' |
Returns the requested generated thumbnail URL. |
thumbnails.enabled = true |
'invalid_size' (Non-existent size) |
Safe Fallback: Automatically returns the original file URL (prevents broken images). |
thumbnails.enabled = false |
Any size ('small', 'medium', or 'original') |
Safe Fallback: Always returns the original file URL directly without errors. |
// Example: Safe URL Resolution in Blade Views
<img src="{{ $user->getMediaUrl('image', 'small') }}" alt="User Avatar">
<!-- If thumbnails.enabled = true => Outputs: https://cdn.site.com/uploads/thumbs/avatar_small.webp -->
<!-- If thumbnails.enabled = false => Outputs: https://cdn.site.com/uploads/avatar.webp (Original) -->
Deleting Files
use MohamedSamy902\LaravelMediaVault\Facades\MediaVault;
// Delete by Database ID
$result = MediaVault::delete(15);
// Delete by Path
$result = MediaVault::delete('uploads/default/image.webp');
// Bulk Delete by Array of IDs or Paths
$results = MediaVault::delete([15, 16, 'uploads/default/test.png']);
Bulk Deletion Guard
Available in the BulkDeletionGuard utility service. Always preview a massive deletion action and generate a secure token to proceed.
use MohamedSamy902\LaravelMediaVault\Security\BulkDeletionGuard;
use MohamedSamy902\LaravelMediaVault\Contracts\FileRepositoryContract;
$guard = new BulkDeletionGuard(app(FileRepositoryContract::class));
// Step 1: Preview and obtain a token
$preview = $guard->preview($paths, 'api');
// returns: ['token' => '...', 'count' => 150, 'requires_extra_warning' => true, 'sample' => [...]]
// Step 2: Execute using the token
$result = $guard->execute($preview['token'], forceHardDelete: true);
7. Response Format
All successful uploads return an UploadResult object. It seamlessly implements ArrayAccess and JsonSerializable.
{
"status": true,
"id": 142,
"path": "uploads/avatars/4f1a2...webp",
"url": "https://cdn.example.com/uploads/avatars/4f1a2...webp",
"disk": "s3",
"original_name": "profile_pic.jpg",
"mime_type": "image/webp",
"size": 1048576,
"thumbnail_urls": {
"small": "https://cdn.example.com/uploads/avatars/thumbs/4f1a2..._small.webp",
"medium": "https://cdn.example.com/uploads/avatars/thumbs/4f1a2..._medium.webp"
}
}
Failed items in a batch upload will return an array:
{
"status": false,
"error": "The uploaded file is invalid or missing.",
"original_name": "bad_file.exe"
}
8. Database Model
If database.enabled is true, all uploads go to the file_uploads table. The FileUpload model includes powerful query scopes:
use MohamedSamy902\LaravelMediaVault\Models\FileUpload;
$images = FileUpload::images()->get();
$videos = FileUpload::videos()->forUser(auth()->id())->get();
$orphans = FileUpload::unused()->get();
// File properties
$file->url; // Resolves CDN automatically
$file->human_size; // "1.5 MB"
$file->existsOnDisk(); // bool
$file->owner_exists; // Safely checks if the polymorphic owner model still exists
9. Dashboard & Media Manager UI
The package provides a built-in zero-dependency Single Page Application (SPA) dashboard to manage files, view sessions, configure settings, and scan for orphaned files.
- Frontend Tech Stack: Built with pure Vanilla JS PJAX SPA architecture, custom CSS Design Tokens, FontAwesome 6 icons, and SweetAlert2 notifications. Requires zero node_modules or Vite/Mix compilation.
- Route:
your-app.com/media-vault(Changeable viaui.route_prefix). - Security Warning: You must attach the
authmiddleware (or a custom admin middleware) inconfig/media-vault.phpunderui.middleware. Without this, your entire media library is public. - Capabilities: Real-time search/filter, orphan disk scanner, token-based bulk forced deletions, session manager, and storage quota statistics.
10. Cloud Storage Setup
To use Amazon S3 or Google Cloud Storage, you must require their Flysystem adapters. The package will intelligently throw a clear exception if they are missing.
- S3:
composer require league/flysystem-aws-s3-v3:^3.0 - GCS:
composer require spatie/laravel-google-cloud-storage:^2.0
11. Console Commands
The package registers several utilities to simplify file management:
php artisan media-vault:discover-models— Scans theapp/Modelsdirectory forHasMediaFieldsusage and caches them.php artisan media-vault:regenerate-thumbnails {--force}— Iterates over thefile_uploadstable and regenerates missing sizes based on config.php artisan media-vault:import-orphans {disk?}— Scans a physical disk and imports untracked files into thefile_uploadstable to prevent them from being considered orphans.php artisan media-vault:prune-unused {--days=30} {--force} {--dry-run}— Irreversibly deletes files that have been marked as unused forNdays.php artisan media-vault:prune-sessions— Removes expiredUploadSessionrecords and their physical incomplete temporary chunks.
12. ⚠️ Danger Zone / Known Limitations
[!CAUTION] Pay strict attention to these operational warnings to prevent data loss in a production environment:
- Thumbnails Backfill Limitation: The command
media-vault:regenerate-thumbnailsworks flawlessly for the centralfile_uploadsrepository. However, it does NOT support Custom Models (Multi-Source) currently. Enablingthumbnails.enabledretroactively will not backfill thumbnails for custom models. - Watermarks are Destructive: Enabling
watermark.enabledprints the watermark directly onto the original image. There is no backfill, and there is no way to remove a watermark once applied. - Removing Custom Models = Data Loss Risk: If you remove a model from
media_modelsin the config after files have been uploaded, the scanner will immediately classify its files as "Orphaned". Running the Orphan Deletion will permanently wipe them. Always migrate or empty tables before changing configuration topology. - Custom Sources Pagination: To prevent Server Crashes (OOM) on huge DB tables, the Dashboard pagination for Custom Sources operates on Database Rows, not individual files. A JSON array of 5 images counts as 1 row in pagination.
- Caching: If you register a new model or trait, you must run
php artisan config:clearso the Dashboard recognizes it.
13. Testing
The package includes a comprehensive test suite. We specifically isolate heavy tests (Benchmarks) to prevent CI timeouts.
Run Unit & Feature Tests:
composer test
# or
vendor/bin/phpunit
Run Memory & Performance Benchmarks:
vendor/bin/phpunit --testsuite=Benchmarks
(Current Test Count: 177 Passing/Skipped Tests).
14. Advanced Security & Performance Features
🛡️ Virus Scanning (ClamAV Integration)
The package includes built-in automated virus and malware scanning via ClamAV before any file is saved to storage.
- CLI Executable: Executes via local
clamscanbinary path configured insecurity.virus_scan.path. - EICAR Detection: Industry-standard test signature
EICAR-STANDARD-ANTIVIRUS-TEST-FILEis detected out-of-the-box in development and production environments.
// config/media-vault.php
'security' => [
'virus_scan' => [
'enabled' => env('FILE_UPLOAD_VIRUS_SCAN', false),
'driver' => 'clamav',
'path' => env('CLAMSCAN_PATH', '/usr/bin/clamscan'),
],
],
⚡ File & Image Compression
Pipeline-integrated file compression reduces storage footprint for non-image document formats.
- Scope: Document formats (
pdf,doc,docx,xls,xlsx,ppt,pptx). - Execution: Runs directly inside the
StorageManagerwrite pipeline using quality parameters based oncompression.quality.
// config/media-vault.php
'compression' => [
'enabled' => env('FILE_UPLOAD_COMPRESSION_ENABLED', false),
'quality' => env('FILE_UPLOAD_COMPRESSION_QUALITY', 80), // 1 - 100 quality percentage
'types' => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'],
],
⏱️ Dual-Layer Rate Limiting
The package distinguishes between two distinct rate limiting safeguards:
-
Package API Route Limiting (
throttle:media-vault-api): Protects your application's endpoints (POST /media-vault/upload,POST /media/bulk-destroy, etc.) from abuse and denial-of-service attacks.// config/media-vault.php 'security' => [ 'rate_limit' => [ 'enabled' => env('FILE_UPLOAD_RATE_LIMIT_ENABLED', true), 'max_uploads' => 60, 'per_minutes' => 1, ], ],When exceeded, endpoints return
HTTP 429 Too Many Requestswith{ "status": false, "message": "Too Many Requests. Rate limit exceeded for file operations." }. -
External Remote URL Throttling (
UrlDownloader): HandlesHTTP 429status responses gracefully when downloading assets from remote third-party CDNs during URL uploads, failing fast with informative error messages.
15. Security
If you discover any security-related issues (such as bypasses for SSRF, traversal attacks, or token leaks in the Bulk Deletion Guard), please email mohamedsamy902@gmail.com directly instead of opening a public issue. Alternatively, you can use GitHub Security Advisories if enabled on the repository.
16. Contributing
- Fork the repository.
- Create your feature branch (
git checkout -b feature/amazing-feature). - Commit your changes (
git commit -m 'Add some amazing feature'). - Ensure all tests pass (
composer test). - Push to the branch (
git push origin feature/amazing-feature). - Open a Pull Request.
17. Changelog
Please see the CHANGELOG.md for more information on what has changed recently.
18. License
The MIT License (MIT). Please see License File for more information.