Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/Actions/Album/Move.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace App\Actions\Album;

use App\Events\AlbumSaved;
use App\Exceptions\ModelDBException;
use App\Models\Album;
use Illuminate\Database\Eloquent\ModelNotFoundException;
Expand All @@ -27,21 +28,46 @@ public function do(?Album $target_album, Collection $albums): void
if ($target_album !== null) {
/** @var Album $album */
foreach ($albums as $album) {
$old_parent_id = $album->parent_id;
// Don't set attribute `parent_id` manually, but use specialized
// methods of the nested set `NodeTrait` to keep the enumeration
// of the tree consistent
// `appendNode` also internally calls `save` on the model
$target_album->appendNode($album);
AlbumSaved::dispatch($album);
$this->dispatchForOldParentIfChanged($old_parent_id, $album->parent_id);
}
$target_album->fixOwnershipOfChildren();
} else {
/** @var Album $album */
foreach ($albums as $album) {
$old_parent_id = $album->parent_id;
// Don't set attribute `parent_id` manually, but use specialized
// methods of the nested set `NodeTrait` to keep the enumeration
// of the tree consistent
$album->saveAsRoot();
AlbumSaved::dispatch($album);
$this->dispatchForOldParentIfChanged($old_parent_id, $album->parent_id);
}
}
}

/**
* Also dispatches `AlbumSaved` for the album's *previous* parent when it
* actually changed, mirroring `Photo\MoveOrDuplicate`'s source/destination
* dispatch pattern: the old parent's set of children changed too, and its
* managed-cache tag (FR-052-06) would otherwise never be evicted since
* nothing else carries its id after the move completes.
*/
private function dispatchForOldParentIfChanged(?string $old_parent_id, ?string $new_parent_id): void
{
if ($old_parent_id === null || $old_parent_id === $new_parent_id) {
return;
}

$old_parent = Album::find($old_parent_id);
if ($old_parent !== null) {
AlbumSaved::dispatch($old_parent);
}
}
}
2 changes: 2 additions & 0 deletions app/Actions/Photo/Pipes/Shared/Save.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use App\Contracts\PhotoCreate\PhotoDTO;
use App\Contracts\PhotoCreate\PhotoPipe;
use App\Events\PhotoSaved;

/**
* Persist current Photo object into database.
Expand All @@ -20,6 +21,7 @@ public function handle(PhotoDTO $state, \Closure $next): PhotoDTO
{
$state->getPhoto()->save();
$state->getPhoto()->tags()->sync($state->getTags()->pluck('id')->all());
PhotoSaved::dispatch($state->getPhoto()->id);

return $next($state);
}
Expand Down
27 changes: 27 additions & 0 deletions app/Events/AccessPermissionChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Events;

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

class AccessPermissionChanged
{
use Dispatchable;
use SerializesModels;

/**
* Create a new event instance.
*
* @param string $base_album_id the ID of the album whose access permissions changed
*/
public function __construct(public string $base_album_id)
{
}
}
27 changes: 27 additions & 0 deletions app/Events/UserGroupMembershipChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Events;

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

