Allow searching for favorites
Signed-off-by: Robin Appelman <robin@icewind.nl>
This commit is contained in:
parent
2a8e922d67
commit
e61606a767
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()])) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue