Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
more work
Browse files Browse the repository at this point in the history
ildyria committed Dec 29, 2024
1 parent b57ddcb commit c63d7c2
Showing 26 changed files with 610 additions and 90 deletions.
5 changes: 3 additions & 2 deletions app/Actions/Photo/Create.php
Original file line number Diff line number Diff line change
@@ -26,10 +26,10 @@
use App\Image\Files\NativeLocalFile;
use App\Legacy\Actions\Photo\Create as LegacyPhotoCreate;
use App\Models\Photo;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Pipeline\Pipeline;
use LycheeVerify\Verify;
use User;

class Create
{
@@ -212,6 +212,7 @@ private function executePipeOnDTO(array $pipes, VideoPartnerDTO|StandaloneDTO|Ph
// If source file could not be put into final destination, remove
// freshly created photo from DB to avoid having "zombie" entries.
try {
/** @disregard */
$dto->getPhoto()->delete();
} catch (\Throwable) {
// Sic! If anything goes wrong here, we still throw the original exception
@@ -311,7 +312,7 @@ private function checkQuota(NativeLocalFile $sourceFile): void
return;
}

$user = \User::find($this->strategyParameters->intendedOwnerId) ?? throw new ModelNotFoundException();
$user = User::find($this->strategyParameters->intendedOwnerId) ?? throw new ModelNotFoundException();

// User does not have quota
if ($user->quota_kb === null) {
18 changes: 18 additions & 0 deletions app/Enum/CacheTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace App\Enum;

/**
* Enum ConfigType.
*
* The most important type possibilities.
*/
enum CacheTag: string
{
case GALLERY = 'gallery';
case AUTH = 'auth';
case USER = 'user';
case SETTINGS = 'settings';
case STATISTICS = 'statistics';
case USERS = 'users';
}
21 changes: 21 additions & 0 deletions app/Events/AlbumRouteCacheUpdated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Events;

use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class AlbumRouteCacheUpdated
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(public ?string $album_id = null)
{
}
}
22 changes: 22 additions & 0 deletions app/Events/TaggedRouteCacheUpdated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Events;

use App\Enum\CacheTag;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class TaggedRouteCacheUpdated
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;

/**
* Create a new event instance.
*/
public function __construct(public CacheTag $tag)
{
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/Admin/Maintenance/FullTree.php
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

namespace App\Http\Controllers\Admin\Maintenance;

use App\Events\AlbumRouteCacheUpdated;
use App\Http\Controllers\Admin\Maintenance\Model\Album;
use App\Http\Requests\Maintenance\FullTreeUpdateRequest;
use App\Http\Requests\Maintenance\MaintenanceRequest;
@@ -26,6 +27,8 @@ public function do(FullTreeUpdateRequest $request): void
$keyName = 'id';
$albumInstance = new Album();
batch()->update($albumInstance, $request->albums(), $keyName);

AlbumRouteCacheUpdated::dispatch();
}

/**
3 changes: 3 additions & 0 deletions app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

use App\Contracts\Models\SizeVariantFactory;
use App\Enum\SizeVariantType;
use App\Events\AlbumRouteCacheUpdated;
use App\Exceptions\MediaFileOperationException;
use App\Http\Requests\Maintenance\CreateThumbsRequest;
use App\Image\PlaceholderEncoder;
@@ -56,6 +57,8 @@ public function do(CreateThumbsRequest $request, SizeVariantFactory $sizeVariant
} catch (MediaFileOperationException $e) {
Log::error('Failed to create ' . $request->kind()->value . ' for photo id ' . $photo->id . '');
}

AlbumRouteCacheUpdated::dispatch();
// @codeCoverageIgnoreEnd
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Admin\Maintenance;

use App\Enum\StorageDiskType;
use App\Events\AlbumRouteCacheUpdated;
use App\Http\Requests\Maintenance\MaintenanceRequest;
use App\Models\SizeVariant;
use Illuminate\Routing\Controller;
@@ -52,6 +53,8 @@ public function do(MaintenanceRequest $request): void
}
// @codeCoverageIgnoreEnd
}

AlbumRouteCacheUpdated::dispatch();
}

/**
4 changes: 4 additions & 0 deletions app/Http/Controllers/Admin/Maintenance/RegisterController.php
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

namespace App\Http\Controllers\Admin\Maintenance;

use App\Enum\CacheTag;
use App\Events\TaggedRouteCacheUpdated;
use App\Http\Requests\Maintenance\RegisterRequest;
use App\Http\Resources\GalleryConfigs\RegisterData;
use App\Models\Configs;
@@ -29,6 +31,8 @@ public function __invoke(RegisterRequest $request): RegisterData
// Not valid, reset the key.
Configs::set('license_key', '');

TaggedRouteCacheUpdated::dispatch(CacheTag::SETTINGS);

return new RegisterData(false);
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/Admin/SettingsController.php
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@

namespace App\Http\Controllers\Admin;

use App\Enum\CacheTag;
use App\Events\TaggedRouteCacheUpdated;
use App\Exceptions\InsufficientFilesystemPermissions;
use App\Http\Requests\Settings\GetAllConfigsRequest;
use App\Http\Requests\Settings\SetConfigsRequest;
@@ -49,6 +51,7 @@ public function setConfigs(SetConfigsRequest $request): ConfigCollectionResource
});

Configs::invalidateCache();
TaggedRouteCacheUpdated::dispatch(CacheTag::SETTINGS);

return new ConfigCollectionResource(Configs::orderBy('cat', 'asc')->get());
}
8 changes: 8 additions & 0 deletions app/Http/Controllers/Admin/UserManagementController.php
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@
use App\Actions\Statistics\Spaces;
use App\Actions\User\Create;
use App\Actions\User\Save;
use App\Enum\CacheTag;
use App\Events\TaggedRouteCacheUpdated;
use App\Exceptions\UnauthorizedException;
use App\Http\Requests\UserManagement\AddUserRequest;
use App\Http\Requests\UserManagement\DeleteUserRequest;
@@ -60,6 +62,8 @@ public function save(SetUserSettingsRequest $request, Save $save): void
quota_kb: $request->quota_kb(),
note: $request->note()
);

TaggedRouteCacheUpdated::dispatch(CacheTag::USERS);
}

/**
@@ -78,6 +82,8 @@ public function delete(DeleteUserRequest $request): void
throw new UnauthorizedException('You are not allowed to delete yourself');
}
$request->user2()->delete();

TaggedRouteCacheUpdated::dispatch(CacheTag::USERS);
}

/**
@@ -99,6 +105,8 @@ public function create(AddUserRequest $request, Create $create): UserManagementR
note: $request->note()
);

TaggedRouteCacheUpdated::dispatch(CacheTag::USERS);

return new UserManagementResource($user, ['id' => $user->id, 'size' => 0], $request->is_se());
}
}
59 changes: 46 additions & 13 deletions app/Http/Controllers/Gallery/AlbumController.php
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
use App\Actions\Album\Transfer;
use App\Actions\Album\Unlock;
use App\Actions\Photo\Archive as PhotoArchive;
use App\Events\AlbumRouteCacheUpdated;
use App\Exceptions\Internal\LycheeLogicException;
use App\Exceptions\UnauthenticatedException;
use App\Http\Requests\Album\AddAlbumRequest;
@@ -108,6 +109,9 @@ public function createAlbum(AddAlbumRequest $request): string
*/
public function createTagAlbum(AddTagAlbumRequest $request, CreateTagAlbum $create): string
{
// Root
AlbumRouteCacheUpdated::dispatch('');

return $create->create($request->title(), $request->tags())->id;
}

@@ -140,7 +144,8 @@ public function updateAlbum(UpdateAlbumRequest $request, SetHeader $setHeader):
album: $album,
is_compact: $request->is_compact(),
photo: $request->photo(),
shall_override: true);
shall_override: true
);

