skyraptor / laravel-versioned-storage
Versioned, safe storage for Laravel.
Package info
github.com/bumbummen99/laravel-versioned-storage
pkg:composer/skyraptor/laravel-versioned-storage
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/concurrency: ^12.0||^13.0
- illuminate/database: ^12.0||^13.0
- illuminate/filesystem: ^12.0||^13.0
- illuminate/queue: ^12.0||^13.0
- illuminate/support: ^12.0||^13.0
- league/flysystem-path-prefixing: ^3.0
- spatie/fork: ^1.2.12
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pao: ^1.0
- laravel/pint: ^1.29
- league/flysystem-aws-s3-v3: ^3.0
- league/flysystem-ftp: ^3.0
- league/flysystem-sftp-v3: ^3.0
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.6||^5.0
- pestphp/pest-plugin-laravel: ^4.1||^5.0
- pestphp/pest-plugin-type-coverage: ^4.0||^5.0
- phpbench/phpbench: ^1.4
- phpstan/extension-installer: ^1.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-06 03:07:35 UTC
README
Store versioned files and directory snapshots on Laravel filesystem disks. Each write gets a unique path; the database tracks historical versions and which version is current. Failed uploads cannot publish a partial snapshot.
Installation
Requires PHP 8.3+, Laravel 12 or 13, and a Unix-like runtime with PCNTL, POSIX, and sockets. Native Windows PHP is unsupported; use WSL2 or a Linux container. Web uploads require PHP-FPM with a hard request termination limit.
composer require skyraptor/laravel-versioned-storage
php artisan vendor:publish --tag="laravel-versioned-storage"
php artisan migrate
The publish tag includes configuration and one initial-schema migration. To
publish only the required migration, use laravel-versioned-storage-migrations;
configuration is optional and uses laravel-versioned-storage-config.
Usage
Files
use SkyRaptor\LaravelVersionedStorage\VersionedStorage; $first = VersionedStorage::put('reports/daily.csv', 'first contents'); $second = VersionedStorage::put('reports/daily.csv', 'second contents'); VersionedStorage::get('reports/daily.csv'); // second contents VersionedStorage::get('reports/daily.csv', $first); // first contents VersionedStorage::exists('reports/daily.csv', $first); VersionedStorage::versions('reports/daily.csv'); // [$first, $second]
put() accepts strings, streams, and Laravel File/UploadedFile objects.
File versions are stored at <path>/<version>, not at the logical path.
Read through this API rather than Storage::get($logicalPath).
Directory snapshots
use Illuminate\Filesystem\FilesystemAdapter; use SkyRaptor\LaravelVersionedStorage\VersionedStorage; $version = VersionedStorage::directory('reports/monthly', function (FilesystemAdapter $files): void { $files->put('summary.txt', 'Monthly summary'); $files->put('details/data.csv', 'Monthly details'); }); $directory = VersionedStorage::directory('reports/monthly'); $directory->get('summary.txt'); // current snapshot $directory->get('summary.txt', $version); // selected snapshot $directory->exists('summary.txt', $version); $directory->versions(); // oldest to newest
Files live at <directory>/<version>/<file>. Each snapshot is independent:
previous files are not copied. Write all contents before the callback returns.
Paths are relative to the disk root/prefix; leading slashes are normalized.
Cleanup
Pass cleanup: true to retire previous versions:
VersionedStorage::put('reports/daily.csv', 'replacement', cleanup: true);
Retired versions disappear from public reads and lists immediately, but their files remain until cleanup. Run or schedule:
php artisan laravel-versioned-storage:cleanup
use Illuminate\Support\Facades\Schedule; Schedule::command('laravel-versioned-storage:cleanup') ->everyFiveMinutes() ->withoutOverlapping();
Ensure Laravel's scheduler is running. Cleanup retries interrupted uploads
and failed deletions; it returns a failure exit code if any operation fails.
--chunk=20 changes the default concurrency batch size of 10.
For synchronous, resource-specific cleanup, use VersionedStorage::cleanup($path)
or $directory->cleanup().
Transactions
Uploads must run outside metadata transactions. Stage first, then publish on the same database connection as your application changes:
use Illuminate\Support\Facades\DB; $stage = VersionedStorage::stage('reports/daily.csv', 'contents'); DB::transaction(function () use ($stage): void { // Update application records here. $stage->publish(cleanup: true); });
Use stageDirectory($path, $callback) for directories. Completed stages never
expire automatically: publish or explicitly discard() them outside a
transaction. After rollback, republish or discard. Across jobs/processes, pass
$stage->version and reload with VersionedStorage::staged($uuid).
Configuration
Defaults in config/laravel-versioned-storage.php:
'disk' => null, // Laravel's default filesystem disk 'connection' => null, // Laravel's default database connection 'upload_timeout' => 60, // seconds 'cleanup_grace' => 10800, // remote-write completion allowance
Configure the metadata connection before migrating. Override the disk per call
with disk: 's3'. The same API is available through
SkyRaptor\LaravelVersionedStorage\Facades\VersionedStorage.
Safety and constraints
- Set real execution limits. Align
upload_timeoutwith FPM'srequest_terminate_timeout. PHPmax_execution_timeand Nginxfastcgi_read_timeoutalone do not terminate workers blocked in I/O. Prevent FPM work from continuing indefinitely after a response. - CLI/development-server uploads use supervised filesystem-operation forks.
Daemon queue jobs use the job/worker timeout;
queue:work --onceuses forks. Queue limits must be positive and enforced; keepretry_after/visibility longer than the job timeout. Arbitrary callback code and database work are not covered by a filesystem-operation alarm. - Own each version container exclusively. Do not share it across resources, disk aliases, or file/directory APIs, nest other resources inside it, or modify published snapshots externally. Overlap guards cannot detect every physical overlap across drivers.
cleanup_gracemust exceed the provider's longest possible completion delay after writer termination. Database/storage recovery must be available. This is not universal fencing against remote writes, database loss, or host failures; uncertain uploads remain until their recovery deadlines.- Forks require no active transactions on resolved database connections and
reproducible disk/custom-driver configuration. Cleanup process workers need
identically configured named disks. Runtime-only disks require explicit
same-process cleanup (
concurrency.default = sync). - Reads hold database locks and can delay publication/cleanup. Keep transactions
short; SQLite serializes metadata writes. Recovery requires
SKIP LOCKEDsupport on row-locking databases, such as MySQL 8+. - Failures throw, even on disks with
throw=false. Missing versions returnfalsefromexists()and throw fromget(). Download URLs are not protected from later cleanup; temporary upload URLs are unsupported. Forked stream reads buffer locally, so allow temporary disk space. - The initial migration replaces the unreleased migration history, not existing schemas. Earlier development installations need a backed-up schema rebuild with writers/cleanup paused and old workers drained.
Testing in Laravel Applications
Migrate the package tables and enable synchronous uploads in test setup:
use Illuminate\Support\Facades\Storage; use SkyRaptor\LaravelVersionedStorage\Testing\VersionedStorageTesting; Storage::fake('local'); VersionedStorageTesting::runUploadsSynchronously(); config(['concurrency.default' => 'sync']); // when testing cleanup with fake disks
Use DatabaseMigrations, not a setup wrapping uploads in database transactions.
Enable the helper for each fresh application. It uses real package behavior but
does not enforce hard timeouts; keep real-process timeout tests separate.
More
License
AGPL-3.0-only with the Library Integration Exception. Independent applications may retain their own license, including proprietary terms; the package and covered modifications retain applicable AGPL obligations. See COPYING for the full license.
By Patrick Henninger and contributors.