Skip to content
Merged
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
31 changes: 31 additions & 0 deletions lib/Db/ShareWrapperRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\ShareWrapperNotFoundException;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\FileCacheWrapper;
use OCA\Circles\Model\Membership;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\ShareWrapper;
Expand Down Expand Up @@ -302,6 +303,36 @@ public function getSharesByFileIds(array $fileIds, bool $getData = false, bool $
return $this->getItemsFromRequest($qb);
}

/**
* returns current file cache entries, not cached, indexed by file id
*
* @param int[] $fileIds
*
* @return array<int, FileCacheWrapper>
*/
public function getFileCaches(array $fileIds): array {
$fileCaches = [];
foreach (array_chunk(array_values(array_unique($fileIds)), 1000) as $chunk) {
$qb = $this->getQueryBuilder();
$qb->select('f.fileid', 'f.path', 's.id')
->from(self::TABLE_FILE_CACHE, 'f')
->leftJoin('f', self::TABLE_STORAGES, 's', $qb->expr()->eq('f.storage', 's.numeric_id'))
->where($qb->expr()->in('f.fileid', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));

$result = $qb->executeQuery();
while ($row = $result->fetch()) {
$fileCache = new FileCacheWrapper();
$fileCache->setId((int)$row['fileid'])
->setPath((string)$row['path'])
->setStorage((string)$row['id']);
$fileCaches[$fileCache->getId()] = $fileCache;
}
$result->closeCursor();
}

return $fileCaches;
}

/**
* @param FederatedUser $federatedUser
* @param int $nodeId
Expand Down
13 changes: 12 additions & 1 deletion lib/Model/FileCacheWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,18 @@ public function isAccessible(): bool {
return (in_array($storageType, ['local', 'webdav', 'ftp', 'sftp', 'swift', 'smb', 'amazon']));
}

return !(explode('/', $path, 2)[0] !== 'files' && $storageType === 'home');
// on home storages and on group folders with a separate storage, only files/ is accessible
$storage = $this->getStorage();
if (explode('/', $path, 2)[0] !== 'files'
&& (str_starts_with($storage, 'home::')
|| str_starts_with($storage, 'object::user')
|| str_starts_with($storage, 'object::groupfolder:')
|| preg_match('#^local::.*/__groupfolders/\d+/$#', $storage) === 1)) {
return false;
}

// group folders stored in the root storage have their trashbin in __groupfolders/trash/
return !str_starts_with($path, '__groupfolders/trash/');
}

/**
Expand Down
33 changes: 31 additions & 2 deletions lib/Service/ShareWrapperService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\Exceptions\ShareWrapperNotFoundException;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\FileCacheWrapper;
use OCA\Circles\Model\Probes\CircleProbe;
use OCA\Circles\Model\ShareWrapper;
use OCA\Circles\Tools\Exceptions\InvalidItemException;
Expand Down Expand Up @@ -169,7 +170,7 @@ public function getSharedWith(
throw new InvalidItemException();
}

return $this->deserializeList($cachedData, ShareWrapper::class);
return $this->refreshFileCaches($this->deserializeList($cachedData, ShareWrapper::class));
} catch (InvalidItemException) {
// Cache miss, continue to fetch from database
}
Expand Down Expand Up @@ -197,7 +198,7 @@ public function getSharedWithByPath(
throw new InvalidItemException();
}

return $this->deserializeList($cachedData, ShareWrapper::class);
return $this->refreshFileCaches($this->deserializeList($cachedData, ShareWrapper::class));
} catch (InvalidItemException) {
// Cache miss, continue to fetch from database
}
Expand All @@ -208,6 +209,34 @@ public function getSharedWithByPath(
return $shares;
}

/**
* Cached file cache entries are outdated when a shared file is moved to the trashbin or restored
*
* @param ShareWrapper[] $shares
*
* @return ShareWrapper[]
*/
private function refreshFileCaches(array $shares): array {
$fileCaches = $this->shareWrapperRequest->getFileCaches(
array_map(fn (ShareWrapper $share): int => $share->getFileSource(), $shares)
);

$refreshed = [];
foreach ($shares as $share) {
if (!array_key_exists($share->getFileSource(), $fileCaches)) {
continue;
}

$fileCache = $share->hasFileCache() ? $share->getFileCache() : new FileCacheWrapper();
$fileCache->setPath($fileCaches[$share->getFileSource()]->getPath())
->setStorage($fileCaches[$share->getFileSource()]->getStorage());
$share->setFileCache($fileCache);
$refreshed[] = $share;
}

return $refreshed;
}

