]> source.dussan.org Git - nextcloud-server.git/commitdiff
Implement webdav SEARCH
authorRobin Appelman <robin@icewind.nl>
Thu, 2 Feb 2017 17:20:08 +0000 (18:20 +0100)
committerRobin Appelman <robin@icewind.nl>
Wed, 1 Mar 2017 13:06:39 +0000 (14:06 +0100)
Signed-off-by: Robin Appelman <robin@icewind.nl>
18 files changed:
apps/dav/lib/Files/FileSearchBackend.php
apps/dav/lib/Server.php
lib/private/Files/Cache/Cache.php
lib/private/Files/Cache/FailedCache.php
lib/private/Files/Cache/QuerySearchHelper.php
lib/private/Files/Cache/Wrapper/CacheJail.php
lib/private/Files/Cache/Wrapper/CacheWrapper.php
lib/private/Files/Node/Folder.php
lib/private/Files/Search/SearchBinaryOperator.php
lib/private/Files/Search/SearchQuery.php
lib/private/Lockdown/Filesystem/NullCache.php
lib/public/Files/Cache/ICache.php
lib/public/Files/Folder.php
lib/public/Files/Search/ISearchBinaryOperator.php
lib/public/Files/Search/ISearchComparison.php
lib/public/Files/Search/ISearchOperator.php
lib/public/Files/Search/ISearchQuery.php
tests/lib/Files/Cache/QuerySearchHelperTest.php

index 2a89c756d3515440429fa3f89fcd97a87d5b3440..3d87612aad74648d1595f5181fd3ed0455e008c8 100644 (file)
 
 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;
+       }
 }
index 79c4301a8d8d5e3caa8afc80a8b1574cda2f747b..57f89f708a76d593fa8fee21b1a3de5d378e613a 100644 (file)
@@ -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
+                                       )));
                                }
                        }
                });
index 7e7ebd795a0db61b08592e4658cd044f2bbe65ff..b0527d801d6f02908fd674e1983904fe6cd0b173 100644 (file)
 
 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.
index 3a0424b5e2690e0e07eebfab19843384c5fe189b..932a5e5181aee2b0314b158bda55dffdb353fc11 100644 (file)
@@ -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 [];
        }
index 60f4e484da5fddff25ac760b0a64a2c4abf918ae..931f258ec5b50c9ce87c7ad76497d03c29da386d 100644 (file)
@@ -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) {
index 894fbcc803d34faa7ea3f0ad9cfd58158d206012..ebab20fbaedb19b1a76ce50d21688ccbecabb543 100644 (file)
@@ -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
         *
index 83fe7e5f43e89674a59dba1fad380ccea84862c0..1463d1467b8524651656bbc5926fd439a64af309 100644 (file)
@@ -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
         *
index fd907f708f39376810c7dba042780c2aca568c83..45372d0fedf59a66ada603dbb2f0d3fbf53f2f0d 100644 (file)
@@ -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));
+               }
        }
 
        /**
index 15944e277689bca3779f9aa4a0944594105cb5d3..c9466d8b9ea4fd05068fc5feb4457b6c14b49ad0 100644 (file)
  * 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;
+       }
+}
index a017d3e245bc42fc309bea85b012ab13779ec937..8a0478ae98e6b5d62d217d377c76237cd28f139c 100644 (file)
 
 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;
+       }
+}
index 8c6b5258aa8b47b3661b7fc06b3b6d5992aa4310..9cb8016194b61a32f513f6cce58a99e89e838d83 100644 (file)
@@ -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 [];
        }
index 7d01b1a290881fe3498fca698d8fabe9689ed582..63993d0a8cbdc96680f7ec594dac8b6d82b59dce 100644 (file)
@@ -21,6 +21,8 @@
  */
 
 namespace OCP\Files\Cache;
+use OCP\Files\Search\ISearchOperator;
+use OCP\Files\Search\ISearchQuery;
 
 /**
  * Metadata cache for a storage
@@ -212,6 +214,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.
         *
index 8f8576d8503f290bc9eded60d7ddd64a819302d0..52a4b303196d19a9e7d36199b1a5e71b6b111bf4 100644 (file)
@@ -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
         */
index 248e3de56b81a31994013447d6352278bd04d5a7..d5a2d5dc02db95d6c77b4f6a5bead1377d072935 100644 (file)
@@ -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();
 }
index 0534221861601b010add033784e50d473b97bdbc..e5175ee3117479f9f98c4e792a3e0af73b526d92 100644 (file)
@@ -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();
 }
index fc1db57f4ca809dd783484f8574290e1c12d7c12..047792bc782bb86872ab7d5dc5b3efbf1e793bdc 100644 (file)
@@ -21,6 +21,9 @@
 
 namespace OCP\Files\Search;
 
-interface ISearchCondition {
+/**
+ * @since 12.0.0
+ */
+interface ISearchOperator {
 
 }
index f9c07533a1efdaf2da9a1291d9559c0094517192..5a701b321b12eca6a798f032b8ca2e63afb06cec 100644 (file)
@@ -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();
 }
index bdf3d79432f8ad2982230dc3db64208e4ca6c141..beb0981d2df24214014a4b656ff4c90e198c1d7f 100644 (file)
 
 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);
+       }
 }