Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[uc] Introduce pagination in files-filter report #4530

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 56 additions & 60 deletions apps/dav/lib/Connector/Sabre/FilesReportPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
namespace OCA\DAV\Connector\Sabre;

use OC\Files\View;
use OCA\DAV\Files\Xml\FilterRequest;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\ServerPlugin;
Expand Down Expand Up @@ -139,6 +140,8 @@ public function initialize(\Sabre\DAV\Server $server) {

$server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc';

$server->xml->elementMap[self::REPORT_NAME] = FilterRequest::class;

$this->server = $server;
$this->server->on('report', array($this, 'onReport'));
}
Expand All @@ -159,58 +162,52 @@ public function getSupportedReportSet($uri) {
* REPORT operations to look for files
*
* @param string $reportName
* @param $report
* @param mixed $report
* @param string $uri
* @return bool
* @throws BadRequest
* @throws PreconditionFailed
* @internal param $ [] $report
*/
public function onReport($reportName, $report, $uri) {

$reportTargetNode = $this->server->tree->getNodeForPath($uri);
if (!$reportTargetNode instanceof Directory || $reportName !== self::REPORT_NAME) {
return;
}

$ns = '{' . $this::NS_OWNCLOUD . '}';
$requestedProps = [];
$filterRules = [];

// parse report properties and gather filter info
foreach ($report as $reportProps) {
$name = $reportProps['name'];
if ($name === $ns . 'filter-rules') {
$filterRules = $reportProps['value'];
} else if ($name === '{DAV:}prop') {
// propfind properties
foreach ($reportProps['value'] as $propVal) {
$requestedProps[] = $propVal['name'];
}
$requestedProps = $report->properties;
$filterRules = $report->filters;

// "systemtag" is always an array of tags, favorite a string/int/null
if (empty($filterRules['systemtag']) && is_null($filterRules['favorite'])) {
// FIXME: search currently not possible because results are missing properties!
throw new BadRequest('No filter criteria specified');
} else {
if (isset($report->search['pattern'])) {
// TODO: implement this at some point...
throw new BadRequest('Search pattern cannot be combined with filter');
}
}

if (empty($filterRules)) {
// an empty filter would return all existing files which would be slow
throw new BadRequest('Missing filter-rule block in request');
}
// gather all file ids matching filter
try {
$resultFileIds = $this->processFilterRules($filterRules);
} catch (TagNotFoundException $e) {
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
}

// gather all file ids matching filter
try {
$resultFileIds = $this->processFilterRules($filterRules);
} catch (TagNotFoundException $e) {
throw new PreconditionFailed('Cannot filter by non-existing tag', 0, $e);
}
// pre-slice the results if needed for pagination to not waste
// time resolving nodes that will not be returned anyway
$resultFileIds = $this->slice($resultFileIds, $report);

// find sabre nodes by file id, restricted to the root node path
$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
// find sabre nodes by file id, restricted to the root node path
$results = $this->findNodesByFileIds($reportTargetNode, $resultFileIds);
}

$filesUri = $this->getFilesBaseUri($uri, $reportTargetNode->getPath());
$responses = $this->prepareResponses($filesUri, $requestedProps, $results);
$results = $this->prepareResponses($filesUri, $requestedProps, $results);

$xml = $this->server->xml->write(
'{DAV:}multistatus',
new MultiStatus($responses)
);
$xml = $this->server->generateMultiStatus($results);

$this->server->httpResponse->setStatus(207);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
Expand All @@ -219,6 +216,15 @@ public function onReport($reportName, $report, $uri) {
return false;
}

private function slice($results, $report) {
if (!is_null($report->search)) {
$length = $report->search['limit'];
$offset = $report->search['offset'];
$results = array_slice($results, $offset, $length);
}
return $results;
}

/**
* Returns the base uri of the files root by removing
* the subpath from the URI
Expand Down Expand Up @@ -252,18 +258,9 @@ private function getFilesBaseUri($uri, $subPath) {
* @throws TagNotFoundException whenever a tag was not found
*/
protected function processFilterRules($filterRules) {
$ns = '{' . $this::NS_OWNCLOUD . '}';
$resultFileIds = null;
$systemTagIds = [];
$favoriteFilter = null;
foreach ($filterRules as $filterRule) {
if ($filterRule['name'] === $ns . 'systemtag') {
$systemTagIds[] = $filterRule['value'];
}
if ($filterRule['name'] === $ns . 'favorite') {
$favoriteFilter = true;
}
}
$systemTagIds = $filterRules['systemtag'];
$favoriteFilter = $filterRules['favorite'];

if ($favoriteFilter !== null) {
$resultFileIds = $this->fileTagger->load('files')->getFavorites();
Expand Down Expand Up @@ -337,7 +334,7 @@ private function getSystemTagFileIds($systemTagIds) {
* @return Response[]
*/
public function prepareResponses($filesUri, $requestedProps, $nodes) {
$responses = [];
$results = [];
foreach ($nodes as $node) {
$propFind = new PropFind($filesUri . $node->getPath(), $requestedProps);

Expand All @@ -346,18 +343,9 @@ public function prepareResponses($filesUri, $requestedProps, $nodes) {
$result = $propFind->getResultForMultiStatus();
$result['href'] = $propFind->getPath();

$resourceType = $this->server->getResourceTypeForNode($node);
if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
$result['href'] .= '/';
}

$responses[] = new Response(
rtrim($this->server->getBaseUri(), '/') . $filesUri . $node->getPath(),
$result,
200
);
$results[] = $result;
}
return $responses;
return $results;
}

/**
Expand All @@ -378,17 +366,25 @@ public function findNodesByFileIds($rootNode, $fileIds) {
$entry = $folder->getById($fileId);
if ($entry) {
$entry = current($entry);
if ($entry instanceof \OCP\Files\File) {
$results[] = new File($this->fileView, $entry);
} else if ($entry instanceof \OCP\Files\Folder) {
$results[] = new Directory($this->fileView, $entry);
$node = $this->makeSabreNode($entry);
if ($node) {
$results[] = $node;
}
}
}

return $results;
}

private function makeSabreNode(\OCP\Files\Node $filesNode) {
if ($filesNode instanceof \OCP\Files\File) {
return new File($this->fileView, $filesNode);
} else if ($filesNode instanceof \OCP\Files\Folder) {
return new Directory($this->fileView, $filesNode);
}
throw new \Exception('Unrecognized Files API node returned, aborting');
}

/**
* Returns whether the currently logged in user is an administrator
*/
Expand Down
110 changes: 110 additions & 0 deletions apps/dav/lib/Files/Xml/FilterRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace OCA\DAV\Files\Xml;

use Sabre\Xml\Element\Base;
use Sabre\Xml\Element\KeyValue;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;

class FilterRequest implements XmlDeserializable {

/**
* An array with requested properties.
*
* @var array
*/
public $properties;

/**
* @var array
*/
public $filters;

/**
* @var array
*/
public $search;

/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @param Reader $reader
* @return mixed
*/
static function xmlDeserialize(Reader $reader) {
$elems = (array)$reader->parseInnerTree([
'{DAV:}prop' => KeyValue::class,
'{http://owncloud.org/ns}filter-rules' => Base::class,
'{http://owncloud.org/ns}search' => KeyValue::class,
]);

$newProps = [
'filters' => [
'systemtag' => [],
'favorite' => null
],
'properties' => [],
'search' => null,
];

if (!is_array($elems)) {
$elems = [];
}

foreach ($elems as $elem) {

switch ($elem['name']) {

case '{DAV:}prop' :
$newProps['properties'] = array_keys($elem['value']);
break;
case '{http://owncloud.org/ns}filter-rules' :

foreach ($elem['value'] as $tag) {
if ($tag['name'] === '{http://owncloud.org/ns}systemtag') {
$newProps['filters']['systemtag'][] = $tag['value'];
}
if ($tag['name'] === '{http://owncloud.org/ns}favorite') {
$newProps['filters']['favorite'] = true;
}
}
break;
case '{http://owncloud.org/ns}search' :
$value = $elem['value'];
if (isset($value['{http://owncloud.org/ns}pattern'])) {
$newProps['search']['pattern'] = $value['{http://owncloud.org/ns}pattern'];
}
if (isset($value['{http://owncloud.org/ns}limit'])) {
$newProps['search']['limit'] = (int)$value['{http://owncloud.org/ns}limit'];
}
if (isset($value['{http://owncloud.org/ns}offset'])) {
$newProps['search']['offset'] = (int)$value['{http://owncloud.org/ns}offset'];
}
break;
}
}

$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}

return $obj;
}
}
Loading