Преглед на файлове

Merge pull request #37961 from nextcloud/poc/noid/systemtags-perf

SystemTags endpoint to return tags used by a user with meta data
Arthur Schiwon преди 1 година
родител
ревизия
b6c034ac57

+ 1 - 0
apps/dav/composer/composer/autoload_classmap.php

@@ -311,6 +311,7 @@ return array(
     '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',

+ 1 - 0
apps/dav/composer/composer/autoload_static.php

@@ -326,6 +326,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',

+ 4 - 0
apps/dav/lib/RootCollection.php

@@ -48,6 +48,7 @@ use OCP\Accounts\IAccountManager;
 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 @@ class RootCollection extends SimpleCollection {
 		$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,
@@ -131,6 +133,7 @@ class RootCollection extends SimpleCollection {
 			$groupManager,
 			\OC::$server->getEventDispatcher()
 		);
+		$systemTagInUseCollection = \OCP\Server::get(SystemTag\SystemTagsInUseCollection::class);
 		$commentsCollection = new Comments\RootCollection(
 			\OC::$server->getCommentsManager(),
 			$userManager,
@@ -179,6 +182,7 @@ class RootCollection extends SimpleCollection {
 				$systemAddressBookRoot]),
 			$systemTagCollection,
 			$systemTagRelationsCollection,
+			$systemTagInUseCollection,
 			$commentsCollection,
 			$uploadCollection,
 			$avatarCollection,

+ 19 - 0
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
 	 *
@@ -179,4 +182,20 @@ class SystemTagNode implements \Sabre\DAV\INode {
 			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;
+	}
 }

+ 24 - 0
apps/dav/lib/SystemTag/SystemTagPlugin.php

@@ -62,6 +62,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
 	public const GROUPS_PROPERTYNAME = '{http://owncloud.org/ns}groups';
 	public const CANASSIGN_PROPERTYNAME = '{http://owncloud.org/ns}can-assign';
 	public const SYSTEM_TAGS_PROPERTYNAME = '{http://nextcloud.org/ns}system-tags';
+	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
@@ -243,6 +245,11 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
 			return;
 		}
 
+		// child nodes from systemtags-assigned should point to normal tag endpoint
+		if (preg_match('/^systemtags-assigned\/[0-9]+/', $propFind->getPath())) {
+			$propFind->setPath(str_replace('systemtags-assigned/', 'systemtags/', $propFind->getPath()));
+		}
+
 		$propFind->handle(self::ID_PROPERTYNAME, function () use ($node) {
 			return $node->getSystemTag()->getId();
 		});
@@ -277,6 +284,16 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
 			}
 			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();
+			});
+		}
 	}
 
 	private function propfindForFile(PropFind $propFind, Node $node): void {
@@ -374,6 +391,8 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
 			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();
@@ -410,6 +429,11 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
 				$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);
 			}

+ 108 - 0
apps/dav/lib/SystemTag/SystemTagsInUseCollection.php

@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @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 <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\DAV\SystemTag;
+
+use OC\SystemTag\SystemTag;
+use OC\SystemTag\SystemTagsInFilesDetector;
+use OC\User\NoUserException;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotPermittedException;
+use OCP\IUserSession;
+use OCP\SystemTag\ISystemTagManager;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\SimpleCollection;
+
+class SystemTagsInUseCollection extends SimpleCollection {
+	protected IUserSession $userSession;
+	protected IRootFolder $rootFolder;
+	protected string $mediaType;
+	protected ISystemTagManager $systemTagManager;
+	protected SystemTagsInFilesDetector $systemTagsInFilesDetector;
+
+	/** @noinspection PhpMissingParentConstructorInspection */
+	public function __construct(
+		IUserSession $userSession,
+		IRootFolder $rootFolder,
+		ISystemTagManager $systemTagManager,
+		SystemTagsInFilesDetector $systemTagsInFilesDetector,
+		string $mediaType = ''
+	) {
+		$this->userSession = $userSession;
+		$this->rootFolder = $rootFolder;
+		$this->systemTagManager = $systemTagManager;
+		$this->mediaType = $mediaType;
+		$this->systemTagsInFilesDetector = $systemTagsInFilesDetector;
+		$this->name = 'systemtags-assigned';
+		if ($this->mediaType != '') {
+			$this->name .= '/' . $this->mediaType;
+		}
+	}
+
+	public function setName($name): void {
+		throw new Forbidden('Permission denied to rename this collection');
+	}
+
+	public function getChild($name): self {
+		if ($this->mediaType !== '') {
+			throw new NotFound('Invalid media type');
+		}
+		return new self($this->userSession, $this->rootFolder, $this->systemTagManager, $this->systemTagsInFilesDetector, $name);
+	}
+
+	/**
+	 * @return SystemTagNode[]
+	 * @throws NotPermittedException
+	 * @throws Forbidden
+	 */
+	public function getChildren(): array {
+		$user = $this->userSession->getUser();
+		$userFolder = null;
+		try {
+			if ($user) {
+				$userFolder = $this->rootFolder->getUserFolder($user->getUID());
+			}
+		} catch (NoUserException) {
+			// will throw a Sabre exception in the next step.
+		}
+		if ($user === null || $userFolder === null) {
+			throw new Forbidden('Permission denied to read this collection');
+		}
+
+		$result = $this->systemTagsInFilesDetector->detectAssignedSystemTagsIn($userFolder, $this->mediaType);
+		$children = [];
+		foreach ($result as $tagData) {
+			$tag = new SystemTag((string)$tagData['id'], $tagData['name'], (bool)$tagData['visibility'], (bool)$tagData['editable']);
+			// read only, so we can submit the isAdmin parameter as false generally
+			$node = new SystemTagNode($tag, $user, false, $this->systemTagManager);
+			$node->setNumberOfFiles($tagData['number_files']);
+			$node->setReferenceFileId($tagData['ref_file_id']);
+			$children[] = $node;
+		}
+		return $children;
+	}
+}

