diff --git a/app/Actions/Photo/Create.php b/app/Actions/Photo/Create.php index 060595e9259..87107c187f2 100644 --- a/app/Actions/Photo/Create.php +++ b/app/Actions/Photo/Create.php @@ -32,10 +32,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 { @@ -218,6 +218,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 @@ -317,7 +318,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) { diff --git a/app/Assets/Helpers.php b/app/Assets/Helpers.php index d73aff0d12e..018c50d0dff 100644 --- a/app/Assets/Helpers.php +++ b/app/Assets/Helpers.php @@ -10,6 +10,7 @@ use App\Exceptions\Internal\ZeroModuloException; use Exception; +use Illuminate\Http\Request; use Illuminate\Support\Facades\File; use function Safe\ini_get; @@ -289,4 +290,23 @@ public function exceptionTraceToText(\Exception $e): string 'line' => $err['line'] ?? '?', 'function' => $err['function']])->all()); } + + /** + * Given a request return the uri WITH the query paramters. + * This makes sure that we handle the case where the query parameters are empty or contains an album id or pagination. + * + * @param Request $request + * + * @return string + */ + public function getUriWithQueryString(Request $request): string + { + /** @var array|null $query */ + $query = $request->query(); + if ($query === null || $query === []) { + return $request->path(); + } + + return $request->path() . '?' . http_build_query($query); + } } diff --git a/app/Enum/CacheTag.php b/app/Enum/CacheTag.php new file mode 100644 index 00000000000..892cbdf2d69 --- /dev/null +++ b/app/Enum/CacheTag.php @@ -0,0 +1,24 @@ +update($albumInstance, $request->albums(), $keyName); + + AlbumRouteCacheUpdated::dispatch(); } /** diff --git a/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php b/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php index e6fb97b2484..d160961839b 100644 --- a/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php +++ b/app/Http/Controllers/Admin/Maintenance/GenSizeVariants.php @@ -10,6 +10,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; @@ -62,6 +63,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 } } diff --git a/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php b/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php index a5133124a6c..b5880abb148 100644 --- a/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php +++ b/app/Http/Controllers/Admin/Maintenance/MissingFileSizes.php @@ -9,6 +9,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; @@ -58,6 +59,8 @@ public function do(MaintenanceRequest $request): void } // @codeCoverageIgnoreEnd } + + AlbumRouteCacheUpdated::dispatch(); } /** diff --git a/app/Http/Controllers/Admin/Maintenance/RegisterController.php b/app/Http/Controllers/Admin/Maintenance/RegisterController.php index 166aee77d40..e882f3c7c00 100644 --- a/app/Http/Controllers/Admin/Maintenance/RegisterController.php +++ b/app/Http/Controllers/Admin/Maintenance/RegisterController.php @@ -8,6 +8,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; @@ -37,6 +39,8 @@ public function __invoke(RegisterRequest $request): RegisterData // Not valid, reset the key. Configs::set('license_key', ''); + TaggedRouteCacheUpdated::dispatch(CacheTag::SETTINGS); + return new RegisterData(false); } } diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 21c9f8d37c3..b29269cc62f 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -8,6 +8,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; @@ -55,6 +57,7 @@ public function setConfigs(SetConfigsRequest $request): ConfigCollectionResource }); Configs::invalidateCache(); + TaggedRouteCacheUpdated::dispatch(CacheTag::SETTINGS); return new ConfigCollectionResource(Configs::orderBy('cat', 'asc')->get()); } diff --git a/app/Http/Controllers/Admin/UserManagementController.php b/app/Http/Controllers/Admin/UserManagementController.php index 62fe52908e1..00ced871baa 100644 --- a/app/Http/Controllers/Admin/UserManagementController.php +++ b/app/Http/Controllers/Admin/UserManagementController.php @@ -11,6 +11,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; @@ -66,6 +68,8 @@ public function save(SetUserSettingsRequest $request, Save $save): void quota_kb: $request->quota_kb(), note: $request->note() ); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USERS); } /** @@ -84,6 +88,8 @@ public function delete(DeleteUserRequest $request): void throw new UnauthorizedException('You are not allowed to delete yourself'); } $request->user2()->delete(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USERS); } /** @@ -105,6 +111,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()); } } \ No newline at end of file diff --git a/app/Http/Controllers/Gallery/AlbumController.php b/app/Http/Controllers/Gallery/AlbumController.php index 2a73eb4518d..dd39bd1d6e2 100644 --- a/app/Http/Controllers/Gallery/AlbumController.php +++ b/app/Http/Controllers/Gallery/AlbumController.php @@ -21,6 +21,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; @@ -114,6 +115,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; } @@ -146,7 +150,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); } @@ -173,6 +178,7 @@ public function updateTagAlbum(UpdateTagAlbumRequest $request): EditableBaseAlbu $album->photo_timeline = $request->photo_timeline(); $album->save(); + // Root return EditableBaseAlbumResource::fromModel($album); } @@ -185,26 +191,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()); @@ -250,6 +276,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()); } @@ -263,6 +290,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()); } @@ -373,4 +401,4 @@ public function deleteTrack(DeleteTrackRequest $request): void { $request->album()->deleteTrack(); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 749b327f38c..b6ec134bb49 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -9,7 +9,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; @@ -98,6 +100,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(); } @@ -127,6 +131,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); } /** diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 5b1a1d938bc..840a600b34f 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -11,6 +11,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; @@ -54,6 +56,9 @@ public function update(UpdateProfileRequest $request, UpdateLogin $updateLogin): ); $currentUser->save(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); + // Update the session with the new credentials of the user. // Otherwise, the session is out-of-sync and falsely assumes the user // to be unauthenticated upon the next request. @@ -75,6 +80,8 @@ public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset): { $token = $tokenReset->do(); + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); + return new UserToken($token); } @@ -89,5 +96,7 @@ public function resetToken(ChangeTokenRequest $request, TokenReset $tokenReset): public function unsetToken(ChangeTokenRequest $request, TokenDisable $tokenDisable): void { $tokenDisable->do(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); } } diff --git a/app/Http/Controllers/WebAuthn/WebAuthnManageController.php b/app/Http/Controllers/WebAuthn/WebAuthnManageController.php index 445ae2ed5c3..b8c04a4e212 100644 --- a/app/Http/Controllers/WebAuthn/WebAuthnManageController.php +++ b/app/Http/Controllers/WebAuthn/WebAuthnManageController.php @@ -8,6 +8,8 @@ namespace App\Http\Controllers\WebAuthn; +use App\Enum\CacheTag; +use App\Events\TaggedRouteCacheUpdated; use App\Exceptions\UnauthenticatedException; use App\Http\Requests\WebAuthn\DeleteCredentialRequest; use App\Http\Requests\WebAuthn\EditCredentialRequest; @@ -47,6 +49,8 @@ public function delete(DeleteCredentialRequest $request): void $user = Auth::user() ?? throw new UnauthenticatedException(); $user->webAuthnCredentials()->where('id', $request->getId())->delete(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); } /** @@ -61,5 +65,7 @@ public function edit(EditCredentialRequest $request): void $credential = $request->getCredential(); $credential->alias = $request->getAlias(); $credential->save(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); } } diff --git a/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php b/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php index 7b5d3fb791d..4f9996a56b7 100644 --- a/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php +++ b/app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php @@ -8,6 +8,8 @@ namespace App\Http\Controllers\WebAuthn; +use App\Enum\CacheTag; +use App\Events\TaggedRouteCacheUpdated; use App\Exceptions\UnauthenticatedException; use Illuminate\Contracts\Support\Responsable; use Illuminate\Routing\Controller; @@ -46,5 +48,7 @@ public function register(AttestedRequest $request): void /** @disregard P1014 */ $request->user = Auth::user() ?? throw new UnauthenticatedException(); $request->save(); + + TaggedRouteCacheUpdated::dispatch(CacheTag::USER); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 6f0c3226415..a27cc4266c5 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -77,6 +77,8 @@ class Kernel extends HttpKernel \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\Latency::class, + 'response_cache', + 'album_cache_refresher', ], ]; @@ -96,9 +98,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\Caching\ResponseCache::class, + 'album_cache_refresher' => \App\Http\Middleware\Caching\AlbumRouteCacheRefresher::class, ]; } diff --git a/app/Http/Middleware/Caching/AlbumRouteCacheRefresher.php b/app/Http/Middleware/Caching/AlbumRouteCacheRefresher.php new file mode 100644 index 00000000000..a5fcb67c6b7 --- /dev/null +++ b/app/Http/Middleware/Caching/AlbumRouteCacheRefresher.php @@ -0,0 +1,130 @@ +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); + } +} diff --git a/app/Http/Middleware/CacheControl.php b/app/Http/Middleware/Caching/CacheControl.php similarity index 95% rename from app/Http/Middleware/CacheControl.php rename to app/Http/Middleware/Caching/CacheControl.php index 72a9aae96ec..312b62622ec 100644 --- a/app/Http/Middleware/CacheControl.php +++ b/app/Http/Middleware/Caching/CacheControl.php @@ -6,7 +6,7 @@ * Copyright (c) 2018-2025 LycheeOrg. */ -namespace App\Http\Middleware; +namespace App\Http\Middleware\Caching; use Illuminate\Http\Request; diff --git a/app/Http/Middleware/Caching/ResponseCache.php b/app/Http/Middleware/Caching/ResponseCache.php new file mode 100644 index 00000000000..33c238a26bc --- /dev/null +++ b/app/Http/Middleware/Caching/ResponseCache.php @@ -0,0 +1,66 @@ +method() !== 'GET') { + return $next($request); + } + + if (Configs::getValueAsBool('cache_enabled') === false) { + return $next($request); + } + + $uri = $request->route()->uri; + $config = $this->route_cache_manager->get_config($uri); + + // Check with the route manager if we can cache this route. + if ($config === false) { + return $next($request); + } + + $key = $this->route_cache_manager->get_key($request, $config); + + $extras = []; + foreach ($config->extra as $extra) { + $extras[] = $request->input($extra) ?? ''; + } + + return $this->route_cacher->remember($key, $uri, Configs::getValueAsInt('cache_ttl'), fn () => $next($request), $extras); + } +} diff --git a/app/Http/Middleware/ConfigIntegrity.php b/app/Http/Middleware/ConfigIntegrity.php index 0af67abf85f..0a1dd293086 100644 --- a/app/Http/Middleware/ConfigIntegrity.php +++ b/app/Http/Middleware/ConfigIntegrity.php @@ -29,6 +29,7 @@ class ConfigIntegrity 'timeline_album_date_format_month', 'timeline_album_date_format_day', 'number_albums_per_row_mobile', + 'cache_ttl', ]; /** diff --git a/app/Listeners/AlbumCacheCleaner.php b/app/Listeners/AlbumCacheCleaner.php new file mode 100644 index 00000000000..f94085293b4 --- /dev/null +++ b/app/Listeners/AlbumCacheCleaner.php @@ -0,0 +1,60 @@ +album_id === null) { + // this is a clear all. + $routes = $this->route_cache_manager->retrieve_routes_for_tag(CacheTag::GALLERY, 0); + foreach ($routes as $route) { + $this->route_cacher->forgetRoute($route); + } + + return; + } + + $routes = $this->route_cache_manager->retrieve_routes_for_tag(CacheTag::GALLERY, RouteCacheManager::ONLY_WITHOUT_EXTRA); + foreach ($routes as $route) { + $this->route_cacher->forgetRoute($route); + } + + // Clear smart albums. Simple. + collect(SmartAlbumType::cases())->each(function (SmartAlbumType $type) { + $this->route_cacher->forgetTag($type->value); + }); + + if ($event->album_id !== '') { + $this->route_cacher->forgetTag($event->album_id); + } + } +} diff --git a/app/Listeners/CacheListener.php b/app/Listeners/CacheListener.php new file mode 100644 index 00000000000..476821aec10 --- /dev/null +++ b/app/Listeners/CacheListener.php @@ -0,0 +1,55 @@ +key, 'lv:dev-lycheeOrg')) { + return; + } + + if (Configs::getValueAsBool('cache_event_logging') === false) { + return; + } + + match (get_class($event)) { + CacheMissed::class => Log::debug('CacheListener: Miss for ' . $event->key), + CacheHit::class => Log::debug('CacheListener: Hit for ' . $event->key), + KeyForgotten::class => Log::info('CacheListener: Forgetting key ' . $event->key), + KeyWritten::class => $this->keyWritten($event), + default => '', + }; + } + + private function keyWritten(KeyWritten $event): void + { + if (!str_starts_with($event->key, 'api/')) { + Log::info('CacheListener: Writing key ' . $event->key); + + return; + } + + Log::debug('CacheListener: Writing key ' . $event->key . ' with value: ' . var_export($event->value, true)); + } +} diff --git a/app/Listeners/TaggedRouteCacheCleaner.php b/app/Listeners/TaggedRouteCacheCleaner.php new file mode 100644 index 00000000000..0865e114a6f --- /dev/null +++ b/app/Listeners/TaggedRouteCacheCleaner.php @@ -0,0 +1,36 @@ +route_cache_manager->retrieve_routes_for_tag($event->tag, 0); + foreach ($cached_routes as $route) { + $this->route_cacher->forgetRoute($route); + } + } +} diff --git a/app/Metadata/Cache/RouteCacheConfig.php b/app/Metadata/Cache/RouteCacheConfig.php new file mode 100644 index 00000000000..1cf86a848d9 --- /dev/null +++ b/app/Metadata/Cache/RouteCacheConfig.php @@ -0,0 +1,30 @@ + */ + private array $cache_list; + + /** + * Initalize the cache list. + * + * @return void + */ + public function __construct() + { + $this->cache_list = [ + '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: 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), + + // We do not want to cache diagnostics errors and config as they are a debugging tool. The MUST represent the state of Lychee at any time. + 'api/v2/Diagnostics' => false, + 'api/v2/Diagnostics::config' => false, + 'api/v2/Diagnostics::info' => false, + 'api/v2/Diagnostics::permissions' => false, + // We can cache the space computation because it is not changing often and very computationally heavy. + '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: 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: CacheTag::SETTINGS), + + // We do not need to cache those. + 'api/v2/Maintenance::cleaning' => false, + 'api/v2/Maintenance::fullTree' => false, + 'api/v2/Maintenance::genSizeVariants' => false, + 'api/v2/Maintenance::jobs' => false, + 'api/v2/Maintenance::missingFileSize' => false, + 'api/v2/Maintenance::tree' => false, + 'api/v2/Maintenance::update' => false, + + '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), + 'api/v2/WebAuthn' => new RouteCacheConfig(tag: CacheTag::USER, user_dependant: true), + + // Response must be different for each call. + 'api/v2/Photo::random' => false, + + // Ideally we should cache the search results, unfortunately it is not clear how to handle the pagination and the parts of the query. + // Furthermore the result of the serach depends of the user. Making the caching strategy more complex. + // TODO: how to support pagination ?? new RouteCacheConfig(tag: CacheTag::GALLERY, user_dependant: true, extra: ['album_id', 'terms']), + 'api/v2/Search' => false, + '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, + + // This is returning a stream, we do not cache it. + 'api/v2/Zip' => 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); + + return false; + } + + return $this->cache_list[$uri]; + } + + public function get_key(Request $request, RouteCacheConfig $config): string + { + $key = self::REQUEST . Helpers::getUriWithQueryString($request); + + // If the request is user dependant, we add the user id to the key. + // That way we ensure that this does not contaminate between logged in and looged out users. + if ($config->user_dependant) { + $key .= self::USER . Auth::id(); + } + + return $key; + } + + /** + * Return the tag associated to the route if there is one. + * Return false if there is no tag for this route or if the route is not cached (just safe precaution). + * + * @param string $uri + * + * @return CacheTag|false + */ + public function get_tag(string $uri): CacheTag|false + { + if (!array_key_exists($uri, $this->cache_list)) { + return false; + } + + if ($this->cache_list[$uri] === false) { + return false; + } + + return $this->cache_list[$uri]->tag; + } + + /** + * Given a tag, return all the routes associated to this tag. + * + * @param CacheTag $tag + * @param int $flag composed of RouteCacheManager::ONLY_WITH_EXTRA and RouteCacheManager::ONLY_WITHOUT_EXTRA + * + * @return string[] + */ + public function retrieve_routes_for_tag(CacheTag $tag, int $flag): array + { + $routes = []; + foreach ($this->cache_list as $uri => $value) { + if ( + $value !== false && + $value->tag === $tag && + // Either with ONLY_WITH_EXTRA flag not set => ignore condition + // Or with ONLY_WITH_EXTRA flag set and we have extra parameters => ignore condition + (($flag & self::ONLY_WITH_EXTRA) === 0 || count($value->extra) > 0) && + (($flag & self::ONLY_WITHOUT_EXTRA) === 0 || count($value->extra) === 0) + ) { + $routes[] = $uri; + } + } + + return $routes; + } +} diff --git a/app/Metadata/Cache/RouteCacher.php b/app/Metadata/Cache/RouteCacher.php new file mode 100644 index 00000000000..ddca4dc78a2 --- /dev/null +++ b/app/Metadata/Cache/RouteCacher.php @@ -0,0 +1,142 @@ +rememberRoute($route, $key); + + // Update the tags for the given key. + $this->rememberTags($tags, $key); + + return $value; + } + + /** + * Forget all the keys related to the given route. + * + * @param string $route + * + * @return void + */ + public function forgetRoute(string $route): void + { + $keys = Cache::get($route, []); + + foreach (array_keys($keys) as $key) { + if (!is_string($key)) { + throw new LycheeLogicException('The keys should be a string'); + } + + Cache::forget($key); + } + + Cache::forget($route); + } + + /** + * Forget all the keys related to the given tag. + * + * @param string $tag + * + * @return void + */ + public function forgetTag(string $tag): void + { + $keys = Cache::get(self::TAG . $tag, []); + + foreach (array_keys($keys) as $key) { + if (!is_string($key)) { + throw new LycheeLogicException('The keys should be a string'); + } + + Cache::forget($key); + } + + Cache::forget(self::TAG . $tag); + } + + /** + * Remember the route for the given key. + * This allows to later erase all the keys related to the route. + * + * @param string $route + * @param string $key + * + * @return void + */ + private function rememberRoute(string $route, string $key): void + { + $already_cached_for_routes = Cache::get($route, []); + $already_cached_for_routes[$key] = true; + Cache::put($route, $already_cached_for_routes); + } + + /** + * This is like the function above: rememberRoute() but with specific tags. + * That way we can later erase all the keys related to the tag (e.g. the album id). + * + * @param string[] $tags + * @param string $key + * + * @return void + */ + private function rememberTags(array $tags, string $key): void + { + foreach ($tags as $tag) { + $already_cached_for_tag = Cache::get(self::TAG . $tag, []); + $already_cached_for_tag[$key] = true; + Cache::put(self::TAG . $tag, $already_cached_for_tag); + } + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7ba1d204452..a641db3a630 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -14,9 +14,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; @@ -29,11 +34,16 @@ use App\Policies\AlbumQueryPolicy; use App\Policies\PhotoQueryPolicy; use App\Policies\SettingsPolicy; +use Illuminate\Cache\Events\CacheHit; +use Illuminate\Cache\Events\CacheMissed; +use Illuminate\Cache\Events\KeyForgotten; +use Illuminate\Cache\Events\KeyWritten; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\URL; @@ -93,6 +103,14 @@ class AppServiceProvider extends ServiceProvider */ 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(KeyWritten::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'); diff --git a/database/migrations/2024_12_28_190150_caching_config.php b/database/migrations/2024_12_28_190150_caching_config.php new file mode 100644 index 00000000000..ee4df0475cf --- /dev/null +++ b/database/migrations/2024_12_28_190150_caching_config.php @@ -0,0 +1,47 @@ + 'cache_enabled', + 'value' => '0', + 'cat' => 'Mod Cache', + 'type_range' => self::BOOL, + 'description' => 'Enable caching of responses given requests.', + 'details' => 'This will significantly speed up the response time of Lychee. If you are using password protected albums, you should not enable this.', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'cache_event_logging', + 'value' => '0', + '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' => '300', + 'cat' => 'Mod Cache', + 'type_range' => self::POSITIVE, + 'description' => 'Number of seconds responses should be cached.', + 'details' => 'Longer TTL will save more resources but may result in outdated responses.', + 'is_secret' => false, + 'level' => 1, + ], + ]; + } +}; diff --git a/lang/cz/maintenance.php b/lang/cz/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/cz/maintenance.php +++ b/lang/cz/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/de/maintenance.php b/lang/de/maintenance.php index d632d542d1e..6b9b0ec426a 100644 --- a/lang/de/maintenance.php +++ b/lang/de/maintenance.php @@ -56,4 +56,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'Keine Updates verfügbar.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; diff --git a/lang/el/maintenance.php b/lang/el/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/el/maintenance.php +++ b/lang/el/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/en/maintenance.php b/lang/en/maintenance.php index f54a9f54ca3..44b2ed00e9a 100644 --- a/lang/en/maintenance.php +++ b/lang/en/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; diff --git a/lang/es/maintenance.php b/lang/es/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/es/maintenance.php +++ b/lang/es/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/fr/maintenance.php b/lang/fr/maintenance.php index 3df6d002f01..35fa82b53fc 100644 --- a/lang/fr/maintenance.php +++ b/lang/fr/maintenance.php @@ -56,4 +56,9 @@ 'update-button' => 'Mettre à jour', 'no-pending-updates' => 'Aucune mise-à-jour disponible', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; diff --git a/lang/hu/maintenance.php b/lang/hu/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/hu/maintenance.php +++ b/lang/hu/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/it/maintenance.php b/lang/it/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/it/maintenance.php +++ b/lang/it/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/ja/maintenance.php b/lang/ja/maintenance.php index da622e41e20..547f4ca1245 100644 --- a/lang/ja/maintenance.php +++ b/lang/ja/maintenance.php @@ -56,4 +56,9 @@ 'update-button' => '更新', 'no-pending-updates' => '保留中の更新はありません', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; diff --git a/lang/nl/maintenance.php b/lang/nl/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/nl/maintenance.php +++ b/lang/nl/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/no/maintenance.php b/lang/no/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/no/maintenance.php +++ b/lang/no/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/pl/maintenance.php b/lang/pl/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/pl/maintenance.php +++ b/lang/pl/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/pt/maintenance.php b/lang/pt/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/pt/maintenance.php +++ b/lang/pt/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/ru/maintenance.php b/lang/ru/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/ru/maintenance.php +++ b/lang/ru/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/sk/maintenance.php b/lang/sk/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/sk/maintenance.php +++ b/lang/sk/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/sv/maintenance.php b/lang/sv/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/sv/maintenance.php +++ b/lang/sv/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/vi/maintenance.php b/lang/vi/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/vi/maintenance.php +++ b/lang/vi/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/zh_CN/maintenance.php b/lang/zh_CN/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/zh_CN/maintenance.php +++ b/lang/zh_CN/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/lang/zh_TW/maintenance.php b/lang/zh_TW/maintenance.php index f86de3d6f46..6a5f95681f0 100644 --- a/lang/zh_TW/maintenance.php +++ b/lang/zh_TW/maintenance.php @@ -57,4 +57,9 @@ 'update-button' => 'Update', 'no-pending-updates' => 'No pending update.', ], + 'flush-cache' => [ + 'title' => 'Flush Cache', + 'description' => 'Flush the cache of every user to solve invalidation problems.', + 'button' => 'Flush', + ], ]; \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 3665cc969dd..6c74fe3e435 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -37,8 +37,8 @@ - - + + diff --git a/resources/js/components/maintenance/MaintenanceFlushCache.vue b/resources/js/components/maintenance/MaintenanceFlushCache.vue new file mode 100644 index 00000000000..e0b6eba2638 --- /dev/null +++ b/resources/js/components/maintenance/MaintenanceFlushCache.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/resources/js/services/maintenance-service.ts b/resources/js/services/maintenance-service.ts index b4b3ddd66f6..0b174bba9a8 100644 --- a/resources/js/services/maintenance-service.ts +++ b/resources/js/services/maintenance-service.ts @@ -54,6 +54,10 @@ const MaintenanceService = { return axios.post(`${Constants.getApiUrl()}Maintenance::optimize`, {}); }, + flushDo(): Promise> { + return axios.post(`${Constants.getApiUrl()}Maintenance::flushCache`, {}); + }, + register(key: string): Promise> { return axios.post(`${Constants.getApiUrl()}Maintenance::register`, { key: key }); }, diff --git a/resources/js/views/Maintenance.vue b/resources/js/views/Maintenance.vue index 33d65d49140..4ac32223962 100644 --- a/resources/js/views/Maintenance.vue +++ b/resources/js/views/Maintenance.vue @@ -18,6 +18,7 @@ > + @@ -40,5 +41,6 @@ import MaintenanceFixTree from "@/components/maintenance/MaintenanceFixTree.vue" import MaintenanceGenSizevariants from "@/components/maintenance/MaintenanceGenSizevariants.vue"; import MaintenanceOptimize from "@/components/maintenance/MaintenanceOptimize.vue"; import MaintenanceUpdate from "@/components/maintenance/MaintenanceUpdate.vue"; +import MaintenanceFlushCache from "@/components/maintenance/MaintenanceFlushCache.vue"; import OpenLeftMenu from "@/components/headers/OpenLeftMenu.vue"; diff --git a/routes/api_v2.php b/routes/api_v2.php index f5dabb5a22e..a574af9161d 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -213,6 +213,7 @@ Route::get('/Maintenance::missingFileSize', [Admin\Maintenance\MissingFileSizes::class, 'check']); Route::post('/Maintenance::missingFileSize', [Admin\Maintenance\MissingFileSizes::class, 'do']); Route::post('/Maintenance::optimize', [Admin\Maintenance\Optimize::class, 'do']); +Route::post('/Maintenance::flushCache', [Admin\Maintenance\FlushCache::class, 'do']); Route::post('/Maintenance::register', Admin\Maintenance\RegisterController::class); Route::get('/Maintenance::fullTree', [Admin\Maintenance\FullTree::class, 'check']); Route::post('/Maintenance::fullTree', [Admin\Maintenance\FullTree::class, 'do']); diff --git a/tests/Feature_v2/Maintenance/FlushTest.php b/tests/Feature_v2/Maintenance/FlushTest.php new file mode 100644 index 00000000000..58eedde3f43 --- /dev/null +++ b/tests/Feature_v2/Maintenance/FlushTest.php @@ -0,0 +1,42 @@ +postJson('Maintenance::flushCache'); + $this->assertUnauthorized($response); + } + + public function testUser(): void + { + $response = $this->actingAs($this->userLocked)->postJson('Maintenance::flushCache'); + $this->assertForbidden($response); + } + + public function testAdmin(): void + { + $response = $this->actingAs($this->admin)->postJson('Maintenance::flushCache'); + $this->assertNoContent($response); + } +} \ No newline at end of file diff --git a/tests/Feature_v2/Statistics/AlbumSpaceTest.php b/tests/Feature_v2/Statistics/AlbumSpaceTest.php index 25f9b0f16f7..82fbd16110a 100644 --- a/tests/Feature_v2/Statistics/AlbumSpaceTest.php +++ b/tests/Feature_v2/Statistics/AlbumSpaceTest.php @@ -18,6 +18,7 @@ namespace Tests\Feature_v2\Statistics; +use App\Models\Configs; use LycheeVerify\Http\Middleware\VerifySupporterStatus; use Tests\Feature_v2\Base\BaseApiV2Test; @@ -25,6 +26,9 @@ class AlbumSpaceTest extends BaseApiV2Test { public function testAlbumSpaceTestUnauthorized(): void { + Configs::set('cache_enabled', '0'); + Configs::invalidateCache(); + $response = $this->getJson('Statistics::albumSpace'); $this->assertSupporterRequired($response); diff --git a/tests/Unit/Metadata/Cache/RouteCacheManagerTest.php b/tests/Unit/Metadata/Cache/RouteCacheManagerTest.php new file mode 100644 index 00000000000..4de2f4024da --- /dev/null +++ b/tests/Unit/Metadata/Cache/RouteCacheManagerTest.php @@ -0,0 +1,53 @@ +route_cache_manager = new RouteCacheManager(); + } + + public function testNoConfig(): void + { + Log::shouldReceive('warning')->once(); + self::assertFalse($this->route_cache_manager->get_config('fake_url')); + } + + public function testConfigFalse(): void + { + self::assertFalse($this->route_cache_manager->get_config('api/v2/Version')); + } + + public function testConfigValid(): void + { + self::assertIsObject($this->route_cache_manager->get_config('api/v2/Album')); + } +} +