class UserGroupMembershipChanged
{
use Dispatchable;
use SerializesModels;

/**
* Create a new event instance.
*
* @param int $user_id the ID of the user whose group membership changed
*/
public function __construct(public int $user_id)
{
}
}
7 changes: 6 additions & 1 deletion app/Http/Controllers/Admin/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ public function getAll(GetAllConfigsRequest $request, DockerVersionInfo $docker_
$editable_configs = ConfigCategory::with([
'configs' => fn ($query) => $query
->when(config('features.hide-lychee-SE', false) === true, fn ($q) => $q->where('cat', '!=', 'lychee SE'))
->when(config('features.enable-request-caching') === false, fn ($q) => $q->where('cat', '!=', 'Mod Cache'))
// managed_cache_enabled/managed_cache_ttl (Feature 052) share the "Mod Cache" category but must
// stay independent of Feature 040's cache_enabled/enable-request-caching gate (Q-052-07).
->when(config('features.enable-request-caching') === false, fn ($q) => $q->where(fn ($q2) => $q2
->where('cat', '!=', 'Mod Cache')
->orWhereIn('key', ['managed_cache_enabled', 'managed_cache_ttl'])
))
->when($docker_info->isDocker(), fn ($q) => $q->where('not_on_docker', '!=', true))
->when(!$request->verify()->is_supporter() && !$request->configs()->getValueAsBool('enable_se_preview'), fn ($q) => $q->where('level', '=', 0))
->when(!$request->verify()->is_pro(), fn ($q) => $q->where('level', '<', 2))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace App\Http\Controllers\Admin;

use App\Events\UserGroupMembershipChanged;
use App\Http\Requests\UserGroup\GetUserGroupRequest;
use App\Http\Requests\UserGroup\ManageUserGroupRequest;
use App\Http\Resources\Models\UserGroupResource;
Expand All @@ -26,13 +27,15 @@ public function get(GetUserGroupRequest $request): UserGroupResource
public function addUser(ManageUserGroupRequest $request): UserGroupResource
{
$request->user_group()->users()->attach($request->user2()->id, ['role' => $request->role()->value]);
UserGroupMembershipChanged::dispatch($request->user2()->id);

return new UserGroupResource($request->user_group());
}

public function removeUser(ManageUserGroupRequest $request): UserGroupResource
{
$request->user_group()->users()->detach($request->user2()->id);
UserGroupMembershipChanged::dispatch($request->user2()->id);

return new UserGroupResource($request->user_group());
}
Expand All @@ -41,6 +44,7 @@ public function updateUserRole(ManageUserGroupRequest $request): UserGroupResour
{
$request->user_group()->users()->updateExistingPivot($request->user2()->id, ['role' => $request->role()->value]);
$request->user_group()->load('users');
UserGroupMembershipChanged::dispatch($request->user2()->id);

return new UserGroupResource($request->user_group());
}
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/Gallery/PhotoController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use App\Enum\FileStatus;
use App\Enum\SizeVariantType;
use App\Events\PhotoHighlightToggled;
use App\Events\PhotoSaved;
use App\Events\PhotoTagsChanged;
use App\Exceptions\ConfigurationException;
use App\Exceptions\ConflictingPropertyException;
Expand Down Expand Up @@ -176,6 +177,7 @@ public function update(EditPhotoRequest $request): PhotoResource
$photo->taken_at = $request->takenAt() ?? $photo->initial_taken_at;

$photo->save();
PhotoSaved::dispatch($photo->id);

return new PhotoResource(
photo: $photo,
Expand Down
12 changes: 12 additions & 0 deletions app/Http/Controllers/Gallery/SharingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use App\Actions\Sharing\Propagate;
use App\Actions\Sharing\Share;
use App\Constants\AccessPermissionConstants as APC;
use App\Events\AccessPermissionChanged;
use App\Exceptions\Internal\LycheeLogicException;
use App\Http\Requests\Sharing\AddSharingRequest;
use App\Http\Requests\Sharing\DeleteSharingRequest;
Expand Down Expand Up @@ -75,6 +76,8 @@ public function create(AddSharingRequest $request, Share $share): array
base_album_id: $album_id
);
}

AccessPermissionChanged::dispatch($album_id);
}

return AccessPermissionResource::collect($access_permissions);
Expand All @@ -98,6 +101,8 @@ public function edit(EditSharingRequest $request): AccessPermissionResource
'grants_delete' => $request->permResource()->grants_delete,
]);

AccessPermissionChanged::dispatch($perm->base_album_id);

return AccessPermissionResource::fromModel($perm);
}

