Parcourir la source

Merge pull request #3360 from nextcloud/dav-search

Implement webdav SEARCH
Robin Appelman il y a 7 ans
Parent
commit
2a8e922d67

+ 1 - 1
3rdparty

@@ -1 +1 @@
-Subproject commit 489bcf4f81e462f4d74f0b76f58caeabd58e75de
+Subproject commit 98fa92c67d735f82ae012786395e660f1513bef7

+ 265 - 0
apps/dav/lib/Files/FileSearchBackend.php

@@ -0,0 +1,265 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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, 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
+	 *
+	 * @return string
+	 */
+	public function getArbiterPath() {
+		return '';
+	}
+
+	public function isValidScope($href, $depth, $path) {
+		// only allow scopes inside the dav server
+		if (is_null($path)) {
+			return false;
+		}
+
+		try {
+			$node = $this->tree->getNodeForPath($path);
+			return $node instanceof Directory;
+		} catch (NotFound $e) {
+			return false;
+		}
+	}
+
+	public function getPropertyDefinitionsForScope($href, $path) {
+		// all valid scopes support the same schema
+
+		//todo dynamically load all propfind properties that are supported
+		return [
+			// queryable properties
+			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),
+
+			// 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),
+		];
+	}
+
+	/**
+	 * @param BasicSearch $search
+	 * @return SearchResult[]
+	 */
+	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) {
+		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;
+	}
+}

+ 8 - 0
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 {
 
@@ -223,6 +224,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
+					)));
 				}
 			}
 		});

+ 299 - 0
apps/dav/tests/unit/Files/FileSearchBackendTest.php

