]> source.dussan.org Git - nextcloud-server.git/commitdiff
PoC: SystemTags endpoint to return tags used by a user with meta data
authorArthur Schiwon <blizzz@arthur-schiwon.de>
Thu, 27 Apr 2023 20:24:16 +0000 (22:24 +0200)
committerArthur Schiwon <blizzz@arthur-schiwon.de>
Tue, 9 May 2023 21:51:48 +0000 (23:51 +0200)
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:
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
        <d:prop xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
                <oc:id/>
                <oc:display-name/>
                <oc:user-visible/>
                <oc:user-assignable/>
                <oc:can-assign/>
                <nc:files-assigned/>
                <nc:reference-fileid/>
        </d:prop>
</d:propfind>

Example output:
  …
  <d:response>
    <d:href>/master/remote.php/dav/systemtags/84</d:href>
    <d:propstat>
      <d:prop>
        <oc:id>84</oc:id>
        <oc:display-name>Computer</oc:display-name>
        <oc:user-visible>true</oc:user-visible>
        <oc:user-assignable>true</oc:user-assignable>
        <oc:can-assign>true</oc:can-assign>
        <nc:files-assigned>42</nc:files-assigned>
        <nc:reference-fileid>924022</nc:reference-fileid>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
  <d:response>
    <d:href>/remote.php/dav/systemtags/97</d:href>
    <d:propstat>
      <d:prop>
        <oc:id>97</oc:id>
        <oc:display-name>Bear</oc:display-name>
        <oc:user-visible>true</oc:user-visible>
        <oc:user-assignable>true</oc:user-assignable>
        <oc:can-assign>true</oc:can-assign>
        <nc:files-assigned>1</nc:files-assigned>
        <nc:reference-fileid>923422</nc:reference-fileid>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
  …

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
apps/dav/composer/composer/autoload_classmap.php
apps/dav/composer/composer/autoload_static.php
apps/dav/lib/RootCollection.php
apps/dav/lib/SystemTag/SystemTagNode.php
apps/dav/lib/SystemTag/SystemTagPlugin.php
apps/dav/lib/SystemTag/SystemTagsInUseCollection.php [new file with mode: 0644]
lib/private/Files/Cache/CacheQueryBuilder.php
lib/private/Files/Cache/QuerySearchHelper.php
lib/private/Files/Node/Folder.php

index ab7d3e719282d09ffd2610f00cd6d1d23ea86994..03495efc1bd26a2c103169e344584ab8d461637d 100644 (file)
@@ -310,6 +310,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',
index e0e1f86bdbb3eaa66b6854360875e3878d0cb00a..a0b742b35b80d5ceab578b531aafb19b9a9289b0 100644 (file)
@@ -325,6 +325,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',
index bacb550b4154ed077aa324ff46a8b4544d764f68..ffc2c262cae7353491c7c5175c95e2fcd30ab458 100644 (file)
@@ -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,10 @@ class RootCollection extends SimpleCollection {
                        $groupManager,
                        \OC::$server->getEventDispatcher()
                );
+               $systemTagInUseCollection = new SystemTag\SystemTagsInUseCollection(
+                       $userSession,
+                       $rootFolder
+               );
                $commentsCollection = new Comments\RootCollection(
                        \OC::$server->getCommentsManager(),
                        $userManager,
@@ -179,6 +185,7 @@ class RootCollection extends SimpleCollection {
                                $systemAddressBookRoot]),
                        $systemTagCollection,
                        $systemTagRelationsCollection,
+                       $systemTagInUseCollection,
                        $commentsCollection,
                        $uploadCollection,
                        $avatarCollection,
index 7310cdb19a2e5b9baf44975b1cb626d40ceba5c3..8ade5085b0342d147955efc567383ddd0d0c8d90 100644 (file)
@@ -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;
+       }
 }
index c5c828cfbff30618bad7da65cb12b9e9c42bc498..52ddc7b1b4028a18c5d5b85194395e00ec7ae2b8 100644 (file)
@@ -61,6 +61,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
@@ -242,6 +244,11 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
                        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();
                });
@@ -276,6 +283,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 {
@@ -370,6 +387,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();
@@ -406,6 +425,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);
                        }
diff --git a/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php b/apps/dav/lib/SystemTag/SystemTagsInUseCollection.php
new file mode 100644 (file)
index 0000000..938b14e
--- /dev/null
@@ -0,0 +1,67 @@
+<?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 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;
+       }
+}
index 496a8361d77482f8d2dd5b29812b02ee51a385a8..c5563750c4d59b7ab50acd796aafbd79fb8d79cb 100644 (file)
@@ -41,8 +41,28 @@ 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))
+                       ))
+                       ->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);
index eba2aac927bc47a055bcf51e305c25957ee96ae6..c2eed5688b5797c2f4cdd89321d0229d3e595b80 100644 (file)
@@ -74,6 +74,41 @@ 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());
+               }
+       }
+
+       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 +162,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 +174,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) {
index 2c376fe5885f534ed3b0b3025a257d058cb3fd8e..e649e1efc28ac86ebc2bb778ce51176f72c920d3 100644 (file)
@@ -204,7 +204,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,36 +212,17 @@ 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);
        }
 
        /**
-        * search for files with the name matching $query
-        *
-        * @param string|ISearchQuery $query
-        * @return \OC\Files\Node\Node[]
+        * @psalm-return list{0: array<string, \OCP\Files\Cache\ICache>, 1: array<string, \OCP\Files\Mount\IMountPoint>}
         */
-       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 allowed in 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)];
@@ -262,12 +243,36 @@ class Folder extends Node implements \OCP\Files\Folder {
                        }
                }
 
+               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 allowed in 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);
@@ -332,6 +337,17 @@ class Folder extends Node implements \OCP\Files\Folder {
                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[]