iamgerwin / nova-media-hub
Nova Media Hub with chunked upload support - Multi-version compatible (PHP 8.2-8.3, Laravel 10-12, Nova 4-5)
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/iamgerwin/nova-media-hub
Requires
- php: ^8.2 || ^8.3
- laravel/framework: ^10.0 || ^11.0 || ^12.0
- laravel/nova: ^4.0 || ^5.0
- outl1ne/nova-translatable: ^2.0 || ^3.0
- outl1ne/nova-translations-loader: ^4.0 || ^5.0
- spatie/image: ^3.0
- spatie/image-optimizer: ^1.7
Requires (Dev)
- laravel/nova-devtool: ^1.8
- laravel/pint: ^1.25
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0
- phpunit/phpunit: ^10.0 || ^11.0
README
A comprehensive media management package for Laravel Nova with advanced chunked upload support for handling large files efficiently.
Table of Contents
- Features
- Requirements
- Package Structure
- Installation
- Configuration
- Usage
- Advanced Use Cases
- Edge Cases
- Testing
- Security
- Maintenance
- Contributing
- Issues & Support
- Credits
- License
Features
Core Features
- Media Hub Interface - Dedicated Nova tool for managing media assets
- Media Hub Field - Custom field for selecting single or multiple media items
- Image Optimization - Automatic optimization and multiple format conversions
- Collections - Organize media into logical collections
- Dark Mode Support - Full support for Nova's dark mode
- Localization - Multi-language support with translation loader
- Custom Fields - Extensible metadata fields (e.g., copyright, alt text)
Chunked Upload System
- Large File Support - Upload files up to 1GB+ without server configuration changes
- Real-time Progress - Visual progress tracking with percentage display
- Automatic Retry - Intelligent retry logic for failed chunk uploads
- Smart Fallback - Automatic fallback to standard upload for small files
- Memory Efficient - Streaming-based chunk combination prevents memory exhaustion
- Session Management - Cache-based upload state persistence
- Configurable - Adjustable chunk sizes, retry attempts, and thresholds
Developer Features
- Multi-version Compatibility - Supports PHP 8.2-8.3, Laravel 10-12, Nova 4-5
- Extensive Testing - Comprehensive test suite with CI/CD pipeline
- Type Safety - Full type hints and strict type checking
- Clean API - Intuitive, well-documented API
- Extensible - Hooks and events for customization
Requirements
| Component | Version |
|---|---|
| PHP | 8.2, 8.3+ |
| Laravel | 10.x, 11.x, 12.x |
| Laravel Nova | 4.x, 5.x |
Package Structure
nova-media-hub/
├── .github/
│ └── workflows/
│ └── tests.yml # CI/CD pipeline
├── config/
│ └── nova-media-hub.php # Main configuration file
├── database/
│ └── migrations/
│ ├── 2022_06_15_000000_create_media_library_table.php
│ └── 2024_01_29_000000_add_nova_media_hub_indexes.php
├── dist/ # Compiled assets
│ ├── css/
│ │ └── entry.css
│ ├── js/
│ │ ├── entry.js
│ │ └── entry.js.LICENSE.txt
│ └── mix-manifest.json
├── docs/ # Documentation images
│ ├── choose-media-dark.jpeg
│ └── media-hub-dark.jpeg
├── lang/ # Translation files
│ ├── en.json
│ ├── fa.json
│ └── it.json
├── resources/
│ ├── css/
│ │ └── entry.css # Source styles
│ └── js/
│ ├── api.js # API client
│ ├── entry.js # Main entry point
│ ├── components/ # Vue components
│ │ ├── ChunkedFileUpload.vue
│ │ ├── DropZone.vue
│ │ ├── MediaItem.vue
│ │ ├── MediaItemContextMenu.vue
│ │ ├── MediaOrderSelect.vue
│ │ ├── MediaViewModalInfoListItem.vue
│ │ ├── ModalFilterItem.vue
│ │ └── PaginationLinks.vue
│ ├── composables/ # Vue composables
│ │ └── useDragAndDrop.js
│ ├── fields/ # Nova field components
│ │ └── MediaField/
│ │ ├── DetailMediaHubField.vue
│ │ ├── FormMediaHubField.vue
│ │ └── IndexMediaHubField.vue
│ ├── icons/ # SVG icon components
│ │ ├── AudioIcon.vue
│ │ ├── CheckMarkIcon.vue
│ │ ├── OtherIcon.vue
│ │ └── VideoIcon.vue
│ ├── mixins/ # Vue mixins
│ │ ├── HandlesMediaHubFieldValue.js
│ │ ├── HandlesMediaLists.js
│ │ └── HandlesMediaUpload.js
│ ├── modals/ # Modal components
│ │ ├── ChooseMediaModal.vue
│ │ ├── ConfirmDeleteModal.vue
│ │ ├── MediaReplaceModal.vue
│ │ ├── MediaUploadModal.vue
│ │ ├── MediaViewModal.vue
│ │ └── MoveToCollectionModal.vue
│ ├── utils/ # Utility classes
│ │ └── ChunkedUploader.js # Chunked upload handler
│ └── views/ # Main views
│ └── NovaMediaHub.vue
├── routes/
│ └── api.php # API routes
├── src/
│ ├── Casts/
│ │ └── MediaCast.php # Eloquent cast
│ ├── Console/
│ │ └── Commands/
│ │ └── CleanupOldChunksCommand.php # Cleanup command
│ ├── Exceptions/ # Custom exceptions
│ │ ├── DiskDoesNotExistException.php
│ │ ├── FileDoesNotExistException.php
│ │ ├── FileTooLargeException.php
│ │ ├── FileValidationException.php
│ │ ├── MimeTypeNotAllowedException.php
│ │ ├── NoFileProvidedException.php
│ │ └── UnknownFileTypeException.php
│ ├── Filters/ # Nova filters
│ │ ├── Collection.php
│ │ ├── Search.php
│ │ └── Sort.php
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── ChunkedMediaUploadController.php
│ │ │ └── MediaHubController.php
│ │ └── Middleware/
│ │ └── Authorize.php
│ ├── Jobs/
│ │ └── MediaHubOptimizeAndConvertJob.php # Image optimization job
│ ├── MediaHandler/
│ │ ├── FileHandler.php # Main file handler
│ │ └── Support/
│ │ ├── Base64File.php
│ │ ├── DatePathMaker.php
│ │ ├── FileHelpers.php
│ │ ├── FileNamer.php
│ │ ├── FileValidator.php
│ │ ├── Filesystem.php
│ │ ├── MediaManipulator.php
│ │ ├── MediaOptimizer.php
│ │ ├── PathMaker.php
│ │ ├── RemoteFile.php
│ │ └── Traits/
│ │ └── PathMakerHelpers.php
│ ├── Models/
│ │ └── Media.php # Media Eloquent model
│ ├── Nova/
│ │ ├── Fields/
│ │ │ └── MediaHubField.php # Nova field
│ │ └── Resources/
│ │ └── Media.php # Nova resource
│ ├── MediaHub.php # Nova tool class
│ └── MediaHubServiceProvider.php # Service provider
├── tests/ # Test suite
│ ├── TestCase.php
│ └── Unit/
│ └── PackageTest.php
├── workbench/ # Development workbench
│ ├── app/
│ ├── bootstrap/
│ ├── database/
│ ├── resources/
│ └── routes/
├── .editorconfig # Editor configuration
├── .gitignore # Git ignore rules
├── .prettierrc # Prettier configuration
├── CHANGELOG.md # Version history
├── composer.json # PHP dependencies
├── LICENSE.md # MIT license
├── package.json # NPM dependencies
├── phpunit.xml # PHPUnit configuration
├── README.md # This file
├── tailwind.config.js # Tailwind CSS config
├── testbench.yaml # Testbench configuration
└── webpack.mix.js # Laravel Mix config
Installation
Install the package via Composer:
composer require iamgerwin/nova-media-hub
Run the migrations to create the media library table:
php artisan migrate
Publish the configuration file (optional):
php artisan vendor:publish --provider="Iamgerwin\NovaMediaHub\MediaHubServiceProvider" --tag="config"
Publish translations (optional):
php artisan vendor:publish --provider="Iamgerwin\NovaMediaHub\MediaHubServiceProvider" --tag="translations"
Register the tool in your NovaServiceProvider:
// app/Providers/NovaServiceProvider.php use Iamgerwin\NovaMediaHub\MediaHub; public function tools() { return [ MediaHub::make(), ]; }
Configuration
The package configuration file is located at config/nova-media-hub.php. Key configuration options include:
Storage Configuration
'disk' => env('MEDIA_HUB_DISK', 'public'), 'path' => env('MEDIA_HUB_PATH', 'media'),
Chunked Upload Configuration
'chunked_upload' => [ // Enable/disable chunked upload feature 'enabled' => true, // Size of each chunk (adjust based on server limits) 'chunk_size' => 5 * 1024 * 1024, // 5MB // Maximum file size for uploads 'max_file_size' => 1024 * 1024 * 1024, // 1GB // Upload session lifetime 'session_lifetime_hours' => 24, // Retry attempts for failed chunks 'retry_attempts' => 3, // Auto-enable chunked upload for files larger than this 'auto_threshold' => 10 * 1024 * 1024, // 10MB // Cleanup old chunks after this many hours 'cleanup_old_chunks_after_hours' => 48, ],
Image Optimization
'image_conversions' => [ 'thumbnail' => [ 'width' => 150, 'height' => 150, 'fit' => 'crop', ], 'medium' => [ 'width' => 800, 'height' => 600, 'fit' => 'contain', ], ], 'image_optimizer' => [ 'enabled' => true, 'quality' => 85, ],
Usage
Basic Usage
Add the MediaHubField to your Nova resource:
use Iamgerwin\NovaMediaHub\Nova\Fields\MediaHubField; class Product extends Resource { public function fields(Request $request) { return [ ID::make()->sortable(), Text::make('Name'), MediaHubField::make('Image', 'image'), ]; } }
Field Configuration
Single Media Selection
MediaHubField::make('Featured Image', 'featured_image') ->defaultCollection('featured') ->rules('required');
Multiple Media Selection
MediaHubField::make('Gallery', 'gallery_images') ->multiple() ->defaultCollection('galleries') ->max(10);
Collection-specific Selection
MediaHubField::make('Product Images', 'images') ->multiple() ->defaultCollection('products') ->filterCollection('products'); // Only show media from this collection
Hide from Listing
MediaHubField::make('Images', 'images') ->hideFromIndex();
Model Integration
Using Media Cast
The package provides a custom cast for seamless model integration:
use Iamgerwin\NovaMediaHub\Casts\MediaCast; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $casts = [ 'image' => MediaCast::class, 'gallery_images' => MediaCast::class, ]; }
Accessing Media in Blade
{{-- Single media --}} <img src="{{ $product->image->url }}" alt="{{ $product->image->alt }}"> {{-- Multiple media --}} @foreach($product->gallery_images as $image) <img src="{{ $image->url }}" alt="{{ $image->alt }}"> @endforeach {{-- Specific conversion --}} <img src="{{ $product->image->url('thumbnail') }}" alt="{{ $product->image->alt }}">
Accessing Media in Controllers
// Get media URL $url = $product->image->url; $thumbnailUrl = $product->image->url('thumbnail'); // Get media metadata $filename = $product->image->filename; $mimeType = $product->image->mime_type; $size = $product->image->size; $collection = $product->image->collection; // Check media type $isImage = $product->image->isImage(); $isVideo = $product->image->isVideo(); $isPdf = $product->image->isPdf();
Chunked Upload API
For custom implementations or advanced use cases, you can use the ChunkedUploader JavaScript class:
import { ChunkedUploader } from './utils/ChunkedUploader'; const uploader = new ChunkedUploader({ baseUrl: '/nova-vendor/media-hub/chunked', chunkSize: 5 * 1024 * 1024, // 5MB retryAttempts: 3, onProgress: (chunkIndex, totalChunks, percentage) => { console.log(`Uploading: ${percentage}%`); updateProgressBar(percentage); }, onComplete: (media) => { console.log('Upload complete:', media); displayMedia(media); }, onError: (error) => { console.error('Upload failed:', error); showErrorMessage(error.message); } }); // Start upload const file = document.getElementById('file-input').files[0]; await uploader.upload(file, 'my-collection', { alt: 'My Image' }); // Cancel upload await uploader.cancel();
Advanced Use Cases
Custom Media Fields
Add custom metadata fields to the Media Hub:
// app/Providers/NovaServiceProvider.php MediaHub::make() ->withCustomFields([ 'copyright' => __('Copyright'), 'photographer' => __('Photographer'), 'license' => __('License'), ]);
Programmatic Media Upload
use Iamgerwin\NovaMediaHub\MediaHandler\FileHandler; $fileHandler = app(FileHandler::class); // Upload from file path $media = $fileHandler->fromFile('/path/to/file.jpg', 'products', [ 'alt' => 'Product image', 'copyright' => '© 2025', ]); // Upload from URL $media = $fileHandler->fromUrl('https://example.com/image.jpg', 'external', [ 'alt' => 'External image', ]); // Upload from base64 $media = $fileHandler->fromBase64($base64Data, 'uploads', [ 'alt' => 'Base64 image', ]);
Batch Operations
use Iamgerwin\NovaMediaHub\Models\Media; // Move media to another collection Media::whereCollection('old-collection') ->update(['collection' => 'new-collection']); // Delete unused media Media::whereDoesntHave('models') ->where('created_at', '<', now()->subMonths(6)) ->delete(); // Get media by type $images = Media::where('mime_type', 'like', 'image/%')->get(); $videos = Media::where('mime_type', 'like', 'video/%')->get();
Custom Image Conversions
// config/nova-media-hub.php 'image_conversions' => [ 'thumbnail' => [ 'width' => 150, 'height' => 150, 'fit' => 'crop', ], 'square' => [ 'width' => 500, 'height' => 500, 'fit' => 'crop', ], 'hero' => [ 'width' => 1920, 'height' => 1080, 'fit' => 'contain', ], ],
Queue Image Processing
For better performance, queue image optimization:
// config/nova-media-hub.php 'queue_conversions' => true, 'queue_name' => 'media',
Don't forget to run the queue worker:
php artisan queue:work --queue=media
Edge Cases
Large File Uploads (>100MB)
For very large files, adjust the chunk size and timeout settings:
// config/nova-media-hub.php 'chunked_upload' => [ 'chunk_size' => 10 * 1024 * 1024, // 10MB chunks 'session_lifetime_hours' => 48, // Longer session 'retry_attempts' => 5, // More retry attempts ],
Also, ensure your server has adequate disk space for temporary chunks.
Slow Network Connections
The chunked upload system handles slow connections gracefully with automatic retries. Configure retry behavior:
'chunked_upload' => [ 'retry_attempts' => 5, 'retry_delay' => 2000, // milliseconds between retries ],
Concurrent Uploads
The package supports concurrent uploads using UUID-based session management. Each upload maintains its own isolated state.
// Upload multiple files concurrently const uploaders = files.map(file => { const uploader = new ChunkedUploader(config); return uploader.upload(file, 'collection'); }); await Promise.all(uploaders);
Memory-Constrained Environments
The package uses streaming for chunk combination to minimize memory usage:
// Chunks are combined using stream_copy_to_stream() // Memory usage stays constant regardless of file size
Interrupted Uploads
If an upload is interrupted (browser closed, connection lost), you can resume:
// Check for existing upload session const uploadId = localStorage.getItem('upload-session-id'); if (uploadId) { const status = await fetch(`/chunked/status/${uploadId}`).then(r => r.json()); if (status.chunks_uploaded < status.total_chunks) { // Resume from last uploaded chunk uploader.resume(uploadId, status.chunks_uploaded); } }
Cross-Domain Uploads
When uploading from a different domain, ensure CORS is properly configured:
// config/cors.php 'paths' => ['nova-vendor/media-hub/*'], 'allowed_methods' => ['POST', 'GET', 'OPTIONS'], 'allowed_origins' => ['https://your-domain.com'], 'allowed_headers' => ['Content-Type', 'X-Requested-With', 'X-CSRF-TOKEN'],
File Type Restrictions
Restrict allowed file types:
// config/nova-media-hub.php 'allowed_mime_types' => [ 'image/jpeg', 'image/png', 'image/webp', 'image/svg+xml', 'video/mp4', 'video/webm', 'application/pdf', ], 'max_file_size' => 50 * 1024 * 1024, // 50MB
Testing
The package includes a comprehensive test suite to ensure stability and reliability.
Running Tests
# Run all tests composer test # Run tests with coverage composer test-coverage # Run specific test file ./vendor/bin/phpunit tests/Unit/PackageTest.php # Run tests in verbose mode ./vendor/bin/phpunit --verbose
Test Structure
tests/
├── TestCase.php # Base test case
└── Unit/
└── PackageTest.php # Package loading tests
Writing Tests
When contributing, please include tests for new features:
namespace Iamgerwin\NovaMediaHub\Tests\Unit; use Iamgerwin\NovaMediaHub\Tests\TestCase; class YourFeatureTest extends TestCase { /** @test */ public function it_does_something() { // Arrange $input = 'test'; // Act $result = yourFunction($input); // Assert $this->assertEquals('expected', $result); } }
Continuous Integration
The package uses GitHub Actions for automated testing across multiple PHP and Laravel versions:
- PHP Versions: 8.2, 8.3
- Laravel Versions: 10.x, 11.x
- Test Matrix: 12+ version combinations
View test results: GitHub Actions
Security
Security Vulnerabilities
If you discover a security vulnerability within Nova Media Hub, please send an email to iamgerwin@live.com. All security vulnerabilities will be promptly addressed.
Please do not create public GitHub issues for security vulnerabilities.
Security Features
The package implements several security measures:
File Upload Security
- MIME Type Validation - Validates file types before upload
- File Size Limits - Enforces maximum file size restrictions
- Filename Sanitization - Removes dangerous characters from filenames
- Extension Whitelisting - Only allows specified file extensions
// config/nova-media-hub.php 'allowed_mime_types' => [ 'image/jpeg', 'image/png', 'image/webp', 'video/mp4', ], 'max_file_size' => 50 * 1024 * 1024, // 50MB
Chunked Upload Security
- UUID Session IDs - Prevents session guessing attacks
- CSRF Protection - All endpoints require valid CSRF tokens
- Chunk Index Validation - Prevents malicious chunk injection
- File Hash Verification - Validates file integrity after assembly
- Temporary File Isolation - Chunks stored in isolated directories
Authorization
- Nova Authorization - Respects Nova's authorization policies
- Middleware Protection - All routes protected by authentication middleware
- Permission Checks - User permissions verified before operations
// Define authorization in Nova resource public static function authorizedToCreate(Request $request) { return $request->user()->can('create-media'); }
Best Practices
- Validate User Input - Always validate and sanitize user-provided data
- Restrict File Types - Only allow necessary MIME types
- Set Size Limits - Configure appropriate file size limits
- Use HTTPS - Always serve media over HTTPS in production
- Regular Updates - Keep the package and dependencies up to date
- Monitor Uploads - Log and monitor upload activity for suspicious patterns
Reported Vulnerabilities
None reported as of the latest release (0.0.3).
Maintenance
Cleanup Command
The package includes a cleanup command to remove old temporary chunk files:
# Run cleanup manually php artisan media-hub:cleanup-chunks # Dry run (see what would be deleted) php artisan media-hub:cleanup-chunks --dry-run # Custom time threshold (default: 48 hours) php artisan media-hub:cleanup-chunks --hours=24
Scheduled Cleanup
Add the cleanup command to your scheduler in app/Console/Kernel.php:
protected function schedule(Schedule $schedule) { // Clean up chunks older than 48 hours, daily at 3am $schedule->command('media-hub:cleanup-chunks')->dailyAt('03:00'); // Or use cron expression $schedule->command('media-hub:cleanup-chunks')->cron('0 3 * * *'); }
Monitoring Disk Usage
Monitor storage disk usage to prevent issues:
# Check disk usage df -h # Check media directory size du -sh storage/app/public/media # Check chunks directory size du -sh storage/app/chunks
Contributing
Contributions are welcome! Please follow these guidelines:
-
Fork the repository
git clone https://github.com/iamgerwin/nova-media-hub.git cd nova-media-hub -
Create a feature branch
git checkout -b feature/your-feature-name
-
Install dependencies
composer install npm install
-
Make your changes
- Write tests for new features
- Follow PSR-12 coding standards
- Update documentation as needed
-
Run tests
composer test ./vendor/bin/pint -
Commit your changes
git add . git commit -m "Add feature: your feature description"
-
Push to your fork
git push origin feature/your-feature-name
-
Create a Pull Request
- Provide a clear description of the changes
- Reference any related issues
- Ensure CI tests pass
Development Setup
# Install dependencies composer install # Run tests composer test # Run tests with coverage composer test-coverage # Format code ./vendor/bin/pint # Serve development environment composer serve
Issues & Support
Found a bug or have a feature request? Please create an issue on GitHub:
When submitting an issue, please include:
- Description - Clear description of the issue or feature request
- Steps to Reproduce - Detailed steps to reproduce the issue (for bugs)
- Expected Behavior - What you expected to happen
- Actual Behavior - What actually happened
- Environment:
- PHP version
- Laravel version
- Nova version
- Package version
- Operating system
- Code Samples - Relevant code snippets or configuration
- Screenshots - If applicable
Before Submitting an Issue
- Check if the issue already exists
- Update to the latest version
- Check the documentation
- Review closed issues for similar problems
Security Vulnerabilities
If you discover a security vulnerability, please email iamgerwin@live.com instead of using the issue tracker.
Credits
- Author: John Gerwin De las Alas
- Contributors: All Contributors
License
The MIT License (MIT). Please see License File for more information.
Links