QuerySearchHelper.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Files\Cache;
  7. use OC\Files\Cache\Wrapper\CacheJail;
  8. use OC\Files\Search\QueryOptimizer\QueryOptimizer;
  9. use OC\Files\Search\SearchBinaryOperator;
  10. use OC\SystemConfig;
  11. use OCP\DB\QueryBuilder\IQueryBuilder;
  12. use OCP\Files\Cache\ICache;
  13. use OCP\Files\Cache\ICacheEntry;
  14. use OCP\Files\IMimeTypeLoader;
  15. use OCP\Files\IRootFolder;
  16. use OCP\Files\Mount\IMountPoint;
  17. use OCP\Files\Search\ISearchBinaryOperator;
  18. use OCP\Files\Search\ISearchQuery;
  19. use OCP\FilesMetadata\IFilesMetadataManager;
  20. use OCP\FilesMetadata\IMetadataQuery;
  21. use OCP\IDBConnection;
  22. use OCP\IGroupManager;
  23. use OCP\IUser;
  24. use Psr\Log\LoggerInterface;
  25. class QuerySearchHelper {
  26. public function __construct(
  27. private IMimeTypeLoader $mimetypeLoader,
  28. private IDBConnection $connection,
  29. private SystemConfig $systemConfig,
  30. private LoggerInterface $logger,
  31. private SearchBuilder $searchBuilder,
  32. private QueryOptimizer $queryOptimizer,
  33. private IGroupManager $groupManager,
  34. private IFilesMetadataManager $filesMetadataManager,
  35. ) {
  36. }
  37. protected function getQueryBuilder() {
  38. return new CacheQueryBuilder(
  39. $this->connection,
  40. $this->systemConfig,
  41. $this->logger,
  42. $this->filesMetadataManager,
  43. );
  44. }
  45. /**
  46. * @param CacheQueryBuilder $query
  47. * @param ISearchQuery $searchQuery
  48. * @param array $caches
  49. * @param IMetadataQuery|null $metadataQuery
  50. */
  51. protected function applySearchConstraints(
  52. CacheQueryBuilder $query,
  53. ISearchQuery $searchQuery,
  54. array $caches,
  55. ?IMetadataQuery $metadataQuery = null
  56. ): void {
  57. $storageFilters = array_values(array_map(function (ICache $cache) {
  58. return $cache->getQueryFilterForStorage();
  59. }, $caches));
  60. $storageFilter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters);
  61. $filter = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [$searchQuery->getSearchOperation(), $storageFilter]);
  62. $this->queryOptimizer->processOperator($filter);
  63. $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($query, $filter, $metadataQuery);
  64. if ($searchExpr) {
  65. $query->andWhere($searchExpr);
  66. }
  67. $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder(), $metadataQuery);
  68. if ($searchQuery->getLimit()) {
  69. $query->setMaxResults($searchQuery->getLimit());
  70. }
  71. if ($searchQuery->getOffset()) {
  72. $query->setFirstResult($searchQuery->getOffset());
  73. }
  74. }
  75. /**
  76. * @return array<array-key, array{id: int, name: string, visibility: int, editable: int, ref_file_id: int, number_files: int}>
  77. */
  78. public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): array {
  79. $query = $this->getQueryBuilder();
  80. $query->selectTagUsage();
  81. $this->applySearchConstraints($query, $searchQuery, $caches);
  82. $result = $query->execute();
  83. $tags = $result->fetchAll();
  84. $result->closeCursor();
  85. return $tags;
  86. }
  87. protected function equipQueryForSystemTags(CacheQueryBuilder $query, IUser $user): void {
  88. $query->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $query->expr()->andX(
  89. $query->expr()->eq('file.fileid', $query->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)),
  90. $query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files'))
  91. ));
  92. $on = $query->expr()->andX($query->expr()->eq('systemtag.id', 'systemtagmap.systemtagid'));
  93. if (!$this->groupManager->isAdmin($user->getUID())) {
  94. $on->add($query->expr()->eq('systemtag.visibility', $query->createNamedParameter(true)));
  95. }
  96. $query->leftJoin('systemtagmap', 'systemtag', 'systemtag', $on);
  97. }
  98. protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): void {
  99. $query
  100. ->leftJoin('file', 'vcategory_to_object', 'tagmap', $query->expr()->eq('file.fileid', 'tagmap.objid'))
  101. ->leftJoin('tagmap', 'vcategory', 'tag', $query->expr()->andX(
  102. $query->expr()->eq('tagmap.type', 'tag.type'),
  103. $query->expr()->eq('tagmap.categoryid', 'tag.id'),
  104. $query->expr()->eq('tag.type', $query->createNamedParameter('files')),
  105. $query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID()))
  106. ));
  107. }
  108. protected function equipQueryForShares(CacheQueryBuilder $query): void {
  109. $query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source'));
  110. }
  111. /**
  112. * Perform a file system search in multiple caches
  113. *
  114. * the results will be grouped by the same array keys as the $caches argument to allow
  115. * post-processing based on which cache the result came from
  116. *
  117. * @template T of array-key
  118. * @param ISearchQuery $searchQuery
  119. * @param array<T, ICache> $caches
  120. * @return array<T, ICacheEntry[]>
  121. */
  122. public function searchInCaches(ISearchQuery $searchQuery, array $caches): array {
  123. // search in multiple caches at once by creating one query in the following format
  124. // SELECT ... FROM oc_filecache WHERE
  125. // <filter expressions from the search query>
  126. // AND (
  127. // <filter expression for storage1> OR
  128. // <filter expression for storage2> OR
  129. // ...
  130. // );
  131. //
  132. // This gives us all the files matching the search query from all caches
  133. //
  134. // while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id)
  135. // we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result.
  136. $builder = $this->getQueryBuilder();
  137. $query = $builder->selectFileCache('file', false);
  138. $requestedFields = $this->searchBuilder->extractRequestedFields($searchQuery->getSearchOperation());
  139. if (in_array('systemtag', $requestedFields)) {
  140. $this->equipQueryForSystemTags($query, $this->requireUser($searchQuery));
  141. }
  142. if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) {
  143. $this->equipQueryForDavTags($query, $this->requireUser($searchQuery));
  144. }
  145. if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
  146. $this->equipQueryForShares($query);
  147. }
  148. $metadataQuery = $query->selectMetadata();
  149. $this->applySearchConstraints($query, $searchQuery, $caches, $metadataQuery);
  150. $result = $query->execute();
  151. $files = $result->fetchAll();
  152. $rawEntries = array_map(function (array $data) use ($metadataQuery) {
  153. $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
  154. return Cache::cacheEntryFromData($data, $this->mimetypeLoader);
  155. }, $files);
  156. $result->closeCursor();
  157. // loop through all caches for each result to see if the result matches that storage
  158. // results are grouped by the same array keys as the caches argument to allow the caller to distinguish the source of the results
  159. $results = array_fill_keys(array_keys($caches), []);
  160. foreach ($rawEntries as $rawEntry) {
  161. foreach ($caches as $cacheKey => $cache) {
  162. $entry = $cache->getCacheEntryFromSearchResult($rawEntry);
  163. if ($entry) {
  164. $results[$cacheKey][] = $entry;
  165. }
  166. }
  167. }
  168. return $results;
  169. }
  170. protected function requireUser(ISearchQuery $searchQuery): IUser {
  171. $user = $searchQuery->getUser();
  172. if ($user === null) {
  173. throw new \InvalidArgumentException("This search operation requires the user to be set in the query");
  174. }
  175. return $user;
  176. }
  177. /**
  178. * @return list{0?: array<array-key, ICache>, 1?: array<array-key, IMountPoint>}
  179. */
  180. public function getCachesAndMountPointsForSearch(IRootFolder $root, string $path, bool $limitToHome = false): array {
  181. $rootLength = strlen($path);
  182. $mount = $root->getMount($path);
  183. $storage = $mount->getStorage();
  184. if ($storage === null) {
  185. return [];
  186. }
  187. $internalPath = $mount->getInternalPath($path);
  188. if ($internalPath !== '') {
  189. // a temporary CacheJail is used to handle filtering down the results to within this folder
  190. /** @var ICache[] $caches */
  191. $caches = ['' => new CacheJail($storage->getCache(''), $internalPath)];
  192. } else {
  193. /** @var ICache[] $caches */
  194. $caches = ['' => $storage->getCache('')];
  195. }
  196. /** @var IMountPoint[] $mountByMountPoint */
  197. $mountByMountPoint = ['' => $mount];
  198. if (!$limitToHome) {
  199. $mounts = $root->getMountsIn($path);
  200. foreach ($mounts as $mount) {
  201. $storage = $mount->getStorage();
  202. if ($storage) {
  203. $relativeMountPoint = ltrim(substr($mount->getMountPoint(), $rootLength), '/');
  204. $caches[$relativeMountPoint] = $storage->getCache('');
  205. $mountByMountPoint[$relativeMountPoint] = $mount;
  206. }
  207. }
  208. }
  209. return [$caches, $mountByMountPoint];
  210. }
  211. }