Signed-off-by: Robin Appelman <robin@icewind.nl>tags/v12.0.0beta1
@@ -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() { |