From 5d0ab68c878a34f9a85183089b242657a3e3484f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 6 Mar 2024 16:15:57 +0100 Subject: [PATCH] refactor: don't join on filecache in defaultshareprovider Signed-off-by: Robin Appelman --- lib/private/Share20/DefaultShareProvider.php | 163 ++++++------------ lib/private/Share20/ProviderFactory.php | 13 +- .../lib/Share20/DefaultShareProviderTest.php | 68 ++++++-- 3 files changed, 103 insertions(+), 141 deletions(-) diff --git a/lib/private/Share20/DefaultShareProvider.php b/lib/private/Share20/DefaultShareProvider.php index 318c898621967..f590d8d9ecae1 100644 --- a/lib/private/Share20/DefaultShareProvider.php +++ b/lib/private/Share20/DefaultShareProvider.php @@ -35,6 +35,8 @@ namespace OC\Share20; use OC\Files\Cache\Cache; +use OC\Files\Cache\FileAccess; +use OC\Files\Cache\CacheEntry; use OC\Share20\Exception\BackendError; use OC\Share20\Exception\InvalidShare; use OC\Share20\Exception\ProviderException; @@ -67,52 +69,21 @@ class DefaultShareProvider implements IShareProvider { // Special share type for user modified group shares public const SHARE_TYPE_USERGROUP = 2; - /** @var IDBConnection */ - private $dbConn; - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IMailer */ - private $mailer; - - /** @var Defaults */ - private $defaults; - - /** @var IFactory */ - private $l10nFactory; - - /** @var IURLGenerator */ - private $urlGenerator; - - private ITimeFactory $timeFactory; + private IDBConnection $dbConn; public function __construct( - IDBConnection $connection, - IUserManager $userManager, - IGroupManager $groupManager, - IRootFolder $rootFolder, - IMailer $mailer, - Defaults $defaults, - IFactory $l10nFactory, - IURLGenerator $urlGenerator, - ITimeFactory $timeFactory, + IDBConnection $connection, + private IUserManager $userManager, + private IGroupManager $groupManager, + private IRootFolder $rootFolder, + private IMailer $mailer, + private Defaults $defaults, + private IFactory $l10nFactory, + private IURLGenerator $urlGenerator, + private ITimeFactory $timeFactory, + private FileAccess $cacheAccess, ) { $this->dbConn = $connection; - $this->userManager = $userManager; - $this->groupManager = $groupManager; - $this->rootFolder = $rootFolder; - $this->mailer = $mailer; - $this->defaults = $defaults; - $this->l10nFactory = $l10nFactory; - $this->urlGenerator = $urlGenerator; - $this->timeFactory = $timeFactory; } /** @@ -657,10 +628,7 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t } $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum') + $qb->select('s.*') ->from('share', 's') ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), @@ -687,27 +655,17 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t ); } - // todo? maybe get these from the oc_mounts table - $childMountNodes = array_filter($node->getDirectoryListing(), function (Node $node): bool { - return $node->getInternalPath() === ''; - }); - $childMountRootIds = array_map(function (Node $node): int { + $childFileIds = array_map(function (Node $node): int { return $node->getId(); - }, $childMountNodes); + }, $node->getDirectoryListing()); - $qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')); - $qb->andWhere( - $qb->expr()->orX( - $qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())), - $qb->expr()->in('f.fileid', $qb->createParameter('chunk')) - ) - ); + $qb->andWhere($qb->expr()->in('s.file_source', $qb->createParameter('chunk'))); $qb->orderBy('id'); $shares = []; - $chunks = array_chunk($childMountRootIds, 1000); + $chunks = array_chunk($childFileIds, 1000); // Force the request to be run when there is 0 mount. if (count($chunks) === 0) { @@ -718,7 +676,7 @@ public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = t $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY); $cursor = $qb->executeQuery(); while ($data = $cursor->fetch()) { - $shares[$data['fileid']][] = $this->createShare($data); + $shares[$data['file_source']][] = $this->createShare($data); } $cursor->closeCursor(); } @@ -860,25 +818,9 @@ public function getSharesByPath(Node $path) { * Returns whether the given database result can be interpreted as * a share with accessible file (not trashed, not deleted) */ - private function isAccessibleResult($data) { - // exclude shares leading to deleted file entries - if ($data['fileid'] === null || $data['path'] === null) { - return false; - } - - // exclude shares leading to trashbin on home storages - $pathSections = explode('/', $data['path'], 2); - // FIXME: would not detect rare md5'd home storage case properly - if ($pathSections[0] !== 'files' - && (str_starts_with($data['storage_string_id'], 'home::') || str_starts_with($data['storage_string_id'], 'object::user'))) { - return false; - } elseif ($pathSections[0] === '__groupfolders' - && str_starts_with($pathSections[1], 'trash/') - ) { - // exclude shares leading to trashbin on group folders storages - return false; - } - return true; + private function isAccessibleResult(CacheEntry $data) { + $path = $data->getPath(); + return !(str_starts_with($path, 'files_trashbin/') || str_starts_with($path, '__groupfolders/trash/')); } /** @@ -891,15 +833,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { if ($shareType === IShare::TYPE_USER) { //Get shares directly with this user $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' - ) - ->selectAlias('st.id', 'storage_string_id') - ->from('share', 's') - ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) - ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')); + $qb->select('s.*') + ->from('share', 's'); // Order by id $qb->orderBy('s.id'); @@ -925,14 +860,7 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { $cursor = $qb->execute(); while ($data = $cursor->fetch()) { - if ($data['fileid'] && $data['path'] === null) { - $data['path'] = (string) $data['path']; - $data['name'] = (string) $data['name']; - $data['checksum'] = (string) $data['checksum']; - } - if ($this->isAccessibleResult($data)) { - $shares[] = $this->createShare($data); - } + $shares[] = $this->createShare($data); } $cursor->closeCursor(); } elseif ($shareType === IShare::TYPE_GROUP) { @@ -952,15 +880,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { } $qb = $this->dbConn->getQueryBuilder(); - $qb->select('s.*', - 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', - 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', - 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' - ) - ->selectAlias('st.id', 'storage_string_id') + $qb->select('s.*') ->from('share', 's') - ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) - ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) ->orderBy('s.id') ->setFirstResult(0); @@ -993,10 +914,8 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { continue; } - if ($this->isAccessibleResult($data)) { - $share = $this->createShare($data); - $shares2[$share->getId()] = $share; - } + $share = $this->createShare($data); + $shares2[$share->getId()] = $share; } $cursor->closeCursor(); } @@ -1010,7 +929,31 @@ public function getSharedWith($userId, $shareType, $node, $limit, $offset) { } - return $shares; + return $this->setNodes($shares); + } + + /** + * @param IShare[] $shares + * @return IShare[] + */ + private function setNodes(array $shares): array { + $fileIds = array_map(function (IShare $share): int { + return $share->getNodeId(); + }, $shares); + $files = $this->cacheAccess->getByFileIds($fileIds); + + $sharesWithFiles = []; + foreach ($shares as $share) { + if (isset($files[$share->getNodeId()])) { + $cacheItem = $files[$share->getNodeId()]; + if ($this->isAccessibleResult($cacheItem)) { + $share->setNodeCacheEntry($cacheItem); + $sharesWithFiles[] = $share; + } + } + } + + return $sharesWithFiles; } /** diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index cac25226f1ae9..99bd8d52b4819 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -41,7 +41,6 @@ use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\ShareByMailProvider; use OCA\Talk\Share\RoomShareProvider; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudFederationFactory; @@ -99,17 +98,7 @@ public function registerProvider(string $shareProviderClass): void { */ protected function defaultShareProvider() { if ($this->defaultProvider === null) { - $this->defaultProvider = new DefaultShareProvider( - $this->serverContainer->getDatabaseConnection(), - $this->serverContainer->getUserManager(), - $this->serverContainer->getGroupManager(), - $this->serverContainer->get(IRootFolder::class), - $this->serverContainer->getMailer(), - $this->serverContainer->query(Defaults::class), - $this->serverContainer->getL10NFactory(), - $this->serverContainer->getURLGenerator(), - $this->serverContainer->query(ITimeFactory::class), - ); + $this->defaultProvider = $this->serverContainer->get(DefaultShareProvider::class); } return $this->defaultProvider; diff --git a/tests/lib/Share20/DefaultShareProviderTest.php b/tests/lib/Share20/DefaultShareProviderTest.php index 72ef83331e950..ec93e27900926 100644 --- a/tests/lib/Share20/DefaultShareProviderTest.php +++ b/tests/lib/Share20/DefaultShareProviderTest.php @@ -7,6 +7,8 @@ namespace Test\Share20; +use OC\Files\Cache\FileAccess; +use OC\Files\Cache\CacheEntry; use OC\Share20\DefaultShareProvider; use OC\Share20\ShareAttributes; use OCP\AppFramework\Utility\ITimeFactory; @@ -68,6 +70,11 @@ class DefaultShareProviderTest extends \Test\TestCase { /** @var ITimeFactory|MockObject */ protected $timeFactory; + /** @var FileAccess|MockObject */ + protected $cacheAccess; + /** @var array */ + protected $cacheItems = []; + protected function setUp(): void { $this->dbConn = \OC::$server->getDatabaseConnection(); $this->userManager = $this->createMock(IUserManager::class); @@ -79,6 +86,26 @@ protected function setUp(): void { $this->defaults = $this->getMockBuilder(Defaults::class)->disableOriginalConstructor()->getMock(); $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->cacheAccess = $this->createMock(FileAccess::class); + $this->cacheItems = []; + $this->cacheAccess->method('getByFileIds')->willReturnCallback(function (array $fileIds) { + $result = []; + foreach ($fileIds as $fileId) { + if (isset($this->cacheItems[$fileId])) { + $result[$fileId] = $this->cacheItems[$fileId]; + } + } + return $result; + }); + $this->cacheAccess->method('getByFileIdsInStorage')->willReturnCallback(function (array $fileIds, int $storageId) { + $result = []; + foreach ($fileIds as $fileId) { + if (isset($this->cacheItems[$fileId]) && $this->cacheItems[$fileId]->getStorageId() === $storageId) { + $result[$fileId] = $this->cacheItems[$fileId]; + } + } + return $result; + }); $this->userManager->expects($this->any())->method('userExists')->willReturn(true); $this->timeFactory->expects($this->any())->method('now')->willReturn(new \DateTimeImmutable("2023-05-04 00:00 Europe/Berlin")); @@ -95,13 +122,13 @@ protected function setUp(): void { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); } protected function tearDown(): void { $this->dbConn->getQueryBuilder()->delete('share')->execute(); - $this->dbConn->getQueryBuilder()->delete('filecache')->execute(); $this->dbConn->getQueryBuilder()->delete('storages')->execute(); } @@ -456,7 +483,8 @@ public function testDeleteSingleShare() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ]) ->setMethods(['getShareById']) ->getMock(); @@ -551,7 +579,8 @@ public function testDeleteGroupShareWithUserGroupShares() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ]) ->setMethods(['getShareById']) ->getMock(); @@ -906,16 +935,15 @@ private function createTestStorageEntry($storageStringId) { } private function createTestFileEntry($path, $storage = 1) { - $qb = $this->dbConn->getQueryBuilder(); - $qb->insert('filecache') - ->values([ - 'storage' => $qb->expr()->literal($storage), - 'path' => $qb->expr()->literal($path), - 'path_hash' => $qb->expr()->literal(md5($path)), - 'name' => $qb->expr()->literal(basename($path)), - ]); - $this->assertEquals(1, $qb->execute()); - return $qb->getLastInsertId(); + $id = count($this->cacheItems); + $this->cacheItems[$id] = new CacheEntry([ + 'fileid' => $id, + 'storage' => $storage, + 'path' => $path, + 'path_hash' => md5($path), + 'name' => basename($path), + ]); + return $id; } public function storageAndFileNameProvider() { @@ -924,8 +952,6 @@ public function storageAndFileNameProvider() { ['home::shareOwner', 'files/test.txt', 'files/test2.txt'], // regular file on external storage ['smb::whatever', 'files/test.txt', 'files/test2.txt'], - // regular file on external storage in trashbin-like folder, - ['smb::whatever', 'files_trashbin/files/test.txt', 'files_trashbin/files/test2.txt'], ]; } @@ -1273,6 +1299,7 @@ public function testGetSharedWithWithDeletedFile($shareType, $trashed) { $user = $this->createMock(IUser::class); $user->method('getUID')->willReturn('sharedWith'); + $user->method('getDisplayName')->willReturn('sharedWith'); $owner = $this->createMock(IUser::class); $owner->method('getUID')->willReturn('shareOwner'); $initiator = $this->createMock(IUser::class); @@ -2511,7 +2538,8 @@ public function testGetSharesInFolder() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $password = md5(time()); @@ -2609,7 +2637,8 @@ public function testGetAccessListNoCurrentAccessRequired() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $u1 = $userManager->createUser('testShare1', 'test'); @@ -2705,7 +2734,8 @@ public function testGetAccessListCurrentAccessRequired() { $this->defaults, $this->l10nFactory, $this->urlGenerator, - $this->timeFactory + $this->timeFactory, + $this->cacheAccess, ); $u1 = $userManager->createUser('testShare1', 'test');