return EditableBaseAlbumResource::fromModel($album);
}
@@ -167,6 +172,7 @@ public function updateTagAlbum(UpdateTagAlbumRequest $request): EditableBaseAlbu
$album->photo_timeline = $request->photo_timeline();
$album->save();

// Root
return EditableBaseAlbumResource::fromModel($album);
}

@@ -179,26 +185,46 @@ public function updateTagAlbum(UpdateTagAlbumRequest $request): EditableBaseAlbu
*
* @return AlbumProtectionPolicy
*/
public function updateProtectionPolicy(SetAlbumProtectionPolicyRequest $request,
public function updateProtectionPolicy(
SetAlbumProtectionPolicyRequest $request,
SetProtectionPolicy $setProtectionPolicy,
SetSmartProtectionPolicy $setSmartProtectionPolicy): AlbumProtectionPolicy
{
SetSmartProtectionPolicy $setSmartProtectionPolicy,
): AlbumProtectionPolicy {
if ($request->album() instanceof BaseSmartAlbum) {
$setSmartProtectionPolicy->do(
$request->album(),
$request->albumProtectionPolicy()->is_public
);

return AlbumProtectionPolicy::ofSmartAlbum($request->album());
return $this->updateProtectionPolicySmart($request->album(), $request->albumProtectionPolicy()->is_public, $setSmartProtectionPolicy);
}

/** @var BaseAlbum $album */
$album = $request->album();
$setProtectionPolicy->do(

return $this->updateProtectionPolicyBase(
$album,
$request->albumProtectionPolicy(),
$request->isPasswordProvided(),
$request->password()
$request->password(),
$setProtectionPolicy
);
}

private function updateProtectionPolicySmart(BaseSmartAlbum $album, bool $is_public, SetSmartProtectionPolicy $setSmartProtectionPolicy): AlbumProtectionPolicy
{
$setSmartProtectionPolicy->do($album, $is_public);

return AlbumProtectionPolicy::ofSmartAlbum($album);
}

private function updateProtectionPolicyBase(
BaseAlbum $album,
AlbumProtectionPolicy $protectionPolicy,
bool $shallSetPassword,
?string $password,
SetProtectionPolicy $setProtectionPolicy): AlbumProtectionPolicy
{
$setProtectionPolicy->do(
$album,
$protectionPolicy,
$shallSetPassword,
$password
);

return AlbumProtectionPolicy::ofBaseAlbum($album->refresh());
@@ -214,6 +240,11 @@ public function updateProtectionPolicy(SetAlbumProtectionPolicyRequest $request,
*/
public function delete(DeleteAlbumsRequest $request, Delete $delete): void
{
Album::select('parent_id')
->whereIn('id', $request->albumIds())
->get()
->each(fn (Album $album) => AlbumRouteCacheUpdated::dispatch($album->parent_id ?? ''));

$fileDeleter = $delete->do($request->albumIds());
App::terminating(fn () => $fileDeleter->do());
}
@@ -244,6 +275,7 @@ public function getTargetListAlbums(TargetListAlbumRequest $request, ListAlbums
*/
public function merge(MergeAlbumsRequest $request, Merge $merge): void
{
$request->albums()->each(fn (Album $album) => AlbumRouteCacheUpdated::dispatch($album->id));
$merge->do($request->album(), $request->albums());
}

@@ -257,6 +289,7 @@ public function merge(MergeAlbumsRequest $request, Merge $merge): void
*/
public function move(MoveAlbumsRequest $request, Move $move): void
{
$request->albums()->each(fn (Album $album) => AlbumRouteCacheUpdated::dispatch($album->id));
$move->do($request->album(), $request->albums());
}

@@ -367,4 +400,4 @@ public function deleteTrack(DeleteTrackRequest $request): void
{
$request->album()->deleteTrack();
}
}
}
6 changes: 6 additions & 0 deletions app/Http/Controllers/OauthController.php
Original file line number Diff line number Diff line change
@@ -3,7 +3,9 @@
namespace App\Http\Controllers;

use App\Actions\Oauth\Oauth as OauthAction;
use App\Enum\CacheTag;
use App\Enum\OauthProvidersType;
use App\Events\TaggedRouteCacheUpdated;
use App\Exceptions\UnauthenticatedException;
use App\Exceptions\UnauthorizedException;
use App\Http\Requests\Profile\ClearOauthRequest;
@@ -86,6 +88,8 @@ public function register(string $provider)
$providerEnum = $this->oauth->validateProviderOrDie($provider);
Session::put($providerEnum->value, OauthAction::OAUTH_REGISTER);

TaggedRouteCacheUpdated::dispatch(CacheTag::USER);

return Socialite::driver($providerEnum->value)->redirect();
}

@@ -115,6 +119,8 @@ public function clear(ClearOauthRequest $request): void
/** @var User $user */
$user = Auth::user() ?? throw new UnauthenticatedException();
$user->oauthCredentials()->where('provider', '=', $request->provider())->delete();

TaggedRouteCacheUpdated::dispatch(CacheTag::USER);
}

/**
8 changes: 8 additions & 0 deletions app/Http/Controllers/ProfileController.php
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@
use App\Actions\Profile\UpdateLogin;
use App\Actions\User\TokenDisable;
use App\Actions\User\TokenReset;
use App\Enum\CacheTag;
use App\Events\TaggedRouteCacheUpdated;
use App\Exceptions\ModelDBException;
use App\Exceptions\UnauthenticatedException;
use App\Http\Requests\Profile\ChangeTokenRequest;
@@ -53,6 +55,8 @@ public function update(UpdateProfileRequest $request, UpdateLogin $updateLogin):
// to be unauthenticated upon the next request.
Auth::login($currentUser);

TaggedRouteCacheUpdated::dispatch(CacheTag::USER);

return new UserResource($currentUser);
}

@@ -69,6 +73,8 @@ public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset):
{
$token = $tokenReset->do();

TaggedRouteCacheUpdated::dispatch(CacheTag::USER);

return new UserToken($token);
}

@@ -82,6 +88,8 @@ public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset):
*/
public function unsetToken(ChangeTokenRequest $request, TokenDisable $tokenDisable): void
{
TaggedRouteCacheUpdated::dispatch(CacheTag::USER);

$tokenDisable->do();
}
}
6 changes: 4 additions & 2 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'response_cache',
'album_cache_refresher',
],
];

