diff --git a/app/Jobs/InternalPipeline/RecalculateAllUserStoragePipeline.php b/app/Jobs/InternalPipeline/RecalculateAllUserStoragePipeline.php new file mode 100644 index 000000000..a87691236 --- /dev/null +++ b/app/Jobs/InternalPipeline/RecalculateAllUserStoragePipeline.php @@ -0,0 +1,72 @@ + + */ + public function middleware(): array + { + return [(new WithoutOverlapping('ip:recalculate-all-user-storage'))->shared()->dontRelease()]; + } + + /** + * Execute the job. + */ + public function handle(): void + { + User::whereNull('status') + ->chunkById(500, function ($users) { + foreach ($users as $user) { + UserStorageService::recalculateUpdateStorageUsed($user->id); + } + }); + } +} diff --git a/database/migrations/2026_09_10_000001_backfill_user_storage_used.php b/database/migrations/2026_09_10_000001_backfill_user_storage_used.php new file mode 100644 index 000000000..711810812 --- /dev/null +++ b/database/migrations/2026_09_10_000001_backfill_user_storage_used.php @@ -0,0 +1,25 @@ +onQueue('low'); + } + + public function down(): void + { + // Data backfill only; nothing to reverse. + } +}; diff --git a/tests/Feature/InternalPipeline/RecalculateAllUserStoragePipelineTest.php b/tests/Feature/InternalPipeline/RecalculateAllUserStoragePipelineTest.php new file mode 100644 index 000000000..5eeac5db8 --- /dev/null +++ b/tests/Feature/InternalPipeline/RecalculateAllUserStoragePipelineTest.php @@ -0,0 +1,51 @@ + null, + 'profile_id' => $user->profile->id, + 'user_id' => $user->id, + 'media_path' => 'public/m/_v2/1/'.uniqid().'.jpeg', + 'mime' => 'image/jpeg', + 'size' => $bytes, + 'order' => 1, + ]); +} + +/* +| The backfill job repairs storage_used counters that drifted before the +| self-heal logic existed (#7169). It recomputes every active user from their +| actual media, so stale inflated values are corrected in bulk. +*/ + +it('recalculates storage_used from actual media for all active users', function () { + $a = User::factory()->create(); + $b = User::factory()->create(); + $a->refresh(); + $b->refresh(); + + // Both start with wildly inflated stale counters. + foreach ([$a, $b] as $u) { + $u->storage_used = 999999; + $u->storage_used_updated_at = now()->subYear(); + $u->save(); + } + + makeUserMedia($a, 400000); // 400 KB real usage + // $b has no media. + + (new RecalculateAllUserStoragePipeline)->handle(); + + $a->refresh(); + $b->refresh(); + expect((int) $a->storage_used)->toBe(400); + expect((int) $b->storage_used)->toBe(0); +});