From fc2b60ca930c574adea7441c798d4d285cecce4e Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Mon, 25 Sep 2023 15:07:29 +0200 Subject: [PATCH] feat(files): Allow advanced search for files Signed-off-by: Benjamin Gaussorgues --- apps/files/lib/Search/FilesSearchProvider.php | 105 +++++++++++++++--- lib/private/Files/Cache/CacheQueryBuilder.php | 2 +- lib/private/Files/Cache/QuerySearchHelper.php | 8 ++ lib/private/Files/Cache/SearchBuilder.php | 8 ++ 4 files changed, 105 insertions(+), 18 deletions(-) diff --git a/apps/files/lib/Search/FilesSearchProvider.php b/apps/files/lib/Search/FilesSearchProvider.php index ba2d4bafa3062..35dd0e214639c 100644 --- a/apps/files/lib/Search/FilesSearchProvider.php +++ b/apps/files/lib/Search/FilesSearchProvider.php @@ -29,25 +29,32 @@ */ namespace OCA\Files\Search; +use InvalidArgumentException; +use OCP\Files\Search\ISearchOperator; +use OCP\Search\FilterDefinition; +use OCP\Search\IFilter; +use OCP\Search\IFilteringProvider; +use OCP\Share\IShare; +use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchOrder; use OC\Files\Search\SearchQuery; use OCP\Files\FileInfo; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; -use OCP\Files\Search\ISearchComparison; use OCP\Files\Node; +use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOrder; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; -use OCP\Search\IProvider; use OCP\Search\ISearchQuery; use OCP\Search\SearchResult; use OCP\Search\SearchResultEntry; +use OC\Search\Filter\GroupFilter; +use OC\Search\Filter\UserFilter; -class FilesSearchProvider implements IProvider { - +class FilesSearchProvider implements IFilteringProvider { /** @var IL10N */ private $l10n; @@ -97,21 +104,38 @@ public function getOrder(string $route, array $routeParameters): int { return 5; } - /** - * @inheritDoc - */ + public function getSupportedFilters(): array { + return [ + 'term', + 'since', + 'until', + 'person', + 'min-size', + 'max-size', + 'mime', + 'type', + 'is-favorite', + 'title-only', + ]; + } + + public function getAlternateIds(): array { + return []; + } + + public function getCustomFilters(): array { + return [ + new FilterDefinition('min-size', FilterDefinition::TYPE_INT), + new FilterDefinition('max-size', FilterDefinition::TYPE_INT), + new FilterDefinition('mime', FilterDefinition::TYPE_STRING), + new FilterDefinition('type', FilterDefinition::TYPE_STRING), + new FilterDefinition('is-favorite', FilterDefinition::TYPE_BOOL), + ]; + } + public function search(IUser $user, ISearchQuery $query): SearchResult { $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - $fileQuery = new SearchQuery( - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query->getTerm() . '%'), - $query->getLimit(), - (int)$query->getCursor(), - $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC ? [ - new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime'), - ] : [], - $user - ); - + $fileQuery = $this->buildSearchQuery($query, $user); return SearchResult::paginated( $this->l10n->t('Files'), array_map(function (Node $result) use ($userFolder) { @@ -141,6 +165,53 @@ public function search(IUser $user, ISearchQuery $query): SearchResult { ); } + private function buildSearchQuery(ISearchQuery $query, IUser $user): SearchQuery { + $comparisons = []; + foreach ($query->getFilters() as $name => $filter) { + $comparisons[] = match ($name) { + 'term' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $filter->get() . '%'), + 'since' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()), + 'until' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'mtime', $filter->get()->getTimestamp()), + 'min-size' => new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', $filter->get()), + 'max-size' => new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'size', $filter->get()), + 'mime' => new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $filter->get()), + 'type' => new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $filter->get() . '/%'), + 'person' => $this->buildPersonSearchQuery($filter), + default => throw new InvalidArgumentException('Unsupported comparison'), + }; + } + + return new SearchQuery( + new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, $comparisons), + $query->getLimit(), + (int) $query->getCursor(), + $query->getSortOrder() === ISearchQuery::SORT_DATE_DESC + ? [new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')] + : [], + $user + ); + } + + private function buildPersonSearchQuery(IFilter $person): ISearchOperator { + if ($person instanceof UserFilter) { + return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_OR, [ + new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getUID()), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_USER), + ]), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'owner', $person->get()->getUID()), + ]); + } + if ($person instanceof GroupFilter) { + return new SearchBinaryOperator(SearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_with', $person->get()->getGID()), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'share_type', IShare::TYPE_GROUP), + ]); + } + + throw new InvalidArgumentException('Unsupported filter type'); + } + /** * Format subline for files * diff --git a/lib/private/Files/Cache/CacheQueryBuilder.php b/lib/private/Files/Cache/CacheQueryBuilder.php index 27f66e63e7bf3..7971b5c630715 100644 --- a/lib/private/Files/Cache/CacheQueryBuilder.php +++ b/lib/private/Files/Cache/CacheQueryBuilder.php @@ -71,7 +71,7 @@ public function selectTagUsage(): self { public function selectFileCache(string $alias = null, bool $joinExtendedCache = true) { $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') + 'storage_mtime', 'encrypted', 'etag', "$name.permissions", 'checksum', 'unencrypted_size') ->from('filecache', $name); if ($joinExtendedCache) { diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index f8e5d1608f742..6aa230c842f2d 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -134,6 +134,11 @@ protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): )); } + + protected function equipQueryForShares(CacheQueryBuilder $query): void { + $query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source')); + } + /** * Perform a file system search in multiple caches * @@ -172,6 +177,9 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array if (in_array('tagname', $requestedFields) || in_array('favorite', $requestedFields)) { $this->equipQueryForDavTags($query, $this->requireUser($searchQuery)); } + if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) { + $this->equipQueryForShares($query); + } $metadataQuery = $query->selectMetadata(); diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 1f9a6af931b2d..860d5e41d89a1 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -192,6 +192,8 @@ private function getOperatorFieldAndValue(ISearchComparison $operator) { } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) { $field = 'path_hash'; $value = md5((string)$value); + } elseif ($field === 'owner') { + $field = 'uid_owner'; } return [$field, $value, $type]; } @@ -208,6 +210,9 @@ private function validateComparison(ISearchComparison $operator) { 'favorite' => 'boolean', 'fileid' => 'integer', 'storage' => 'integer', + 'share_with' => 'string', + 'share_type' => 'integer', + 'owner' => 'string', ]; $comparisons = [ 'mimetype' => ['eq', 'like'], @@ -220,6 +225,9 @@ private function validateComparison(ISearchComparison $operator) { 'favorite' => ['eq'], 'fileid' => ['eq'], 'storage' => ['eq'], + 'share_with' => ['eq'], + 'share_type' => ['eq'], + 'owner' => ['eq'], ]; if (!isset($types[$operator->getField()])) {