@@ -92,10 +93,11 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'login_required_v1' => \App\Legacy\V1\Middleware\LoginRequiredV1::class, // remove me in non-legacy build
'login_required' => \App\Http\Middleware\LoginRequired::class,
'cache_control' => \App\Http\Middleware\CacheControl::class,
'cache_control' => \App\Http\Middleware\Caching\CacheControl::class,
'support' => \LycheeVerify\Http\Middleware\VerifySupporterStatus::class,
'config_integrity' => \App\Http\Middleware\ConfigIntegrity::class,
'unlock_with_password' => \App\Http\Middleware\UnlockWithPassword::class,
'response_cache' => \App\Http\Middleware\ResponseCache::class,
'response_cache' => \App\Http\Middleware\Caching\ResponseCache::class,
'album_cache_refresher' => \App\Http\Middleware\Caching\AlbumRouteCacheRefresher::class,
];
}
124 changes: 124 additions & 0 deletions app/Http/Middleware/Caching/AlbumRouteCacheRefresher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace App\Http\Middleware\Caching;

use App\Contracts\Http\Requests\RequestAttribute;
use App\Events\AlbumRouteCacheUpdated;
use App\Models\Configs;
use Illuminate\Foundation\Http\Middleware\Concerns\ExcludesPaths;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;