+ 1 - 0
lib/composer/composer/autoload_classmap.php

@@ -1620,6 +1620,7 @@ return array(
     'OC\\SystemTag\\SystemTag' => $baseDir . '/lib/private/SystemTag/SystemTag.php',
     'OC\\SystemTag\\SystemTagManager' => $baseDir . '/lib/private/SystemTag/SystemTagManager.php',
     'OC\\SystemTag\\SystemTagObjectMapper' => $baseDir . '/lib/private/SystemTag/SystemTagObjectMapper.php',
+    'OC\\SystemTag\\SystemTagsInFilesDetector' => $baseDir . '/lib/private/SystemTag/SystemTagsInFilesDetector.php',
     'OC\\TagManager' => $baseDir . '/lib/private/TagManager.php',
     'OC\\Tagging\\Tag' => $baseDir . '/lib/private/Tagging/Tag.php',
     'OC\\Tagging\\TagMapper' => $baseDir . '/lib/private/Tagging/TagMapper.php',

+ 1 - 0
lib/composer/composer/autoload_static.php

@@ -1653,6 +1653,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\SystemTag\\SystemTag' => __DIR__ . '/../../..' . '/lib/private/SystemTag/SystemTag.php',
         'OC\\SystemTag\\SystemTagManager' => __DIR__ . '/../../..' . '/lib/private/SystemTag/SystemTagManager.php',
         'OC\\SystemTag\\SystemTagObjectMapper' => __DIR__ . '/../../..' . '/lib/private/SystemTag/SystemTagObjectMapper.php',
+        'OC\\SystemTag\\SystemTagsInFilesDetector' => __DIR__ . '/../../..' . '/lib/private/SystemTag/SystemTagsInFilesDetector.php',
         'OC\\TagManager' => __DIR__ . '/../../..' . '/lib/private/TagManager.php',
         'OC\\Tagging\\Tag' => __DIR__ . '/../../..' . '/lib/private/Tagging/Tag.php',
         'OC\\Tagging\\TagMapper' => __DIR__ . '/../../..' . '/lib/private/Tagging/TagMapper.php',

+ 20 - 1
lib/private/Files/Cache/CacheQueryBuilder.php

@@ -41,8 +41,27 @@ class CacheQueryBuilder extends QueryBuilder {
 		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))
+			))
+			->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);

+ 78 - 21
lib/private/Files/Cache/QuerySearchHelper.php

@@ -25,13 +25,17 @@
  */
 namespace OC\Files\Cache;
 
+use OC\Files\Cache\Wrapper\CacheJail;
+use OC\Files\Node\Root;
 use OC\Files\Search\QueryOptimizer\QueryOptimizer;
 use OC\Files\Search\SearchBinaryOperator;
 use OC\SystemConfig;
 use OCP\DB\QueryBuilder\IQueryBuilder;
 use OCP\Files\Cache\ICache;
 use OCP\Files\Cache\ICacheEntry;
+use OCP\Files\Folder;
 use OCP\Files\IMimeTypeLoader;
+use OCP\Files\Mount\IMountPoint;
 use OCP\Files\Search\ISearchBinaryOperator;
 use OCP\Files\Search\ISearchQuery;
 use OCP\IDBConnection;
@@ -74,6 +78,45 @@ class QuerySearchHelper {
 		);
 	}
 
+	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());
+		}
+	}
+
+
+	/**
+	 * @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
+	 */
+	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
 	 *
@@ -127,26 +170,7 @@ class QuerySearchHelper {
 				));
 		}
 
-		$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();
@@ -158,7 +182,7 @@ class QuerySearchHelper {
 		$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) {
@@ -170,4 +194,37 @@ class QuerySearchHelper {
 		}
 		return $results;
 	}
