Implement webdav SEARCH
Signed-off-by: Robin Appelman <robin@icewind.nl>
This commit is contained in:
parent
706131b394
commit
df2063ee7b
|
@ -21,32 +21,73 @@
|
|||
|
||||
namespace OCA\DAV\Files;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OC\Files\Search\SearchOrder;
|
||||
use OC\Files\Search\SearchQuery;
|
||||
use OC\Files\View;
|
||||
use OCA\DAV\Connector\Sabre\Directory;
|
||||
use OCA\DAV\Connector\Sabre\FilesPlugin;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
use OCP\Files\Search\ISearchOrder;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\IManager;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\Tree;
|
||||
use SearchDAV\Backend\ISearchBackend;
|
||||
use SearchDAV\Backend\SearchPropertyDefinition;
|
||||
use SearchDAV\Backend\SearchResult;
|
||||
use SearchDAV\XML\BasicSearch;
|
||||
use SearchDAV\XML\Literal;
|
||||
use SearchDAV\XML\Operator;
|
||||
use SearchDAV\XML\Order;
|
||||
|
||||
class FileSearchBackend implements ISearchBackend {
|
||||
/** @var Tree */
|
||||
private $tree;
|
||||
|
||||
/** @var IUser */
|
||||
private $user;
|
||||
|
||||
/** @var IRootFolder */
|
||||
private $rootFolder;
|
||||
|
||||
/** @var IManager */
|
||||
private $shareManager;
|
||||
|
||||
/** @var View */
|
||||
private $view;
|
||||
|
||||
/**
|
||||
* FileSearchBackend constructor.
|
||||
*
|
||||
* @param Tree $tree
|
||||
* @param IUser $user
|
||||
* @param IRootFolder $rootFolder
|
||||
* @param IManager $shareManager
|
||||
* @param View $view
|
||||
* @internal param IRootFolder $rootFolder
|
||||
*/
|
||||
public function __construct(Tree $tree) {
|
||||
public function __construct(Tree $tree, IUser $user, IRootFolder $rootFolder, IManager $shareManager, View $view) {
|
||||
$this->tree = $tree;
|
||||
$this->user = $user;
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->shareManager = $shareManager;
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search endpoint will be remote.php/dav/files
|
||||
* Search endpoint will be remote.php/dav
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getArbiterPath() {
|
||||
return 'files';
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isValidScope($href, $depth, $path) {
|
||||
|
@ -66,11 +107,162 @@ class FileSearchBackend implements ISearchBackend {
|
|||
public function getPropertyDefinitionsForScope($href, $path) {
|
||||
// all valid scopes support the same schema
|
||||
|
||||
//todo dynamically load all propfind properties that are supported
|
||||
return [
|
||||
new SearchPropertyDefinition('{DAV:}getcontentlength', true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
|
||||
// queryable properties
|
||||
new SearchPropertyDefinition('{DAV:}displayname', true, false, true),
|
||||
new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true),
|
||||
new SearchPropertyDefinition('{DAV:}displayname', true, true, true),
|
||||
new SearchPropertyDefinition('{http://ns.nextcloud.com:}fileid', false, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
|
||||
new SearchPropertyDefinition('{DAV:}getlastmodifed', true, true, true, SearchPropertyDefinition::DATATYPE_DATETIME),
|
||||
new SearchPropertyDefinition(FilesPlugin::SIZE_PROPERTYNAME, true, true, true, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
|
||||
|
||||
// select only properties
|
||||
new SearchPropertyDefinition('{DAV:}resourcetype', false, true, false),
|
||||
new SearchPropertyDefinition('{DAV:}getcontentlength', false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::CHECKSUMS_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::PERMISSIONS_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::GETETAG_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, false, true, false),
|
||||
new SearchPropertyDefinition(FilesPlugin::HAS_PREVIEW_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_BOOLEAN),
|
||||
new SearchPropertyDefinition(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
|
||||
new SearchPropertyDefinition(FilesPlugin::FILEID_PROPERTYNAME, false, true, false, SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER),
|
||||
];
|
||||
}
|
||||
|
||||
public function search(BasicSearch $search) {
|
||||
if (count($search->from) !== 1) {
|
||||
throw new \InvalidArgumentException('Searching more than one folder is not supported');
|
||||
}
|
||||
$query = $this->transformQuery($search);
|
||||
$scope = $search->from[0];
|
||||
if ($scope->path === null) {
|
||||
throw new \InvalidArgumentException('Using uri\'s as scope is not supported, please use a path relative to the search arbiter instead');
|
||||
}
|
||||
$node = $this->tree->getNodeForPath($scope->path);
|
||||
if (!$node instanceof Directory) {
|
||||
throw new \InvalidArgumentException('Search is only supported on directories');
|
||||
}
|
||||
|
||||
$fileInfo = $node->getFileInfo();
|
||||
$folder = $this->rootFolder->get($fileInfo->getPath());
|
||||
/** @var Folder $folder $results */
|
||||
$results = $folder->search($query);
|
||||
|
||||
return array_map(function (Node $node) {
|
||||
if ($node instanceof Folder) {
|
||||
return new SearchResult(new \OCA\DAV\Connector\Sabre\Directory($this->view, $node, $this->tree, $this->shareManager), $this->getHrefForNode($node));
|
||||
} else {
|
||||
return new SearchResult(new \OCA\DAV\Connector\Sabre\File($this->view, $node, $this->shareManager), $this->getHrefForNode($node));
|
||||
}
|
||||
}, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return string
|
||||
*/
|
||||
private function getHrefForNode(Node $node) {
|
||||
$base = '/files/' . $this->user->getUID();
|
||||
return $base . $this->view->getRelativePath($node->getPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BasicSearch $query
|
||||
* @return ISearchQuery
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Operator $operator
|
||||
* @return ISearchOperator
|
||||
*/
|
||||
private function transformSearchOperation(Operator $operator) {
|
||||
list(, $trimmedType) = explode('}', $operator->type);
|
||||
switch ($operator->type) {
|
||||
case Operator::OPERATION_AND:
|
||||
case Operator::OPERATION_OR:
|
||||
case Operator::OPERATION_NOT:
|
||||
$arguments = array_map([$this, 'transformSearchOperation'], $operator->arguments);
|
||||
return new SearchBinaryOperator($trimmedType, $arguments);
|
||||
case Operator::OPERATION_EQUAL:
|
||||
case Operator::OPERATION_GREATER_OR_EQUAL_THAN:
|
||||
case Operator::OPERATION_GREATER_THAN:
|
||||
case Operator::OPERATION_LESS_OR_EQUAL_THAN:
|
||||
case Operator::OPERATION_LESS_THAN:
|
||||
case Operator::OPERATION_IS_LIKE:
|
||||
if (count($operator->arguments) !== 2) {
|
||||
throw new \InvalidArgumentException('Invalid number of arguments for ' . $trimmedType . ' operation');
|
||||
}
|
||||
if (gettype($operator->arguments[0]) !== 'string') {
|
||||
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));
|
||||
case Operator::OPERATION_IS_COLLECTION:
|
||||
return new SearchComparison('eq', 'mimetype', ICacheEntry::DIRECTORY_MIMETYPE);
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unsupported operation ' . $trimmedType. ' (' . $operator->type . ')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $propertyName
|
||||
* @return string
|
||||
*/
|
||||
private function mapPropertyNameToCollumn($propertyName) {
|
||||
/**
|
||||
* new SearchPropertyDefinition('{DAV:}displayname', true, false, true),
|
||||
* 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),
|
||||
*/
|
||||
|
||||
switch ($propertyName) {
|
||||
case '{DAV:}displayname':
|
||||
return 'name';
|
||||
case '{DAV:}getcontenttype':
|
||||
return 'mimetype';
|
||||
case '{DAV:}getlastmodifed':
|
||||
return 'mtime';
|
||||
case FilesPlugin::SIZE_PROPERTYNAME:
|
||||
return 'size';
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unsupported property for search or order: ' . $propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private function castValue($propertyName, $value) {
|
||||
$allProps = $this->getPropertyDefinitionsForScope('', '');
|
||||
foreach ($allProps as $prop) {
|
||||
if ($prop->name === $propertyName) {
|
||||
$dataType = $prop->dataType;
|
||||
switch ($dataType) {
|
||||
case SearchPropertyDefinition::DATATYPE_BOOLEAN:
|
||||
return $value === 'yes';
|
||||
case SearchPropertyDefinition::DATATYPE_DECIMAL:
|
||||
case SearchPropertyDefinition::DATATYPE_INTEGER:
|
||||
case SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER:
|
||||
return 0 + $value;
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ use OCP\SabrePluginEvent;
|
|||
use Sabre\CardDAV\VCFExportPlugin;
|
||||
use Sabre\DAV\Auth\Plugin;
|
||||
use OCA\DAV\Connector\Sabre\TagsPlugin;
|
||||
use SearchDAV\DAV\SearchPlugin;
|
||||
|
||||
class Server {
|
||||
|
||||
|
@ -222,6 +223,13 @@ class Server {
|
|||
\OC::$server->getGroupManager(),
|
||||
$userFolder
|
||||
));
|
||||
$this->server->addPlugin(new SearchPlugin(new \OCA\DAV\Files\FileSearchBackend(
|
||||
$this->server->tree,
|
||||
$user,
|
||||
\OC::$server->getRootFolder(),
|
||||
\OC::$server->getShareManager(),
|
||||
$view
|
||||
)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -36,9 +36,11 @@
|
|||
|
||||
namespace OC\Files\Cache;
|
||||
|
||||
use Doctrine\DBAL\Driver\Statement;
|
||||
use OCP\Files\Cache\ICache;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use \OCP\Files\IMimeTypeLoader;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
/**
|
||||
|
@ -79,6 +81,9 @@ class Cache implements ICache {
|
|||
*/
|
||||
protected $connection;
|
||||
|
||||
/** @var QuerySearchHelper */
|
||||
protected $querySearchHelper;
|
||||
|
||||
/**
|
||||
* @param \OC\Files\Storage\Storage|string $storage
|
||||
*/
|
||||
|
@ -95,6 +100,7 @@ class Cache implements ICache {
|
|||
$this->storageCache = new Storage($storage);
|
||||
$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
|
||||
$this->connection = \OC::$server->getDatabaseConnection();
|
||||
$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -350,7 +356,7 @@ class Cache implements ICache {
|
|||
$queryParts[] = '`mtime`';
|
||||
}
|
||||
} elseif ($name === 'encrypted') {
|
||||
if(isset($data['encryptedVersion'])) {
|
||||
if (isset($data['encryptedVersion'])) {
|
||||
$value = $data['encryptedVersion'];
|
||||
} else {
|
||||
// Boolean to integer conversion
|
||||
|
@ -599,9 +605,17 @@ class Cache implements ICache {
|
|||
[$this->getNumericStorageId(), $pattern]
|
||||
);
|
||||
|
||||
return $this->searchResultToCacheEntries($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Statement $result
|
||||
* @return CacheEntry[]
|
||||
*/
|
||||
private function searchResultToCacheEntries(Statement $result) {
|
||||
$files = $result->fetchAll();
|
||||
|
||||
return array_map(function(array $data) {
|
||||
return array_map(function (array $data) {
|
||||
return self::cacheEntryFromData($data, $this->mimetypeLoader);
|
||||
}, $files);
|
||||
}
|
||||
|
@ -624,14 +638,29 @@ class Cache implements ICache {
|
|||
$mimetype = $this->mimetypeLoader->getId($mimetype);
|
||||
$result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId()));
|
||||
|
||||
$files = $result->fetchAll();
|
||||
|
||||
return array_map(function (array $data) {
|
||||
return self::cacheEntryFromData($data, $this->mimetypeLoader);
|
||||
}, $files);
|
||||
return $this->searchResultToCacheEntries($result);
|
||||
}
|
||||
|
||||
/**
|
||||
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()));
|
||||
|
||||
if ($searchQuery->getLimit()) {
|
||||
$query->setMaxResults($searchQuery->getLimit());
|
||||
}
|
||||
if ($searchQuery->getOffset()) {
|
||||
$query->setFirstResult($searchQuery->getOffset());
|
||||
}
|
||||
|
||||
$result = $query->execute();
|
||||
return $this->searchResultToCacheEntries($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for files by tag of a given users.
|
||||
*
|
||||
* Note that every user can tag files differently.
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace OC\Files\Cache;
|
|||
|
||||
use OCP\Constants;
|
||||
use OCP\Files\Cache\ICache;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
/**
|
||||
* Storage placeholder to represent a missing precondition, storage unavailable
|
||||
|
@ -125,6 +126,10 @@ class FailedCache implements ICache {
|
|||
return [];
|
||||
}
|
||||
|
||||
public function searchQuery(ISearchQuery $query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getAll() {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ use OCP\Files\Search\ISearchOperator;
|
|||
/**
|
||||
* Tools for transforming search queries into database queries
|
||||
*/
|
||||
class QuerySearchUtil {
|
||||
class QuerySearchHelper {
|
||||
static protected $searchOperatorMap = [
|
||||
ISearchComparison::COMPARE_LIKE => 'iLike',
|
||||
ISearchComparison::COMPARE_EQUAL => 'eq',
|
||||
|
@ -73,9 +73,9 @@ class QuerySearchUtil {
|
|||
throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
|
||||
}
|
||||
case ISearchBinaryOperator::OPERATOR_AND:
|
||||
return $expr->andX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1]));
|
||||
return $expr->andX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
|
||||
case ISearchBinaryOperator::OPERATOR_OR:
|
||||
return $expr->orX($this->searchOperatorToDBExpr($operator->getArguments()[0], $operator->getArguments()[1]));
|
||||
return $expr->orX($this->searchOperatorToDBExpr($builder, $operator->getArguments()[0]), $this->searchOperatorToDBExpr($builder, $operator->getArguments()[1]));
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
|
||||
}
|
||||
|
@ -87,13 +87,11 @@ class QuerySearchUtil {
|
|||
}
|
||||
|
||||
private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
|
||||
if (!$this->isValidComparison($comparison)) {
|
||||
throw new \InvalidArgumentException('Invalid comparison ' . $operator->getType() . ' on field ' . $operator->getField());
|
||||
}
|
||||
$this->validateComparison($comparison);
|
||||
|
||||
list($field, $value) = $this->getOperatorFieldAndValue($comparison);
|
||||
if (isset($operatorMap[$comparison->getType()])) {
|
||||
$queryOperator = $operatorMap[$comparison->getType()];
|
||||
list($field, $value, $type) = $this->getOperatorFieldAndValue($comparison);
|
||||
if (isset($operatorMap[$type])) {
|
||||
$queryOperator = $operatorMap[$type];
|
||||
return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
|
||||
|
@ -103,6 +101,7 @@ class QuerySearchUtil {
|
|||
private function getOperatorFieldAndValue(ISearchComparison $operator) {
|
||||
$field = $operator->getField();
|
||||
$value = $operator->getValue();
|
||||
$type = $operator->getType();
|
||||
if ($field === 'mimetype') {
|
||||
if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
|
||||
$value = $this->mimetypeLoader->getId($value);
|
||||
|
@ -111,16 +110,20 @@ class QuerySearchUtil {
|
|||
if (preg_match('|(.+)/%|', $value, $matches)) {
|
||||
$field = 'mimepart';
|
||||
$value = $this->mimetypeLoader->getId($matches[1]);
|
||||
$type = ISearchComparison::COMPARE_EQUAL;
|
||||
}
|
||||
if (strpos($value, '%') !== false) {
|
||||
throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
return [$field, $value];
|
||||
return [$field, $value, $type];
|
||||
}
|
||||
|
||||
private function isValidComparison(ISearchComparison $operator) {
|
||||
private function validateComparison(ISearchComparison $operator) {
|
||||
$types = [
|
||||
'mimetype' => 'string',
|
||||
'mtime' => \DateTime::class,
|
||||
'mtime' => 'integer',
|
||||
'name' => 'string',
|
||||
'size' => 'integer'
|
||||
];
|
||||
|
@ -132,13 +135,15 @@ class QuerySearchUtil {
|
|||
];
|
||||
|
||||
if (!isset($types[$operator->getField()])) {
|
||||
return false;
|
||||
throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
|
||||
}
|
||||
$type = $types[$operator->getField()];
|
||||
if (gettype($operator->getValue()) !== $type && !(is_a($operator->getValue(), $type))) {
|
||||
return false;
|
||||
if (gettype($operator->getValue()) !== $type) {
|
||||
throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
|
||||
}
|
||||
if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
|
||||
throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType());
|
||||
}
|
||||
return in_array($operator->getType(), $comparisons[$operator->getField()]);
|
||||
}
|
||||
|
||||
private function getParameterForValue(IQueryBuilder $builder, $value) {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
namespace OC\Files\Cache\Wrapper;
|
||||
use OC\Files\Cache\Cache;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
/**
|
||||
* Jail to a subdirectory of the wrapped cache
|
||||
|
@ -218,6 +219,11 @@ class CacheJail extends CacheWrapper {
|
|||
return $this->formatSearchResults($results);
|
||||
}
|
||||
|
||||
public function searchQuery(ISearchQuery $query) {
|
||||
$results = $this->getCache()->searchQuery($query);
|
||||
return $this->formatSearchResults($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* search for files by mimetype
|
||||
*
|
||||
|
|
|
@ -31,6 +31,7 @@ namespace OC\Files\Cache\Wrapper;
|
|||
use OC\Files\Cache\Cache;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\Cache\ICache;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
class CacheWrapper extends Cache {
|
||||
/**
|
||||
|
@ -229,6 +230,11 @@ class CacheWrapper extends Cache {
|
|||
return array_map(array($this, 'formatCacheEntry'), $results);
|
||||
}
|
||||
|
||||
public function searchQuery(ISearchQuery $query) {
|
||||
$results = $this->getCache()->searchQuery($query);
|
||||
return array_map(array($this, 'formatCacheEntry'), $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* search for files by tag
|
||||
*
|
||||
|
|
|
@ -33,6 +33,7 @@ use OCP\Files\FileInfo;
|
|||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
class Folder extends Node implements \OCP\Files\Folder {
|
||||
/**
|
||||
|
@ -190,11 +191,15 @@ class Folder extends Node implements \OCP\Files\Folder {
|
|||
/**
|
||||
* search for files with the name matching $query
|
||||
*
|
||||
* @param string $query
|
||||
* @param string|ISearchOperator $query
|
||||
* @return \OC\Files\Node\Node[]
|
||||
*/
|
||||
public function search($query) {
|
||||
return $this->searchCommon('search', array('%' . $query . '%'));
|
||||
if (is_string($query)) {
|
||||
return $this->searchCommon('search', array('%' . $query . '%'));
|
||||
} else {
|
||||
return $this->searchCommon('searchQuery', array($query));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,3 +18,40 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OC\Files\Search;
|
||||
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
class SearchBinaryOperator implements ISearchBinaryOperator {
|
||||
/** @var string */
|
||||
private $type;
|
||||
/** @var ISearchOperator[] */
|
||||
private $arguments;
|
||||
|
||||
/**
|
||||
* SearchBinaryOperator constructor.
|
||||
*
|
||||
* @param string $type
|
||||
* @param ISearchOperator[] $arguments
|
||||
*/
|
||||
public function __construct($type, array $arguments) {
|
||||
$this->type = $type;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ISearchOperator[]
|
||||
*/
|
||||
public function getArguments() {
|
||||
return $this->arguments;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,3 +21,60 @@
|
|||
|
||||
namespace OC\Files\Search;
|
||||
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
use OCP\Files\Search\ISearchOrder;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
class SearchQuery implements ISearchQuery {
|
||||
/** @var ISearchOperator */
|
||||
private $searchOperation;
|
||||
/** @var integer */
|
||||
private $limit;
|
||||
/** @var integer */
|
||||
private $offset;
|
||||
/** @var ISearchOrder[] */
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* SearchQuery constructor.
|
||||
*
|
||||
* @param ISearchOperator $searchOperation
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param array $order
|
||||
*/
|
||||
public function __construct(ISearchOperator $searchOperation, $limit, $offset, array $order) {
|
||||
$this->searchOperation = $searchOperation;
|
||||
$this->limit = $limit;
|
||||
$this->offset = $offset;
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ISearchOperator
|
||||
*/
|
||||
public function getSearchOperation() {
|
||||
return $this->searchOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLimit() {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset() {
|
||||
return $this->offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ISearchOrder[]
|
||||
*/
|
||||
public function getOrder() {
|
||||
return $this->order;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ use OCP\Constants;
|
|||
use OCP\Files\Cache\ICache;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\FileInfo;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
class NullCache implements ICache {
|
||||
public function getNumericStorageId() {
|
||||
|
@ -103,6 +104,10 @@ class NullCache implements ICache {
|
|||
return [];
|
||||
}
|
||||
|
||||
public function searchQuery(ISearchQuery $query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function searchByTag($tag, $userId) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
*/
|
||||
|
||||
namespace OCP\Files\Cache;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
/**
|
||||
* Metadata cache for a storage
|
||||
|
@ -212,6 +214,16 @@ interface ICache {
|
|||
*/
|
||||
public function searchByMime($mimetype);
|
||||
|
||||
/**
|
||||
* Search for files with a flexible query
|
||||
*
|
||||
* @param ISearchQuery $query
|
||||
* @return ICacheEntry[]
|
||||
* @throw \InvalidArgumentException if the cache is unable to perform the query
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function searchQuery(ISearchQuery $query);
|
||||
|
||||
/**
|
||||
* Search for files by tag of a given users.
|
||||
*
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
// use OCP namespace for all classes that are considered public.
|
||||
// This means that they should be used by apps instead of the internal ownCloud classes
|
||||
namespace OCP\Files;
|
||||
use OCP\Files\Search\ISearchQuery;
|
||||
|
||||
/**
|
||||
* @since 6.0.0
|
||||
|
@ -115,7 +116,7 @@ interface Folder extends Node {
|
|||
/**
|
||||
* search for files with the name matching $query
|
||||
*
|
||||
* @param string $query
|
||||
* @param string|ISearchQuery $query
|
||||
* @return \OCP\Files\Node[]
|
||||
* @since 6.0.0
|
||||
*/
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
|
||||
namespace OCP\Files\Search;
|
||||
|
||||
/**
|
||||
* @since 12.0.0
|
||||
*/
|
||||
interface ISearchBinaryOperator extends ISearchOperator {
|
||||
const OPERATOR_AND = 'and';
|
||||
const OPERATOR_OR = 'or';
|
||||
|
@ -32,6 +35,7 @@ interface ISearchBinaryOperator extends ISearchOperator {
|
|||
* One of the ISearchBinaryOperator::OPERATOR_* constants
|
||||
*
|
||||
* @return string
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
|
@ -41,6 +45,7 @@ interface ISearchBinaryOperator extends ISearchOperator {
|
|||
* One argument for the 'not' operator and two for 'and' and 'or'
|
||||
*
|
||||
* @return ISearchOperator[]
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getArguments();
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ interface ISearchComparison extends ISearchOperator {
|
|||
* Get the type of comparison, one of the ISearchComparison::COMPARE_* constants
|
||||
*
|
||||
* @return string
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getType();
|
||||
|
||||
|
@ -42,6 +43,7 @@ interface ISearchComparison extends ISearchOperator {
|
|||
* i.e. 'size', 'name' or 'mimetype'
|
||||
*
|
||||
* @return string
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getField();
|
||||
|
||||
|
@ -49,6 +51,7 @@ interface ISearchComparison extends ISearchOperator {
|
|||
* Get the value to compare the field with
|
||||
*
|
||||
* @return string|integer|\DateTime
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getValue();
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
|
||||
namespace OCP\Files\Search;
|
||||
|
||||
interface ISearchCondition {
|
||||
/**
|
||||
* @since 12.0.0
|
||||
*/
|
||||
interface ISearchOperator {
|
||||
|
||||
}
|
||||
|
|
|
@ -30,4 +30,28 @@ interface ISearchQuery {
|
|||
* @since 12.0.0
|
||||
*/
|
||||
public function getSearchOperation();
|
||||
|
||||
/**
|
||||
* Get the maximum number of results to return
|
||||
*
|
||||
* @return integer
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getLimit();
|
||||
|
||||
/**
|
||||
* Get the offset for returned results
|
||||
*
|
||||
* @return integer
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getOffset();
|
||||
|
||||
/**
|
||||
* The fields and directions to order by
|
||||
*
|
||||
* @return ISearchOrder[]
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getOrder();
|
||||
}
|
||||
|
|
|
@ -21,8 +21,181 @@
|
|||
|
||||
namespace Test\Files\Cache;
|
||||
|
||||
use OC\DB\QueryBuilder\Literal;
|
||||
use OC\Files\Cache\QuerySearchHelper;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\IMimeTypeLoader;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
class QuerySearchHelperTest extends TestCase {
|
||||
/** @var IQueryBuilder */
|
||||
private $builder;
|
||||
|
||||
/** @var IMimeTypeLoader|\PHPUnit_Framework_MockObject_MockObject */
|
||||
private $mimetypeLoader;
|
||||
|
||||
/** @var QuerySearchHelper */
|
||||
private $querySearchHelper;
|
||||
|
||||
/** @var integer */
|
||||
private $numericStorageId;
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$this->builder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
|
||||
$this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class);
|
||||
|
||||
$this->mimetypeLoader->expects($this->any())
|
||||
->method('getId')
|
||||
->willReturnMap([
|
||||
['text', 1],
|
||||
['text/plain', 2],
|
||||
['text/xml', 3],
|
||||
['image/jpg', 4],
|
||||
['image/png', 5],
|
||||
['image', 6],
|
||||
]);
|
||||
|
||||
$this->mimetypeLoader->expects($this->any())
|
||||
->method('getMimetypeById')
|
||||
->willReturnMap([
|
||||
[1, 'text'],
|
||||
[2, 'text/plain'],
|
||||
[3, 'text/xml'],
|
||||
[4, 'image/jpg'],
|
||||
[5, 'image/png'],
|
||||
[6, 'image']
|
||||
]);
|
||||
|
||||
$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
|
||||
$this->numericStorageId = 10000;
|
||||
|
||||
$this->builder->select(['fileid'])
|
||||
->from('filecache')
|
||||
->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId)));
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
parent::tearDown();
|
||||
|
||||
$builder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
|
||||
|
||||
$builder->delete('filecache')
|
||||
->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
$builder->execute();
|
||||
}
|
||||
|
||||
private function addCacheEntry(array $data) {
|
||||
$data['storage'] = $this->numericStorageId;
|
||||
$data['etag'] = 'unimportant';
|
||||
$data['storage_mtime'] = $data['mtime'];
|
||||
if (!isset($data['path'])) {
|
||||
$data['path'] = 'random/' . $this->getUniqueID();
|
||||
}
|
||||
$data['path_hash'] = md5($data['path']);
|
||||
if (!isset($data['mtime'])) {
|
||||
$data['mtime'] = 100;
|
||||
}
|
||||
if (!isset($data['size'])) {
|
||||
$data['size'] = 100;
|
||||
}
|
||||
$data['name'] = basename($data['path']);
|
||||
$data['parent'] = -1;
|
||||
if (isset($data['mimetype'])) {
|
||||
list($mimepart,) = explode('/', $data['mimetype']);
|
||||
$data['mimepart'] = $this->mimetypeLoader->getId($mimepart);
|
||||
$data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']);
|
||||
} else {
|
||||
$data['mimepart'] = 1;
|
||||
$data['mimetype'] = 1;
|
||||
}
|
||||
|
||||
$builder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
|
||||
|
||||
$values = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$values[$key] = $builder->createNamedParameter($value);
|
||||
}
|
||||
|
||||
$builder->insert('filecache')
|
||||
->values($values)
|
||||
->execute();
|
||||
}
|
||||
|
||||
private function search(ISearchOperator $operator) {
|
||||
$dbOperator = $this->querySearchHelper->searchOperatorToDBExpr($this->builder, $operator);
|
||||
$this->builder->andWhere($dbOperator);
|
||||
return $this->builder->execute()->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
public function comparisonProvider() {
|
||||
return [
|
||||
[new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1002]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [1001, 1002]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [1001]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [1001, 1002]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [1001]],
|
||||
[new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [1001, 1002]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50),
|
||||
new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [1001]
|
||||
]), [1001]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100),
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150),
|
||||
]), [1001, 1002]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150),
|
||||
]), [1001]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125),
|
||||
]), [1001]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125),
|
||||
]), [1002]],
|
||||
[new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
|
||||
new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'),
|
||||
]), [1002]],
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider comparisonProvider
|
||||
*
|
||||
* @param ISearchOperator $operator
|
||||
* @param array $fileIds
|
||||
*/
|
||||
public function testComparison(ISearchOperator $operator, array $fileIds) {
|
||||
$this->addCacheEntry([
|
||||
'path' => 'foobar',
|
||||
'fileid' => 1001,
|
||||
'mtime' => 100,
|
||||
'size' => 50,
|
||||
'mimetype' => 'image/jpg'
|
||||
]);
|
||||
|
||||
$this->addCacheEntry([
|
||||
'path' => 'fooasd',
|
||||
'fileid' => 1002,
|
||||
'mtime' => 150,
|
||||
'size' => 50,
|
||||
'mimetype' => 'image/png'
|
||||
]);
|
||||
|
||||
$results = $this->search($operator);
|
||||
|
||||
$this->assertEquals($fileIds, $results);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue