Skip to content

Commit

Permalink
Sharing propagation backend. (#2950)
Browse files Browse the repository at this point in the history
  • Loading branch information
ildyria authored Jan 29, 2025
1 parent a341b80 commit 11a99d3
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 0 deletions.
148 changes: 148 additions & 0 deletions app/Actions/Sharing/Propagate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

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

namespace App\Actions\Sharing;

use App\Constants\AccessPermissionConstants as APC;
use App\Models\AccessPermission;
use App\Models\Album;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;

final class Propagate
{
/**
* Update all descendants with the current album access permissions.
* This is run in a DB transaction for safety.
*
* @param Album $album
*
* @return void
*/
public function update(Album $album): void
{
if (!App::runningUnitTests()) {
// @codeCoverageIgnoreStart
DB::transaction(fn () => $this->applyUpdate($album));

return;
// @codeCoverageIgnoreEnd
}

$this->applyUpdate($album);
}

/**
* Apply the current album access permissions to all descendants.
*
* @param Album $album
*
* @return void
*/
private function applyUpdate(Album $album): void
{
// for each descendant, create a new permission if it does not exist.
// or update the existing permission.
/** @var Collection<int,string> $descendants */
$descendants = $album->descendants()->select('id')->pluck('id');
$permissions = $album->access_permissions()->whereNotNull('user_id')->get();

// This is super inefficient.
// It would be better to do it in a single query...
// But how?
$descendants->each(function (string $descendant, int|string $idx) use ($permissions) {
$permissions->each(function (AccessPermission $permission) use ($descendant) {
$perm = AccessPermission::updateOrCreate([
APC::BASE_ALBUM_ID => $descendant,
APC::USER_ID => $permission->user_id,
], [
APC::GRANTS_FULL_PHOTO_ACCESS => $permission->grants_full_photo_access,
APC::GRANTS_DOWNLOAD => $permission->grants_download,
APC::GRANTS_UPLOAD => $permission->grants_upload,
APC::GRANTS_EDIT => $permission->grants_edit,
APC::GRANTS_DELETE => $permission->grants_delete,
]);
$perm->save();
});
});
}

/**
* Overwrite all descendants with the current album access permissions.
*
* @param Album $album
*
* @return void
*/
public function overwrite(Album $album): void
{
if (!App::runningUnitTests()) {
// @codeCoverageIgnoreStart
DB::transaction(fn () => $this->applyOverwrite($album));

return;
// @codeCoverageIgnoreEnd
}

$this->applyOverwrite($album);
}

/**
* Apply the overwrite of all descendants with the current album access permissions.
*
* @param Album $album
*
* @return void
*/
private function applyOverwrite(Album $album): void
{
// override permission for all descendants albums.
// Faster done by:
// 1. clearing all the permissions.
// 2. applying the new permissions.

DB::table(APC::ACCESS_PERMISSIONS)
->whereNotNull('user_id')
->whereIn(
'base_album_id',
DB::table('albums')
->select('id')
->where('_lft', '>', $album->_lft)
->where('_rgt', '<', $album->_rgt)
)
->delete();

$descendant_ids = DB::table('albums')
->select('id')
->where('_lft', '>', $album->_lft)
->where('_rgt', '<', $album->_rgt)
->pluck('id');

$access_permissions = $album->access_permissions()->whereNotNull('user_id')->get();

$new_perm = $access_permissions->reduce(
fn (?array $acc, AccessPermission $permission) => array_merge(
$acc ?? [],
$descendant_ids->map(
fn ($descendant_id) => [
APC::BASE_ALBUM_ID => $descendant_id,
APC::USER_ID => $permission->user_id,
APC::GRANTS_FULL_PHOTO_ACCESS => $permission->grants_full_photo_access,
APC::GRANTS_DOWNLOAD => $permission->grants_download,
APC::GRANTS_UPLOAD => $permission->grants_upload,
APC::GRANTS_EDIT => $permission->grants_edit,
APC::GRANTS_DELETE => $permission->grants_delete,
]
)->all()
)
);

DB::table(APC::ACCESS_PERMISSIONS)->insert($new_perm);
}
}
25 changes: 25 additions & 0 deletions app/Http/Controllers/Gallery/SharingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@

namespace App\Http\Controllers\Gallery;

use App\Actions\Sharing\Propagate;
use App\Actions\Sharing\Share;
use App\Constants\AccessPermissionConstants as APC;
use App\Exceptions\Internal\LycheeLogicException;
use App\Http\Requests\Sharing\AddSharingRequest;
use App\Http\Requests\Sharing\DeleteSharingRequest;
use App\Http\Requests\Sharing\EditSharingRequest;
use App\Http\Requests\Sharing\ListAllSharingRequest;
use App\Http\Requests\Sharing\ListSharingRequest;
use App\Http\Requests\Sharing\PropagateSharingRequest;
use App\Http\Resources\Models\AccessPermissionResource;
use App\Models\AccessPermission;
use App\Models\Album;
use App\Models\BaseAlbumImpl;
use Illuminate\Routing\Controller;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -122,4 +126,25 @@ public function delete(DeleteSharingRequest $request): void
{
AccessPermission::query()->where('id', '=', $request->perm()->id)->delete();
}

/**
* Propagate sharing permissions.
*
* @param PropagateSharingRequest $request
*
* @return void
*/
public function propagate(PropagateSharingRequest $request, Propagate $propagate): void
{
$album = $request->album();
if (!$album instanceof Album) {
throw new LycheeLogicException('Only albums can have any descandants.');
}

if ($request->shallOverride) {
$propagate->overwrite($album);
} else {
$propagate->update($album);
}
}
}
58 changes: 58 additions & 0 deletions app/Http/Requests/Sharing/PropagateSharingRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

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

