jeffersongoncalves / laravel-report-url
Temporary, signed, expiring download links for generated files in Laravel, with owner checks, download tracking and pruning.
Package info
github.com/jeffersongoncalves/laravel-report-url
pkg:composer/jeffersongoncalves/laravel-report-url
Requires
- php: ^8.2
- laravel/framework: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- jeffersongoncalves/laravel-saved-filter: ^1.9
- jeffersongoncalves/pest-plugin-database-migrations: ^1.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.24
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
- jeffersongoncalves/laravel-saved-filter: Email scheduled reports too large to attach as temporary download links
Provides
None
Conflicts
None
Replaces
None
README
Laravel Report Url
Hand out temporary download links for files your app generates — reports, exports, backups — instead of attaching them to an email. Mail providers cap messages at 10–25 MB; a link has no such limit.
Each file is kept on a private disk and reached through a signed, expiring URL. Optionally only its owner can download it. Downloads are counted, and expired files are pruned every day.
Works on its own, and plugs into laravel-saved-filter: install both and scheduled reports too large to attach are emailed as links.
Installation
You can install the package via composer:
composer require jeffersongoncalves/laravel-report-url
php artisan vendor:publish --tag="report-url-migrations"
php artisan migrate
Optionally publish the config:
php artisan vendor:publish --tag="report-url-config"
return [ 'table' => 'report_urls', 'disk' => env('REPORT_URL_DISK', 'local'), // where the files live; use a private disk 'directory' => 'report-urls', 'ttl' => 60 * 24 * 7, // minutes a link stays valid (7 days); null = never expires 'route' => ['prefix' => 'report-urls', 'middleware' => ['web']], 'restrict_to_owner' => true, // the downloader must be logged in as the file's owner 'integrations' => ['saved_filter' => true], // bind HostsReportFiles when laravel-saved-filter is installed ];
Usage
use JeffersonGoncalves\ReportUrl\Facades\ReportUrl; // From a string $report = ReportUrl::store($csv, 'sales-2026-10.csv', owner: $user); // From a local file, streamed (not loaded into memory) $report = ReportUrl::storeFile(storage_path('app/tmp/export.xlsx'), 'export.xlsx', $user, ttlMinutes: 60); $report->url(); // signed download link $report->expires_at; // Carbon, or null when the ttl is null
Both methods write the file to {directory}/{uuid}/{fileName} on the configured disk and return the JeffersonGoncalves\ReportUrl\Models\ReportUrl model. The owner is optional; without one, anyone holding the link can download the file until it expires.
The model keeps downloads and last_downloaded_at, and each download fires JeffersonGoncalves\ReportUrl\Events\ReportUrlDownloaded ($reportUrl, $user) for audit logs.
Security model
- Signed and expiring. The link is a
URL::temporarySignedRoute(): it can't be guessed or tampered with (403) and stops working atexpires_at. Movingexpires_atearlier on the record also revokes an already sent link (410 Gone). - Owner check. With
restrict_to_owneron and an owner set, a guest is redirected to theloginroute (403 when there is none) and any other user gets 403. - Private disk. The file is only served through the route. Keep
diskprivate (the defaultlocaldisk is). - File names are never paths. Only the base name of
$fileNameis used.
Pruning
php artisan report-url:prune # delete expired links and their files php artisan report-url:prune --dry-run # only count them
The command is scheduled daily with withoutOverlapping()->onOneServer(), so it only needs the Laravel scheduler running. onOneServer needs a cache driver with locks (redis, database, memcached...) when several servers run the scheduler. Links without an expiry are never pruned.
laravel-saved-filter
Install jeffersongoncalves/laravel-saved-filter (1.9+) next to this package and nothing else is needed: large scheduled reports are emailed as links.
When a report is bigger than saved-filter.reports_max_attachment_size, laravel-saved-filter asks the bound HostsReportFiles for a link. This package binds SavedFilterReportHost, which stores the file owned by the recipient and returns its URL and expiry; the email then shows a Download the report button and the expiry date.
Your own HostsReportFiles binding is never overwritten. Set integrations.saved_filter to false to skip the binding.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