+
+	/**
+	 * @return array{array<string, ICache>, array<string, IMountPoint>}
+	 */
+	public function getCachesAndMountPointsForSearch(Root $root, string $path, bool $limitToHome = false): array {
+		$rootLength = strlen($path);
+		$mount = $root->getMount($path);
+		$storage = $mount->getStorage();
+		$internalPath = $mount->getInternalPath($path);
+
+		if ($internalPath !== '') {
+			// a temporary CacheJail is used to handle filtering down the results to within this folder
+			$caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
+		} else {
+			$caches = ['' => $storage->getCache('')];
+		}
+		$mountByMountPoint = ['' => $mount];
+
+		if (!$limitToHome) {
+			/** @var IMountPoint[] $mounts */
+			$mounts = $root->getMountsIn($path);
+			foreach ($mounts as $mount) {
+				$storage = $mount->getStorage();
+				if ($storage) {
+					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
+					$caches[$relativeMountPoint] = $storage->getCache('');
+					$mountByMountPoint[$relativeMountPoint] = $mount;
+				}
+			}
+		}
+
+		return [$caches, $mountByMountPoint];
+	}
 }

+ 4 - 32
lib/private/Files/Node/Folder.php

@@ -33,7 +33,6 @@ namespace OC\Files\Node;
 
 use OC\Files\Cache\QuerySearchHelper;
 use OC\Files\Search\SearchBinaryOperator;
-use OC\Files\Cache\Wrapper\CacheJail;
 use OC\Files\Search\SearchComparison;
 use OC\Files\Search\SearchOrder;
 use OC\Files\Search\SearchQuery;
@@ -204,7 +203,7 @@ class Folder extends Node implements \OCP\Files\Folder {
 		throw new NotPermittedException('No create permission for path "' . $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 {
@@ -212,7 +211,7 @@ class Folder extends Node implements \OCP\Files\Folder {
 			$userManager = \OC::$server->query(IUserManager::class);
 			$user = $userManager->get($uid);
 		}
-		return new SearchQuery($operator, 0, 0, [], $user);
+		return new SearchQuery($operator, $limit, $offset, [], $user);
 	}
 
 	/**
@@ -234,40 +233,13 @@ class Folder extends Node implements \OCP\Files\Folder {
 			throw new \InvalidArgumentException('searching by owner is only allowed in the users home folder');
 		}
 
-		$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)];
-		} else {
-			$caches = ['' => $storage->getCache('')];
-		}
-		$mountByMountPoint = ['' => $mount];
-
-		if (!$limitToHome) {
-			$mounts = $this->root->getMountsIn($this->path);
-			foreach ($mounts as $mount) {
-				$storage = $mount->getStorage();
-				if ($storage) {
-					$relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
-					$caches[$relativeMountPoint] = $storage->getCache('');
-					$mountByMountPoint[$relativeMountPoint] = $mount;
-				}
-			}
-		}
-
 		/** @var QuerySearchHelper $searchHelper */
 		$searchHelper = \OC::$server->get(QuerySearchHelper::class);
+		[$caches, $mountByMountPoint] = $searchHelper->getCachesAndMountPointsForSearch($this->root, $this->path, $limitToHome);
 		$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);

+ 72 - 0
lib/private/SystemTag/SystemTagsInFilesDetector.php

@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @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 <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\SystemTag;
+
+use OC\Files\Cache\QuerySearchHelper;
+use OC\Files\Node\Root;
+use OC\Files\Search\SearchBinaryOperator;
+use OC\Files\Search\SearchComparison;
+use OC\Files\Search\SearchQuery;
+use OCP\Files\Folder;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
+
+class SystemTagsInFilesDetector {
+	public function __construct(protected QuerySearchHelper $searchHelper) {
+	}
+
+	public function detectAssignedSystemTagsIn(
+		Folder $folder,
+		string $filteredMediaType = '',
+		int $limit = 0,
+		int $offset = 0
+	): array {
+		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'systemtag', '%');
+		// Currently query has to have exactly one search condition. If no media type is provided,
+		// we fall back to the presence of a system tag.
+		if ($filteredMediaType !== '') {
+			$mimeOperator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filteredMediaType . '/%');
+			$operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$operator, $mimeOperator]);
+		}
+
+		$query = new SearchQuery($operator, $limit, $offset, []);
+		[$caches, ] = $this->searchHelper->getCachesAndMountPointsForSearch(
+			$this->getRootFolder($folder),
+			$folder->getPath(),
+		);
+		return $this->searchHelper->findUsedTagsInCaches($query, $caches);
+	}
+
+	protected function getRootFolder(?Folder $folder): Root {
+		if ($folder instanceof Root) {
+			return $folder;
+		} elseif ($folder === null) {
+			throw new \LogicException('Could not climb up to root folder');
+		}
+		return $this->getRootFolder($folder->getParent());
+	}
+}