Use new class structure
This commit is contained in:
parent
076abc46ba
commit
1950716f8a
|
@ -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`
|
||||
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
|
|
@ -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
|
||||
|
||||
|
|
86
lib/KaraDAV/NextCloud.php
Normal file
86
lib/KaraDAV/NextCloud.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace KaraDAV;
|
||||
|
||||
use KD2\WebDAV\NextCloud as WebDAV_NextCloud;
|
||||
|
||||
class NextCloud extends WebDAV_NextCloud
|
||||
{
|
||||
protected Users $users;
|
||||
|
||||
public function __construct(Users $users)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
377
lib/KaraDAV/Storage.php
Normal file
377
lib/KaraDAV/Storage.php
Normal file
|
@ -0,0 +1,377 @@
|
|||
<?php
|
||||
|
||||
namespace KaraDAV;
|
||||
|
||||
use KD2\WebDAV\AbstractStorage;
|
||||
|
||||
class Storage extends AbstractStorage
|
||||
{
|
||||
protected Users $users;
|
||||
|
||||
/**
|
||||
* 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 $users)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
12
www/files.js
12
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});
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue