NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Media Library for Laravel

URLs, streaming & downloads

Public media exposes a direct disk URL — through the owner handle or on the Media itself:

MediaLibrary::for($user)->url('avatar');          // first media's URL, the fallback, or ''
MediaLibrary::for($user)->url('avatar', 'thumb'); // first media's variant URL

$media->getUrl();          // original (public media)
$media->getUrl('thumb');   // a named variant (on the variants disk)

Private & temporary URLs

Private media has no public URL, so use a temporary URL: it presigns natively on disks that support it (S3 and any driver implementing temporaryUrl()) or falls back to a Laravel signed streaming route. Without an expiry, the lifetime defaults to temporary_url_default_lifetime. Large files stream efficiently, including resumable partial downloads:

// Private media has no public URL — getUrl() throws. Use a temporary URL:
MediaLibrary::for($user)->temporaryUrl('avatar', expiry: now()->addMinutes(10));
$media->getTemporaryUrl(now()->addMinutes(5));
$media->getTemporaryUrl(now()->addMinutes(5), 'thumb');
// presigned on S3-like disks, or a Laravel signed streaming route on local/cold disks

return $media->toResponse($request);              // stream inline (safe types), range-aware
return $media->toDownloadResponse('invoice.pdf'); // as an attachment

Streaming route

The package registers GET {prefix}/{media}/{variant?}, named media.stream. {media} binds by UUID, and the route runs the configured middleware plus Laravel's signed middleware, which is always appended; ?download=1 in the signed URL serves an attachment. The route never hands out anything without a valid signature (or, on presigning disks, a native presigned URL):

'stream' => [
    'enabled' => true,           // register the signed streaming route
    'route_prefix' => 'media',   // URI prefix
    'middleware' => ['web'],     // Laravel's 'signed' is always appended
],

What is served inline

Only types a browser renders without running anything — raster images, audio, video, PDF and plain text — are served inline. HTML, SVG, XML and every other type are sent as an attachment, and every response carries X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy, so a stored file can never run script on your application’s origin. A variant is served with its own format’s type. An unparseable or multi-range Range header is ignored (the whole file, 200); a range past the end of the file gets 416.

Private media needs a private disk

->private() / withVisibility('private') sets the file’s visibility — an S3 object ACL, or file mode 0600 on a local disk. It doesn’t stop a web server that serves the disk directly: the default public disk is symlinked under public/storage, so a private file stored there can still be fetched from /storage/{uuid}/{file} wherever the web server runs as the PHP user — and the uuid appears in every signed URL. Keep private buckets on a disk the web server doesn’t serve, such as Laravel’s local disk (storage/app/private) or a private S3 bucket:

// Keep private buckets on a disk the web server does not serve.
$this->addMediaBucket('invoices')
    ->useDisk('local')   // storage/app/private — not symlinked under public/storage
    ->private();

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.