From e61606a767cd6e2c28e3897e7e913e39371078e5 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 8 Mar 2017 15:17:39 +0100 Subject: [PATCH] Allow searching for favorites Signed-off-by: Robin Appelman --- apps/dav/lib/Files/FileSearchBackend.php | 16 +++-- .../unit/Files/FileSearchBackendTest.php | 15 ++-- lib/private/Files/Cache/Cache.php | 21 ++++-- lib/private/Files/Cache/QuerySearchHelper.php | 32 ++++++++- lib/private/Files/Search/SearchQuery.php | 14 +++- lib/public/Files/Search/ISearchQuery.php | 10 +++ tests/lib/Files/Cache/CacheTest.php | 70 +++++++++++++++++-- 7 files changed, 155 insertions(+), 23 deletions(-) diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index c429a1727f..afdb425e8e 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -28,6 +28,7 @@ use OC\Files\Search\SearchQuery; use OC\Files\View; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\TagsPlugin; use OCP\Files\Cache\ICacheEntry; use OCP\Files\Folder; use OCP\Files\IRootFolder; @@ -114,6 +115,7 @@ class FileSearchBackend implements ISearchBackend { 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), + new SearchPropertyDefinition(TagsPlugin::FAVORITE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_BOOLEAN), // select only properties new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false), @@ -178,7 +180,7 @@ class FileSearchBackend implements ISearchBackend { 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); + return new SearchQuery($this->transformSearchOperation($query->where), 0, 0, $orders, $this->user); } /** @@ -186,7 +188,7 @@ class FileSearchBackend implements ISearchBackend { * @return ISearchOrder */ private function mapSearchOrder(Order $order) { - return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToCollumn($order->property)); + return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property)); } /** @@ -210,13 +212,13 @@ class FileSearchBackend implements ISearchBackend { if (count($operator->arguments) !== 2) { throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation'); } - if (gettype($operator->arguments[0]) !== 'string') { + if (!is_string($operator->arguments[0])) { 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)); + return new SearchComparison($trimmedType, $this->mapPropertyNameToColumn($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: @@ -228,7 +230,7 @@ class FileSearchBackend implements ISearchBackend { * @param string $propertyName * @return string */ - private function mapPropertyNameToCollumn($propertyName) { + private function mapPropertyNameToColumn($propertyName) { switch ($propertyName) { case '{DAV:}displayname': return 'name'; @@ -238,6 +240,10 @@ class FileSearchBackend implements ISearchBackend { return 'mtime'; case FilesPlugin::SIZE_PROPERTYNAME: return 'size'; + case TagsPlugin::FAVORITE_PROPERTYNAME: + return 'favorite'; + case TagsPlugin::TAGS_PROPERTYNAME: + return 'tagname'; default: throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName); } diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php index 24b9a9c51e..539344b22d 100644 --- a/apps/dav/tests/unit/Files/FileSearchBackendTest.php +++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php @@ -122,7 +122,8 @@ class FileSearchBackendTest extends TestCase { ), 0, 0, - [] + [], + $this->user )) ->will($this->returnValue([ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') @@ -150,7 +151,8 @@ class FileSearchBackendTest extends TestCase { ), 0, 0, - [] + [], + $this->user )) ->will($this->returnValue([ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') @@ -178,7 +180,8 @@ class FileSearchBackendTest extends TestCase { ), 0, 0, - [] + [], + $this->user )) ->will($this->returnValue([ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') @@ -206,7 +209,8 @@ class FileSearchBackendTest extends TestCase { ), 0, 0, - [] + [], + $this->user )) ->will($this->returnValue([ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') @@ -234,7 +238,8 @@ class FileSearchBackendTest extends TestCase { ), 0, 0, - [] + [], + $this->user )) ->will($this->returnValue([ new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path') diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index b0527d801d..c3ac0f8444 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -645,9 +645,22 @@ class Cache implements ICache { $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())); + ->from('filecache', 'file'); + + $query->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->getNumericStorageId()))); + + if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) { + $query + ->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid')) + ->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX( + $builder->expr()->eq('tagmap.type', 'tag.type'), + $builder->expr()->eq('tagmap.categoryid', 'tag.id') + )) + ->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files'))) + ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID()))); + } + + $query->andWhere($this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation())); if ($searchQuery->getLimit()) { $query->setMaxResults($searchQuery->getLimit()); @@ -660,7 +673,7 @@ class Cache implements ICache { return $this->searchResultToCacheEntries($result); } - /** + /** * Search for files by tag of a given users. * * Note that every user can tag files differently. diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 931f258ec5..7d8098f0ef 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -49,6 +49,8 @@ class QuerySearchHelper { ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt' ]; + const TAG_FAVORITE = '_$!!$_'; + /** @var IMimeTypeLoader */ private $mimetypeLoader; @@ -61,6 +63,23 @@ class QuerySearchHelper { $this->mimetypeLoader = $mimetypeLoader; } + /** + * Whether or not the tag tables should be joined to complete the search + * + * @param ISearchOperator $operator + * @return boolean + */ + public function shouldJoinTags(ISearchOperator $operator) { + if ($operator instanceof ISearchBinaryOperator) { + return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) { + return $shouldJoin || $this->shouldJoinTags($operator); + }, false); + } else if ($operator instanceof ISearchComparison) { + return $operator->getField() === 'tagname' || $operator->getField() === 'favorite'; + } + return false; + } + public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) { $expr = $builder->expr(); if ($operator instanceof ISearchBinaryOperator) { @@ -116,6 +135,11 @@ class QuerySearchHelper { throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported'); } } + } else if ($field === 'favorite') { + $field = 'tag.category'; + $value = self::TAG_FAVORITE; + } else if ($field === 'tagname') { + $field = 'tag.category'; } return [$field, $value, $type]; } @@ -125,13 +149,17 @@ class QuerySearchHelper { 'mimetype' => 'string', 'mtime' => 'integer', 'name' => 'string', - 'size' => 'integer' + 'size' => 'integer', + 'tagname' => 'string', + 'favorite' => 'boolean' ]; $comparisons = [ 'mimetype' => ['eq', 'like'], 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], 'name' => ['eq', 'like'], - 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'] + 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'], + 'tagname' => ['eq', 'like'], + 'favorite' => ['eq'], ]; if (!isset($types[$operator->getField()])) { diff --git a/lib/private/Files/Search/SearchQuery.php b/lib/private/Files/Search/SearchQuery.php index 8a0478ae98..c1da522051 100644 --- a/lib/private/Files/Search/SearchQuery.php +++ b/lib/private/Files/Search/SearchQuery.php @@ -24,6 +24,7 @@ namespace OC\Files\Search; use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchOrder; use OCP\Files\Search\ISearchQuery; +use OCP\IUser; class SearchQuery implements ISearchQuery { /** @var ISearchOperator */ @@ -34,6 +35,8 @@ class SearchQuery implements ISearchQuery { private $offset; /** @var ISearchOrder[] */ private $order; + /** @var IUser */ + private $user; /** * SearchQuery constructor. @@ -42,12 +45,14 @@ class SearchQuery implements ISearchQuery { * @param int $limit * @param int $offset * @param array $order + * @param IUser $user */ - public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order) { + public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order, IUser $user) { $this->searchOperation = $searchOperation; $this->limit = $limit; $this->offset = $offset; $this->order = $order; + $this->user = $user; } /** @@ -77,4 +82,11 @@ class SearchQuery implements ISearchQuery { public function getOrder() { return $this->order; } + + /** + * @return IUser + */ + public function getUser() { + return $this->user; + } } diff --git a/lib/public/Files/Search/ISearchQuery.php b/lib/public/Files/Search/ISearchQuery.php index 5a701b321b..531e285a59 100644 --- a/lib/public/Files/Search/ISearchQuery.php +++ b/lib/public/Files/Search/ISearchQuery.php @@ -21,6 +21,8 @@ namespace OCP\Files\Search; +use OCP\IUser; + /** * @since 12.0.0 */ @@ -54,4 +56,12 @@ interface ISearchQuery { * @since 12.0.0 */ public function getOrder(); + + /** + * The user that issued the search + * + * @return IUser + * @since 12.0.0 + */ + public function getUser(); } diff --git a/tests/lib/Files/Cache/CacheTest.php b/tests/lib/Files/Cache/CacheTest.php index 1bcf8832c6..0038cef1f6 100644 --- a/tests/lib/Files/Cache/CacheTest.php +++ b/tests/lib/Files/Cache/CacheTest.php @@ -14,6 +14,7 @@ use OC\Files\Cache\Cache; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; use OCP\Files\Search\ISearchComparison; +use OCP\IUser; class LongId extends \OC\Files\Storage\Temporary { public function getId() { @@ -397,6 +398,61 @@ class CacheTest extends \Test\TestCase { } } + function testSearchQueryByTag() { + $userId = static::getUniqueID('user'); + \OC::$server->getUserManager()->createUser($userId, $userId); + static::loginAsUser($userId); + $user = new \OC\User\User($userId, null); + + $file1 = 'folder'; + $file2 = 'folder/foobar'; + $file3 = 'folder/foo'; + $file4 = 'folder/foo2'; + $file5 = 'folder/foo3'; + $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'); + $fileData['foo2'] = array('size' => 25, 'mtime' => 28, 'mimetype' => 'foo/file'); + $fileData['foo3'] = array('size' => 88, 'mtime' => 34, 'mimetype' => 'foo/file'); + + $id1 = $this->cache->put($file1, $data1); + $id2 = $this->cache->put($file2, $fileData['foobar']); + $id3 = $this->cache->put($file3, $fileData['foo']); + $id4 = $this->cache->put($file4, $fileData['foo2']); + $id5 = $this->cache->put($file5, $fileData['foo3']); + + $tagManager = \OC::$server->getTagManager()->load('files', null, null, $userId); + $this->assertTrue($tagManager->tagAs($id1, 'tag1')); + $this->assertTrue($tagManager->tagAs($id1, 'tag2')); + $this->assertTrue($tagManager->tagAs($id2, 'tag2')); + $this->assertTrue($tagManager->tagAs($id3, 'tag1')); + $this->assertTrue($tagManager->tagAs($id4, 'tag2')); + + $results = $this->cache->searchQuery(new SearchQuery( + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'tagname', 'tag2'), + 0, 0, [], $user + )); + $this->assertEquals(3, count($results)); + + usort($results, function ($value1, $value2) { + return $value1['name'] >= $value2['name']; + }); + + $this->assertEquals('folder', $results[0]['name']); + $this->assertEquals('foo2', $results[1]['name']); + $this->assertEquals('foobar', $results[2]['name']); + + $tagManager->delete('tag1'); + $tagManager->delete('tag2'); + + static::logout(); + $user = \OC::$server->getUserManager()->get($userId); + if ($user !== null) { + $user->delete(); + } + } + function testSearchByQuery() { $file1 = 'folder'; $file2 = 'folder/foobar'; @@ -409,25 +465,27 @@ class CacheTest extends \Test\TestCase { $this->cache->put($file1, $data1); $this->cache->put($file2, $fileData['foobar']); $this->cache->put($file3, $fileData['foo']); + /** @var IUser $user */ + $user = $this->createMock(IUser::class); $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foo') - , 10, 0, []))); + , 10, 0, [], $user))); $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%') - , 10, 0, []))); + , 10, 0, [], $user))); $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'foo/file') - , 10, 0, []))); + , 10, 0, [], $user))); $this->assertCount(3, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'foo/%') - , 10, 0, []))); + , 10, 0, [], $user))); $this->assertCount(1, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'size', 100) - , 10, 0, []))); + , 10, 0, [], $user))); $this->assertCount(2, $this->cache->searchQuery(new SearchQuery( new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN_EQUAL, 'size', 100) - , 10, 0, []))); + , 10, 0, [], $user))); } function testMove() {