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

0.0.3 2025-10-13 18:19 UTC

This package is auto-updated.

Last update: 2025-10-13 18:32:32 UTC


README

Tests Latest Version License Total Downloads

A comprehensive media management package for Laravel Nova with advanced chunked upload support for handling large files efficiently.

Table of Contents

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

  1. Validate User Input - Always validate and sanitize user-provided data
  2. Restrict File Types - Only allow necessary MIME types
  3. Set Size Limits - Configure appropriate file size limits
  4. Use HTTPS - Always serve media over HTTPS in production
  5. Regular Updates - Keep the package and dependencies up to date
  6. 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:

  1. Fork the repository

    git clone https://github.com/iamgerwin/nova-media-hub.git
    cd nova-media-hub
  2. Create a feature branch

    git checkout -b feature/your-feature-name
  3. Install dependencies

    composer install
    npm install
  4. Make your changes

    • Write tests for new features
    • Follow PSR-12 coding standards
    • Update documentation as needed
  5. Run tests

    composer test
    ./vendor/bin/pint
  6. Commit your changes

    git add .
    git commit -m "Add feature: your feature description"
  7. Push to your fork

    git push origin feature/your-feature-name
  8. 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:

Submit an Issue

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

  1. Check if the issue already exists
  2. Update to the latest version
  3. Check the documentation
  4. 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

License

The MIT License (MIT). Please see License File for more information.

Links