/**
* Response caching, this allows to speed up the reponse time of Lychee by hopefully a lot.
*/
class AlbumRouteCacheRefresher
{
use ExcludesPaths;

/** @var string[] */
protected array $except = [
'api/v2/Album',
'api/v2/Album::unlock',
'api/v2/Album::rename',
'api/v2/Album::updateProtectionPolicy',
'api/v2/Album::move',
'api/v2/Album::cover',
'api/v2/Album::header',
'api/v2/Album::merge',
'api/v2/Album::transfer',
'api/v2/Album::track',
'api/v2/TagAlbum',
'api/v2/Sharing',
'api/v2/Photo::fromUrl',
'api/v2/Photo',
'api/v2/Photo::rename',
'api/v2/Photo::tags',
'api/v2/Photo::move',
'api/v2/Photo::copy',
'api/v2/Photo::star',
'api/v2/Photo::rotate',
];

/**
* Handle an incoming request.
*
* @param Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*
* @return Response
*
* @throws \InvalidArgumentException
*/
public function handle(Request $request, \Closure $next): mixed
{
if ($request->method() === 'GET') {
return $next($request);
}

if (Configs::getValueAsBool('cache_enabled') === false) {
return $next($request);
}

// ! We use $except as a ALLOW list instead of a DENY list
if (!$this->inExceptArray($request)) {
return $next($request);
}

$full_album_ids = collect();

/** @var string|null $album_id */
$album_id = $request->input(RequestAttribute::ALBUM_ID_ATTRIBUTE);
if ($album_id !== null) {
$full_album_ids->push($album_id);
}

/** @var string[]|null */
$albums_id = $request->input(RequestAttribute::ALBUM_IDS_ATTRIBUTE);
if ($albums_id !== null) {
$full_album_ids = $full_album_ids->merge($albums_id);
}

/** @var string|null */
$parent_id = $request->input(RequestAttribute::PARENT_ID_ATTRIBUTE);
if ($parent_id !== null) {
$full_album_ids->push($parent_id);
}

/** @var string|null */
$photo_id = $request->input(RequestAttribute::PHOTO_ID_ATTRIBUTE);
/** @var string[]|null */
$photo_ids = $request->input(RequestAttribute::PHOTO_IDS_ATTRIBUTE);

if ($photo_ids !== null || $photo_id !== null) {
$photos_album_ids = DB::table('photos')
->select('album_id')
->whereIn('id', $photo_ids ?? [])
->orWhere('id', '=', $photo_id)
->distinct()
->pluck('album_id')
->all();
if (count($photos_album_ids) > 0) {
$full_album_ids = $full_album_ids->merge($photos_album_ids);
}
}

if ($albums_id !== null || $album_id !== null) {
$albums_parents_ids = DB::table('albums')
->select('parent_id')
->whereIn('id', $albums_id ?? [])
->orWhere('id', '=', $album_id)
->distinct()
->pluck('parent_id')
->all();
if (count($albums_parents_ids) > 0) {
$full_album_ids = $full_album_ids->merge($albums_parents_ids);
}
}

$full_album_ids->each(fn ($album_id) => AlbumRouteCacheUpdated::dispatch($album_id ?? ''));

return $next($request);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<?php

// app/Http/Middleware/CacheControlMiddleware.php

namespace App\Http\Middleware;
namespace App\Http\Middleware\Caching;

use Illuminate\Http\Request;

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace App\Http\Middleware;
namespace App\Http\Middleware\Caching;

use App\Metadata\Cache\RouteCacheConfig;
use App\Metadata\Cache\RouteCacheManager;
@@ -25,8 +25,8 @@ public function __construct(RouteCacheManager $route_cache_manager)
/**
* Handle an incoming request.
*
* @param Request $request
* @param \Closure $next
* @param Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*
* @return Response
*
@@ -43,7 +43,7 @@ public function handle(Request $request, \Closure $next): mixed
return $next($request);
}

$config = $this->route_cache_manager->getConfig($request->route()->uri);
$config = $this->route_cache_manager->get_config($request->route()->uri);

// Check with the route manager if we can cache this route.
if ($config === false) {
@@ -72,7 +72,7 @@ private function chacheWithoutTags(Request $request, \Closure $next, RouteCacheC
return $next($request);
}

$key = $this->route_cache_manager->getKey($request, $config);
$key = $this->route_cache_manager->get_key($request, $config);

return Cache::remember($key, Configs::getValueAsInt('cache_ttl'), fn () => $next($request));
}
@@ -87,7 +87,7 @@ private function chacheWithoutTags(Request $request, \Closure $next, RouteCacheC
*/
private function cacheWithTags(Request $request, \Closure $next, RouteCacheConfig $config): mixed
{
$key = $this->route_cache_manager->getKey($request, $config);
$key = $this->route_cache_manager->get_key($request, $config);

return Cache::tags([$config->tag])->remember($key, Configs::getValueAsInt('cache_ttl'), fn () => $next($request));
}
120 changes: 120 additions & 0 deletions app/Listeners/AlbumCacheCleaner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace App\Listeners;

use App\Enum\CacheTag;
use App\Enum\SmartAlbumType;
use App\Events\AlbumRouteCacheUpdated;
use App\Metadata\Cache\RouteCacheManager;
use App\Models\BaseAlbumImpl;
use Illuminate\Support\Facades\Cache;

class AlbumCacheCleaner
{
/**
* Create the event listener.
*/
public function __construct(
private RouteCacheManager $route_cache_manager,
) {
}

/**
* Handle the event.
*/
public function handle(AlbumRouteCacheUpdated $event): void
{
// The quick way.
if (Cache::supportsTags()) {
Cache::tags(CacheTag::GALLERY->value)->flush();

return;
}

$this->dropCachedRoutesWithoutExtra();

// By default we refresh all the smart albums.
$this->handleSmartAlbums();

if ($event->album_id === null) {
$this->handleAllAlbums();

return;
}

// Root album => already taken care of with the route without extra.
if ($event->album_id === '') {
return;
}

$this->handleAlbumId($event->album_id);
}

/**
* Drop cache for all routes without extra (meaning which do not depend on album_id).
*
* @return void
*/
private function dropCachedRoutesWithoutExtra(): void
{
$cached_routes_without_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, without_extra: true);
foreach ($cached_routes_without_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route);
Cache::forget($cache_key);
}
}

/**
* Drop cache for all routes related to albums.
*
* @return void
*/
private function handleAllAlbums(): void
{
// The long way.
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
BaseAlbumImpl::select('id')->get()->each(function (BaseAlbumImpl $album) use ($cached_routes_with_extra) {
$extra = ['album_id' => $album->id];
foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
}
});
}

