FilesSearchProvider.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Files\Search;
  8. use InvalidArgumentException;
  9. use OC\Files\Search\SearchBinaryOperator;
  10. use OC\Files\Search\SearchComparison;
  11. use OC\Files\Search\SearchOrder;
  12. use OC\Files\Search\SearchQuery;
  13. use OC\Search\Filter\GroupFilter;
  14. use OC\Search\Filter\UserFilter;
  15. use OCP\Files\FileInfo;
  16. use OCP\Files\IMimeTypeDetector;
  17. use OCP\Files\IRootFolder;
  18. use OCP\Files\Node;
  19. use OCP\Files\Search\ISearchComparison;
  20. use OCP\Files\Search\ISearchOperator;
  21. use OCP\Files\Search\ISearchOrder;
  22. use OCP\IL10N;
  23. use OCP\IPreview;
  24. use OCP\IURLGenerator;
  25. use OCP\IUser;
  26. use OCP\Search\FilterDefinition;
  27. use OCP\Search\IFilter;
  28. use OCP\Search\IFilteringProvider;
  29. use OCP\Search\ISearchQuery;
  30. use OCP\Search\SearchResult;
  31. use OCP\Search\SearchResultEntry;
  32. use OCP\Share\IShare;
  33. class FilesSearchProvider implements IFilteringProvider {
  34. public function __construct(
  35. private IL10N $l10n,
  36. private IURLGenerator $urlGenerator,
  37. private IMimeTypeDetector $mimeTypeDetector,
  38. private IRootFolder $rootFolder,
  39. private IPreview $previewManager,
  40. ) {
  41. }
  42. /**
  43. * @inheritDoc
  44. */
  45. public function getId(): string {
  46. return 'files';
  47. }
  48. /**
  49. * @inheritDoc
  50. */
  51. public function getName(): string {
  52. return $this->l10n->t('Files');
  53. }
  54. /**
  55. * @inheritDoc
  56. */
  57. public function getOrder(string $route, array $routeParameters): int {
  58. if ($route === 'files.View.index') {
  59. // Before comments
  60. return -5;
  61. }
  62. return 5;
  63. }
  64. public function getSupportedFilters(): array {
  65. return [
  66. 'term',
  67. 'since',
  68. 'until',
  69. 'person',
  70. 'min-size',
  71. 'max-size',
  72. 'mime',
  73. 'type',
  74. 'path',
  75. 'is-favorite',
  76. 'title-only',
  77. ];
  78. }
  79. public function getAlternateIds(): array {
  80. return [];
  81. }
  82. public function getCustomFilters(): array {
  83. return [
  84. new FilterDefinition('min-size', FilterDefinition::TYPE_INT),
  85. new FilterDefinition('max-size', FilterDefinition::TYPE_INT),
  86. new FilterDefinition('mime', FilterDefinition::TYPE_STRING),
  87. new FilterDefinition('type', FilterDefinition::TYPE_STRING),
  88. new FilterDefinition('path', FilterDefinition::TYPE_STRING),
  89. new FilterDefinition('is-favorite', FilterDefinition::TYPE_BOOL),
  90. ];
  91. }
  92. public function search(IUser $user, ISearchQuery $query): SearchResult {
  93. $userFolder = $this->rootFolder->getUserFolder($user->getUID());
  94. $fileQuery = $this->buildSearchQuery($query, $user);
  95. return SearchResult::paginated(
  96. $this->l10n->t('Files'),
  97. array_map(function (Node $result) use ($userFolder) {
  98. $thumbnailUrl = $this->previewManager->isMimeSupported($result->getMimetype())
  99. ? $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', ['x' => 32, 'y' => 32, 'fileId' => $result->getId()])
  100. : '';
  101. $icon = $result->getMimetype() === FileInfo::MIMETYPE_FOLDER
  102. ? 'icon-folder'
  103. : $this->mimeTypeDetector->mimeTypeIcon($result->getMimetype());
  104. $path = $userFolder->getRelativePath($result->getPath());
  105. // Use shortened link to centralize the various
  106. // files/folder url redirection in files.View.showFile
  107. $link = $this->urlGenerator->linkToRoute(
  108. 'files.View.showFile',
  109. ['fileid' => $result->getId()]
  110. );
  111. $searchResultEntry = new SearchResultEntry(
  112. $thumbnailUrl,
  113. $result->getName(),
  114. $this->formatSubline($path),
  115. $this->urlGenerator->getAbsoluteURL($link),
  116. $icon,
  117. );
  118. $searchResultEntry->addAttribute('fileId', (string)$result->getId());
  119. $searchResultEntry->addAttribute('path', $path);
  120. return $searchResultEntry;
  121. }, $userFolder->search($fileQuery)),
  122. $query->getCursor() + $query->getLimit()
  123. );
  124. }
  125. private function buildSearchQuery(ISearchQuery $query, IUser $user): SearchQuery {
  126. $comparisons = [];
  127. foreach ($query->getFilters() as $name => $filter) {
  128. $comparisons[] = match ($name) {
  129. 'term' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $filter->get() . '%'),
  130. 'since' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
  131. 'until' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()),
  132. 'min-size' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', $filter->get()),
  133. 'max-size' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'size', $filter->get()),
  134. 'mime' => new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $filter->get()),
  135. 'type' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filter->get() . '/%'),
  136. 'path' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'path', 'files/' . ltrim($filter->get(), '/') . '%'),
  137. 'person' => $this->buildPersonSearchQuery($filter),
  138. default => throw new InvalidArgumentException('Unsupported comparison'),
  139. };
  140. }
  141. return new SearchQuery(
  142. new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, $comparisons),
  143. $query->getLimit(),
  144. (int)$query->getCursor(),
  145. $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC
  146. ? [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')]
  147. : [],
  148. $user
  149. );
  150. }
  151. private function buildPersonSearchQuery(IFilter $person): ISearchOperator {
  152. if ($person instanceof UserFilter) {
  153. return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [
  154. new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
  155. new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getUID()),
  156. new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_USER),
  157. ]),
  158. new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'owner', $person->get()->getUID()),
  159. ]);
  160. }
  161. if ($person instanceof GroupFilter) {
  162. return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [
  163. new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getGID()),
  164. new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_GROUP),
  165. ]);
  166. }
  167. throw new InvalidArgumentException('Unsupported filter type');
  168. }
  169. /**
  170. * Format subline for files
  171. *
  172. * @param string $path
  173. * @return string
  174. */
  175. private function formatSubline(string $path): string {
  176. // Do not show the location if the file is in root
  177. if (strrpos($path, '/') > 0) {
  178. $path = ltrim(dirname($path), '/');
  179. return $this->l10n->t('in %s', [$path]);
  180. } else {
  181. return '';
  182. }
  183. }
  184. }