@@ -0,0 +1,299 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\DAV\Tests\Files;
+
+use OC\Files\Search\SearchComparison;
+use OC\Files\Search\SearchQuery;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\File;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCA\DAV\Files\FileSearchBackend;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Search\ISearchComparison;
+use OCP\IUser;
+use OCP\Share\IManager;
+use Sabre\DAV\Tree;
+use SearchDAV\XML\BasicSearch;
+use SearchDAV\XML\Literal;
+use SearchDAV\XML\Operator;
+use SearchDAV\XML\Scope;
+use Test\TestCase;
+
+class FileSearchBackendTest extends TestCase {
+	/** @var Tree|\PHPUnit_Framework_MockObject_MockObject */
+	private $tree;
+
+	/** @var IUser */
+	private $user;
+
+	/** @var IRootFolder|\PHPUnit_Framework_MockObject_MockObject */
+	private $rootFolder;
+
+	/** @var IManager|\PHPUnit_Framework_MockObject_MockObject */
+	private $shareManager;
+
+	/** @var View|\PHPUnit_Framework_MockObject_MockObject */
+	private $view;
+
+	/** @var Folder|\PHPUnit_Framework_MockObject_MockObject */
+	private $searchFolder;
+
+	/** @var FileSearchBackend */
+	private $search;
+
+	/** @var Directory|\PHPUnit_Framework_MockObject_MockObject */
+	private $davFolder;
+
+	protected function setUp() {
+		parent::setUp();
+
+		$this->user = $this->createMock(IUser::class);
+		$this->user->expects($this->any())
+			->method('getUID')
+			->willReturn('test');
+
+		$this->tree = $this->getMockBuilder(Tree::class)
+			->disableOriginalConstructor()
+			->getMock();
+
+		$this->view = $this->getMockBuilder(View::class)
+			->disableOriginalConstructor()
+			->getMock();
+
+		$this->view->expects($this->any())
+			->method('getRelativePath')
+			->willReturnArgument(0);
+
+		$this->rootFolder = $this->createMock(IRootFolder::class);
+
+		$this->shareManager = $this->createMock(IManager::class);
+
+		$this->searchFolder = $this->createMock(Folder::class);
+
+		$fileInfo = $this->createMock(FileInfo::class);
+
+		$this->davFolder = $this->createMock(Directory::class);
+
+		$this->davFolder->expects($this->any())
+			->method('getFileInfo')
+			->willReturn($fileInfo);
+
+		$this->rootFolder->expects($this->any())
+			->method('get')
+			->willReturn($this->searchFolder);
+
+		$this->search = new FileSearchBackend($this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view);
+	}
+
+	public function testSearchFilename() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->once())
+			->method('search')
+			->with(new SearchQuery(
+				new SearchComparison(
+					ISearchComparison::COMPARE_EQUAL,
+					'name',
+					'foo'
+				),
+				0,
+				0,
+				[]
+			))
+			->will($this->returnValue([
+				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+			]));
+
+		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+		$result = $this->search->search($query);
+
+		$this->assertCount(1, $result);
+		$this->assertEquals('/files/test/test/path', $result[0]->href);
+	}
+
+	public function testSearchMimetype() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->once())
+			->method('search')
+			->with(new SearchQuery(
+				new SearchComparison(
+					ISearchComparison::COMPARE_EQUAL,
+					'mimetype',
+					'foo'
+				),
+				0,
+				0,
+				[]
+			))
+			->will($this->returnValue([
+				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+			]));
+
+		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo');
+		$result = $this->search->search($query);
+
+		$this->assertCount(1, $result);
+		$this->assertEquals('/files/test/test/path', $result[0]->href);
+	}
+
+	public function testSearchSize() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->once())
+			->method('search')
+			->with(new SearchQuery(
+				new SearchComparison(
+					ISearchComparison::COMPARE_GREATER_THAN,
+					'size',
+					10
+				),
+				0,
+				0,
+				[]
+			))
+			->will($this->returnValue([
+				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+			]));
+
+		$query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10);
+		$result = $this->search->search($query);
+
+		$this->assertCount(1, $result);
+		$this->assertEquals('/files/test/test/path', $result[0]->href);
+	}
+
+	public function testSearchMtime() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->once())
+			->method('search')
+			->with(new SearchQuery(
+				new SearchComparison(
+					ISearchComparison::COMPARE_GREATER_THAN,
+					'mtime',
+					10
+				),
+				0,
+				0,
+				[]
+			))
+			->will($this->returnValue([
+				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+			]));
+
+		$query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodifed', 10);
+		$result = $this->search->search($query);
+
+		$this->assertCount(1, $result);
+		$this->assertEquals('/files/test/test/path', $result[0]->href);
+	}
+
+	public function testSearchIsCollection() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->once())
+			->method('search')
+			->with(new SearchQuery(
+				new SearchComparison(
+					ISearchComparison::COMPARE_EQUAL,
+					'mimetype',
+					FileInfo::MIMETYPE_FOLDER
+				),
+				0,
+				0,
+				[]
+			))
+			->will($this->returnValue([
+				new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
+			]));
+
+		$query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes');
+		$result = $this->search->search($query);
+
+		$this->assertCount(1, $result);
+		$this->assertEquals('/files/test/test/path', $result[0]->href);
+	}
+
+	/**
+	 * @expectedException \InvalidArgumentException
+	 */
+	public function testSearchInvalidProp() {
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($this->davFolder);
+
+		$this->searchFolder->expects($this->never())
+			->method('search');
+
+		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo');
+		$this->search->search($query);
+	}
+
+	private function getBasicQuery($type, $property, $value = null) {
+		$query = new BasicSearch();
+		$scope = new Scope('/', 'infinite');
+		$scope->path = '/';
+		$query->from = [$scope];
+		$query->orderBy = [];
+		$query->select = [];
+		if (is_null($value)) {
+			$query->where = new Operator(
+				$type,
+				[new Literal($property)]
+			);
+		} else {
+			$query->where = new Operator(
+				$type,
+				[$property, new Literal($value)]
+			);
+		}
+		return $query;
+	}
+
+	/**
+	 * @expectedException \InvalidArgumentException
+	 */
+	public function testSearchNonFolder() {
+		$davNode = $this->createMock(File::class);
+
+		$this->tree->expects($this->any())
+			->method('getNodeForPath')
+			->willReturn($davNode);
+
+		$query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo');
+		$this->search->search($query);
+	}
+}

+ 10 - 0
lib/composer/composer/autoload_classmap.php