/**
* Drop cache fro all routes related to an album.
*
* @param string $album_id
*
* @return void
*/
private function handleAlbumId(string $album_id): void
{
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
$extra = ['album_id' => $album_id];

foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
}
}

/**
* Drop cache for all smart albums too.
*
* @return void
*/
private function handleSmartAlbums(): void
{
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
// Also reset smart albums ;)
collect(SmartAlbumType::cases())->each(function (SmartAlbumType $type) use ($cached_routes_with_extra) {
$extra = ['album_id' => $type->value];
foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
}
});
}
}
16 changes: 8 additions & 8 deletions app/Listeners/CacheListener.php
Original file line number Diff line number Diff line change
@@ -2,31 +2,31 @@

namespace App\Listeners;

use App\Models\Configs;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Support\Facades\Log;

class CacheListener
{
/**
* Create the event listener.
*/
public function __construct()
{
}

/**
* Handle the event.
*/
public function handle(CacheHit|CacheMissed $event): void
public function handle(CacheHit|CacheMissed|KeyForgotten $event): void
{
if (str_contains($event->key, 'lv:dev-lycheeOrg')) {
return;
}

if (Configs::getValueAsBool('cache_event_logging') === false) {
return;
}

match (get_class($event)) {
CacheMissed::class => Log::info('CacheListener: Miss for ' . $event->key),
CacheHit::class => Log::info('CacheListener: Hit for ' . $event->key),
KeyForgotten::class => Log::info('CacheListener: Forgetting key ' . $event->key),
default => '',
};
}
35 changes: 35 additions & 0 deletions app/Listeners/TaggedRouteCacheCleaner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Listeners;

use App\Events\TaggedRouteCacheUpdated;
use App\Metadata\Cache\RouteCacheManager;
use Illuminate\Support\Facades\Cache;

class TaggedRouteCacheCleaner
{
/**
* Create the event listener.
*/
public function __construct(
private RouteCacheManager $route_cache_manager,
) {
}

/**
* Handle the event.
*/
public function handle(TaggedRouteCacheUpdated $event): void
{
$cached_routes = $this->route_cache_manager->retrieve_keys_for_tag($event->tag);

foreach ($cached_routes as $route) {
$cache_key = $this->route_cache_manager->gen_key($route);
Cache::forget($cache_key);
}

if (Cache::supportsTags()) {
Cache::tags($event->tag->value)->flush();
}
}
}
10 changes: 6 additions & 4 deletions app/Metadata/Cache/RouteCacheConfig.php
Original file line number Diff line number Diff line change
@@ -2,19 +2,21 @@

namespace App\Metadata\Cache;

use App\Enum\CacheTag;

final readonly class RouteCacheConfig
{
/**
* Configuration of a route caching.
*
* @param string|null $tag tags to quickly find the keys that need to be cleared
* @param bool $user_dependant whether the route has data depending of the user
* @param string[] $extra extra parameters to be used in the cache key
* @param CacheTag|null $tag tags to quickly find the keys that need to be cleared
* @param bool $user_dependant whether the route has data depending of the user
* @param string[] $extra extra parameters to be used in the cache key
*
* @return void
*/
public function __construct(
public ?string $tag,
public ?CacheTag $tag,
public bool $user_dependant = false,
public array $extra = [],
) {
109 changes: 59 additions & 50 deletions app/Metadata/Cache/RouteCacheManager.php
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@

namespace App\Metadata\Cache;

use App\Contracts\Http\Requests\RequestAttribute;
use App\Enum\CacheTag;
use App\Exceptions\Internal\LycheeLogicException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

final readonly class RouteCacheManager
@@ -23,28 +25,28 @@
public function __construct()
{
$this->cache_list = [
'api/v2/Album' => new RouteCacheConfig(tag: 'gallery', user_dependant: true, extra: ['album_id']),
'api/v2/Album' => new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true, extra: [RequestAttribute::ALBUM_ID_ATTRIBUTE]),
'api/v2/Album::getTargetListAlbums' => false, // TODO: cache me later.
'api/v2/Albums' => new RouteCacheConfig(tag: 'gallery', user_dependant: true),
'api/v2/Auth::config' => new RouteCacheConfig(tag: 'auth', user_dependant: true),
'api/v2/Auth::rights' => new RouteCacheConfig(tag: 'auth', user_dependant: true),
'api/v2/Auth::user' => new RouteCacheConfig(tag: 'user', user_dependant: true),
'api/v2/Albums' => new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true),
'api/v2/Auth::config' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),
'api/v2/Auth::rights' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),
'api/v2/Auth::user' => new RouteCacheConfig(tag: CacheTag::USER, user_dependant: true),
'api/v2/Diagnostics' => false,
'api/v2/Diagnostics::config' => false,
'api/v2/Diagnostics::info' => new RouteCacheConfig(tag: 'settings', user_dependant: true),
'api/v2/Diagnostics::permissions' => new RouteCacheConfig(tag: 'settings', user_dependant: true),
'api/v2/Diagnostics::space' => new RouteCacheConfig(tag: 'settings', user_dependant: true),
'api/v2/Diagnostics::info' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),
'api/v2/Diagnostics::permissions' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),
'api/v2/Diagnostics::space' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),

