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) {
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;
+ }
}
use Sabre\CardDAV\VCFExportPlugin;
use Sabre\DAV\Auth\Plugin;
use OCA\DAV\Connector\Sabre\TagsPlugin;
+use SearchDAV\DAV\SearchPlugin;
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
+ )));
}
}
});
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;
/**
*/
protected $connection;
+ /** @var QuerySearchHelper */
+ protected $querySearchHelper;
+
/**
* @param \OC\Files\Storage\Storage|string $storage
*/
$this->storageCache = new Storage($storage);
$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
$this->connection = \OC::$server->getDatabaseConnection();
+ $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
}
/**
$queryParts[] = '`mtime`';
}
} elseif ($name === 'encrypted') {
- if(isset($data['encryptedVersion'])) {
+ if (isset($data['encryptedVersion'])) {
$value = $data['encryptedVersion'];
} else {
// Boolean to integer conversion
[$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);
}
$mimetype = $this->mimetypeLoader->getId($mimetype);
$result = $this->connection->executeQuery($sql, array($mimetype, $this->getNumericStorageId()));
- $files = $result->fetchAll();
+ return $this->searchResultToCacheEntries($result);
+ }
- return array_map(function (array $data) {
- return self::cacheEntryFromData($data, $this->mimetypeLoader);
- }, $files);
+ 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.
use OCP\Constants;
use OCP\Files\Cache\ICache;
+use OCP\Files\Search\ISearchQuery;
/**
* Storage placeholder to represent a missing precondition, storage unavailable
return [];
}
+ public function searchQuery(ISearchQuery $query) {
+ return [];
+ }
+
public function getAll() {
return [];
}
/**
* Tools for transforming search queries into database queries
*/
-class QuerySearchUtil {
+class QuerySearchHelper {
static protected $searchOperatorMap = [
ISearchComparison::COMPARE_LIKE => 'iLike',
ISearchComparison::COMPARE_EQUAL => 'eq',
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());
}
}
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());
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);
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'
];
];
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) {
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
return $this->formatSearchResults($results);
}
+ public function searchQuery(ISearchQuery $query) {
+ $results = $this->getCache()->searchQuery($query);
+ return $this->formatSearchResults($results);
+ }
+
/**
* search for files by mimetype
*
use OC\Files\Cache\Cache;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Cache\ICache;
+use OCP\Files\Search\ISearchQuery;
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
*
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 {
/**
/**
* 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));
+ }
}
/**
* 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;
+ }
+}
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;
+ }
+}
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() {
return [];
}
+ public function searchQuery(ISearchQuery $query) {
+ return [];
+ }
+
public function searchByTag($tag, $userId) {
return [];
}
*/
namespace OCP\Files\Cache;
+use OCP\Files\Search\ISearchOperator;
+use OCP\Files\Search\ISearchQuery;
/**
* Metadata cache for a storage
*/
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.
*
// 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
/**
* search for files with the name matching $query
*
- * @param string $query
+ * @param string|ISearchQuery $query
* @return \OCP\Files\Node[]
* @since 6.0.0
*/
namespace OCP\Files\Search;
+/**
+ * @since 12.0.0
+ */
interface ISearchBinaryOperator extends ISearchOperator {
const OPERATOR_AND = 'and';
const OPERATOR_OR = 'or';
* One of the ISearchBinaryOperator::OPERATOR_* constants
*
* @return string
+ * @since 12.0.0
*/
public function getType();
* One argument for the 'not' operator and two for 'and' and 'or'
*
* @return ISearchOperator[]
+ * @since 12.0.0
*/
public function getArguments();
}
* Get the type of comparison, one of the ISearchComparison::COMPARE_* constants
*
* @return string
+ * @since 12.0.0
*/
public function getType();
* i.e. 'size', 'name' or 'mimetype'
*
* @return string
+ * @since 12.0.0
*/
public function getField();
* Get the value to compare the field with
*
* @return string|integer|\DateTime
+ * @since 12.0.0
*/
public function getValue();
}
namespace OCP\Files\Search;
-interface ISearchCondition {
+/**
+ * @since 12.0.0
+ */
+interface ISearchOperator {
}
* @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();
}
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);
+ }
}