@@ -141,6 +141,11 @@ return array(
     'OCP\\Files\\Notify\\IRenameChange' => $baseDir . '/lib/public/Files/Notify/IRenameChange.php',
     'OCP\\Files\\ObjectStore\\IObjectStore' => $baseDir . '/lib/public/Files/ObjectStore/IObjectStore.php',
     'OCP\\Files\\ReservedWordException' => $baseDir . '/lib/public/Files/ReservedWordException.php',
+    'OCP\\Files\\Search\\ISearchBinaryOperator' => $baseDir . '/lib/public/Files/Search/ISearchBinaryOperator.php',
+    'OCP\\Files\\Search\\ISearchComparison' => $baseDir . '/lib/public/Files/Search/ISearchComparison.php',
+    'OCP\\Files\\Search\\ISearchOperator' => $baseDir . '/lib/public/Files/Search/ISearchOperator.php',
+    'OCP\\Files\\Search\\ISearchOrder' => $baseDir . '/lib/public/Files/Search/ISearchOrder.php',
+    'OCP\\Files\\Search\\ISearchQuery' => $baseDir . '/lib/public/Files/Search/ISearchQuery.php',
     'OCP\\Files\\SimpleFS\\ISimpleFile' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleFile.php',
     'OCP\\Files\\SimpleFS\\ISimpleFolder' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleFolder.php',
     'OCP\\Files\\SimpleFS\\ISimpleRoot' => $baseDir . '/lib/public/Files/SimpleFS/ISimpleRoot.php',
@@ -509,6 +514,7 @@ return array(
     'OC\\Files\\Cache\\HomePropagator' => $baseDir . '/lib/private/Files/Cache/HomePropagator.php',
     'OC\\Files\\Cache\\MoveFromCacheTrait' => $baseDir . '/lib/private/Files/Cache/MoveFromCacheTrait.php',
     'OC\\Files\\Cache\\Propagator' => $baseDir . '/lib/private/Files/Cache/Propagator.php',
+    'OC\\Files\\Cache\\QuerySearchHelper' => $baseDir . '/lib/private/Files/Cache/QuerySearchHelper.php',
     'OC\\Files\\Cache\\Scanner' => $baseDir . '/lib/private/Files/Cache/Scanner.php',
     'OC\\Files\\Cache\\Storage' => $baseDir . '/lib/private/Files/Cache/Storage.php',
     'OC\\Files\\Cache\\StorageGlobal' => $baseDir . '/lib/private/Files/Cache/StorageGlobal.php',
@@ -548,6 +554,10 @@ return array(
     'OC\\Files\\ObjectStore\\S3ConnectionTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
     'OC\\Files\\ObjectStore\\StorageObjectStore' => $baseDir . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
     'OC\\Files\\ObjectStore\\Swift' => $baseDir . '/lib/private/Files/ObjectStore/Swift.php',
+    'OC\\Files\\Search\\SearchBinaryOperator' => $baseDir . '/lib/private/Files/Search/SearchBinaryOperator.php',
+    'OC\\Files\\Search\\SearchComparison' => $baseDir . '/lib/private/Files/Search/SearchComparison.php',
+    'OC\\Files\\Search\\SearchOrder' => $baseDir . '/lib/private/Files/Search/SearchOrder.php',
+    'OC\\Files\\Search\\SearchQuery' => $baseDir . '/lib/private/Files/Search/SearchQuery.php',
     'OC\\Files\\SimpleFS\\SimpleFile' => $baseDir . '/lib/private/Files/SimpleFS/SimpleFile.php',
     'OC\\Files\\SimpleFS\\SimpleFolder' => $baseDir . '/lib/private/Files/SimpleFS/SimpleFolder.php',
     'OC\\Files\\Storage\\Common' => $baseDir . '/lib/private/Files/Storage/Common.php',

+ 10 - 0
lib/composer/composer/autoload_static.php

@@ -171,6 +171,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OCP\\Files\\Notify\\IRenameChange' => __DIR__ . '/../../..' . '/lib/public/Files/Notify/IRenameChange.php',
         'OCP\\Files\\ObjectStore\\IObjectStore' => __DIR__ . '/../../..' . '/lib/public/Files/ObjectStore/IObjectStore.php',
         'OCP\\Files\\ReservedWordException' => __DIR__ . '/../../..' . '/lib/public/Files/ReservedWordException.php',
+        'OCP\\Files\\Search\\ISearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchBinaryOperator.php',
+        'OCP\\Files\\Search\\ISearchComparison' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchComparison.php',
+        'OCP\\Files\\Search\\ISearchOperator' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchOperator.php',
+        'OCP\\Files\\Search\\ISearchOrder' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchOrder.php',
+        'OCP\\Files\\Search\\ISearchQuery' => __DIR__ . '/../../..' . '/lib/public/Files/Search/ISearchQuery.php',
         'OCP\\Files\\SimpleFS\\ISimpleFile' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleFile.php',
         'OCP\\Files\\SimpleFS\\ISimpleFolder' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleFolder.php',
         'OCP\\Files\\SimpleFS\\ISimpleRoot' => __DIR__ . '/../../..' . '/lib/public/Files/SimpleFS/ISimpleRoot.php',
@@ -539,6 +544,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Files\\Cache\\HomePropagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/HomePropagator.php',
         'OC\\Files\\Cache\\MoveFromCacheTrait' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/MoveFromCacheTrait.php',
         'OC\\Files\\Cache\\Propagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Propagator.php',
+        'OC\\Files\\Cache\\QuerySearchHelper' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/QuerySearchHelper.php',
         'OC\\Files\\Cache\\Scanner' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Scanner.php',
         'OC\\Files\\Cache\\Storage' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Storage.php',
         'OC\\Files\\Cache\\StorageGlobal' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/StorageGlobal.php',
@@ -578,6 +584,10 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Files\\ObjectStore\\S3ConnectionTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
         'OC\\Files\\ObjectStore\\StorageObjectStore' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
         'OC\\Files\\ObjectStore\\Swift' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Swift.php',
+        'OC\\Files\\Search\\SearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchBinaryOperator.php',
+        'OC\\Files\\Search\\SearchComparison' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchComparison.php',
+        'OC\\Files\\Search\\SearchOrder' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchOrder.php',
+        'OC\\Files\\Search\\SearchQuery' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchQuery.php',
         'OC\\Files\\SimpleFS\\SimpleFile' => __DIR__ . '/../../..' . '/lib/private/Files/SimpleFS/SimpleFile.php',
         'OC\\Files\\SimpleFS\\SimpleFolder' => __DIR__ . '/../../..' . '/lib/private/Files/SimpleFS/SimpleFolder.php',
         'OC\\Files\\Storage\\Common' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Common.php',

+ 36 - 7
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.

+ 5 - 0
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 [];
 	}

+ 160 - 0
lib/private/Files/Cache/QuerySearchHelper.php

@@ -0,0 +1,160 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Files\Cache;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\IMimeTypeLoader;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
+use OCP\Files\Search\ISearchOperator;
+
+/**
+ * Tools for transforming search queries into database queries
+ */
+class QuerySearchHelper {
+	static protected $searchOperatorMap = [
+		ISearchComparison::COMPARE_LIKE => 'iLike',
+		ISearchComparison::COMPARE_EQUAL => 'eq',
+		ISearchComparison::COMPARE_GREATER_THAN => 'gt',
+		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
+		ISearchComparison::COMPARE_LESS_THAN => 'lt',
+		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte'
+	];
+
+	static protected $searchOperatorNegativeMap = [
+		ISearchComparison::COMPARE_LIKE => 'notLike',
+		ISearchComparison::COMPARE_EQUAL => 'neq',
+		ISearchComparison::COMPARE_GREATER_THAN => 'lte',
+		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
+		ISearchComparison::COMPARE_LESS_THAN => 'gte',
+		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt'
+	];
+
+	/** @var IMimeTypeLoader */
+	private $mimetypeLoader;
+
+	/**
+	 * QuerySearchUtil constructor.
+	 *
+	 * @param IMimeTypeLoader $mimetypeLoader
+	 */
+	public function __construct(IMimeTypeLoader $mimetypeLoader) {
+		$this->mimetypeLoader = $mimetypeLoader;
+	}
+
+	public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
+		$expr = $builder->expr();
+		if ($operator instanceof ISearchBinaryOperator) {
+			switch ($operator->getType()) {
+				case ISearchBinaryOperator::OPERATOR_NOT:
+					$negativeOperator = $operator->getArguments()[0];
+					if ($negativeOperator instanceof ISearchComparison) {
+						return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
+					} else {
+						throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
+					}
+				case ISearchBinaryOperator::OPERATOR_AND:
+					return $expr->andX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
+				case ISearchBinaryOperator::OPERATOR_OR:
+					return $expr->orX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
+				default:
+					throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
+			}
+		} else if ($operator instanceof ISearchComparison) {
+			return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
+		} else {
+			throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
+		}
+	}
+
+	private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
+		$this->validateComparison($comparison);
+
+		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());
+		}
+	}
+
+	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);
+			} else if ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
+				// transform "mimetype='foo/%'" to "mimepart='foo'"
+				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, $type];
+	}
+
+	private function validateComparison(ISearchComparison $operator) {
+		$types = [
+			'mimetype' => 'string',
+			'mtime' => 'integer',
+			'name' => 'string',
+			'size' => 'integer'
+		];
+		$comparisons = [
+			'mimetype' => ['eq', 'like'],
+			'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
+			'name' => ['eq', 'like'],
+			'size' => ['eq', 'gt', 'lt', 'gte', 'lte']
+		];
+
+		if (!isset($types[$operator->getField()])) {
+			throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
+		}
+		$type = $types[$operator->getField()];
+		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());
+		}
+	}
+
+	private function getParameterForValue(IQueryBuilder $builder, $value) {
+		if ($value instanceof \DateTime) {
+			$value = $value->getTimestamp();
+		}
+		if (is_numeric($value)) {
+			$type = IQueryBuilder::PARAM_INT;
+		} else {
+			$type = IQueryBuilder::PARAM_STR;
+		}
+		return $builder->createNamedParameter($value, $type);
+	}
+}

+ 6 - 0
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
 	 *

+ 6 - 0
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
 	 *

+ 7 - 2
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));
+		}
 	}
 
 	/**

+ 57 - 0
lib/private/Files/Search/SearchBinaryOperator.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * 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;
+	}
+}

+ 67 - 0
lib/private/Files/Search/SearchComparison.php

@@ -0,0 +1,67 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Files\Search;
+
+use OCP\Files\Search\ISearchComparison;
+
+class SearchComparison implements ISearchComparison {
+	/** @var string */
+	private $type;
+	/** @var string */
+	private $field;
+	/** @var string|integer|\DateTime */
+	private $value;
+
+	/**
+	 * SearchComparison constructor.
+	 *
+	 * @param string $type
+	 * @param string $field
+	 * @param \DateTime|int|string $value
+	 */
+	public function __construct($type, $field, $value) {
+		$this->type = $type;
+		$this->field = $field;
+		$this->value = $value;
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getType() {
+		return $this->type;
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getField() {
+		return $this->field;
+	}
+
+	/**
+	 * @return \DateTime|int|string
+	 */
+	public function getValue() {
+		return $this->value;
+	}
+}

+ 57 - 0
lib/private/Files/Search/SearchOrder.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Files\Search;
+
+
+use OCP\Files\Search\ISearchOrder;
+
+class SearchOrder implements ISearchOrder {
+	/** @var  string */
+	private $direction;
+	/** @var  string */
+	private $field;
+
+	/**
+	 * SearchOrder constructor.
+	 *
+	 * @param string $direction
+	 * @param string $field
+	 */
+	public function __construct($direction, $field) {
+		$this->direction = $direction;
+		$this->field = $field;
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getDirection() {
+		return $this->direction;
+	}
+
+	/**
+	 * @return string
+	 */
+	public function getField() {
+		return $this->field;
+	}
+}

+ 80 - 0
lib/private/Files/Search/SearchQuery.php

@@ -0,0 +1,80 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+	}
+}

+ 5 - 0
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 [];
 	}

+ 12 - 0
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
@@ -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.
 	 *

+ 2 - 1
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
 	 */

+ 51 - 0
lib/public/Files/Search/ISearchBinaryOperator.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Search;
+
+/**
+ * @since 12.0.0
+ */
+interface ISearchBinaryOperator extends ISearchOperator {
+	const OPERATOR_AND = 'and';
+	const OPERATOR_OR = 'or';
+	const OPERATOR_NOT = 'not';
+
+	/**
+	 * The type of binary operator
+	 *
+	 * One of the ISearchBinaryOperator::OPERATOR_* constants
+	 *
+	 * @return string
+	 * @since 12.0.0
+	 */
+	public function getType();
+
+	/**
+	 * The arguments for the binary operator
+	 *
+	 * One argument for the 'not' operator and two for 'and' and 'or'
+	 *
+	 * @return ISearchOperator[]
+	 * @since 12.0.0
+	 */
+	public function getArguments();
+}

+ 60 - 0
lib/public/Files/Search/ISearchComparison.php

@@ -0,0 +1,60 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Search;
+
+/**
+ * @since 12.0.0
+ */
+interface ISearchComparison extends ISearchOperator {
+	const COMPARE_EQUAL = 'eq';
+	const COMPARE_GREATER_THAN = 'gt';
+	const COMPARE_GREATER_THAN_EQUAL = 'gte';
+	const COMPARE_LESS_THAN = 'lt';
+	const COMPARE_LESS_THAN_EQUAL = 'lte';
+	const COMPARE_LIKE = 'like';
+
+	/**
+	 * Get the type of comparison, one of the ISearchComparison::COMPARE_* constants
+	 *
+	 * @return string
+	 * @since 12.0.0
+	 */
+	public function getType();
+
+	/**
+	 * Get the name of the field to compare with
+	 *
+	 * i.e. 'size', 'name' or 'mimetype'
+	 *
+	 * @return string
+	 * @since 12.0.0
+	 */
+	public function getField();
+
+	/**
+	 * Get the value to compare the field with
+	 *
+	 * @return string|integer|\DateTime
+	 * @since 12.0.0
+	 */
+	public function getValue();
+}

+ 29 - 0
lib/public/Files/Search/ISearchOperator.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Search;
+
+/**
+ * @since 12.0.0
+ */
+interface ISearchOperator {
+
+}

+ 46 - 0
lib/public/Files/Search/ISearchOrder.php

@@ -0,0 +1,46 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Search;
+
+/**
+ * @since 12.0.0
+ */
+interface ISearchOrder {
+	const DIRECTION_ASCENDING = 'asc';
+	const DIRECTION_DESCENDING = 'desc';
+
+	/**
+	 * The direction to sort in, either ISearchOrder::DIRECTION_ASCENDING or ISearchOrder::DIRECTION_DESCENDING
+	 *
+	 * @return string
+	 * @since 12.0.0
+	 */
+	public function getDirection();
+
+	/**
+	 * The field to sort on
+	 *
+	 * @return string
+	 * @since 12.0.0
+	 */
+	public function getField();
+}

+ 57 - 0
lib/public/Files/Search/ISearchQuery.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Files\Search;
+
+/**
+ * @since 12.0.0
+ */
+interface ISearchQuery {
+	/**
+	 * @return ISearchOperator
+	 * @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();
+}

+ 59 - 15
tests/lib/Files/Cache/CacheTest.php

@@ -11,6 +11,9 @@ namespace Test\Files\Cache;
 
 use Doctrine\DBAL\Platforms\MySqlPlatform;
 use OC\Files\Cache\Cache;
+use OC\Files\Search\SearchComparison;
+use OC\Files\Search\SearchQuery;
+use OCP\Files\Search\ISearchComparison;
 
 class LongId extends \OC\Files\Storage\Temporary {
 	public function getId() {
@@ -111,15 +114,15 @@ class CacheTest extends \Test\TestCase {
 	 * @dataProvider folderDataProvider
 	 */
 	public function testFolder($folder) {
-		if(strpos($folder, 'F09F9890')) {
+		if (strpos($folder, 'F09F9890')) {
 			// 4 byte UTF doesn't work on mysql
 			$params = \OC::$server->getDatabaseConnection()->getParams();
-			if(\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform && $params['charset'] !== 'utf8mb4') {
+			if (\OC::$server->getDatabaseConnection()->getDatabasePlatform() instanceof MySqlPlatform && $params['charset'] !== 'utf8mb4') {
 				$this->markTestSkipped('MySQL doesn\'t support 4 byte UTF-8');
 			}
 		}
-		$file2 = $folder.'/bar';
-		$file3 = $folder.'/foo';
+		$file2 = $folder . '/bar';
+		$file3 = $folder . '/foo';
 		$data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'httpd/unix-directory');
 		$fileData = array();
 		$fileData['bar'] = array('size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file');
@@ -138,7 +141,7 @@ class CacheTest extends \Test\TestCase {
 			}
 		}
 
-		$file4 = $folder.'/unkownSize';
+		$file4 = $folder . '/unkownSize';
 		$fileData['unkownSize'] = array('size' => -1, 'mtime' => 25, 'mimetype' => 'foo/file');
 		$this->cache->put($file4, $fileData['unkownSize']);
 
@@ -155,8 +158,8 @@ class CacheTest extends \Test\TestCase {
 		$this->assertEquals(0, $this->cache->calculateFolderSize($folder));
 
 		$this->cache->remove($folder);
-		$this->assertFalse($this->cache->inCache($folder.'/foo'));
-		$this->assertFalse($this->cache->inCache($folder.'/bar'));
+		$this->assertFalse($this->cache->inCache($folder . '/foo'));
+		$this->assertFalse($this->cache->inCache($folder . '/bar'));
 	}
 
 	public function testRemoveRecursive() {
@@ -165,7 +168,7 @@ class CacheTest extends \Test\TestCase {
 		$folders = ['folder', 'folder/subfolder', 'folder/sub2', 'folder/sub2/sub3'];
 		$files = ['folder/foo.txt', 'folder/bar.txt', 'folder/subfolder/asd.txt', 'folder/sub2/qwerty.txt', 'folder/sub2/sub3/foo.txt'];
 
-		foreach($folders as $folder){
+		foreach ($folders as $folder) {
 			$this->cache->put($folder, $folderData);
 		}
 		foreach ($files as $file) {
@@ -360,7 +363,9 @@ class CacheTest extends \Test\TestCase {
 
 		$this->assertEquals(2, count($results));
 
-		usort($results, function($value1, $value2) { return $value1['name'] >= $value2['name']; });
+		usort($results, function ($value1, $value2) {
+			return $value1['name'] >= $value2['name'];
+		});
 
 		$this->assertEquals('folder', $results[0]['name']);
 		$this->assertEquals('foo', $results[1]['name']);
@@ -368,11 +373,15 @@ class CacheTest extends \Test\TestCase {
 		// use tag id
 		$tags = $tagManager->getTagsForUser($userId);
 		$this->assertNotEmpty($tags);
-		$tags = array_filter($tags, function($tag) { return $tag->getName() === 'tag2'; });
+		$tags = array_filter($tags, function ($tag) {
+			return $tag->getName() === 'tag2';
+		});
 		$results = $this->cache->searchByTag(current($tags)->getId(), $userId);
 		$this->assertEquals(3, count($results));
 
-		usort($results, function($value1, $value2) { return $value1['name'] >= $value2['name']; });
+		usort($results, function ($value1, $value2) {
+			return $value1['name'] >= $value2['name'];
+		});
 
 		$this->assertEquals('folder', $results[0]['name']);
 		$this->assertEquals('foo2', $results[1]['name']);
@@ -383,7 +392,42 @@ class CacheTest extends \Test\TestCase {
 
 		$this->logout();
 		$user = \OC::$server->getUserManager()->get($userId);
-		if ($user !== null) { $user->delete(); }
+		if ($user !== null) {
+			$user->delete();
+		}
+	}
+
+	function testSearchByQuery() {
+		$file1 = 'folder';
+		$file2 = 'folder/foobar';
+		$file3 = 'folder/foo';
+		$data1 = array('size' => 100, 'mtime' => 50, 'mimetype' => 'foo/folder');
+		$fileData = array();
+		$fileData['foobar'] = array('size' => 1000, 'mtime' => 20, 'mimetype' => 'foo/file');
+		$fileData['foo'] = array('size' => 20, 'mtime' => 25, 'mimetype' => 'foo/file');
+
+		$this->cache->put($file1, $data1);
+		$this->cache->put($file2, $fileData['foobar']);
+		$this->cache->put($file3, $fileData['foo']);
+
+		$this->assertCount(1, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo')
+			, 10, 0, [])));
+		$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%')
+			, 10, 0, [])));
+		$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file')
+			, 10, 0, [])));
+		$this->assertCount(3, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'foo/%')
+			, 10, 0, [])));
+		$this->assertCount(1, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', 100)
+			, 10, 0, [])));
+		$this->assertCount(2, $this->cache->searchQuery(new SearchQuery(
+			new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100)
+			, 10, 0, [])));
 	}
 
 	function testMove() {
@@ -626,9 +670,9 @@ class CacheTest extends \Test\TestCase {
 
 	public function escapingProvider() {
 		return [
-				['foo'],
-				['o%'],
-				['oth_r'],
+			['foo'],
+			['o%'],
+			['oth_r'],
 		];
 	}
 

+ 204 - 0
tests/lib/Files/Cache/QuerySearchHelperTest.php

@@ -0,0 +1,204 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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);
+
+		sort($fileIds);
+		sort($results);
+
+		$this->assertEquals($fileIds, $results);
+	}
+}