Implement chunk upload

This commit is contained in:
bohwaz 2022-09-30 15:17:47 +02:00
parent b93f8fa553
commit d38e45b015
6 changed files with 162 additions and 57 deletions

View file

@ -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/)

View file

@ -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;
}
}

View file

@ -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') {

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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 '
<footer>
Powered by <a href="https://github.com/kd2.org/karadav/">KaraDAV</a>
Powered by <a href="https://github.com/kd2org/karadav/">KaraDAV</a>
</footer>
</body>
</html>';