Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow searching for favorites #3772

Merged
merged 1 commit into from
Mar 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions apps/dav/lib/Files/FileSearchBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand Down Expand Up @@ -114,6 +115,7 @@ public function getPropertyDefinitionsForScope($href, $path) {
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),
Expand Down Expand Up @@ -178,15 +180,15 @@ private function getHrefForNode(Node $node) {
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);
}

/**
* @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));
return new SearchOrder($order->order === Order::ASC ? ISearchOrder::DIRECTION_ASCENDING : ISearchOrder::DIRECTION_DESCENDING, $this->mapPropertyNameToColumn($order->property));
}

/**
Expand All @@ -210,13 +212,13 @@ private function transformSearchOperation(Operator $operator) {
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:
Expand All @@ -228,7 +230,7 @@ private function transformSearchOperation(Operator $operator) {
* @param string $propertyName
* @return string
*/
private function mapPropertyNameToCollumn($propertyName) {
private function mapPropertyNameToColumn($propertyName) {
switch ($propertyName) {
case '{DAV:}displayname':
return 'name';
Expand All @@ -238,6 +240,10 @@ private function mapPropertyNameToCollumn($propertyName) {
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);
}
Expand Down
15 changes: 10 additions & 5 deletions apps/dav/tests/unit/Files/FileSearchBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ public function testSearchFilename() {
),
0,
0,
[]
[],
$this->user
))
->will($this->returnValue([
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
Expand Down Expand Up @@ -150,7 +151,8 @@ public function testSearchMimetype() {
),
0,
0,
[]
[],
$this->user
))
->will($this->returnValue([
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
Expand Down Expand Up @@ -178,7 +180,8 @@ public function testSearchSize() {
),
0,
0,
[]
[],
$this->user
))
->will($this->returnValue([
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
Expand Down Expand Up @@ -206,7 +209,8 @@ public function testSearchMtime() {
),
0,
0,
[]
[],
$this->user
))
->will($this->returnValue([
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
Expand Down Expand Up @@ -234,7 +238,8 @@ public function testSearchIsCollection() {
),
0,
0,
[]
[],
$this->user
))
->will($this->returnValue([
new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path')
Expand Down
21 changes: 17 additions & 4 deletions lib/private/Files/Cache/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,22 @@ 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()));
->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());
Expand All @@ -660,7 +673,7 @@ public function searchQuery(ISearchQuery $searchQuery) {
return $this->searchResultToCacheEntries($result);
}

/**
/**
* Search for files by tag of a given users.
*
* Note that every user can tag files differently.
Expand Down
32 changes: 30 additions & 2 deletions lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class QuerySearchHelper {
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt'
];

const TAG_FAVORITE = '_$!<Favorite>!$_';

/** @var IMimeTypeLoader */
private $mimetypeLoader;

Expand All @@ -61,6 +63,23 @@ public function __construct(IMimeTypeLoader $mimetypeLoader) {
$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) {
Expand Down Expand Up @@ -116,6 +135,11 @@ private function getOperatorFieldAndValue(ISearchComparison $operator) {
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];
}
Expand All @@ -125,13 +149,17 @@ private function validateComparison(ISearchComparison $operator) {
'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()])) {
Expand Down
14 changes: 13 additions & 1 deletion lib/private/Files/Search/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCP\Files\Search\ISearchOperator;
use OCP\Files\Search\ISearchOrder;
use OCP\Files\Search\ISearchQuery;
use OCP\IUser;

class SearchQuery implements ISearchQuery {
/** @var ISearchOperator */
Expand All @@ -34,6 +35,8 @@ class SearchQuery implements ISearchQuery {
private $offset;
/** @var ISearchOrder[] */
private $order;
/** @var IUser */
private $user;

/**
* SearchQuery constructor.
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -77,4 +82,11 @@ public function getOffset() {
public function getOrder() {
return $this->order;
}

/**
* @return IUser
*/
public function getUser() {
return $this->user;
}
}
10 changes: 10 additions & 0 deletions lib/public/Files/Search/ISearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

namespace OCP\Files\Search;

use OCP\IUser;

/**
* @since 12.0.0
*/
Expand Down Expand Up @@ -54,4 +56,12 @@ public function getOffset();
* @since 12.0.0
*/
public function getOrder();

/**
* The user that issued the search
*
* @return IUser
* @since 12.0.0
*/
public function getUser();
}
70 changes: 64 additions & 6 deletions tests/lib/Files/Cache/CacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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() {
Expand Down Expand Up @@ -397,6 +398,61 @@ function testSearchByTag() {
}
}

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';
Expand All @@ -409,25 +465,27 @@ function testSearchByQuery() {
$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() {
Expand Down