From 279822c2170d105e2fafe831b5729c82cacab235 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 22 Jun 2023 14:33:18 +0200 Subject: [PATCH] implement optimized getDirectoryContent for DAV Signed-off-by: Robin Appelman --- lib/private/Files/Storage/DAV.php | 221 +++++++++++++++++------------- 1 file changed, 125 insertions(+), 96 deletions(-) diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index b5b8b548787..6eab2df7f2f 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -47,6 +47,7 @@ use OCP\Constants; use OCP\Diagnostics\IEventLogger; use OCP\Files\FileInfo; use OCP\Files\ForbiddenException; +use OCP\Files\IMimeTypeDetector; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; use OCP\Http\Client\IClientService; @@ -93,10 +94,22 @@ class DAV extends Common { protected $certManager; protected LoggerInterface $logger; protected IEventLogger $eventLogger; + protected IMimeTypeDetector $mimeTypeDetector; /** @var int */ private $timeout; + protected const PROPFIND_PROPS = [ + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{http://owncloud.org/ns}permissions', + '{http://open-collaboration-services.org/ns}share-permissions', + '{DAV:}resourcetype', + '{DAV:}getetag', + '{DAV:}quota-available-bytes', + ]; + /** * @param array $params * @throws \Exception @@ -141,6 +154,7 @@ class DAV extends Common { $this->eventLogger = \OC::$server->get(IEventLogger::class); // This timeout value will be used for the download and upload of files $this->timeout = \OC::$server->get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', 30); + $this->mimeTypeDetector = \OC::$server->getMimeTypeDetector(); } protected function init() { @@ -239,31 +253,12 @@ class DAV extends Common { $this->init(); $path = $this->cleanPath($path); try { - $response = $this->client->propFind( - $this->encodePath($path), - ['{DAV:}getetag'], - 1 - ); - if ($response === false) { - return false; - } - $content = []; - $files = array_keys($response); - array_shift($files); //the first entry is the current directory - - if (!$this->statCache->hasKey($path)) { - $this->statCache->set($path, true); - } - foreach ($files as $file) { - $file = urldecode($file); - // do not store the real entry, we might not have all properties - if (!$this->statCache->hasKey($path)) { - $this->statCache->set($file, true); - } - $file = basename($file); - $content[] = $file; + $content = $this->getDirectoryContent($path); + $files = []; + foreach ($content as $child) { + $files[] = $child['name']; } - return IteratorDirectory::wrap($content); + return IteratorDirectory::wrap($files); } catch (\Exception $e) { $this->convertException($e, $path); } @@ -291,16 +286,7 @@ class DAV extends Common { try { $response = $this->client->propFind( $this->encodePath($path), - [ - '{DAV:}getlastmodified', - '{DAV:}getcontentlength', - '{DAV:}getcontenttype', - '{http://owncloud.org/ns}permissions', - '{http://open-collaboration-services.org/ns}share-permissions', - '{DAV:}resourcetype', - '{DAV:}getetag', - '{DAV:}quota-available-bytes', - ] + self::PROPFIND_PROPS ); $this->statCache->set($path, $response); } catch (ClientHttpException $e) { @@ -607,53 +593,84 @@ class DAV extends Common { return false; } - /** {@inheritdoc} */ - public function stat($path) { - try { - $response = $this->propfind($path); - if (!$response) { - return false; + public function getMetaData($path) { + if (Filesystem::isFileBlacklisted($path)) { + throw new ForbiddenException('Invalid path: ' . $path, false); + } + $response = $this->propfind($path); + if (!$response) { + return null; + } else { + return $this->getMetaFromPropfind($path, $response); + } + } + private function getMetaFromPropfind(string $path, array $response): array { + if (isset($response['{DAV:}getetag'])) { + $etag = trim($response['{DAV:}getetag'], '"'); + if (strlen($etag) > 40) { + $etag = md5($etag); } - return [ - 'mtime' => isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null, - 'size' => Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0), - ]; - } catch (\Exception $e) { - $this->convertException($e, $path); + } else { + $etag = parent::getETag($path); + } + + $responseType = []; + if (isset($response["{DAV:}resourcetype"])) { + /** @var ResourceType[] $response */ + $responseType = $response["{DAV:}resourcetype"]->getValue(); + } + $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; + if ($type === 'dir') { + $mimeType = 'httpd/unix-directory'; + } elseif (isset($response['{DAV:}getcontenttype'])) { + $mimeType = $response['{DAV:}getcontenttype']; + } else { + $mimeType = $this->mimeTypeDetector->detectPath($path); } - return []; + + if (isset($response['{http://owncloud.org/ns}permissions'])) { + $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); + } elseif ($type === 'dir') { + $permissions = Constants::PERMISSION_ALL; + } else { + $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + } + + $mtime = isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null; + + if ($type === 'dir') { + $size = -1; + } else { + $size = Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0); + } + + return [ + 'name' => basename($path), + 'mtime' => $mtime, + 'storage_mtime' => $mtime, + 'size' => $size, + 'permissions' => $permissions, + 'etag' => $etag, + 'mimetype' => $mimeType, + ]; } /** {@inheritdoc} */ - public function getMimeType($path) { - $remoteMimetype = $this->getMimeTypeFromRemote($path); - if ($remoteMimetype === 'application/octet-stream') { - return \OC::$server->getMimeTypeDetector()->detectPath($path); + public function stat($path) { + $meta = $this->getMetaData($path); + if (!$meta) { + return false; } else { - return $remoteMimetype; + return $meta; } } - public function getMimeTypeFromRemote($path) { - try { - $response = $this->propfind($path); - if ($response === false) { - return false; - } - $responseType = []; - if (isset($response["{DAV:}resourcetype"])) { - /** @var ResourceType[] $response */ - $responseType = $response["{DAV:}resourcetype"]->getValue(); - } - $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; - if ($type == 'dir') { - return 'httpd/unix-directory'; - } elseif (isset($response['{DAV:}getcontenttype'])) { - return $response['{DAV:}getcontenttype']; - } else { - return 'application/octet-stream'; - } - } catch (\Exception $e) { + /** {@inheritdoc} */ + public function getMimeType($path) { + $meta = $this->getMetaData($path); + if ($meta) { + return $meta['mimetype']; + } else { return false; } } @@ -739,18 +756,9 @@ class DAV extends Common { /** {@inheritdoc} */ public function getPermissions($path) { - $this->init(); - $path = $this->cleanPath($path); - $response = $this->propfind($path); - if ($response === false) { - return 0; - } - if (isset($response['{http://owncloud.org/ns}permissions'])) { - return $this->parsePermissions($response['{http://owncloud.org/ns}permissions']); - } elseif ($this->is_dir($path)) { - return Constants::PERMISSION_ALL; - } elseif ($this->file_exists($path)) { - return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; + $stat = $this->getMetaData($path); + if ($stat) { + return $stat['permissions']; } else { return 0; } @@ -758,20 +766,12 @@ class DAV extends Common { /** {@inheritdoc} */ public function getETag($path) { - $this->init(); - $path = $this->cleanPath($path); - $response = $this->propfind($path); - if ($response === false) { + $meta = $this->getMetaData($path); + if ($meta) { + return $meta['etag']; + } else { return null; } - if (isset($response['{DAV:}getetag'])) { - $etag = trim($response['{DAV:}getetag'], '"'); - if (strlen($etag) > 40) { - $etag = md5($etag); - } - return $etag; - } - return parent::getEtag($path); } /** @@ -901,4 +901,33 @@ class DAV extends Common { // TODO: only log for now, but in the future need to wrap/rethrow exception } + + public function getDirectoryContent($directory): \Traversable { + $this->init(); + $directory = $this->cleanPath($directory); + try { + $responses = $this->client->propFind( + $this->encodePath($directory), + self::PROPFIND_PROPS, + 1 + ); + if ($responses === false) { + return; + } + + array_shift($responses); //the first entry is the current directory + if (!$this->statCache->hasKey($directory)) { + $this->statCache->set($directory, true); + } + + foreach ($responses as $file => $response) { + $file = urldecode($file); + $file = substr($file, strlen($this->root)); + $this->statCache->set($file, $response); + yield $this->getMetaFromPropfind($file, $response); + } + } catch (\Exception $e) { + $this->convertException($e, $directory); + } + } } -- 2.39.5