A comprehensive media management package for Laravel Nova with advanced chunked upload support for handling large files efficiently.
- Features
- Requirements
- Package Structure
- Installation
- Configuration
- Usage
- Advanced Use Cases
- Edge Cases
- Testing
- Security
- Maintenance
- Contributing
- Issues & Support
- Credits
- License
- 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)
- 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
- 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
| Component | Version |
|---|---|
| PHP | 8.2, 8.3+ |
| Laravel | 10.x, 11.x, 12.x |
| Laravel Nova | 4.x, 5.x |
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
Install the package via Composer:
composer require iamgerwin/nova-media-hubRun the migrations to create the media library table:
php artisan migratePublish 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(),
];
}The package configuration file is located at config/nova-media-hub.php. Key configuration options include:
'disk' => env('MEDIA_HUB_DISK', 'public'),
'path' => env('MEDIA_HUB_PATH', 'media'),'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_conversions' => [
'thumbnail' => [
'width' => 150,
'height' => 150,
'fit' => 'crop',
],
'medium' => [
'width' => 800,
'height' => 600,
'fit' => 'contain',
],
],
'image_optimizer' => [
'enabled' => true,
'quality' => 85,
],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'),
];
}
}MediaHubField::make('Featured Image', 'featured_image')
->defaultCollection('featured')
->rules('required');MediaHubField::make('Gallery', 'gallery_images')
->multiple()
->defaultCollection('galleries')
->max(10);MediaHubField::make('Product Images', 'images')
->multiple()
->defaultCollection('products')
->filterCollection('products'); // Only show media from this collectionMediaHubField::make('Images', 'images')
->hideFromIndex();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,
];
}{{-- 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 }}">// 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();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();Add custom metadata fields to the Media Hub:
// app/Providers/NovaServiceProvider.php
MediaHub::make()
->withCustomFields([
'copyright' => __('Copyright'),
'photographer' => __('Photographer'),
'license' => __('License'),
]);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',
]);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();// 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',
],
],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=mediaFor 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.
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
],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);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 sizeIf 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);
}
}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'],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, // 50MBThe package includes a comprehensive test suite to ensure stability and reliability.
# 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 --verbosetests/
├── TestCase.php # Base test case
└── Unit/
└── PackageTest.php # Package loading 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);
}
}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
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.
The package implements several security measures:
- 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- 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
- 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');
}- 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
None reported as of the latest release (0.0.3).
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=24Add 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 * * *');
}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/chunksContributions 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
# Install dependencies
composer install
# Run tests
composer test
# Run tests with coverage
composer test-coverage
# Format code
./vendor/bin/pint
# Serve development environment
composer serveFound 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
- Check if the issue already exists
- Update to the latest version
- Check the documentation
- Review closed issues for similar problems
If you discover a security vulnerability, please email iamgerwin@live.com instead of using the issue tracker.
- Author: John Gerwin De las Alas
- Contributors: All Contributors
The MIT License (MIT). Please see License File for more information.
Links