From d38e45b015077eb30b5225efd2db3cbf8809e80b Mon Sep 17 00:00:00 2001 From: bohwaz Date: Fri, 30 Sep 2022 15:17:47 +0200 Subject: [PATCH] Implement chunk upload --- README.md | 2 +- lib/KaraDAV/NextCloud.php | 84 +++++++++++++++++++++++++++++++++++++- lib/KaraDAV/Server.php | 2 +- lib/KaraDAV/Storage.php | 86 ++++++++++++++++++++++++++++++++++----- lib/KaraDAV/Users.php | 2 +- www/_inc.php | 43 +------------------- 6 files changed, 162 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index bb698ad..f1800b8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This server features: * Desktop app (tested on Debian) * [NextCloud CLI client](https://docs.nextcloud.com/desktop/3.5/advancedusage.html) * Support for [Direct download API](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html#direct-download) +* Support for [Chunk upload](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/chunking.html) ## WebDAV clients compatibility @@ -44,7 +45,6 @@ This server features: This might get supported in future (maybe): * [Partial upload via PATCH](https://github.com/miquels/webdav-handler-rs/blob/master/doc/SABREDAV-partialupdate.md) -* [Chunk upload](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/chunking.html) * [NextCloud Trashbin](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/trashbin.html) * [NextCloud sharing](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html) (maybe?) * [WebDAV sharing](https://evertpot.com/webdav-caldav-carddav-sharing/) diff --git a/lib/KaraDAV/NextCloud.php b/lib/KaraDAV/NextCloud.php index bfeb824..f32a8c7 100644 --- a/lib/KaraDAV/NextCloud.php +++ b/lib/KaraDAV/NextCloud.php @@ -3,14 +3,17 @@ namespace KaraDAV; use KD2\WebDAV\NextCloud as WebDAV_NextCloud; +use KD2\WebDAV\Exception as WebDAV_Exception; class NextCloud extends WebDAV_NextCloud { protected Users $users; + protected string $temporary_chunks_path; - public function __construct(Users $users) + public function __construct(Users $users, string $temporary_chunks_path) { $this->users = $users; + $this->temporary_chunks_path = $temporary_chunks_path; } public function auth(?string $login, ?string $password): bool @@ -83,4 +86,83 @@ class NextCloud extends WebDAV_NextCloud return hash('sha256', $uri . $user->login . $user->password); } + + protected function cleanChunks(): void + { + $expire = time() - 36*3600; + + foreach (glob($this->temporary_chunks_path . '/*/*/*') as $file) { + if (filemtime($file) < $expire) { + Storage::deleteDirectory(dirname($file)); + } + } + } + + public function storeChunk(string $login, string $name, string $part, $pointer): void + { + $this->cleanChunks(); + + $path = $this->temporary_chunks_path . '/' . $login . '/' . $name; + @mkdir($path, 0777, true); + + $file_path = $path . '/' . $part; + $out = fopen($file_path, 'wb'); + $quota = $this->getUserQuota(); + $used = $quota['used'] + Storage::getDirectorySize($path); + + while (!feof($pointer)) { + $data = fread($pointer, 8192); + $used += strlen($used); + + if ($used > $quota['free']) { + $this->deleteChunks($login, $name); + throw new WebDAV_Exception('Your quota does not allow for the upload of this file', 403); + } + + fwrite($out, $data); + } + + fclose($out); + fclose($pointer); + } + + public function deleteChunks(string $login, string $name): void + { + $path = $this->temporary_chunks_path . '/' . $login . '/' . $name; + Storage::deleteDirectory($path); + } + + public function assembleChunks(string $login, string $name, string $target): bool + { + $target = $this->users->current()->path . $target; + $parent = dirname($target); + + if (!is_dir($parent)) { + throw new WebDAV_Exception('Target parent directory does not exist', 409); + } + + $path = $this->temporary_chunks_path . '/' . $login . '/' . $name; + $exists = file_exists($target); + + if ($exists && is_dir($target)) { + + } + + $out = fopen($target, 'wb'); + + foreach (glob($path . '/*') as $file) { + $in = fopen($file, 'rb'); + + while (!feof($in)) { + fwrite($out, fread($in, 8192)); + } + + fclose($in); + } + + fclose($out); + $this->deleteChunks($login, $name); + return !$exists; + } + } diff --git a/lib/KaraDAV/Server.php b/lib/KaraDAV/Server.php index 0eb4c44..a10d0f0 100644 --- a/lib/KaraDAV/Server.php +++ b/lib/KaraDAV/Server.php @@ -19,7 +19,7 @@ class Server extends WebDAV_Server public function route(?string $uri = null): bool { - $nc = new NextCloud($this->users); + $nc = new NextCloud($this->users, sprintf(STORAGE_PATH, '_chunks')); if ($r = $nc->route($uri)) { if ($r['route'] == 'direct') { diff --git a/lib/KaraDAV/Storage.php b/lib/KaraDAV/Storage.php index 5ab97b9..609fbb5 100644 --- a/lib/KaraDAV/Storage.php +++ b/lib/KaraDAV/Storage.php @@ -4,6 +4,7 @@ namespace KaraDAV; use KD2\WebDAV\AbstractStorage; use KD2\WebDAV\Server as WebDAV_Server; +use KD2\WebDAV\Exception as WebDAV_Exception; class Storage extends AbstractStorage { @@ -91,7 +92,7 @@ class Storage extends AbstractStorage return is_dir($target) ? 'collection' : ''; case 'DAV::getlastmodified': if (!$uri && $depth == 0 && is_dir($target)) { - $mtime = get_directory_mtime($target); + $mtime = self::getDirectoryMTime($target); } else { $mtime = filemtime($target); @@ -108,7 +109,7 @@ class Storage extends AbstractStorage return basename($target)[0] == '.'; case 'DAV::getetag': if (!$uri && !$depth) { - $hash = get_directory_size($target) . get_directory_mtime($target); + $hash = self::getDirectorySize($target) . self::getDirectoryMTime($target); } else { $hash = filemtime($target) . filesize($target); @@ -123,12 +124,13 @@ class Storage extends AbstractStorage return md5_file($target); // NextCloud stuff case Nextcloud::PROP_OC_ID: - return $this->nc_direct_id($uri); + $username = $this->users->current()->login; + return NextCloud::getDirectID($username, $uri); case Nextcloud::PROP_OC_PERMISSIONS: return implode('', [NextCloud::PERM_READ, NextCloud::PERM_WRITE, NextCloud::PERM_CREATE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME_MOVE]); case Nextcloud::PROP_OC_SIZE: if (is_dir($target)) { - return get_directory_size($target); + return self::getDirectorySize($target); } else { return filesize($target); @@ -137,12 +139,11 @@ class Storage extends AbstractStorage break; } - if (in_array($name, Server::NC_PROPERTIES) || in_array($name, Server::BASIC_PROPERTIES) || in_array($name, Server::EXTENDED_PROPERTIES)) { + if (in_array($name, NextCloud::NC_PROPERTIES) || in_array($name, Server::BASIC_PROPERTIES) || in_array($name, Server::EXTENDED_PROPERTIES)) { return null; } - return null; - //return $this->getResourceProperties($uri)->get($name); + return $this->getResourceProperties($uri)->get($name); } public function properties(string $uri, ?array $properties, int $depth): ?array @@ -248,7 +249,7 @@ class Storage extends AbstractStorage unlink($target); } - //$this->getResourceProperties($uri)->clear(); + $this->getResourceProperties($uri)->clear(); } public function copymove(bool $move, string $uri, string $destination): bool @@ -296,7 +297,7 @@ class Storage extends AbstractStorage else { $method($source, $target); - //$this->getResourceProperties($uri)->move($destination); + $this->getResourceProperties($uri)->move($destination); } return $overwritten; @@ -365,7 +366,28 @@ class Storage extends AbstractStorage throw new WebDAV_Exception('Empty xmlns', 400); } - $this->getResourceProperties($uri)->set(key($ns), $prop->getName(), array_filter($ns, 'trim'), $prop->asXML()); + $name = key($ns) . ':' . $prop->getName(); + + $attributes = iterator_to_array($prop->attributes()); + + foreach ($ns as $xmlns => $alias) { + foreach (iterator_to_array($prop->attributes($alias)) as $key => $v) { + $attributes[$xmlns . ':' . $key] = $value; + } + } + + if ($prop->count() > 1) { + $text = ''; + + foreach ($prop->children() as $c) { + $text .= $c->asXML(); + } + } + else { + $text = (string)$prop; + } + + $this->getResourceProperties($uri)->set($name, $attributes ?: null, $text ?: null); } } @@ -373,7 +395,8 @@ class Storage extends AbstractStorage foreach ($xml->remove as $prop) { $prop = $prop->prop->children(); $ns = $prop->getNamespaces(); - $this->getResourceProperties($uri)->remove(current($ns), $prop->getName()); + $name = current($ns) . ':' . $prop->getName(); + $this->getResourceProperties($uri)->remove($name); } } @@ -381,4 +404,45 @@ class Storage extends AbstractStorage return; } + + static public function getDirectorySize(string $path): int + { + $total = 0; + $path = rtrim($path, '/'); + + foreach (glob($path . '/*', GLOB_NOSORT) as $f) { + if (is_dir($f)) { + $total += self::getDirectorySize($f); + } + else { + $total += filesize($f); + } + } + + return $total; + } + + static public function getDirectoryMTime(string $path): int + { + $last = 0; + $path = rtrim($path, '/'); + + foreach (glob($path . '/*', GLOB_NOSORT) as $f) { + if (is_dir($f)) { + $m = self::getDirectoryMTime($f); + + if ($m > $last) { + $last = $m; + } + } + + $m = filemtime($f); + + if ($m > $last) { + $last = $m; + } + } + + return $last; + } } diff --git a/lib/KaraDAV/Users.php b/lib/KaraDAV/Users.php index 78acb8a..4e42442 100644 --- a/lib/KaraDAV/Users.php +++ b/lib/KaraDAV/Users.php @@ -247,7 +247,7 @@ class Users $used = $total = $free = 0; if ($user) { - $used = get_directory_size($user->path); + $used = Storage::getDirectorySize($user->path); $total = $user->quota; $free = $user->quota - $used; } diff --git a/www/_inc.php b/www/_inc.php index 2818f90..9a5a2a4 100644 --- a/www/_inc.php +++ b/www/_inc.php @@ -35,47 +35,6 @@ if (!file_exists(DB_FILE)) { $db->exec('END;'); } -function get_directory_size(string $path): int -{ - $total = 0; - $path = rtrim($path, '/'); - - foreach (glob($path . '/*', GLOB_NOSORT) as $f) { - if (is_dir($f)) { - $total += get_directory_size($f); - } - else { - $total += filesize($f); - } - } - - return $total; -} - -function get_directory_mtime(string $path): int -{ - $last = 0; - $path = rtrim($path, '/'); - - foreach (glob($path . '/*', GLOB_NOSORT) as $f) { - if (is_dir($f)) { - $m = get_directory_mtime($f); - - if ($m > $last) { - $last = $m; - } - } - - $m = filemtime($f); - - if ($m > $last) { - $last = $m; - } - } - - return $last; -} - function html_head(string $title): void { $title = htmlspecialchars($title); @@ -97,7 +56,7 @@ function html_foot(): void { echo ' ';