// Response must be different for each call.
'api/v2/Frame' => false,

'api/v2/Gallery::Footer' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Gallery::Init' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Gallery::getLayout' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Gallery::getUploadLimits' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Gallery::Footer' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Gallery::Init' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Gallery::getLayout' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Gallery::getUploadLimits' => new RouteCacheConfig(tag: CacheTag::SETTINGS),

'api/v2/Jobs' => false, // TODO: fix me later
'api/v2/LandingPage' => new RouteCacheConfig(tag: 'settings'),
'api/v2/LandingPage' => new RouteCacheConfig(tag: CacheTag::SETTINGS),

// We do not need to cache those.
'api/v2/Maintenance::cleaning' => false,
@@ -55,26 +57,26 @@ public function __construct()
'api/v2/Maintenance::tree' => false,
'api/v2/Maintenance::update' => false,

'api/v2/Map' => new RouteCacheConfig(tag: 'gallery', user_dependant: true, extra: ['album_id']),
'api/v2/Map::provider' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Oauth' => new RouteCacheConfig(tag: 'user', user_dependant: true),
'api/v2/Map' => new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true, extra: [RequestAttribute::ALBUM_ID_ATTRIBUTE]),
'api/v2/Map::provider' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Oauth' => new RouteCacheConfig(tag: CacheTag::USER, user_dependant: true),

// Response must be different for each call.
'api/v2/Photo::random' => false,