/**
* @return ShareWrapper[]
* @throws RequestBuilderException
Expand Down
12 changes: 12 additions & 0 deletions lib/ShareByCircleProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,12 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset): arra
$probe
);

// exclude shares leading to deleted files or to the trashbin
$wrappedShares = array_filter(
$wrappedShares,
fn (ShareWrapper $wrapper): bool => $wrapper->hasFileCache() && $wrapper->getFileCache()->isAccessible()
);

return array_filter(
array_map(
fn (ShareWrapper $wrapper) => $wrapper->getShare(
Expand Down Expand Up @@ -584,6 +590,12 @@ public function getSharedWithByPath(
$probe,
);

// exclude shares leading to deleted files or to the trashbin
$wrappedShares = array_filter(
$wrappedShares,
fn (ShareWrapper $wrapper): bool => $wrapper->hasFileCache() && $wrapper->getFileCache()->isAccessible()
);

/** @var array<string, IShare> */
return array_filter(
array_map(
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/lib/Model/FileCacheWrapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Tests\Model;

use OCA\Circles\Model\FileCacheWrapper;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;

class FileCacheWrapperTest extends TestCase {
public static function dataIsAccessible(): array {
return [
'home file' => ['home::alice', 'files/doc.txt', true],
'home trashbin' => ['home::alice', 'files_trashbin/files/doc.txt.d1700000000', false],
'home versions' => ['home::alice', 'files_versions/doc.txt.v1700000000', false],
'object store home file' => ['object::user:alice', 'files/doc.txt', true],
'object store home trashbin' => ['object::user:alice', 'files_trashbin/files/doc.txt.d1700000000', false],
'group folder in root storage' => ['local::/var/www/html/data/', '__groupfolders/1/doc.txt', true],
'group folder trashbin in root storage' => ['local::/var/www/html/data/', '__groupfolders/trash/1/doc.txt.d1700000000', false],
'group folder in root object storage' => ['object::store:amazon::bucket', '__groupfolders/1/doc.txt', true],
'group folder trashbin in root object storage' => ['object::store:amazon::bucket', '__groupfolders/trash/1/doc.txt.d1700000000', false],
'group folder in separate local storage' => ['local::/var/www/html/data/__groupfolders/1/', 'files/doc.txt', true],
'group folder trashbin in separate local storage' => ['local::/var/www/html/data/__groupfolders/1/', 'trash/doc.txt.d1700000000', false],
'group folder versions in separate local storage' => ['local::/var/www/html/data/__groupfolders/1/', 'versions/1/1700000000', false],
'group folder in separate object storage' => ['object::groupfolder:1.object::store:amazon::bucket', 'files/doc.txt', true],
'group folder trashbin in separate object storage' => ['object::groupfolder:1.object::store:amazon::bucket', 'trash/doc.txt.d1700000000', false],
'external storage file' => ['local::/mnt/external/', 'trash/doc.txt', true],
'external storage root' => ['local::/mnt/external/', '', true],
'home root' => ['home::alice', '', false],
];
}

#[DataProvider('dataIsAccessible')]
public function testIsAccessible(string $storage, string $path, bool $expected): void {
$fileCache = new FileCacheWrapper();
$fileCache->setId(42)
->setStorage($storage)
->setPath($path);

$this->assertSame($expected, $fileCache->isAccessible());
}

public function testIsAccessibleWithoutFileId(): void {
$fileCache = new FileCacheWrapper();
$fileCache->setStorage('home::alice')
->setPath('files/doc.txt');

$this->assertFalse($fileCache->isAccessible());
}
}
Loading