diff --git a/NEXTCLOUD.md b/NEXTCLOUD.md index 75f2af2..97a2752 100644 --- a/NEXTCLOUD.md +++ b/NEXTCLOUD.md @@ -88,4 +88,10 @@ https://github.com/nextcloud/desktop/issues/4873 ## Use a proxy with desktop client 1. start mitmweb -2. run `export http_proxy=http://localhost:8080 && nextcloud -l --logdebug` \ No newline at end of file +2. run `export http_proxy=http://localhost:8080 && nextcloud -l --logdebug` + +## Chunked file upload + +* https://github.com/miquels/webdav-handler-rs/blob/master/doc/SABREDAV-partialupdate.md +* https://github.com/rclone/rclone/pull/6133 +* https://github.com/rclone/rclone/issues/3666 \ No newline at end of file diff --git a/README.md b/README.md index 3289b4d..fc8ce9e 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,22 @@ This server features: * [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) +## WebDAV clients compatibility + +* [FUSE webdavfs](https://github.com/miquels/webdavfs) is recommended for Linux +* davfs2 is NOT recommended: it is very slow, and it is using a local cache, meaning changing a file locally may not be synced to the server for a few minutes, leading to things getting out of sync. If you have to use it, at least disable locks, by setting `use_locks=0` in the config. + ## Future development 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/) * [Extended MKCOL](https://www.rfc-editor.org/rfc/rfc5689) if CalDAV support is implemented -* CalDAV/CardDAV support: maybe, why not, we'll see, in the mean time see [Sabre/DAV](https://sabre.io/dav/) for that. +* CalDAV/CardDAV support: maybe, [why not](https://evertpot.com/227/), we'll see, in the mean time see [Sabre/DAV](https://sabre.io/dav/) for that. ## Dependencies diff --git a/lib/KaraDAV/NextCloud.php b/lib/KaraDAV/NextCloud.php new file mode 100644 index 0000000..bfeb824 --- /dev/null +++ b/lib/KaraDAV/NextCloud.php @@ -0,0 +1,86 @@ +users = $users; + } + + public function auth(?string $login, ?string $password): bool + { + $user = $this->users->appSessionLogin($login, $password); + + if (!$user) { + return false; + } + + $this->user = $user; + + return true; + } + + public function getUserName(): ?string + { + return $this->users->current()->login ?? null; + } + + public function setUserName(string $login): bool + { + $ok = $this->users->setCurrent($login); + + if ($ok) { + $this->user = $this->users->current(); + } + + return $ok; + } + + public function getUserQuota(): array + { + return (array) $this->users->quota($this->users->current()); + } + + public function generateToken(): string + { + return sha1(random_bytes(16)); + } + + public function validateToken(string $token): ?array + { + $session = $this->users->appSessionValidateToken($token); + + if (!$session) { + return null; + } + + return ['login' => $session->user, 'password' => $session->password]; + } + + public function getLoginURL(?string $token): string + { + if ($token) { + return sprintf('%slogin.php?nc=%s', WWW_URL, $token); + } + else { + return sprintf('%slogin.php?nc=redirect', WWW_URL); + } + } + + public function getDirectDownloadSecret(string $uri, string $login): string + { + $user = $this->users->get($login); + + if (!$user) { + throw new WebDAV_Exception('No user with that name', 401); + } + + return hash('sha256', $uri . $user->login . $user->password); + } +} diff --git a/lib/KaraDAV/Server.php b/lib/KaraDAV/Server.php index f6b3376..0eb4c44 100644 --- a/lib/KaraDAV/Server.php +++ b/lib/KaraDAV/Server.php @@ -2,435 +2,60 @@ namespace KaraDAV; -use KD2\WebDAV; -use KD2\WebDAV_Exception; -use KD2\WebDAV_NextCloud; -use KD2\WebDAV_NextCloud_Exception; +use KD2\WebDAV\Server as WebDAV_Server; +use KD2\WebDAV\Exception; -class Server extends WebDAV_NextCloud +class Server extends WebDAV_Server { protected Users $users; - protected \stdClass $user; - const LOCK = true; - protected bool $parse_propfind = true; - - /** - * These file names will be ignored when doing a PUT - * as they are garbage, coming from some OS - */ - const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!'; public function __construct() { + $users = new Users; $this->users = new Users; - $this->root_url = WWW_URL; + $storage = new Storage($this->users); + $this->setStorage($storage); } public function route(?string $uri = null): bool { - if (parent::route($uri)) { - return true; - } + $nc = new NextCloud($this->users); - if (0 !== strpos($uri, '/files/')) { - return false; - } - - // If NextCloud layer didn't return anything - // it means we fall back to the default WebDAV server - // available on the root path. We need to handle a - // classic login/password auth here. - - $users = new Users; - $user = $users->login($_SERVER['PHP_AUTH_USER'] ?? null, $_SERVER['PHP_AUTH_PW'] ?? null); - - if (!$user) { - http_response_code(401); - header('WWW-Authenticate: Basic realm="Please login"'); - return true; - } - - $this->user = $user; - $this->setBaseURI('/files/' . $user->login . '/'); - - return WebDAV::route($uri); - } - - public function nc_auth(?string $login, ?string $password): bool - { - $user = $this->users->appSessionLogin($login, $password); - - if (!$user) { - return false; - } - - $this->user = $user; - - return true; - } - - public function nc_get_user(): ?string - { - return $this->users->current()->login ?? null; - } - - public function nc_set_user(string $login): bool - { - $ok = $this->users->setCurrent($login); - - if ($ok) { - $this->user = $this->users->current(); - } - - return $ok; - } - - public function nc_get_quota(): array - { - return (array) $this->users->quota($this->users->current()); - } - - public function nc_generate_token(): string - { - return sha1(random_bytes(16)); - } - - public function nc_validate_token(string $token): ?array - { - $session = $this->users->appSessionValidateToken($token); - - if (!$session) { - return null; - } - - return ['login' => $session->user, 'password' => $session->password]; - } - - public function nc_login_url(?string $token): string - { - if ($token) { - return sprintf('%slogin.php?nc=%s', $this->root_url, $token); - } - else { - return sprintf('%slogin.php?nc=redirect', $this->root_url); - } - } - - public function nc_direct_get_secret(string $uri, string $login): string - { - $user = $this->users->get($login); - - if (!$user) { - throw new WebDAV_Exception('No user with that name', 401); - } - - return hash('sha256', $uri . $user->login . $user->password); - } - - protected function getLock(string $uri, ?string $token = null): ?string - { - // It is important to check also for a lock on parent directory as we support depth=1 - $sql = 'SELECT scope FROM locks WHERE user = ? AND (uri = ? OR uri = ?)'; - $params = [$this->user->login, $uri, dirname($uri)]; - - if ($token) { - $sql .= ' AND token = ?'; - $params[] = $token; - } - - $sql .= ' LIMIT 1'; - - return DB::getInstance()->firstColumn($sql, ...$params); - } - - protected function lock(string $uri, string $token, string $scope): void - { - DB::getInstance()->run('REPLACE INTO locks VALUES (?, ?, ?, ?, datetime(\'now\', \'+5 minutes\'));', $this->user->login, $uri, $token, $scope); - } - - protected function unlock(string $uri, string $token): void - { - DB::getInstance()->run('DELETE FROM locks WHERE user = ? AND uri = ? AND token = ?;', $this->user->login, $uri, $token); - } - - protected function list(string $uri, ?array $properties): iterable - { - $dirs = glob($this->user->path . $uri . '/*', \GLOB_ONLYDIR); - $dirs = array_map('basename', $dirs); - natcasesort($dirs); - - $files = glob($this->user->path . $uri . '/*'); - $files = array_map('basename', $files); - $files = array_diff($files, $dirs); - natcasesort($files); - - $files = array_flip(array_merge($dirs, $files)); - $files = array_map(fn($a) => null, $files); - return $files; - } - - protected function get(string $uri): ?array - { - if (!file_exists($this->user->path . $uri)) { - return null; - } - - //return ['content' => file_get_contents($this->path . $uri)]; - //return ['resource' => fopen($this->path . $uri, 'r')]; - return ['path' => $this->user->path . $uri]; - } - - protected function exists(string $uri): bool - { - return file_exists($this->user->path . $uri); - } - - protected function get_file_property(string $uri, string $name, int $depth) - { - $target = $this->user->path . $uri; - - switch ($name) { - case 'DAV::getcontentlength': - return is_dir($target) ? 0 : filesize($target); - case 'DAV::getcontenttype': - return mime_content_type($target); - case 'DAV::resourcetype': - return is_dir($target) ? 'collection' : ''; - case 'DAV::getlastmodified': - if (!$uri && $depth == 0 && is_dir($target)) { - $mtime = get_directory_mtime($target); - } - else { - $mtime = filemtime($target); - } - - if (!$mtime) { - return null; - } - - return new \DateTime('@' . $mtime); - case 'DAV::displayname': - return basename($target); - case 'DAV::ishidden': - return basename($target)[0] == '.'; - case 'DAV::getetag': - if (!$uri && !$depth) { - $hash = get_directory_size($target) . get_directory_mtime($target); - } - else { - $hash = filemtime($target) . filesize($target); - } - - return md5($hash . $target); - case 'DAV::lastaccessed': - return new \DateTime('@' . fileatime($target)); - case 'DAV::creationdate': - return new \DateTime('@' . filectime($target)); - // NextCloud stuff - case self::PROP_OC_ID: - return $this->nc_direct_id($uri); - case self::PROP_OC_PERMISSIONS: - return implode('', [self::PERM_READ, self::PERM_WRITE, self::PERM_CREATE, self::PERM_DELETE, self::PERM_RENAME_MOVE]); - case self::PROP_OC_SIZE: - if (is_dir($target)) { - return get_directory_size($target); - } - else { - return filesize($target); - } - default: - break; - } - - if (in_array($name, self::NC_PROPERTIES) || in_array($name, self::BASIC_PROPERTIES) || in_array($name, self::EXTENDED_PROPERTIES)) { - return null; - } - - return null; - //return $this->getResourceProperties($uri)->get($name); - } - - protected function properties(string $uri, ?array $properties, int $depth): ?array - { - $target = $this->user->path . $uri; - - if (!file_exists($target)) { - return null; - } - - if (null === $properties) { - $properties = array_merge(self::BASIC_PROPERTIES, ['DAV::getetag', self::PROP_OC_ID]); - } - - $out = []; - - foreach ($properties as $name) { - $v = $this->get_file_property($uri, $name, $depth); - - if (null !== $v) { - $out[$name] = $v; + if ($r = $nc->route($uri)) { + if ($r['route'] == 'direct') { + $this->http_get($r['uri']); + return true; } - } - - return $out; - } - - protected function put(string $uri, $pointer): bool - { - if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) { - return false; - } - - $target = $this->user->path . $uri; - $parent = dirname($target); - - if (is_dir($target)) { - throw new WebDAV_Exception('Target is a directory', 409); - } - - if (!file_exists($parent)) { - mkdir($parent, 0770, true); - } - - $new = !file_exists($target); - $delete = false; - $size = 0; - $quota = $this->users->quota($this->user); - - if (!$new) { - $size -= filesize($target); - } - - $tmp_file = '.tmp.' . sha1($target); - $out = fopen($tmp_file, 'w'); - - while (!feof($pointer)) { - $bytes = fread($pointer, 8192); - $size += strlen($bytes); - - if ($size > $quota->free) { - $delete = true; - break; + elseif ($r['route'] == 'webdav') { + $this->setBaseURI($r['base_uri']); } - - fwrite($out, $bytes); - } - - fclose($out); - fclose($pointer); - - if ($delete) { - @unlink($tmp_file); - throw new WebDAV_Exception('Your quota is exhausted', 403); - } - else { - rename($tmp_file, $target); - } - - return $new; - } - - protected function delete(string $uri): void - { - $target = $this->user->path . $uri; - - if (!file_exists($target)) { - throw new WebDAV_Exception('Target does not exist', 404); - } - - if (is_dir($target)) { - foreach (glob($target . '/*') as $file) { - $this->delete(substr($file, strlen($this->user->path))); - } - - rmdir($target); - } - else { - unlink($target); - } - - //$this->getResourceProperties($uri)->clear(); - } - - protected function copymove(bool $move, string $uri, string $destination): bool - { - $source = $this->user->path . $uri; - $target = $this->user->path . $destination; - $parent = dirname($target); - - if (!file_exists($source)) { - throw new WebDAV_Exception('File not found', 404); - } - - $overwritten = file_exists($target); - - if (!is_dir($parent)) { - throw new WebDAV_Exception('Target parent directory does not exist', 409); - } - - if (false === $move) { - $quota = $this->users->quota($this->user); - - if (filesize($source) > $quota->free) { - throw new WebDAV_Exception('Your quota is exhausted', 403); - } - } - - if ($overwritten) { - $this->delete($destination); - } - - $method = $move ? 'rename' : 'copy'; - - if ($method == 'copy' && is_dir($source)) { - @mkdir($target, 0770, true); - - foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source), \RecursiveIteratorIterator::SELF_FIRST) as $item) - { - if ($item->isDir()) { - @mkdir($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname()); - } else { - copy($item, $target . DIRECTORY_SEPARATOR . $iterator->getSubPathname()); - } + else { + // NextCloud route already replied something, stop here + return true; } } else { - $method($source, $target); + // If NextCloud layer didn't return anything + // it means we fall back to the default WebDAV server + // available on the root path. We need to handle a + // classic login/password auth here. - //$this->getResourceProperties($uri)->move($destination); + if (0 !== strpos($uri, '/files/')) { + return false; + } + + $user = $this->users->login($_SERVER['PHP_AUTH_USER'] ?? null, $_SERVER['PHP_AUTH_PW'] ?? null); + + if (!$user) { + http_response_code(401); + header('WWW-Authenticate: Basic realm="Please login"'); + return true; + } + + $this->setBaseURI('/files/' . $user->login . '/'); } - return $overwritten; - } - - protected function copy(string $uri, string $destination): bool - { - return $this->copymove(false, $uri, $destination); - } - - protected function move(string $uri, string $destination): bool - { - return $this->copymove(true, $uri, $destination); - } - - protected function mkcol(string $uri): void - { - if (!$this->user->quota) { - throw new WebDAV_Exception('Your quota is exhausted', 403); - } - - $target = $this->user->path . $uri; - $parent = dirname($target); - - if (file_exists($target)) { - throw new WebDAV_Exception('There is already a file with that name', 405); - } - - if (!file_exists($parent)) { - throw new WebDAV_Exception('The parent directory does not exist', 409); - } - - mkdir($target, 0770); + return parent::route($uri); } protected function html_directory(string $uri, iterable $list, array $strings = self::LANGUAGE_STRINGS): ?string @@ -444,53 +69,4 @@ class Server extends WebDAV_NextCloud return $out; } - - protected function getResourceProperties(string $uri): Properties - { - if (!isset($this->properties[$uri])) { - $this->properties[$uri] = new Properties($this->user->login, $uri); - } - - return $this->properties[$uri]; - } - - protected function set_extra_properties(string $uri, string $body): void - { - $xml = @simplexml_load_string($body); - // Select correct namespace if required - if (!empty(key($xml->getDocNameSpaces()))) { - $xml = $xml->children('DAV:'); - } - - $db = DB::getInstance(); - - $db->exec('BEGIN;'); - $i = 0; - - if (isset($xml->set)) { - foreach ($xml->set as $prop) { - $prop = $prop->prop->children(); - $ns = $prop->getNamespaces(true); - $ns = array_flip($ns); - - if (!key($ns)) { - throw new WebDAV_Exception('Empty xmlns', 400); - } - - $this->getResourceProperties($uri)->set(key($ns), $prop->getName(), array_filter($ns, 'trim'), $prop->asXML()); - } - } - - if (isset($xml->remove)) { - foreach ($xml->remove as $prop) { - $prop = $prop->prop->children(); - $ns = $prop->getNamespaces(); - $this->getResourceProperties($uri)->remove(current($ns), $prop->getName()); - } - } - - $db->exec('END'); - - return; - } } diff --git a/lib/KaraDAV/Storage.php b/lib/KaraDAV/Storage.php new file mode 100644 index 0000000..ffb3fb1 --- /dev/null +++ b/lib/KaraDAV/Storage.php @@ -0,0 +1,377 @@ +users = $users; + } + + public function getLock(string $uri, ?string $token = null): ?string + { + // It is important to check also for a lock on parent directory as we support depth=1 + $sql = 'SELECT scope FROM locks WHERE user = ? AND (uri = ? OR uri = ?)'; + $params = [$this->users->current()->login, $uri, dirname($uri)]; + + if ($token) { + $sql .= ' AND token = ?'; + $params[] = $token; + } + + $sql .= ' LIMIT 1'; + + return DB::getInstance()->firstColumn($sql, ...$params); + } + + public function lock(string $uri, string $token, string $scope): void + { + DB::getInstance()->run('REPLACE INTO locks VALUES (?, ?, ?, ?, datetime(\'now\', \'+5 minutes\'));', $this->users->current()->login, $uri, $token, $scope); + } + + public function unlock(string $uri, string $token): void + { + DB::getInstance()->run('DELETE FROM locks WHERE user = ? AND uri = ? AND token = ?;', $this->users->current()->login, $uri, $token); + } + + public function list(string $uri, ?array $properties): iterable + { + $dirs = glob($this->users->current()->path . $uri . '/*', \GLOB_ONLYDIR); + $dirs = array_map('basename', $dirs); + natcasesort($dirs); + + $files = glob($this->users->current()->path . $uri . '/*'); + $files = array_map('basename', $files); + $files = array_diff($files, $dirs); + natcasesort($files); + + $files = array_flip(array_merge($dirs, $files)); + $files = array_map(fn($a) => null, $files); + return $files; + } + + public function get(string $uri): ?array + { + if (!file_exists($this->users->current()->path . $uri)) { + return null; + } + + //return ['content' => file_get_contents($this->path . $uri)]; + //return ['resource' => fopen($this->path . $uri, 'r')]; + return ['path' => $this->users->current()->path . $uri]; + } + + public function exists(string $uri): bool + { + return file_exists($this->users->current()->path . $uri); + } + + public function get_file_property(string $uri, string $name, int $depth) + { + $target = $this->users->current()->path . $uri; + + switch ($name) { + case 'DAV::getcontentlength': + return is_dir($target) ? 0 : filesize($target); + case 'DAV::getcontenttype': + return mime_content_type($target); + case 'DAV::resourcetype': + return is_dir($target) ? 'collection' : ''; + case 'DAV::getlastmodified': + if (!$uri && $depth == 0 && is_dir($target)) { + $mtime = get_directory_mtime($target); + } + else { + $mtime = filemtime($target); + } + + if (!$mtime) { + return null; + } + + return new \DateTime('@' . $mtime); + case 'DAV::displayname': + return basename($target); + case 'DAV::ishidden': + return basename($target)[0] == '.'; + case 'DAV::getetag': + if (!$uri && !$depth) { + $hash = get_directory_size($target) . get_directory_mtime($target); + } + else { + $hash = filemtime($target) . filesize($target); + } + + return md5($hash . $target); + case 'DAV::lastaccessed': + return new \DateTime('@' . fileatime($target)); + case 'DAV::creationdate': + return new \DateTime('@' . filectime($target)); + // NextCloud stuff + case self::PROP_OC_ID: + return $this->nc_direct_id($uri); + case self::PROP_OC_PERMISSIONS: + return implode('', [self::PERM_READ, self::PERM_WRITE, self::PERM_CREATE, self::PERM_DELETE, self::PERM_RENAME_MOVE]); + case self::PROP_OC_SIZE: + if (is_dir($target)) { + return get_directory_size($target); + } + else { + return filesize($target); + } + default: + break; + } + + if (in_array($name, self::NC_PROPERTIES) || in_array($name, self::BASIC_PROPERTIES) || in_array($name, self::EXTENDED_PROPERTIES)) { + return null; + } + + return null; + //return $this->getResourceProperties($uri)->get($name); + } + + public function properties(string $uri, ?array $properties, int $depth): ?array + { + $target = $this->users->current()->path . $uri; + + if (!file_exists($target)) { + return null; + } + + if (null === $properties) { + $properties = array_merge(self::BASIC_PROPERTIES, ['DAV::getetag', self::PROP_OC_ID]); + } + + $out = []; + + foreach ($properties as $name) { + $v = $this->get_file_property($uri, $name, $depth); + + if (null !== $v) { + $out[$name] = $v; + } + } + + return $out; + } + + public function put(string $uri, $pointer): bool + { + if (preg_match(self::PUT_IGNORE_PATTERN, basename($uri))) { + return false; + } + + $target = $this->users->current()->path . $uri; + $parent = dirname($target); + + if (is_dir($target)) { + throw new WebDAV_Exception('Target is a directory', 409); + } + + if (!file_exists($parent)) { + mkdir($parent, 0770, true); + } + + $new = !file_exists($target); + $delete = false; + $size = 0; + $quota = $this->users->quota($this->users->current()); + + if (!$new) { + $size -= filesize($target); + } + + $tmp_file = '.tmp.' . sha1($target); + $out = fopen($tmp_file, 'w'); + + while (!feof($pointer)) { + $bytes = fread($pointer, 8192); + $size += strlen($bytes); + + if ($size > $quota->free) { + $delete = true; + break; + } + + fwrite($out, $bytes); + } + + fclose($out); + fclose($pointer); + + if ($delete) { + @unlink($tmp_file); + throw new WebDAV_Exception('Your quota is exhausted', 403); + } + else { + rename($tmp_file, $target); + } + + return $new; + } + + public function delete(string $uri): void + { + $target = $this->users->current()->path . $uri; + + if (!file_exists($target)) { + throw new WebDAV_Exception('Target does not exist', 404); + } + + if (is_dir($target)) { + foreach (glob($target . '/*') as $file) { + $this->delete(substr($file, strlen($this->users->current()->path))); + } + + rmdir($target); + } + else { + unlink($target); + } + + //$this->getResourceProperties($uri)->clear(); + } + + public function copymove(bool $move, string $uri, string $destination): bool + { + $source = $this->users->current()->path . $uri; + $target = $this->users->current()->path . $destination; + $parent = dirname($target); + + if (!file_exists($source)) { + throw new WebDAV_Exception('File not found', 404); + } + + $overwritten = file_exists($target); + + if (!is_dir($parent)) { + throw new WebDAV_Exception('Target parent directory does not exist', 409); + } + + if (false === $move) { + $quota = $this->users->quota($this->users->current()); + + if (filesize($source) > $quota->free) { + throw new WebDAV_Exception('Your quota is exhausted', 403); + } + } + + if ($overwritten) { + $this->delete($destination); + } + + $method = $move ? 'rename' : 'copy'; + + if ($method == 'copy' && is_dir($source)) { + @mkdir($target, 0770, true); + + foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source), \RecursiveIteratorIterator::SELF_FIRST) as $item) + { + if ($item->isDir()) { + @mkdir($target . DIRECTORY_SEPARATOR . $iterator->getSubPathname()); + } else { + copy($item, $target . DIRECTORY_SEPARATOR . $iterator->getSubPathname()); + } + } + } + else { + $method($source, $target); + + //$this->getResourceProperties($uri)->move($destination); + } + + return $overwritten; + } + + public function copy(string $uri, string $destination): bool + { + return $this->copymove(false, $uri, $destination); + } + + public function move(string $uri, string $destination): bool + { + return $this->copymove(true, $uri, $destination); + } + + public function mkcol(string $uri): void + { + if (!$this->users->current()->quota) { + throw new WebDAV_Exception('Your quota is exhausted', 403); + } + + $target = $this->users->current()->path . $uri; + $parent = dirname($target); + + if (file_exists($target)) { + throw new WebDAV_Exception('There is already a file with that name', 405); + } + + if (!file_exists($parent)) { + throw new WebDAV_Exception('The parent directory does not exist', 409); + } + + mkdir($target, 0770); + } + + + public function getResourceProperties(string $uri): Properties + { + if (!isset($this->properties[$uri])) { + $this->properties[$uri] = new Properties($this->users->current()->login, $uri); + } + + return $this->properties[$uri]; + } + + public function setProperties(string $uri, string $body): void + { + $xml = @simplexml_load_string($body); + // Select correct namespace if required + if (!empty(key($xml->getDocNameSpaces()))) { + $xml = $xml->children('DAV:'); + } + + $db = DB::getInstance(); + + $db->exec('BEGIN;'); + $i = 0; + + if (isset($xml->set)) { + foreach ($xml->set as $prop) { + $prop = $prop->prop->children(); + $ns = $prop->getNamespaces(true); + $ns = array_flip($ns); + + if (!key($ns)) { + throw new WebDAV_Exception('Empty xmlns', 400); + } + + $this->getResourceProperties($uri)->set(key($ns), $prop->getName(), array_filter($ns, 'trim'), $prop->asXML()); + } + } + + if (isset($xml->remove)) { + foreach ($xml->remove as $prop) { + $prop = $prop->prop->children(); + $ns = $prop->getNamespaces(); + $this->getResourceProperties($uri)->remove(current($ns), $prop->getName()); + } + } + + $db->exec('END'); + + return; + } +} diff --git a/www/files.css b/www/files.css index 7d6e1c0..5f8412a 100644 --- a/www/files.css +++ b/www/files.css @@ -134,7 +134,7 @@ iframe, .md_preview { justify-content: center; } -.preview form div { +.preview form > div { height: 100%; margin: 0; display: flex; @@ -166,4 +166,8 @@ iframe, .md_preview { .preview img { max-width: 95%; max-height: 95%; +} + +input[name=rename] { + width: 30em; } \ No newline at end of file diff --git a/www/files.js b/www/files.js index 388d39e..6a5ae7b 100644 --- a/www/files.js +++ b/www/files.js @@ -82,13 +82,15 @@ Array.from($('table').rows).forEach((tr) => { var file_url = $$('a').href; var file_name = $$('a').innerText; var dir = $$('[colspan]'); - var type = !dir ? $$('td:nth-child(3)').innerText : null; + var type = !dir ? $$('td:nth-child(3)').innerText : 'dir'; - if (dir && url.match('..')) { + // For back link + if (dir && $$('a').getAttribute('href').indexOf('..') != -1) { dir.setAttribute('colspan', 4); return; } + // Add rename/delete buttons tr.insertAdjacentHTML('beforeend', file_buttons); if (type.match(PREVIEW_TYPES)) { @@ -160,13 +162,15 @@ Array.from($('table').rows).forEach((tr) => { dialog('rename', rename_dialog); let t = $('input[name=rename]'); t.value = file_name; - t.select(); + t.focus(); + t.selectionStart = 0; + t.selectionEnd = file_name.lastIndexOf('.'); document.forms[0].onsubmit = () => { var name = t.value; if (!name) return false; - return req('MOVE', file_url, '', {'Destination': location.href + name}); + return req('MOVE', file_url, '', {'Destination': location.pathname + name}); }; };