namespace App\Http\Requests\Sharing;

use App\Contracts\Http\Requests\HasBaseAlbum;
use App\Contracts\Http\Requests\RequestAttribute;
use App\Contracts\Models\AbstractAlbum;
use App\Http\Requests\BaseApiRequest;
use App\Http\Requests\Traits\HasBaseAlbumTrait;
use App\Policies\AlbumPolicy;
use App\Rules\RandomIDRule;
use Illuminate\Support\Facades\Gate;

/**
* Represents a request for listing the shares of a specific album.
*
* Only the owner of the album (or the admin) can set the shares.
*/
class PropagateSharingRequest extends BaseApiRequest implements HasBaseAlbum
{
use HasBaseAlbumTrait;

public bool $shallOverride;

/**
* {@inheritDoc}
*/
public function authorize(): bool
{
return Gate::check(AlbumPolicy::CAN_SHARE_WITH_USERS, [AbstractAlbum::class, $this->album]);
}

/**
* {@inheritDoc}
*/
public function rules(): array
{
return [
RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)],
RequestAttribute::SHALL_OVERRIDE_ATTRIBUTE => ['required', 'boolean'],
];
}

/**
* {@inheritDoc}
*/
protected function processValidatedValues(array $values, array $files): void
{
$this->album = $this->albumFactory->findBaseAlbumOrFail($values[RequestAttribute::ALBUM_ID_ATTRIBUTE]);
$this->shallOverride = static::toBoolean($values[RequestAttribute::SHALL_OVERRIDE_ATTRIBUTE]);
}
}
1 change: 1 addition & 0 deletions routes/api_v2.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
*/
Route::get('/Sharing', [Gallery\SharingController::class, 'list']);
Route::post('/Sharing', [Gallery\SharingController::class, 'create']);
Route::put('/Sharing', [Gallery\SharingController::class, 'propagate']);
Route::patch('/Sharing', [Gallery\SharingController::class, 'edit']);
Route::delete('/Sharing', [Gallery\SharingController::class, 'delete']);
Route::get('/Sharing::all', [Gallery\SharingController::class, 'listAll']);
Expand Down
Loading

0 comments on commit 11a99d3

Please sign in to comment.