codprez/laravel-media-library
Laravel Media Library
A media library for Laravel applications built on Inertia.js and React: uploads, resized image variants, any storage disk (local or S3), a media manager screen and a picker you can drop into your own forms.
Features
- Media management — upload (single or bulk), rename, delete, search, filter by type, multi-select and bulk delete.
- Image variants — every uploaded raster image is resized into the variants
you configure (
thumbnail,medium,largeby default) withcover,containorstretchfits, in WebP/AVIF/JPEG/PNG. Powered by GD or Imagick, with no extra dependency. - Any disk — local, S3 or anything Flysystem supports. The disk, the file visibility and a CDN/base URL are configuration, and every record remembers where its file lives.
- Works with remote disks — variants are rendered locally and then uploaded, so an S3 bucket behaves exactly like a local disk.
- Queued variants — generate derivatives during the upload or push them to a queue.
- Backfill command —
php artisan media:variants(re)builds variants for media uploaded before this feature existed. - Video thumbnails — optional single-frame extraction through ffmpeg.
- Google Drive import — paste a public file or folder link, preview the files, import some or all of them in the background.
- Model helpers —
getOptimizedUrl(),getImageVariants(),getSrcset(),temporaryUrl()and a typedvariantspayload. - Inertia + React UI — packaged media manager page, upload modal and selector components.
Requirements
- PHP 8.2+ with the GD extension (or Imagick),
fileinfoandexif - Laravel 12.x | 13.x
- Inertia.js with React for the bundled admin UI
Installation
composer require codprez/laravel-media-library
Publish the configuration and migrations, then migrate and link the storage
directory (needed when files live on the local public disk):
php artisan vendor:publish --tag="media-library-config"
php artisan vendor:publish --tag="media-library-migrations"
php artisan migrate
php artisan storage:link
Configuration
config/media-library.php is optional as a whole: every value has a default, so
a config file published by an older version of the package keeps working.
return [
'disk' => env('MEDIA_LIBRARY_DISK', 'public'),
'visibility' => env('MEDIA_LIBRARY_VISIBILITY', 'public'),
'url' => env('MEDIA_LIBRARY_URL'), // CDN or custom domain
'path_prefix' => env('MEDIA_LIBRARY_PATH_PREFIX', 'media'),
'uploads' => [
'max_size' => 51200, // kilobytes
'mime_types' => ['image/jpeg', 'image/png', /* ... */],
],
'images' => [
'enabled' => true,
'driver' => null, // gd | imagick | null (auto-detect)
'format' => 'webp', // webp | avif | jpeg | png | null
'quality' => 82,
'upscale' => false,
'webp' => false, // extra full-size WebP copy
'variants' => [
'thumbnail' => ['width' => 300, 'height' => 300, 'fit' => 'cover'],
'medium' => ['width' => 800, 'height' => null, 'fit' => 'contain'],
'large' => ['width' => 1600, 'height' => null, 'fit' => 'contain'],
],
],
'queue' => [
'enabled' => false,
'connection' => null,
'name' => null,
],
'video' => [
'thumbnails' => true,
'ffmpeg_path' => env('MEDIA_LIBRARY_FFMPEG_PATH', '/usr/bin/ffmpeg'),
],
'user_model' => \App\Models\User::class,
'google_drive' => [
'api_key' => env('GOOGLE_DRIVE_API_KEY'),
'allowed_mime_types' => ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml'],
],
'routing' => [
'prefix' => 'builder',
'middleware' => ['web', 'auth'],
],
];
Storing on S3 (or any other disk)
MEDIA_LIBRARY_DISK=s3
MEDIA_LIBRARY_VISIBILITY=public
MEDIA_LIBRARY_URL=https://cdn.example.com
Install the Flysystem adapter (composer require league/flysystem-aws-s3-v3) and
configure the disk in config/filesystems.php as usual:
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
],
disk— where uploads, variants and thumbnails are written.visibility—publicfor public buckets and the localpublicdisk, orprivatefor locked-down buckets.url— optional. When set, every media URL (variants included) is rewritten onto it, so moving to a CDN is a config change rather than a migration.- Every record stores the disk it was written to, so changing
media-library.disklater never orphans existing files. - For private buckets,
Media::temporaryUrl(60)returns a signed URL andnullwhen the disk cannot produce one.
Image variants
Uploading an image generates every configured variant next to the original:
media/wb2ZgO3W.jpeg ← the original
media/thumbnail/wb2ZgO3W-thumbnail.webp
media/medium/wb2ZgO3W-medium.webp
media/large/wb2ZgO3W-large.webp
The paths and URLs are stored as JSON in the variants column (which supports
any variant name, not just the built-in three) and mirrored into the legacy
thumbnail_*, medium_*, large_* and webp_* columns.
$media->variants; // ['thumbnail' => ['path' => ..., 'url' => ..., 'width' => 300, 'height' => 300, 'size' => 9123], ...]
$media->variantMap(); // the same payload, typed and filtered
$media->getOptimizedUrl('thumbnail'); // variant URL, or the original file
$media->getImageVariants(); // ['original' => ..., 'thumbnail' => ..., ...]
$media->getSrcset(); // "https://… 300w, https://… 800w, https://… 1600w"
<img src="{{ $media->getOptimizedUrl('large') }}"
srcset="{{ $media->getSrcset() }}"
width="{{ $media->width }}"
height="{{ $media->height }}"
alt="">
Drivers and formats
| Driver | Decodes | Encodes |
|---|---|---|
gd (default, auto-detected first) |
JPEG, PNG, WebP, BMP, AVIF* | JPEG, PNG, WebP, AVIF* |
imagick |
whatever ImageMagick supports | whatever ImageMagick supports |
* when the extension was compiled with that format. If the requested
images.format is unavailable, the driver falls back to the source format and
then to PNG, so an upload never fails because of a missing codec.
Formats that cannot be rasterised safely are stored untouched — SVG (scalable)
and GIF (possibly animated) — and fall back to the original file through
getOptimizedUrl().
Fits
| Fit | Behaviour |
|---|---|
cover |
Crops the source to the target aspect ratio, then scales to exactly width x height. |
contain |
Scales the whole image to fit the box; leave height null to scale by width only. |
stretch |
Uses width x height verbatim, ignoring the aspect ratio. |
With images.upscale set to false (the default) a variant is never larger than
the original. JPEGs are auto-rotated from their EXIF orientation and transparency
is preserved for WebP/PNG/AVIF (and flattened onto white for JPEG).
Regenerating variants
php artisan media:variants # only records that have no variants yet
php artisan media:variants --force # rebuild everything
php artisan media:variants --id=12 --id=15
php artisan media:variants --type=video # re-extract video thumbnails
Per record, from code:
$media->regenerateVariants(); // true when something was generated
or over HTTP: POST /{prefix}/media/{media}/variants.
HTTP API
Routes are registered under the configured routing.prefix (/builder/media by
default) behind routing.middleware (['web', 'auth'] by default). Requests
that expect JSON receive JSON; Inertia requests receive redirects and flash
messages.
| Method | URI | Description |
|---|---|---|
GET |
/{prefix}/media |
Paginated listing (search, type, per_page — capped at 100). Renders the media manager page for Inertia. |
POST |
/{prefix}/media |
Upload one (file) or many (files[]) files. Answers 201 with media, media_items and count. |
GET |
/{prefix}/media/{media} |
A single record. |
PUT |
/{prefix}/media/{media} |
Rename (name). |
POST |
/{prefix}/media/{media}/variants |
Regenerate the variants of one record. |
DELETE |
/{prefix}/media/{media} |
Delete one record and its files. |
DELETE |
/{prefix}/media |
Bulk delete (ids[]). |
curl -X POST https://example.com/builder/media \
-H "Accept: application/json" \
-F "file=@photo.jpg"
{
"success": true,
"count": 1,
"media": {
"id": 1,
"disk": "s3",
"url": "https://cdn.example.com/media/wb2ZgO3W.jpg",
"is_image": true,
"width": 1600,
"height": 900,
"variants": {
"thumbnail": {
"path": "media/thumbnail/wb2ZgO3W-thumbnail.webp",
"url": "https://cdn.example.com/media/thumbnail/wb2ZgO3W-thumbnail.webp",
"width": 300,
"height": 300,
"size": 9123
}
},
"srcset": "https://cdn.example.com/media/thumbnail/wb2ZgO3W-thumbnail.webp 300w, …"
}
}
Using the package in your application
use Codprez\MediaLibrary\Services\MediaUploader;
$media = app(MediaUploader::class)->upload($request->file('image'));
$media->getOptimizedUrl('medium');
$media->getSrcset();
Register a file that already lives on a disk (an export, an import, a sync):
app(MediaUploader::class)->register('exports/report.pdf', [
'name' => 'Q3 report',
'original_name' => 'report.pdf',
'mime_type' => 'application/pdf',
]);
Listen for the Media model in your own code as usual — it is a plain Eloquent
model with SoftDeletes, the variants cast and a url accessor that follows
the configured CDN.
Google Drive import
GOOGLE_DRIVE_API_KEY=your_google_api_key
Paste a public file or folder link in the media manager, preview what it finds and import the files you select. Large folders are imported in the background through the queue, with progress shown in the UI.
Frontend components
The React components ship as source under resources/js:
// Either alias the package source in your bundler, e.g. in vite.config.js:
// resolve: { alias: { '@medialibrary': path.resolve('vendor/codprez/laravel-media-library/resources/js') } }
import { MediaSelector } from "@medialibrary/components/media-selector";
// …or import it relatively, since the package ships raw source.
import { MediaSelector } from "../../vendor/codprez/laravel-media-library/resources/js/components/media-selector";
| Component | Purpose |
|---|---|
MediaSelector |
Modal picker with search, type filters, upload and Google Drive import. |
ImageUpload |
Single image field that opens the picker or accepts a drop. |
DocumentUpload |
The same, for documents. |
Tailwind 4 users: add the package source to your stylesheet so the utility classes used by the bundled UI are generated:
@source '../../vendor/codprez/laravel-media-library/resources/js';
Testing and formatting
composer test # vendor/bin/phpunit
composer lint # vendor/bin/pint
composer lint:check # vendor/bin/pint --test
The suite runs on Orchestra Testbench and covers variant generation, disk handling (including a faked S3 disk), the HTTP API and the backfill command (GitHub Actions matrix: PHP 8.2–8.4, Laravel 12 and 13).
Upgrading from 1.6
composer update codprez/laravel-media-libraryphp artisan vendor:publish --tag="media-library-config" --force(optional — every new key has a default, so you can also add only the ones you want)php artisan migrate— adds the nullablediskandvariantscolumnsphp artisan media:variants— backfills variants for existing images
There are no breaking changes: existing columns, routes, components and the
behaviour of getOptimizedUrl() / getImageVariants() are unchanged.
License
The MIT License (MIT). Please see License File for more information.
Related Packages
A simple package to help you easily upload files to your laravel project.
Elegant polymorphic media uploads for Laravel with Inertia.js and zone-based org...
Illuminate your API — interactive inspector for Laravel 12 with Inertia + React
Laravel packages that provide a basic user interface for browsing a server folde...