Skip to content

Commit

Permalink
Merge pull request #26507 from owncloud/filelist-pagination-webdav-re…
Browse files Browse the repository at this point in the history
…port

Introduce pagination in files-filter report
  • Loading branch information
Vincent Petry authored Mar 22, 2017
2 parents 659b584 + fad6a05 commit b31294a
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 184 deletions.
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', [$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

0 comments on commit b31294a

Please sign in to comment.