'api/v2/Search' => false, // TODO: how to support pagination ?? new RouteCacheConfig(tag: 'gallery', user_dependant: true, extra: ['album_id', 'terms']),
'api/v2/Search::init' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Settings' => new RouteCacheConfig(tag: 'settings', user_dependant: true),
'api/v2/Settings::getLanguages' => new RouteCacheConfig(tag: 'settings'),
'api/v2/Sharing' => new RouteCacheConfig(tag: 'gallery', user_dependant: true, extra: ['album_id']),
'api/v2/Sharing::all' => new RouteCacheConfig(tag: 'gallery', user_dependant: true),
'api/v2/Statistics::albumSpace' => new RouteCacheConfig(tag: 'statistics', user_dependant: true),
'api/v2/Statistics::sizeVariantSpace' => new RouteCacheConfig(tag: 'statistics', user_dependant: true),
'api/v2/Statistics::totalAlbumSpace' => new RouteCacheConfig(tag: 'statistics', user_dependant: true),
'api/v2/Statistics::userSpace' => new RouteCacheConfig(tag: 'statistics', user_dependant: true),
'api/v2/UserManagement' => new RouteCacheConfig(tag: 'users', user_dependant: true),
'api/v2/Users' => new RouteCacheConfig(tag: 'users', user_dependant: true),
'api/v2/Users::count' => new RouteCacheConfig(tag: 'users', user_dependant: true),
'api/v2/Search' => false, // TODO: how to support pagination ?? new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true, extra: ['album_id', 'terms']),
'api/v2/Search::init' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Settings' => new RouteCacheConfig(tag: CacheTag::SETTINGS, user_dependant: true),
'api/v2/Settings::getLanguages' => new RouteCacheConfig(tag: CacheTag::SETTINGS),
'api/v2/Sharing' => new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true, extra: [RequestAttribute::ALBUM_ID_ATTRIBUTE]),
'api/v2/Sharing::all' => new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true),
'api/v2/Statistics::albumSpace' => new RouteCacheConfig(tag: CacheTag::STATISTICS, user_dependant: true),
'api/v2/Statistics::sizeVariantSpace' => new RouteCacheConfig(tag: CacheTag::STATISTICS, user_dependant: true),
'api/v2/Statistics::totalAlbumSpace' => new RouteCacheConfig(tag: CacheTag::STATISTICS, user_dependant: true),
'api/v2/Statistics::userSpace' => new RouteCacheConfig(tag: CacheTag::STATISTICS, user_dependant: true),
'api/v2/UserManagement' => new RouteCacheConfig(tag: CacheTag::USERS, user_dependant: true),
'api/v2/Users' => new RouteCacheConfig(tag: CacheTag::USERS, user_dependant: true),
'api/v2/Users::count' => new RouteCacheConfig(tag: CacheTag::USERS, user_dependant: true),
'api/v2/Version' => false,
'api/v2/WebAuthn' => false,

@@ -83,7 +85,7 @@ public function __construct()
];
}

