diff options
author | Robin Appelman <robin@icewind.nl> | 2017-02-02 18:20:08 +0100 |
---|---|---|
committer | Robin Appelman <robin@icewind.nl> | 2017-03-01 14:06:39 +0100 |
commit | df2063ee7b49d051f9081c6fd416dd8791358ada (patch) | |
tree | a90e9f6dbae0767247593941e7c6e95b9c41befb | |
parent | 706131b394eef4d346f8019b4978f9a735139b03 (diff) | |
download | nextcloud-server-df2063ee7b49d051f9081c6fd416dd8791358ada.tar.gz nextcloud-server-df2063ee7b49d051f9081c6fd416dd8791358ada.zip |
Implement webdav SEARCH
Signed-off-by: Robin Appelman <robin@icewind.nl>
-rw-r--r-- | apps/dav/lib/Files/FileSearchBackend.php | 204 | ||||
-rw-r--r-- | apps/dav/lib/Server.php | 8 | ||||
-rw-r--r-- | lib/private/Files/Cache/Cache.php | 43 | ||||
-rw-r--r-- | lib/private/Files/Cache/FailedCache.php | 5 | ||||
-rw-r--r-- | lib/private/Files/Cache/QuerySearchHelper.php | 37 | ||||
-rw-r--r-- | lib/private/Files/Cache/Wrapper/CacheJail.php | 6 | ||||
-rw-r--r-- | lib/private/Files/Cache/Wrapper/CacheWrapper.php | 6 | ||||
-rw-r--r-- | lib/private/Files/Node/Folder.php | 9 | ||||
-rw-r--r-- | lib/private/Files/Search/SearchBinaryOperator.php | 37 | ||||
-rw-r--r-- | lib/private/Files/Search/SearchQuery.php | 57 | ||||
-rw-r--r-- | lib/private/Lockdown/Filesystem/NullCache.php | 5 | ||||
-rw-r--r-- | lib/public/Files/Cache/ICache.php | 12 | ||||
-rw-r--r-- | lib/public/Files/Folder.php | 3 | ||||
-rw-r--r-- | lib/public/Files/Search/ISearchBinaryOperator.php | 5 | ||||
-rw-r--r-- | lib/public/Files/Search/ISearchComparison.php | 3 | ||||
-rw-r--r-- | lib/public/Files/Search/ISearchOperator.php | 5 | ||||
-rw-r--r-- | lib/public/Files/Search/ISearchQuery.php | 24 | ||||
-rw-r--r-- | tests/lib/Files/Cache/QuerySearchHelperTest.php | 173 |
18 files changed, 609 insertions, 33 deletions
diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 2a89c756d35..3d87612aad7 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -21,32 +21,73 @@ namespace OCA\DAV\Files; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchOrder; +use OC\Files\Search\SearchQuery; +use OC\Files\View; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; +use OCP\Files\Search\ISearchQuery; +use OCP\IUser; +use OCP\Share\IManager; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Tree; use SearchDAV\Backend\ISearchBackend; use SearchDAV\Backend\SearchPropertyDefinition; +use SearchDAV\Backend\SearchResult; +use SearchDAV\XML\BasicSearch; +use SearchDAV\XML\Literal; +use SearchDAV\XML\Operator; +use SearchDAV\XML\Order; class FileSearchBackend implements ISearchBackend { /** @var Tree */ private $tree; + /** @var IUser */ + private $user; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IManager */ + private $shareManager; + + /** @var View */ + private $view; + /** * FileSearchBackend constructor. * * @param Tree $tree + * @param IUser $user + * @param IRootFolder $rootFolder + * @param IManager $shareManager + * @param View $view + * @internal param IRootFolder $rootFolder */ - public function __construct(Tree $tree) { + public function __construct(Tree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) { $this->tree = $tree; + $this->user = $user; + $this->rootFolder = $rootFolder; + $this->shareManager = $shareManager; + $this->view = $view; } /** - * Search endpoint will be remote.php/dav/files + * Search endpoint will be remote.php/dav * * @return string */ public function getArbiterPath() { - return 'files'; + return ''; } public function isValidScope($href, $depth, $path) { @@ -66,11 +107,162 @@ class FileSearchBackend implements ISearchBackend { public function getPropertyDefinitionsForScope($href, $path) { // all valid scopes support the same schema + //todo dynamically load all propfind properties that are supported return [ - new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + // queryable properties + new SearchPropertyDefinition('{DAV:}displayname', true, false, true), new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), - new SearchPropertyDefinition('{DAV:}displayname', true, true, true), - new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), + new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + + // select only properties + new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false), + new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false), + new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false), + new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN), + new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), ]; } + + public function search(BasicSearch $search) { + if (count($search->from) !== 1) { + throw new \InvalidArgumentException('Searching more than one folder is not supported'); + } + $query = $this->transformQuery($search); + $scope = $search->from[0]; + if ($scope->path === null) { + throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead'); + } + $node = $this->tree->getNodeForPath($scope->path); + if (!$node instanceof Directory) { + throw new \InvalidArgumentException('Search is only supported on directories'); + } + + $fileInfo = $node->getFileInfo(); + $folder = $this->rootFolder->get($fileInfo->getPath()); + /** @var Folder $folder $results */ + $results = $folder->search($query); + + return array_map(function (Node $node) { + if ($node instanceof Folder) { + return new SearchResult(new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager), $this->getHrefForNode($node)); + } else { + return new SearchResult(new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager), $this->getHrefForNode($node)); + } + }, $results); + } + + /** + * @param Node $node + * @return string + */ + private function getHrefForNode(Node $node) { + $base = '/files/' . $this->user->getUID(); + return $base . $this->view->getRelativePath($node->getPath()); + } + + /** + * @param BasicSearch $query + * @return ISearchQuery + */ + private function transformQuery(BasicSearch $query) { + // TODO offset, limit + $orders = array_map([$this, 'mapSearchOrder'], $query->orderBy); + return new SearchQuery($this->transformSearchOperation($query->where), 0, 0, $orders); + } + + /** + * @param Order $order + * @return ISearchOrder + */ + private function mapSearchOrder(Order $order) { + return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToCollumn($order->property)); + } + + /** + * @param Operator $operator + * @return ISearchOperator + */ + private function transformSearchOperation(Operator $operator) { + list(, $trimmedType) = explode('}', $operator->type); + switch ($operator->type) { + case Operator::OPERATION_AND: + case Operator::OPERATION_OR: + case Operator::OPERATION_NOT: + $arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments); + return new SearchBinaryOperator($trimmedType, $arguments); + case Operator::OPERATION_EQUAL: + case Operator::OPERATION_GREATER_OR_EQUAL_THAN: + case Operator::OPERATION_GREATER_THAN: + case Operator::OPERATION_LESS_OR_EQUAL_THAN: + case Operator::OPERATION_LESS_THAN: + case Operator::OPERATION_IS_LIKE: + if (count($operator->arguments) !== 2) { + throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation'); + } + if (gettype($operator->arguments[0]) !== 'string') { + throw new \InvalidArgumentException('Invalid argument 1 for ' . $trimmedType . ' operation, expected property'); + } + if (!($operator->arguments[1] instanceof Literal)) { + throw new \InvalidArgumentException('Invalid argument 2 for ' . $trimmedType . ' operation, expected literal'); + } + return new SearchComparison($trimmedType, $this->mapPropertyNameToCollumn($operator->arguments[0]), $this->castValue($operator->arguments[0], $operator->arguments[1]->value)); + case Operator::OPERATION_IS_COLLECTION: + return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE); + default: + throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType. ' (' . $operator->type . ')'); + } + } + + /** + * @param string $propertyName + * @return string + */ + private function mapPropertyNameToCollumn($propertyName) { + /** + * new SearchPropertyDefinition('{DAV:}displayname', true, false, true), + * new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), + * new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME), + * new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER), + */ + + switch ($propertyName) { + case '{DAV:}displayname': + return 'name'; + case '{DAV:}getcontenttype': + return 'mimetype'; + case '{DAV:}getlastmodifed': + return 'mtime'; + case FilesPlugin::SIZE_PROPERTYNAME: + return 'size'; + default: + throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName); + } + } + + private function castValue($propertyName, $value) { + $allProps = $this->getPropertyDefinitionsForScope('', ''); + foreach ($allProps as $prop) { + if ($prop->name === $propertyName) { + $dataType = $prop->dataType; + switch ($dataType) { + case SearchPropertyDefinition::DATATYPE_BOOLEAN: + return $value === 'yes'; + case SearchPropertyDefinition::DATATYPE_DECIMAL: + case SearchPropertyDefinition::DATATYPE_INTEGER: + case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER: + return 0 + $value; + default: + return $value; + } + } + } + return $value; + } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 79c4301a8d8..57f89f708a7 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -51,6 +51,7 @@ use OCP\SabrePluginEvent; use Sabre\CardDAV\VCFExportPlugin; use Sabre\DAV\Auth\Plugin; use OCA\DAV\Connector\Sabre\TagsPlugin; +use SearchDAV\DAV\SearchPlugin; class Server { @@ -222,6 +223,13 @@ class Server { \OC::$server->getGroupManager(), $userFolder )); + $this->server->addPlugin(new SearchPlugin(new \OCA\DAV\Files\FileSearchBackend( + $this->server->tree, + $user, + \OC::$server->getRootFolder(), + \OC::$server->getShareManager(), + $view + ))); } } }); diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 7e7ebd795a0..b0527d801d6 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -36,9 +36,11 @@ namespace OC\Files\Cache; +use Doctrine\DBAL\Driver\Statement; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use \OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchQuery; use OCP\IDBConnection; /** @@ -79,6 +81,9 @@ class Cache implements ICache { */ protected $connection; + /** @var QuerySearchHelper */ + protected $querySearchHelper; + /** * @param \OC\Files\Storage\Storage|string $storage */ @@ -95,6 +100,7 @@ class Cache implements ICache { $this->storageCache = new Storage($storage); $this->mimetypeLoader = \OC::$server->getMimeTypeLoader(); $this->connection = \OC::$server->getDatabaseConnection(); + $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader); } /** @@ -350,7 +356,7 @@ class Cache implements ICache { $queryParts[] = '`mtime`'; } } elseif ($name === 'encrypted') { - if(isset($data['encryptedVersion'])) { + if (isset($data['encryptedVersion'])) { $value = $data['encryptedVersion']; } else { // Boolean to integer conversion @@ -599,9 +605,17 @@ class Cache implements ICache { [$this->getNumericStorageId(), $pattern] ); + return $this->searchResultToCacheEntries($result); + } + + /** + * @param Statement $result + * @return CacheEntry[] + */ + private function searchResultToCacheEntries(Statement $result) { $files = $result->fetchAll(); - return array_map(function(array $data) { + return array_map(function (array $data) { return self::cacheEntryFromData($data, $this->mimetypeLoader); }, $files); } @@ -624,14 +638,29 @@ class Cache implements ICache { $mimetype = $this->mimetypeLoader->getId($mimetype); $result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId())); - $files = $result->fetchAll(); + return $this->searchResultToCacheEntries($result); + } - return array_map(function (array $data) { - return self::cacheEntryFromData($data, $this->mimetypeLoader); - }, $files); + public function searchQuery(ISearchQuery $searchQuery) { + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $query = $builder->select(['fileid', 'storage', 'path', 'parent', 'name', 'mimetype', 'mimepart', 'size', 'mtime', 'storage_mtime', 'encrypted', 'etag', 'permissions', 'checksum']) + ->from('filecache') + ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->getNumericStorageId()))) + ->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation())); + + if ($searchQuery->getLimit()) { + $query->setMaxResults($searchQuery->getLimit()); + } + if ($searchQuery->getOffset()) { + $query->setFirstResult($searchQuery->getOffset()); + } + + $result = $query->execute(); + return $this->searchResultToCacheEntries($result); } - /** + /** * Search for files by tag of a given users. * * Note that every user can tag files differently. diff --git a/lib/private/Files/Cache/FailedCache.php b/lib/private/Files/Cache/FailedCache.php index 3a0424b5e26..932a5e5181a 100644 --- a/lib/private/Files/Cache/FailedCache.php +++ b/lib/private/Files/Cache/FailedCache.php @@ -24,6 +24,7 @@ namespace OC\Files\Cache; use OCP\Constants; use OCP\Files\Cache\ICache; +use OCP\Files\Search\ISearchQuery; /** * Storage placeholder to represent a missing precondition, storage unavailable @@ -125,6 +126,10 @@ class FailedCache implements ICache { return []; } + public function searchQuery(ISearchQuery $query) { + return []; + } + public function getAll() { return []; } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 60f4e484da5..931f258ec5b 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -30,7 +30,7 @@ use OCP\Files\Search\ISearchOperator; /** * Tools for transforming search queries into database queries */ -class QuerySearchUtil { +class QuerySearchHelper { static protected $searchOperatorMap = [ ISearchComparison::COMPARE_LIKE => 'iLike', ISearchComparison::COMPARE_EQUAL => 'eq', @@ -73,9 +73,9 @@ class QuerySearchUtil { throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); } case ISearchBinaryOperator::OPERATOR_AND: - return $expr->andX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + return $expr->andX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1])); case ISearchBinaryOperator::OPERATOR_OR: - return $expr->orX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1])); + return $expr->orX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1])); default: throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); } @@ -87,13 +87,11 @@ class QuerySearchUtil { } private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { - if (!$this->isValidComparison($comparison)) { - throw new \InvalidArgumentException('Invalid comparison ' . $operator->getType() . ' on field ' . $operator->getField()); - } + $this->validateComparison($comparison); - list($field, $value) = $this->getOperatorFieldAndValue($comparison); - if (isset($operatorMap[$comparison->getType()])) { - $queryOperator = $operatorMap[$comparison->getType()]; + list($field, $value, $type) = $this->getOperatorFieldAndValue($comparison); + if (isset($operatorMap[$type])) { + $queryOperator = $operatorMap[$type]; return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); } else { throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); @@ -103,6 +101,7 @@ class QuerySearchUtil { private function getOperatorFieldAndValue(ISearchComparison $operator) { $field = $operator->getField(); $value = $operator->getValue(); + $type = $operator->getType(); if ($field === 'mimetype') { if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { $value = $this->mimetypeLoader->getId($value); @@ -111,16 +110,20 @@ class QuerySearchUtil { if (preg_match('|(.+)/%|', $value, $matches)) { $field = 'mimepart'; $value = $this->mimetypeLoader->getId($matches[1]); + $type = ISearchComparison::COMPARE_EQUAL; + } + if (strpos($value, '%') !== false) { + throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported'); } } } - return [$field, $value]; + return [$field, $value, $type]; } - private function isValidComparison(ISearchComparison $operator) { + private function validateComparison(ISearchComparison $operator) { $types = [ 'mimetype' => 'string', - 'mtime' => \DateTime::class, + 'mtime' => 'integer', 'name' => 'string', 'size' => 'integer' ]; @@ -132,13 +135,15 @@ class QuerySearchUtil { ]; if (!isset($types[$operator->getField()])) { - return false; + throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField()); } $type = $types[$operator->getField()]; - if (gettype($operator->getValue()) !== $type && !(is_a($operator->getValue(), $type))) { - return false; + if (gettype($operator->getValue()) !== $type) { + throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField()); + } + if (!in_array($operator->getType(), $comparisons[$operator->getField()])) { + throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType()); } - return in_array($operator->getType(), $comparisons[$operator->getField()]); } private function getParameterForValue(IQueryBuilder $builder, $value) { diff --git a/lib/private/Files/Cache/Wrapper/CacheJail.php b/lib/private/Files/Cache/Wrapper/CacheJail.php index 894fbcc803d..ebab20fbaed 100644 --- a/lib/private/Files/Cache/Wrapper/CacheJail.php +++ b/lib/private/Files/Cache/Wrapper/CacheJail.php @@ -28,6 +28,7 @@ namespace OC\Files\Cache\Wrapper; use OC\Files\Cache\Cache; use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Search\ISearchQuery; /** * Jail to a subdirectory of the wrapped cache @@ -218,6 +219,11 @@ class CacheJail extends CacheWrapper { return $this->formatSearchResults($results); } + public function searchQuery(ISearchQuery $query) { + $results = $this->getCache()->searchQuery($query); + return $this->formatSearchResults($results); + } + /** * search for files by mimetype * diff --git a/lib/private/Files/Cache/Wrapper/CacheWrapper.php b/lib/private/Files/Cache/Wrapper/CacheWrapper.php index 83fe7e5f43e..1463d1467b8 100644 --- a/lib/private/Files/Cache/Wrapper/CacheWrapper.php +++ b/lib/private/Files/Cache/Wrapper/CacheWrapper.php @@ -31,6 +31,7 @@ namespace OC\Files\Cache\Wrapper; use OC\Files\Cache\Cache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Cache\ICache; +use OCP\Files\Search\ISearchQuery; class CacheWrapper extends Cache { /** @@ -229,6 +230,11 @@ class CacheWrapper extends Cache { return array_map(array($this, 'formatCacheEntry'), $results); } + public function searchQuery(ISearchQuery $query) { + $results = $this->getCache()->searchQuery($query); + return array_map(array($this, 'formatCacheEntry'), $results); + } + /** * search for files by tag * diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index fd907f708f3..45372d0fedf 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -33,6 +33,7 @@ use OCP\Files\FileInfo; use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Files\Search\ISearchOperator; class Folder extends Node implements \OCP\Files\Folder { /** @@ -190,11 +191,15 @@ class Folder extends Node implements \OCP\Files\Folder { /** * search for files with the name matching $query * - * @param string $query + * @param string|ISearchOperator $query * @return \OC\Files\Node\Node[] */ public function search($query) { - return $this->searchCommon('search', array('%' . $query . '%')); + if (is_string($query)) { + return $this->searchCommon('search', array('%' . $query . '%')); + } else { + return $this->searchCommon('searchQuery', array($query)); + } } /** diff --git a/lib/private/Files/Search/SearchBinaryOperator.php b/lib/private/Files/Search/SearchBinaryOperator.php index 15944e27768..c9466d8b9ea 100644 --- a/lib/private/Files/Search/SearchBinaryOperator.php +++ b/lib/private/Files/Search/SearchBinaryOperator.php @@ -18,3 +18,40 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + +namespace OC\Files\Search; + +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchOperator; + +class SearchBinaryOperator implements ISearchBinaryOperator { + /** @var string */ + private $type; + /** @var ISearchOperator[] */ + private $arguments; + + /** + * SearchBinaryOperator constructor. + * + * @param string $type + * @param ISearchOperator[] $arguments + */ + public function __construct($type, array $arguments) { + $this->type = $type; + $this->arguments = $arguments; + } + + /** + * @return string + */ + public function getType() { + return $this->type; + } + + /** + * @return ISearchOperator[] + */ + public function getArguments() { + return $this->arguments; + } +} diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php index a017d3e245b..8a0478ae98e 100644 --- a/lib/private/Files/Search/SearchQuery.php +++ b/lib/private/Files/Search/SearchQuery.php @@ -21,3 +21,60 @@ namespace OC\Files\Search; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; +use OCP\Files\Search\ISearchQuery; + +class SearchQuery implements ISearchQuery { + /** @var ISearchOperator */ + private $searchOperation; + /** @var integer */ + private $limit; + /** @var integer */ + private $offset; + /** @var ISearchOrder[] */ + private $order; + + /** + * SearchQuery constructor. + * + * @param ISearchOperator $searchOperation + * @param int $limit + * @param int $offset + * @param array $order + */ + public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order) { + $this->searchOperation = $searchOperation; + $this->limit = $limit; + $this->offset = $offset; + $this->order = $order; + } + + /** + * @return ISearchOperator + */ + public function getSearchOperation() { + return $this->searchOperation; + } + + /** + * @return int + */ + public function getLimit() { + return $this->limit; + } + + /** + * @return int + */ + public function getOffset() { + return $this->offset; + } + + /** + * @return ISearchOrder[] + */ + public function getOrder() { + return $this->order; + } +} diff --git a/lib/private/Lockdown/Filesystem/NullCache.php b/lib/private/Lockdown/Filesystem/NullCache.php index 8c6b5258aa8..9cb8016194b 100644 --- a/lib/private/Lockdown/Filesystem/NullCache.php +++ b/lib/private/Lockdown/Filesystem/NullCache.php @@ -24,6 +24,7 @@ use OCP\Constants; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\FileInfo; +use OCP\Files\Search\ISearchQuery; class NullCache implements ICache { public function getNumericStorageId() { @@ -103,6 +104,10 @@ class NullCache implements ICache { return []; } + public function searchQuery(ISearchQuery $query) { + return []; + } + public function searchByTag($tag, $userId) { return []; } diff --git a/lib/public/Files/Cache/ICache.php b/lib/public/Files/Cache/ICache.php index 7d01b1a2908..63993d0a8cb 100644 --- a/lib/public/Files/Cache/ICache.php +++ b/lib/public/Files/Cache/ICache.php @@ -21,6 +21,8 @@ */ namespace OCP\Files\Cache; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchQuery; /** * Metadata cache for a storage @@ -213,6 +215,16 @@ interface ICache { public function searchByMime($mimetype); /** + * Search for files with a flexible query + * + * @param ISearchQuery $query + * @return ICacheEntry[] + * @throw \InvalidArgumentException if the cache is unable to perform the query + * @since 12.0.0 + */ + public function searchQuery(ISearchQuery $query); + + /** * Search for files by tag of a given users. * * Note that every user can tag files differently. diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php index 8f8576d8503..52a4b303196 100644 --- a/lib/public/Files/Folder.php +++ b/lib/public/Files/Folder.php @@ -30,6 +30,7 @@ // use OCP namespace for all classes that are considered public. // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP\Files; +use OCP\Files\Search\ISearchQuery; /** * @since 6.0.0 @@ -115,7 +116,7 @@ interface Folder extends Node { /** * search for files with the name matching $query * - * @param string $query + * @param string|ISearchQuery $query * @return \OCP\Files\Node[] * @since 6.0.0 */ diff --git a/lib/public/Files/Search/ISearchBinaryOperator.php b/lib/public/Files/Search/ISearchBinaryOperator.php index 248e3de56b8..d5a2d5dc02d 100644 --- a/lib/public/Files/Search/ISearchBinaryOperator.php +++ b/lib/public/Files/Search/ISearchBinaryOperator.php @@ -21,6 +21,9 @@ namespace OCP\Files\Search; +/** + * @since 12.0.0 + */ interface ISearchBinaryOperator extends ISearchOperator { const OPERATOR_AND = 'and'; const OPERATOR_OR = 'or'; @@ -32,6 +35,7 @@ interface ISearchBinaryOperator extends ISearchOperator { * One of the ISearchBinaryOperator::OPERATOR_* constants * * @return string + * @since 12.0.0 */ public function getType(); @@ -41,6 +45,7 @@ interface ISearchBinaryOperator extends ISearchOperator { * One argument for the 'not' operator and two for 'and' and 'or' * * @return ISearchOperator[] + * @since 12.0.0 */ public function getArguments(); } diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php index 05342218616..e5175ee3117 100644 --- a/lib/public/Files/Search/ISearchComparison.php +++ b/lib/public/Files/Search/ISearchComparison.php @@ -33,6 +33,7 @@ interface ISearchComparison extends ISearchOperator { * Get the type of comparison, one of the ISearchComparison::COMPARE_* constants * * @return string + * @since 12.0.0 */ public function getType(); @@ -42,6 +43,7 @@ interface ISearchComparison extends ISearchOperator { * i.e. 'size', 'name' or 'mimetype' * * @return string + * @since 12.0.0 */ public function getField(); @@ -49,6 +51,7 @@ interface ISearchComparison extends ISearchOperator { * Get the value to compare the field with * * @return string|integer|\DateTime + * @since 12.0.0 */ public function getValue(); } diff --git a/lib/public/Files/Search/ISearchOperator.php b/lib/public/Files/Search/ISearchOperator.php index fc1db57f4ca..047792bc782 100644 --- a/lib/public/Files/Search/ISearchOperator.php +++ b/lib/public/Files/Search/ISearchOperator.php @@ -21,6 +21,9 @@ namespace OCP\Files\Search; -interface ISearchCondition { +/** + * @since 12.0.0 + */ +interface ISearchOperator { } diff --git a/lib/public/Files/Search/ISearchQuery.php b/lib/public/Files/Search/ISearchQuery.php index f9c07533a1e..5a701b321b1 100644 --- a/lib/public/Files/Search/ISearchQuery.php +++ b/lib/public/Files/Search/ISearchQuery.php @@ -30,4 +30,28 @@ interface ISearchQuery { * @since 12.0.0 */ public function getSearchOperation(); + + /** + * Get the maximum number of results to return + * + * @return integer + * @since 12.0.0 + */ + public function getLimit(); + + /** + * Get the offset for returned results + * + * @return integer + * @since 12.0.0 + */ + public function getOffset(); + + /** + * The fields and directions to order by + * + * @return ISearchOrder[] + * @since 12.0.0 + */ + public function getOrder(); } diff --git a/tests/lib/Files/Cache/QuerySearchHelperTest.php b/tests/lib/Files/Cache/QuerySearchHelperTest.php index bdf3d79432f..beb0981d2df 100644 --- a/tests/lib/Files/Cache/QuerySearchHelperTest.php +++ b/tests/lib/Files/Cache/QuerySearchHelperTest.php @@ -21,8 +21,181 @@ namespace Test\Files\Cache; +use OC\DB\QueryBuilder\Literal; +use OC\Files\Cache\QuerySearchHelper; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; use Test\TestCase; +/** + * @group DB + */ class QuerySearchHelperTest extends TestCase { + /** @var IQueryBuilder */ + private $builder; + + /** @var IMimeTypeLoader|\PHPUnit_Framework_MockObject_MockObject */ + private $mimetypeLoader; + + /** @var QuerySearchHelper */ + private $querySearchHelper; + + /** @var integer */ + private $numericStorageId; + + public function setUp() { + parent::setUp(); + $this->builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class); + + $this->mimetypeLoader->expects($this->any()) + ->method('getId') + ->willReturnMap([ + ['text', 1], + ['text/plain', 2], + ['text/xml', 3], + ['image/jpg', 4], + ['image/png', 5], + ['image', 6], + ]); + + $this->mimetypeLoader->expects($this->any()) + ->method('getMimetypeById') + ->willReturnMap([ + [1, 'text'], + [2, 'text/plain'], + [3, 'text/xml'], + [4, 'image/jpg'], + [5, 'image/png'], + [6, 'image'] + ]); + + $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader); + $this->numericStorageId = 10000; + + $this->builder->select(['fileid']) + ->from('filecache') + ->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId))); + } + + public function tearDown() { + parent::tearDown(); + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $builder->delete('filecache') + ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT))); + + $builder->execute(); + } + + private function addCacheEntry(array $data) { + $data['storage'] = $this->numericStorageId; + $data['etag'] = 'unimportant'; + $data['storage_mtime'] = $data['mtime']; + if (!isset($data['path'])) { + $data['path'] = 'random/' . $this->getUniqueID(); + } + $data['path_hash'] = md5($data['path']); + if (!isset($data['mtime'])) { + $data['mtime'] = 100; + } + if (!isset($data['size'])) { + $data['size'] = 100; + } + $data['name'] = basename($data['path']); + $data['parent'] = -1; + if (isset($data['mimetype'])) { + list($mimepart,) = explode('/', $data['mimetype']); + $data['mimepart'] = $this->mimetypeLoader->getId($mimepart); + $data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']); + } else { + $data['mimepart'] = 1; + $data['mimetype'] = 1; + } + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $values = []; + foreach ($data as $key => $value) { + $values[$key] = $builder->createNamedParameter($value); + } + + $builder->insert('filecache') + ->values($values) + ->execute(); + } + + private function search(ISearchOperator $operator) { + $dbOperator = $this->querySearchHelper->searchOperatorToDBExpr($this->builder, $operator); + $this->builder->andWhere($dbOperator); + return $this->builder->execute()->fetchAll(\PDO::FETCH_COLUMN); + } + + public function comparisonProvider() { + return [ + [new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1002]], + [new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [1001, 1002]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [1001, 1002]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [1001]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [1001, 1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001] + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [1001, 1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), + ]), [1001]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), + ]), [1002]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'), + ]), [1002]], + + ]; + } + + /** + * @dataProvider comparisonProvider + * + * @param ISearchOperator $operator + * @param array $fileIds + */ + public function testComparison(ISearchOperator $operator, array $fileIds) { + $this->addCacheEntry([ + 'path' => 'foobar', + 'fileid' => 1001, + 'mtime' => 100, + 'size' => 50, + 'mimetype' => 'image/jpg' + ]); + + $this->addCacheEntry([ + 'path' => 'fooasd', + 'fileid' => 1002, + 'mtime' => 150, + 'size' => 50, + 'mimetype' => 'image/png' + ]); + + $results = $this->search($operator); + $this->assertEquals($fileIds, $results); + } } |