Ensure story media lands on and stays on cloud storage for S3 instances.
- StoryExpire: archive expiring story media on the same explicit disk the
media lives on (S3 move is a server-side copy+delete), with error handling
- Add admin:StoryMoveStorageLocalToCloud to migrate local story media
(active + story_archives) to cloud: copy, verify by size, then delete local
- --orphans option relocates untracked story_archives/ files to cloud using
the same copy/verify/delete flow (media is moved, never discarded)
- Schedule it hourly alongside the media migration when cloud storage is on
The remote avatar/media fetchers wrote temp files to storage/app/remcache/
and only unlinked them on the happy path. Any exception between the write
and the unlink (e.g. a cloud upload failure) leaked the file, and nothing
swept the directory.
- Wrap post-write logic in fetchAvatar() and remoteToCloud() in try/finally
so the temp file is always removed, even on failure
- Add gc:remcache command to delete stale remcache files (default >24h old,
preserves .gitignore, supports --hours and --dry-run)
- Schedule gc:remcache daily to clean up any stragglers
StoryFetch already handled cleanup via try/catch and was left unchanged.
Add admin:MediaMoveStorageLocalToCloud and admin:MediaMoveStorageCloudToLocal:
- Copy media (+thumbnail) between local and cloud disks, verify by size (and
sha256 against original_sha256 when present) before deleting the source.
- Integrated GC: delete the verified source copy (local on upload, cloud on
download), set version=4 / reset to 3, and bust MediaService/StatusService
caches. --keep-local / --keep-cloud opt out.
- Manage PF_ENABLE_CLOUD in .env AND the live runtime + config cache so new
uploads route to the correct backend mid-migration on a hot server. Uses the
installer's atomic .env writer (shared ManagesMediaStorageEnv trait).
- --limit / --dry-run / --force.
Replaces media:migrate2cloud (CloudMediaMigrate) and media:s3gc
(MediaS3GarbageCollector); scheduler now runs MediaMoveStorageLocalToCloud
hourly for straggler upload + GC. Keeps media:fix-nonlocal-driver.
Adds feature tests (download+GC, --keep-cloud, dry-run, env-flag flip both
directions, unknown-disk guard).
Prepares for Laravel 13 where VerifyCsrfToken is deprecated in favor
of PreventRequestForgery. The old class remains as an alias in v13 but
will be removed in a future version.
Adds a global rate limiter (240 req/min per user or IP) to all API
routes. Previously rate limiting was only applied ad-hoc on individual
routes, leaving some endpoints unprotected.
Laravel Passport 13 renamed:
- CheckScopes → CheckToken (verifies ALL listed scopes)
- CheckForAnyScope → CheckTokenForAnyScope (verifies ANY listed scope)
The old class names no longer exist, causing BindingResolutionException
on all /api/v1/admin/* routes that use the 'scope' or 'scopes' middleware
aliases.
Delete 4 middleware wrapper classes that added no custom logic:
- EncryptCookies (empty $except)
- TrimStrings ($except matches framework default)
- VerifyCsrfToken (exceptions moved to validateCsrfTokens() in bootstrap)
- TrustProxies (headers matched framework default)
CSRF exceptions (/api/v1/*, oauth/token) are now configured via
$middleware->validateCsrfTokens(except: [...]) in bootstrap/app.php.
All 107 tests pass.