From c91cf3be645ce294fd9fd5b8266828c6ad83b721 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 27 Apr 2023 22:24:16 +0200 Subject: [PATCH 1/3] PoC: SystemTags endpoint to return tags used by a user with meta data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Target case is photos app: when visiting the tags category, all systemtags of the whole cloud are retrieved. In subequent steps the next tag is requested until the browser view is filled with tag tiles (i.e. previews are requested just as well). With this approach, we incorpoate the dav search and look for user related tags that are used by them, and already returns the statistics (number of files tagged with the respective tag) as well as a file id for the purpose to load the preview. This defaults to the file with the highest id. Call: curl -s -u 'user:password' \ 'https://my.nc.srv/remote.php/dav/systemtags-current' \ -X PROPFIND -H 'Accept: text/plain' \ -H 'Accept-Language: en-US,en;q=0.5' -H 'Depth: 1' \ -H 'Content-Type: text/plain;charset=UTF-8' \ --data @/home/doe/request-systemtag-props.xml With request-systemtag-props.xml: Example output: … /master/remote.php/dav/systemtags/84 84 Computer true true true 42 924022 HTTP/1.1 200 OK /remote.php/dav/systemtags/97 97 Bear true true true 1 923422 HTTP/1.1 200 OK … Signed-off-by: Arthur Schiwon --- .../composer/composer/autoload_classmap.php | 1 + .../dav/composer/composer/autoload_static.php | 1 + apps/dav/lib/RootCollection.php | 7 ++ apps/dav/lib/SystemTag/SystemTagNode.php | 19 ++++++ apps/dav/lib/SystemTag/SystemTagPlugin.php | 24 +++++++ .../SystemTag/SystemTagsInUseCollection.php | 67 +++++++++++++++++++ lib/private/Files/Cache/CacheQueryBuilder.php | 22 +++++- lib/private/Files/Cache/QuerySearchHelper.php | 58 ++++++++++------ lib/private/Files/Node/Folder.php | 64 +++++++++++------- 9 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 apps/dav/lib/SystemTag/SystemTagsInUseCollection.php diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index d3290c4e79260..a29e7e72327cb 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -301,6 +301,7 @@ 'OCA\\DAV\\SystemTag\\SystemTagNode' => $baseDir . '/../lib/SystemTag/SystemTagNode.php', 'OCA\\DAV\\SystemTag\\SystemTagPlugin' => $baseDir . '/../lib/SystemTag/SystemTagPlugin.php', 'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => $baseDir . '/../lib/SystemTag/SystemTagsByIdCollection.php', + 'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => $baseDir . '/../lib/SystemTag/SystemTagsInUseCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => $baseDir . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => $baseDir . '/../lib/SystemTag/SystemTagsRelationsCollection.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 4d425f70f3b84..1a7e239acd1cf 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -316,6 +316,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\SystemTag\\SystemTagNode' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagNode.php', 'OCA\\DAV\\SystemTag\\SystemTagPlugin' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagPlugin.php', 'OCA\\DAV\\SystemTag\\SystemTagsByIdCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsByIdCollection.php', + 'OCA\\DAV\\SystemTag\\SystemTagsInUseCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsInUseCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsObjectMappingCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectMappingCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsObjectTypeCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsObjectTypeCollection.php', 'OCA\\DAV\\SystemTag\\SystemTagsRelationsCollection' => __DIR__ . '/..' . '/../lib/SystemTag/SystemTagsRelationsCollection.php', diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 29ab65d46a923..88517ee83f7d6 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -48,6 +48,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IRootFolder; use OCP\IConfig; use Psr\Log\LoggerInterface; use Sabre\DAV\SimpleCollection; @@ -65,6 +66,7 @@ public function __construct() { $dispatcher = \OC::$server->get(IEventDispatcher::class); $config = \OC::$server->get(IConfig::class); $proxyMapper = \OC::$server->query(ProxyMapper::class); + $rootFolder = \OCP\Server::get(IRootFolder::class); $userPrincipalBackend = new Principal( $userManager, @@ -132,6 +134,10 @@ public function __construct() { $groupManager, \OC::$server->getEventDispatcher() ); + $systemTagInUseCollection = new SystemTag\SystemTagsInUseCollection( + $userSession, + $rootFolder + ); $commentsCollection = new Comments\RootCollection( \OC::$server->getCommentsManager(), $userManager, @@ -180,6 +186,7 @@ public function __construct() { $systemAddressBookRoot]), $systemTagCollection, $systemTagRelationsCollection, + $systemTagInUseCollection, $commentsCollection, $uploadCollection, $avatarCollection, diff --git a/apps/dav/lib/SystemTag/SystemTagNode.php b/apps/dav/lib/SystemTag/SystemTagNode.php index a31deb59a93a8..597aff8420376 100644 --- a/apps/dav/lib/SystemTag/SystemTagNode.php +++ b/apps/dav/lib/SystemTag/SystemTagNode.php @@ -64,6 +64,9 @@ class SystemTagNode implements \Sabre\DAV\INode { */ protected $isAdmin; + protected int $numberOfFiles = -1; + protected int $referenceFileId = -1; + /** * Sets up the node, expects a full path name * @@ -172,4 +175,20 @@ public function delete() { throw new NotFound('Tag with id ' . $this->tag->getId() . ' not found', 0, $e); } } + + public function getNumberOfFiles(): int { + return $this->numberOfFiles; + } + + public function setNumberOfFiles(int $numberOfFiles): void { + $this->numberOfFiles = $numberOfFiles; + } + + public function getReferenceFileId(): int { + return $this->referenceFileId; + } + + public function setReferenceFileId(int $referenceFileId): void { + $this->referenceFileId = $referenceFileId; + } } diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index b6bd7d3b7cdb9..115f126aef088 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -56,6 +56,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { public const USERASSIGNABLE_PROPERTYNAME = '{http://owncloud.org/ns}user-assignable'; public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups'; public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign'; + public const NUM_FILES_PROPERTYNAME = '{http://nextcloud.org/ns}files-assigned'; + public const FILEID_PROPERTYNAME = '{http://nextcloud.org/ns}reference-fileid'; /** * @var \Sabre\DAV\Server $server @@ -224,6 +226,11 @@ public function handleGetProperties( return; } + // child nodes from systemtags-current should point to normal tag endpoint + if (preg_match('/^systemtags-current\/[0-9]+/', $propFind->getPath())) { + $propFind->setPath(str_replace('systemtags-current/', 'systemtags/', $propFind->getPath())); + } + $propFind->handle(self::ID_PROPERTYNAME, function () use ($node) { return $node->getSystemTag()->getId(); }); @@ -258,6 +265,16 @@ public function handleGetProperties( } return implode('|', $groups); }); + + if ($node instanceof SystemTagNode) { + $propFind->handle(self::NUM_FILES_PROPERTYNAME, function () use ($node): int { + return $node->getNumberOfFiles(); + }); + + $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node): int { + return $node->getReferenceFileId(); + }); + } } /** @@ -279,6 +296,8 @@ public function handleUpdateProperties($path, PropPatch $propPatch) { self::USERVISIBLE_PROPERTYNAME, self::USERASSIGNABLE_PROPERTYNAME, self::GROUPS_PROPERTYNAME, + self::NUM_FILES_PROPERTYNAME, + self::FILEID_PROPERTYNAME, ], function ($props) use ($node) { $tag = $node->getSystemTag(); $name = $tag->getName(); @@ -315,6 +334,11 @@ public function handleUpdateProperties($path, PropPatch $propPatch) { $this->tagManager->setTagGroups($tag, $groupIds); } + if (isset($props[self::NUM_FILES_PROPERTYNAME]) || isset($props[self::FILEID_PROPERTYNAME])) { + // read-only properties + throw new Forbidden(); + } + if ($updateTag) { $node->update($name, $userVisible, $userAssignable); } diff --git a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php new file mode 100644 index 0000000000000..938b14e1f651e --- /dev/null +++ b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php @@ -0,0 +1,67 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\SystemTag; + +use OC\SystemTag\SystemTag; +use OCP\Files\IRootFolder; +use OCP\IUserSession; +use OCP\SystemTag\ISystemTagManager; +use Sabre\DAV\Exception\Forbidden; + +class SystemTagsInUseCollection extends \Sabre\DAV\SimpleCollection { + protected IUserSession $userSession; + protected IRootFolder $rootFolder; + + public function __construct(IUserSession $userSession, IRootFolder $rootFolder) { + $this->userSession = $userSession; + $this->rootFolder = $rootFolder; + $this->name = 'systemtags-current'; + } + + public function setName($name): void { + throw new Forbidden('Permission denied to rename this collection'); + } + + public function getChildren() { + $user = $this->userSession->getUser(); + if ($user === null) { + throw new Forbidden('Permission denied to read this collection'); + } + + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $result = $userFolder->getSystemTags('image'); + $children = []; + foreach ($result as $tagData) { + $tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']); + $node = new SystemTagNode($tag, $user, false, \OCP\Server::get(ISystemTagManager::class)); + $node->setNumberOfFiles($tagData['number_files']); + $node->setReferenceFileId($tagData['ref_file_id']); + $children[] = $node; + } + return $children; + } +} diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php index 496a8361d7748..c5563750c4d59 100644 --- a/lib/private/Files/Cache/CacheQueryBuilder.php +++ b/lib/private/Files/Cache/CacheQueryBuilder.php @@ -41,8 +41,28 @@ public function __construct(IDBConnection $connection, SystemConfig $systemConfi parent::__construct($connection, $systemConfig, $logger); } + public function selectTagUsage(): self { + $this + ->select('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable') + ->selectAlias($this->createFunction('COUNT(filecache.fileid)'), 'number_files') + ->selectAlias($this->createFunction('MAX(filecache.fileid)'), 'ref_file_id') + ->from('filecache', 'filecache') + ->leftJoin('filecache', 'systemtag_object_mapping', 'systemtagmap', $this->expr()->andX( + $this->expr()->eq('filecache.fileid', $this->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)), + $this->expr()->eq('systemtagmap.objecttype', $this->createNamedParameter('files')) + )) + ->leftJoin('systemtagmap', 'systemtag', 'systemtag', $this->expr()->andX( + $this->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'), + $this->expr()->eq('systemtag.visibility', $this->createNamedParameter(true)) + )) + ->where($this->expr()->like('systemtag.name', $this->createNamedParameter('_%'))) + ->groupBy('systemtag.name', 'systemtag.id', 'systemtag.visibility', 'systemtag.editable'); + + return $this; + } + public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) { - $name = $alias ? $alias : 'filecache'; + $name = $alias ?: 'filecache'; $this->select("$name.fileid", 'storage', 'path', 'path_hash', "$name.parent", "$name.name", 'mimetype', 'mimepart', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum', 'unencrypted_size') ->from('filecache', $name); diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 3529ede9746d7..fdba8a1dd7442 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -75,6 +75,41 @@ protected function getQueryBuilder() { ); } + protected function applySearchConstraints(CacheQueryBuilder $query, ISearchQuery $searchQuery, array $caches): void { + $storageFilters = array_values(array_map(function (ICache $cache) { + return $cache->getQueryFilterForStorage(); + }, $caches)); + $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters); + $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]); + $this->queryOptimizer->processOperator($filter); + + $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter); + if ($searchExpr) { + $query->andWhere($searchExpr); + } + + $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder()); + + if ($searchQuery->getLimit()) { + $query->setMaxResults($searchQuery->getLimit()); + } + if ($searchQuery->getOffset()) { + $query->setFirstResult($searchQuery->getOffset()); + } + } + + public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array { + $query = $this->getQueryBuilder(); + $query->selectTagUsage(); + + $this->applySearchConstraints($query, $searchQuery, $caches); + + $result = $query->execute(); + $tags = $result->fetchAll(); + $result->closeCursor(); + return $tags; + } + /** * Perform a file system search in multiple caches * @@ -128,26 +163,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array )); } - $storageFilters = array_values(array_map(function (ICache $cache) { - return $cache->getQueryFilterForStorage(); - }, $caches)); - $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters); - $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]); - $this->queryOptimizer->processOperator($filter); - - $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($builder, $filter); - if ($searchExpr) { - $query->andWhere($searchExpr); - } - - $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder()); - - if ($searchQuery->getLimit()) { - $query->setMaxResults($searchQuery->getLimit()); - } - if ($searchQuery->getOffset()) { - $query->setFirstResult($searchQuery->getOffset()); - } + $this->applySearchConstraints($query, $searchQuery, $caches); $result = $query->execute(); $files = $result->fetchAll(); @@ -159,7 +175,7 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array $result->closeCursor(); // loop through all caches for each result to see if the result matches that storage - // results are grouped by the same array keys as the caches argument to allow the caller to distringuish the source of the results + // results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results $results = array_fill_keys(array_keys($caches), []); foreach ($rawEntries as $rawEntry) { foreach ($caches as $cacheKey => $cache) { diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index bf9ae3c148d5c..e8d507b8a8f3b 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -203,7 +203,7 @@ public function newFile($path, $content = null) { throw new NotPermittedException('No create permission for path'); } - private function queryFromOperator(ISearchOperator $operator, string $uid = null): ISearchQuery { + private function queryFromOperator(ISearchOperator $operator, string $uid = null, int $limit = 0, int $offset = 0): ISearchQuery { if ($uid === null) { $user = null; } else { @@ -211,36 +211,17 @@ private function queryFromOperator(ISearchOperator $operator, string $uid = null $userManager = \OC::$server->query(IUserManager::class); $user = $userManager->get($uid); } - return new SearchQuery($operator, 0, 0, [], $user); + return new SearchQuery($operator, $limit, $offset, [], $user); } /** - * search for files with the name matching $query - * - * @param string|ISearchQuery $query - * @return \OC\Files\Node\Node[] + * @psalm-return list{0: array, 1: array} */ - public function search($query) { - if (is_string($query)) { - $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%')); - } - - // search is handled by a single query covering all caches that this folder contains - // this is done by collect - - $limitToHome = $query->limitToHome(); - if ($limitToHome && count(explode('/', $this->path)) !== 3) { - throw new \InvalidArgumentException('searching by owner is only allows on the users home folder'); - } - + protected function getCachesAndMountpointsForSearch(bool $limitToHome = false): array { $rootLength = strlen($this->path); $mount = $this->root->getMount($this->path); $storage = $mount->getStorage(); $internalPath = $mount->getInternalPath($this->path); - - // collect all caches for this folder, indexed by their mountpoint relative to this folder - // and save the mount which is needed later to construct the FileInfo objects - if ($internalPath !== '') { // a temporary CacheJail is used to handle filtering down the results to within this folder $caches = ['' => new CacheJail($storage->getCache(''), $internalPath)]; @@ -261,12 +242,36 @@ public function search($query) { } } + return [$caches, $mountByMountPoint]; + } + + /** + * search for files with the name matching $query + * + * @param string|ISearchQuery $query + * @return \OC\Files\Node\Node[] + */ + public function search($query) { + if (is_string($query)) { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%')); + } + + // search is handled by a single query covering all caches that this folder contains + // this is done by collect + + $limitToHome = $query->limitToHome(); + if ($limitToHome && count(explode('/', $this->path)) !== 3) { + throw new \InvalidArgumentException('searching by owner is only allows on the users home folder'); + } + + [$caches, $mountByMountPoint] = $this->getCachesAndMountpointsForSearch($limitToHome); + /** @var QuerySearchHelper $searchHelper */ $searchHelper = \OC::$server->get(QuerySearchHelper::class); $resultsPerCache = $searchHelper->searchInCaches($query, $caches); // loop through all results per-cache, constructing the FileInfo object from the CacheEntry and merge them all - $files = array_merge(...array_map(function (array $results, $relativeMountPoint) use ($mountByMountPoint) { + $files = array_merge(...array_map(function (array $results, string $relativeMountPoint) use ($mountByMountPoint) { $mount = $mountByMountPoint[$relativeMountPoint]; return array_map(function (ICacheEntry $result) use ($relativeMountPoint, $mount) { return $this->cacheEntryToFileInfo($mount, $relativeMountPoint, $result); @@ -331,6 +336,17 @@ public function searchByTag($tag, $userId) { return $this->search($query); } + /** + * @return Node[] + */ + public function getSystemTags(string $mediaType, int $limit = 0, int $offset = 0): array { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mediaType . '/%'), null, $limit, $offset); + [$caches, ] = $this->getCachesAndMountpointsForSearch(); + /** @var QuerySearchHelper $searchHelper */ + $searchHelper = \OCP\Server::get(QuerySearchHelper::class); + return $searchHelper->findUsedTagsInCaches($query, $caches); + } + /** * @param int $id * @return \OC\Files\Node\Node[] From 72d1488b02e1e2f07e2321aa8a3802a6465b5bef Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 28 Apr 2023 14:09:22 +0200 Subject: [PATCH 2/3] use efficient tag retrieval on DAV report request - uses DAV search approach against valid files joined by systemtag selector - reduced table join for tag/systemtag search - supports pagination - no changes to the output formats or similar Example request body: 32 50 0 Signed-off-by: Arthur Schiwon --- .../lib/Connector/Sabre/FilesReportPlugin.php | 78 +++++++++++++------ lib/private/Files/Cache/QuerySearchHelper.php | 37 +++++---- lib/private/Files/Node/Folder.php | 7 +- .../SystemTag/SystemTagObjectMapper.php | 1 + 4 files changed, 84 insertions(+), 39 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php index 4876e9ad8f3d7..c30c7bc51d009 100644 --- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php @@ -48,6 +48,7 @@ class FilesReportPlugin extends ServerPlugin { // namespace public const NS_OWNCLOUD = 'http://owncloud.org/ns'; + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; public const REPORT_NAME = '{http://owncloud.org/ns}filter-files'; public const SYSTEMTAG_PROPERTYNAME = '{http://owncloud.org/ns}systemtag'; public const CIRCLE_PROPERTYNAME = '{http://owncloud.org/ns}circle'; @@ -186,6 +187,7 @@ public function onReport($reportName, $report, $uri) { } $ns = '{' . $this::NS_OWNCLOUD . '}'; + $ncns = '{' . $this::NS_NEXTCLOUD . '}'; $requestedProps = []; $filterRules = []; @@ -199,6 +201,14 @@ public function onReport($reportName, $report, $uri) { foreach ($reportProps['value'] as $propVal) { $requestedProps[] = $propVal['name']; } + } elseif ($name === '{DAV:}limit') { + foreach ($reportProps['value'] as $propVal) { + if ($propVal['name'] === '{DAV:}nresults') { + $limit = (int)$propVal['value']; + } elseif ($propVal['name'] === $ncns . 'firstresult') { + $offset = (int)$propVal['value']; + } + } } } @@ -209,13 +219,24 @@ public function onReport($reportName, $report, $uri) { // gather all file ids matching filter try { - $resultFileIds = $this->processFilterRules($filterRules); + $resultFileIds = $this->processFilterRulesForFileIDs($filterRules); + $resultNodes = $this->processFilterRulesForFileNodes($filterRules, $limit ?? null, $offset ?? null); } catch (TagNotFoundException $e) { throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e); } + $results = []; + foreach ($resultNodes as $entry) { + if ($entry) { + $results[] = $this->wrapNode($entry); + } + } + // find sabre nodes by file id, restricted to the root node path - $results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); + $additionalNodes = $this->findNodesByFileIds($reportTargetNode, $resultFileIds); + if (!empty($additionalNodes)) { + $results = array_intersect($results, $additionalNodes); + } $filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath()); $responses = $this->prepareResponses($filesUri, $requestedProps, $results); @@ -264,16 +285,12 @@ private function getFilesBaseUri(string $uri, string $subPath): string { * * @throws TagNotFoundException whenever a tag was not found */ - protected function processFilterRules($filterRules) { + protected function processFilterRulesForFileIDs($filterRules) { $ns = '{' . $this::NS_OWNCLOUD . '}'; $resultFileIds = null; - $systemTagIds = []; $circlesIds = []; $favoriteFilter = null; foreach ($filterRules as $filterRule) { - if ($filterRule['name'] === $ns . 'systemtag') { - $systemTagIds[] = $filterRule['value']; - } if ($filterRule['name'] === self::CIRCLE_PROPERTYNAME) { $circlesIds[] = $filterRule['value']; } @@ -289,15 +306,6 @@ protected function processFilterRules($filterRules) { } } - if (!empty($systemTagIds)) { - $fileIds = $this->getSystemTagFileIds($systemTagIds); - if (empty($resultFileIds)) { - $resultFileIds = $fileIds; - } else { - $resultFileIds = array_intersect($fileIds, $resultFileIds); - } - } - if (!empty($circlesIds)) { $fileIds = $this->getCirclesFileIds($circlesIds); if (empty($resultFileIds)) { @@ -310,6 +318,25 @@ protected function processFilterRules($filterRules) { return $resultFileIds; } + protected function processFilterRulesForFileNodes(array $filterRules, ?int $limit, ?int $offset): array { + $systemTagIds = []; + foreach ($filterRules as $filterRule) { + if ($filterRule['name'] === self::SYSTEMTAG_PROPERTYNAME) { + $systemTagIds[] = $filterRule['value']; + } + } + + $nodes = []; + + if (!empty($systemTagIds)) { + $tags = $this->tagManager->getTagsByIds($systemTagIds); + $tagName = (current($tags))->getName(); + $nodes = $this->userFolder->searchBySystemTag($tagName, $this->userSession->getUser()->getUID(), $limit ?? 0, $offset ?? 0); + } + + return $nodes; + } + private function getSystemTagFileIds($systemTagIds) { $resultFileIds = null; @@ -406,7 +433,10 @@ public function prepareResponses($filesUri, $requestedProps, $nodes) { * @param array $fileIds file ids * @return Node[] array of Sabre nodes */ - public function findNodesByFileIds($rootNode, $fileIds) { + public function findNodesByFileIds($rootNode, $fileIds): array { + if (empty($fileIds)) { + return []; + } $folder = $this->userFolder; if (trim($rootNode->getPath(), '/') !== '') { $folder = $folder->get($rootNode->getPath()); @@ -417,17 +447,21 @@ public function findNodesByFileIds($rootNode, $fileIds) { $entry = $folder->getById($fileId); if ($entry) { $entry = current($entry); - if ($entry instanceof \OCP\Files\File) { - $results[] = new File($this->fileView, $entry); - } elseif ($entry instanceof \OCP\Files\Folder) { - $results[] = new Directory($this->fileView, $entry); - } + $results[] = $this->wrapNode($entry); } } return $results; } + protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory { + if ($node instanceof \OCP\Files\File) { + return new File($this->fileView, $node); + } else { + return new Directory($this->fileView, $node); + } + } + /** * Returns whether the currently logged in user is an administrator */ diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index fdba8a1dd7442..3c67425c78c46 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -33,6 +33,7 @@ use OCP\Files\Cache\ICacheEntry; use OCP\Files\IMimeTypeLoader; use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchQuery; use OCP\IDBConnection; use Psr\Log\LoggerInterface; @@ -145,22 +146,26 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array if ($user === null) { throw new \InvalidArgumentException("Searching by tag requires the user to be set in the query"); } - $query - ->leftJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid')) - ->leftJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX( - $builder->expr()->eq('tagmap.type', 'tag.type'), - $builder->expr()->eq('tagmap.categoryid', 'tag.id'), - $builder->expr()->eq('tag.type', $builder->createNamedParameter('files')), - $builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID())) - )) - ->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $builder->expr()->andX( - $builder->expr()->eq('file.fileid', $builder->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)), - $builder->expr()->eq('systemtagmap.objecttype', $builder->createNamedParameter('files')) - )) - ->leftJoin('systemtagmap', 'systemtag', 'systemtag', $builder->expr()->andX( - $builder->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'), - $builder->expr()->eq('systemtag.visibility', $builder->createNamedParameter(true)) - )); + if ($searchQuery->getSearchOperation() instanceof ISearchComparison && $searchQuery->getSearchOperation()->getField() === 'systemtag') { + $query + ->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $builder->expr()->andX( + $builder->expr()->eq('file.fileid', $builder->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)), + $builder->expr()->eq('systemtagmap.objecttype', $builder->createNamedParameter('files')) + )) + ->leftJoin('systemtagmap', 'systemtag', 'systemtag', $builder->expr()->andX( + $builder->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'), + $builder->expr()->eq('systemtag.visibility', $builder->createNamedParameter(true)) + )); + } else { + $query + ->leftJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid')) + ->leftJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX( + $builder->expr()->eq('tagmap.type', 'tag.type'), + $builder->expr()->eq('tagmap.categoryid', 'tag.id'), + $builder->expr()->eq('tag.type', $builder->createNamedParameter('files')), + $builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID())) + )); + } } $this->applySearchConstraints($query, $searchQuery, $caches); diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index e8d507b8a8f3b..90d33f538f8fe 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -332,7 +332,12 @@ public function searchByMime($mimetype) { * @return Node[] */ public function searchByTag($tag, $userId) { - $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', $tag), $userId); + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tag', $tag), $userId); + return $this->search($query); + } + + public function searchBySystemTag(string $tag, string $userId, int $limit = 0, int $offset = 0): array { + $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'systemtag', $tag), $userId, $limit, $offset); return $this->search($query); } diff --git a/lib/private/SystemTag/SystemTagObjectMapper.php b/lib/private/SystemTag/SystemTagObjectMapper.php index b61a81a1fa701..864ba3e91a898 100644 --- a/lib/private/SystemTag/SystemTagObjectMapper.php +++ b/lib/private/SystemTag/SystemTagObjectMapper.php @@ -135,6 +135,7 @@ public function getObjectIdsForTags($tagIds, string $objectType, int $limit = 0, while ($row = $result->fetch()) { $objectIds[] = $row['objectid']; } + $result->closeCursor(); return $objectIds; } From 1e97275cee040d896d8af96ba6bbf356212482d5 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 2 May 2023 09:42:32 +0200 Subject: [PATCH 3/3] adjustments to 25 Signed-off-by: Arthur Schiwon --- apps/dav/lib/Connector/Sabre/FilesReportPlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php index c30c7bc51d009..85819f7fed2ab 100644 --- a/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesReportPlugin.php @@ -454,10 +454,10 @@ public function findNodesByFileIds($rootNode, $fileIds): array { return $results; } - protected function wrapNode(\OCP\Files\File|\OCP\Files\Folder $node): File|Directory { + protected function wrapNode(\OCP\Files\Node $node): \Sabre\DAV\INode { if ($node instanceof \OCP\Files\File) { return new File($this->fileView, $node); - } else { + } elseif ($node instanceof \OCP\Files\Folder) { return new Directory($this->fileView, $node); } }