public function getConfig(string $uri): RouteCacheConfig|false
public function get_config(string $uri): RouteCacheConfig|false
{
if (!array_key_exists($uri, $this->cache_list)) {
Log::warning('ResponseCache: No cache config for ' . $uri);
@@ -94,7 +96,7 @@ public function getConfig(string $uri): RouteCacheConfig|false
return $this->cache_list[$uri];
}

public function getKey(Request $request, RouteCacheConfig $config): string
public function get_key(Request $request, RouteCacheConfig $config): string
{
$key = self::REQUEST . $request->route()->uri;

@@ -119,15 +121,25 @@ public function getKey(Request $request, RouteCacheConfig $config): string
/**
* Generate a key for the cache.
*
* @param RouteCacheConfig $config
* @param string $uri
* @param int|null $userId
* @param array<string,string> $extras
* @param ?RouteCacheConfig $config
*
* @return string
*/
public function genKey(RouteCacheConfig $config, string $uri, ?int $userId, array $extras): string
{
public function gen_key(
string $uri,
?int $userId = null,
array $extras = [],
?RouteCacheConfig $config = null,
): string {
$config ??= $this->cache_list[$uri] ?? throw new LycheeLogicException('No cache config for ' . $uri);

if ($config === false) {
throw new LycheeLogicException($uri . ' is not supposed to be cached.');
}

$key = self::REQUEST . $uri;

// If the request is user dependant, we add the user id to the key.
@@ -152,9 +164,9 @@ public function genKey(RouteCacheConfig $config, string $uri, ?int $userId, arra
*
* @param string $uri
*
* @return string|false
* @return CacheTag|false
*/
public function getTag(string $uri): string|false
public function get_tag(string $uri): CacheTag|false
{
if (!array_key_exists($uri, $this->cache_list)) {
return false;
@@ -170,29 +182,26 @@ public function getTag(string $uri): string|false
/**
* Given a tag, return all the routes associated to this tag.
*
* @param string $tag
* @param CacheTag $tag
*
* @return string[]
*/
public function retrieveKeysForTag(string $tag): array
public function retrieve_keys_for_tag(CacheTag $tag, bool $with_extra = false, bool $without_extra = false): array
{
$keys = [];
foreach ($this->cache_list as $uri => $value) {
if (is_array($value) && array_key_exists('tag', $value) && $value['tag'] === $tag) {
if (
$value !== false &&
$value->tag === $tag &&
// Either with extra is set to false => ignore condition
// Or with extra is set to true and we have extra parameters => ignore condition
($with_extra === false || count($value->extra) > 0) &&
($without_extra === false || count($value->extra) === 0)
) {
$keys[] = $uri;
}
}

return $keys;
}

public function clearTag(string $tag): void
{
$keys = $this->retrieveKeysForTag($tag);
// TODO: refine.

foreach ($keys as $key) {
Cache::forget($key);
}
}
}
9 changes: 9 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
@@ -8,10 +8,14 @@
use App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy;
use App\Contracts\Models\AbstractSizeVariantNamingStrategy;
use App\Contracts\Models\SizeVariantFactory;
use App\Events\AlbumRouteCacheUpdated;
use App\Events\TaggedRouteCacheUpdated;
use App\Factories\AlbumFactory;
use App\Image\SizeVariantDefaultFactory;
use App\Image\StreamStatFilter;
use App\Listeners\AlbumCacheCleaner;
use App\Listeners\CacheListener;
use App\Listeners\TaggedRouteCacheCleaner;
use App\Metadata\Json\CommitsRequest;
use App\Metadata\Json\UpdateRequest;
use App\Metadata\Versions\FileVersion;
@@ -26,6 +30,7 @@
use App\Policies\SettingsPolicy;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -93,6 +98,10 @@ public function boot()
{
Event::listen(CacheHit::class, CacheListener::class . '@handle');
Event::listen(CacheMissed::class, CacheListener::class . '@handle');
Event::listen(KeyForgotten::class, CacheListener::class . '@handle');

Event::listen(AlbumRouteCacheUpdated::class, AlbumCacheCleaner::class . '@handle');
Event::listen(TaggedRouteCacheUpdated::class, TaggedRouteCacheCleaner::class . '@handle');

// Prohibits: db:wipe, migrate:fresh, migrate:refresh, and migrate:reset
DB::prohibitDestructiveCommands(config('app.env', 'production') !== 'dev');
10 changes: 10 additions & 0 deletions database/migrations/2024_12_28_190150_caching_config.php
Original file line number Diff line number Diff line change
@@ -16,6 +16,16 @@ public function getConfigs(): array
'is_secret' => false,
'level' => 0,
],
[
'key' => 'cache_event_logging',
'value' => '1', // TODO: flip to false
'cat' => 'Mod Cache',
'type_range' => self::BOOL,
'description' => 'Add log lines for events related to caching.',
'details' => 'This may result in large amount of logs',
'is_secret' => true,
'level' => 0,
],
[
'key' => 'cache_ttl',
'value' => '60',
4 changes: 2 additions & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -126,15 +126,15 @@ parameters:
# paths:
# - tests
-
message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assert(Is)?(Not)?(True|False|Equals|Int|Null|Empty|Count)\(\)#'
message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assert(Is)?(Not)?(True|False|Equals|Int|Null|Empty|Count|Array|Object)\(\)#'
paths:
- tests
-
message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assertFile(DoesNot)?Exists?\(\)#'
paths:
- tests
-
message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assertString(Not)?(Contains|Ends|Starts)(String|With)\(\)#'
message: '#Dynamic call to static method PHPUnit\\Framework\\Assert::assert(String)?(Not)?(Contains|Ends|Starts)(String|With)?\(\)#'
paths:
- tests
-
78 changes: 78 additions & 0 deletions tests/Unit/Metadata/Cache/RouteCacheManagerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/**
* We don't care for unhandled exceptions in tests.
* It is the nature of a test to throw an exception.
* Without this suppression we had 100+ Linter warning in this file which
* don't help anything.
*
* @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection
*/

namespace Tests\Unit\Metadata\Cache;

use App\Enum\CacheTag;
use App\Metadata\Cache\RouteCacheManager;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Log;
use Tests\AbstractTestCase;

class RouteCacheManagerTest extends AbstractTestCase
{
use DatabaseTransactions;
private RouteCacheManager $route_cache_manager;

public function setUp(): void
{
parent::setUp();
$this->route_cache_manager = new RouteCacheManager();
}

public function testNoConfig(): void
{
Log::shouldReceive('warning')->once();
$this->assertFalse($this->route_cache_manager->get_config('fake_url'));
}

public function testConfigFalse(): void
{
$this->assertFalse($this->route_cache_manager->get_config('api/v2/Version'));
}

public function testConfigValid(): void
{
$this->assertIsObject($this->route_cache_manager->get_config('api/v2/Album'));
}

public function testGenKey(): void
{
$this->assertEquals('R:api/v2/Albums|U:', $this->route_cache_manager->gen_key('api/v2/Albums'));
$this->assertEquals('R:api/v2/Albums|U:1', $this->route_cache_manager->gen_key('api/v2/Albums', 1));
$this->assertEquals('R:api/v2/Album|U:1|E::2', $this->route_cache_manager->gen_key('api/v2/Album', 1, ['album_id' => '2']));
}

public function testGetFromTag(): void
{
$routes = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY);
$this->assertIsArray($routes);
$this->assertContains('api/v2/Album', $routes);
}

public function testGetFromTagWithExtra(): void
{
$routes = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
$this->assertIsArray($routes);
$this->assertContains('api/v2/Album', $routes);
$this->assertNotContains('api/v2/Albums', $routes);
}

public function testGetFromTagWithOutExtra(): void
{
$routes = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, without_extra: true);
$this->assertIsArray($routes);
$this->assertContains('api/v2/Albums', $routes);
$this->assertNotContains('api/v2/Album', $routes);
}
}

0 comments on commit c63d7c2

Please sign in to comment.