Allow searching for favorites

Signed-off-by: Robin Appelman <robin@icewind.nl>
This commit is contained in:
Robin Appelman 2017-03-08 15:17:39 +01:00
parent 2a8e922d67
commit e61606a767
No known key found for this signature in database
GPG Key ID: CBCA68FBAEBF98C9
7 changed files with 155 additions and 23 deletions

View File

@ -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);
}

View File

@ -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')

View File

@ -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.

View File

@ -49,6 +49,8 @@ class QuerySearchHelper {
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt'
];
const TAG_FAVORITE = '_$!<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()])) {

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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() {