Expand Down Expand Up @@ -173,7 +178,9 @@ public function listAlbums(ListAllSharingRequest $request, ListAlbums $list_albu
*/
public function delete(DeleteSharingRequest $request): void
{
$base_album_id = $request->perm()->base_album_id;
AccessPermission::query()->where('id', '=', $request->perm()->id)->delete();
AccessPermissionChanged::dispatch($base_album_id);
}

/**
Expand All @@ -195,5 +202,10 @@ public function propagate(PropagateSharingRequest $request, Propagate $propagate
} else {
$propagate->update($album);
}

$affected_album_ids = $album->descendants()->getQuery()->select('id')->pluck('id')->push($album->id);
foreach ($affected_album_ids as $affected_album_id) {
AccessPermissionChanged::dispatch($affected_album_id);
}
}
}
111 changes: 111 additions & 0 deletions app/Listeners/ManagedCacheAlbumInvalidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Listeners;

use App\Constants\PhotoAlbum as PA;
use App\Events\AccessPermissionChanged;
use App\Events\AlbumDeleted;
use App\Events\AlbumSaved;
use App\Events\PhotoAdded;
use App\Events\PhotoDeleted;
use App\Events\PhotoMoved;
use App\Events\PhotoSaved;
use App\Services\Cache\ManagedCacheService;
use Illuminate\Support\Facades\DB;

/**
* Evicts ManagedCacheService tags for an album and its immediate parent whenever
* something changes that could affect a cached listing of that album's children
* or photos (FR-052-06).
*/
class ManagedCacheAlbumInvalidator
{
private const PREFIX = 'album:';

public function __construct(
private ManagedCacheService $managed_cache_service,
) {
}

public function handleAlbumSaved(AlbumSaved $event): void
{
$this->evictAlbumAndParent($event->album->id, $event->album->parent_id);
}

/**
* `AlbumDeleted` carries only the deleted album's parent id, not its own id
* (the row is already gone by the time the event fires). Only the parent's
* tag is evicted, which is functionally sufficient: nothing can ever query
* a deleted album's own cached listings again (Q-052-06, Option A).
*/
public function handleAlbumDeleted(AlbumDeleted $event): void
{
$this->managed_cache_service->forgetTag(self::PREFIX . ($event->parent_id ?? 'root'));
}

public function handleAccessPermissionChanged(AccessPermissionChanged $event): void
{
$this->evictAlbumAndParentById($event->base_album_id);
}

public function handlePhotoSaved(PhotoSaved $event): void
{
$this->evictAlbumsForPhoto($event->photo_id);
}

public function handlePhotoAdded(PhotoAdded $event): void
{
$this->evictAlbumsForPhoto($event->photo_id);
}

public function handlePhotoDeleted(PhotoDeleted $event): void
{
$this->evictAlbumAndParentById($event->album_id);
}

public function handlePhotoMoved(PhotoMoved $event): void
{
$this->evictAlbumAndParentById($event->from_album_id);
$this->evictAlbumAndParentById($event->to_album_id);
}

/**
* Resolve a photo to its containing album(s) via the `photo_album` pivot,
* mirroring `AlbumRouteCacheRefresher::handle()`.
*/
private function evictAlbumsForPhoto(string $photo_id): void
{
$album_ids = DB::table(PA::PHOTO_ALBUM)
->select(PA::ALBUM_ID)
->where(PA::PHOTO_ID, '=', $photo_id)
->distinct()
->pluck('album_id')
->all();

foreach ($album_ids as $album_id) {
/** @var string $album_id */
$this->evictAlbumAndParentById($album_id);
}
}

private function evictAlbumAndParentById(string $album_id): void
{
// Plain query builder (not the Eloquent `Album` model) to avoid pulling in
// `Album`'s eager-loaded relations for what is otherwise a single-column read.
$parent_id = DB::table('albums')->where('id', '=', $album_id)->value('parent_id');
/** @var string|null $parent_id */
$this->evictAlbumAndParent($album_id, $parent_id);
}

private function evictAlbumAndParent(string $album_id, ?string $parent_id): void
{
$this->managed_cache_service->forgetTag(self::PREFIX . $album_id);
$this->managed_cache_service->forgetTag(self::PREFIX . ($parent_id ?? 'root'));
}
}
29 changes: 29 additions & 0 deletions app/Listeners/ManagedCacheUserInvalidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* SPDX-License-Identifier: MIT
* Copyright (c) 2017-2018 Tobias Reich
* Copyright (c) 2018-2026 LycheeOrg.
*/

namespace App\Listeners;

use App\Events\UserGroupMembershipChanged;
use App\Services\Cache\ManagedCacheService;

/**
* Evicts the ManagedCacheService tag for a user whenever their group
* membership changes (FR-052-07).
*/
class ManagedCacheUserInvalidator
{
public function __construct(
private ManagedCacheService $managed_cache_service,
) {
}

public function handle(UserGroupMembershipChanged $event): void
{
$this->managed_cache_service->forgetTag('user:' . $event->user_id);
}
}
Loading
Loading