You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pixelfed/app/Services/UserStorageService.php

160 lines
5.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\Media;
use App\Models\User;
use Carbon\Carbon;
class UserStorageService
{
const CACHE_KEY = 'pf:services:user-storage:byId:';
/**
* How long (in hours) a cached storage_used value is trusted for
* incremental add/subtract before the hot path recalculates it from source.
*
* Mirrors the scheduled `user:storage:recalculate --stale=168` reconciler:
* an active user who uploads or deletes will self-heal a stale counter
* without waiting for the nightly job.
*/
const STALE_AFTER_HOURS = 168;
/**
* Whether a user's cached storage_used is too old (or missing) to be
* trusted for an incremental delta and should be recalculated from source.
*/
protected static function isStale(User $user): bool
{
$updatedAt = $user->storage_used_updated_at;
if (! $updatedAt) {
return true;
}
// Be defensive: the value is normally cast to Carbon on the User model,
// but tolerate a raw string if a model is hydrated without casts.
if (! $updatedAt instanceof Carbon) {
$updatedAt = Carbon::parse($updatedAt);
}
return $updatedAt->lt(now()->subHours(self::STALE_AFTER_HOURS));
}
public static function get($id)
{
$user = User::find($id);
if (! $user || $user->status) {
return -1;
}
if ($user->storage_used_updated_at) {
return (int) $user->storage_used;
}
$updatedVal = self::calculateStorageUsed($id);
$user->storage_used = $updatedVal;
$user->storage_used_updated_at = now();
$user->save();
return $user->storage_used;
}
public static function calculateStorageUsed($id)
{
return (int) floor(Media::whereUserId($id)->sum('size') / 1000);
}
public static function recalculateUpdateStorageUsed($id)
{
$user = User::find($id);
if (! $user || $user->status) {
return;
}
$updatedVal = (int) floor(Media::whereUserId($id)->sum('size') / 1000);
$user->storage_used = $updatedVal;
$user->storage_used_updated_at = now();
$user->save();
return $updatedVal;
}
/**
* Increment a user's cached storage_used by the size (in bytes) of newly
* stored media, without re-summing the whole media table.
*
* This is the counterpart to decrementStorageUsed and is used on the upload
* path, AFTER the new media row has been saved.
*
* Self-healing: if the cached counter is missing or stale (see
* STALE_AFTER_HOURS) it recalculates from source instead of trusting the
* incremental value. Because callers save the media row before calling
* this, a from-source recalc already accounts for the new media, so the
* delta is NOT re-applied on the recalc path (that would double count).
*
* @param int $id User id
* @param int $sizeInBytes Size of the added media in bytes
* @return int|null New storage_used value in KB, or null when skipped
*/
public static function increaseStorageUsed($id, $sizeInBytes)
{
$user = User::find($id);
if (! $user || $user->status) {
return null;
}
// Stale or uncalculated: recompute from source. The just-saved media is
// already included, so return the source value without adding the delta.
if (self::isStale($user)) {
return self::recalculateUpdateStorageUsed($id);
}
$sizeInKbs = (int) floor(((int) $sizeInBytes) / 1000);
$updatedVal = max(0, (int) $user->storage_used + $sizeInKbs);
$user->storage_used = $updatedVal;
$user->storage_used_updated_at = now();
$user->save();
return $updatedVal;
}
/**
* Decrement a user's cached storage_used by the size (in bytes) of removed
* media, without re-summing the whole media table.
*
* This is the fast path used when media is deleted, AFTER the media row has
* been removed. The value is clamped at zero so it can never go negative.
*
* Self-healing: if the cached counter is missing or stale (see
* STALE_AFTER_HOURS) it recalculates from source instead of trusting the
* incremental value. Because callers delete the media row before calling
* this, a from-source recalc already excludes the removed media, so the
* delta is NOT re-applied on the recalc path (that would over-subtract).
*
* @param int $id User id
* @param int $sizeInBytes Size of the removed media in bytes
* @return int|null New storage_used value in KB, or null when skipped
*/
public static function decrementStorageUsed($id, $sizeInBytes)
{
$user = User::find($id);
if (! $user || $user->status) {
return null;
}
// Stale or uncalculated: recompute from source. The removed media is
// already excluded, so return the source value without subtracting.
if (self::isStale($user)) {
return self::recalculateUpdateStorageUsed($id);
}
$sizeInKbs = (int) floor(((int) $sizeInBytes) / 1000);
$updatedVal = max(0, (int) $user->storage_used - $sizeInKbs);
$user->storage_used = $updatedVal;
$user->storage_used_updated_at = now();
$user->save();
return $updatedVal;
}
}