Implement chunk upload
This commit is contained in:
parent
b93f8fa553
commit
d38e45b015
|
@ -33,6 +33,7 @@ This server features:
|
||||||
* Desktop app (tested on Debian)
|
* Desktop app (tested on Debian)
|
||||||
* [NextCloud CLI client](https://docs.nextcloud.com/desktop/3.5/advancedusage.html)
|
* [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 [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
|
## WebDAV clients compatibility
|
||||||
|
|
||||||
|
@ -44,7 +45,6 @@ This server features:
|
||||||
This might get supported in future (maybe):
|
This might get supported in future (maybe):
|
||||||
|
|
||||||
* [Partial upload via PATCH](https://github.com/miquels/webdav-handler-rs/blob/master/doc/SABREDAV-partialupdate.md)
|
* [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 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?)
|
* [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/)
|
* [WebDAV sharing](https://evertpot.com/webdav-caldav-carddav-sharing/)
|
||||||
|
|
|
@ -3,14 +3,17 @@
|
||||||
namespace KaraDAV;
|
namespace KaraDAV;
|
||||||
|
|
||||||
use KD2\WebDAV\NextCloud as WebDAV_NextCloud;
|
use KD2\WebDAV\NextCloud as WebDAV_NextCloud;
|
||||||
|
use KD2\WebDAV\Exception as WebDAV_Exception;
|
||||||
|
|
||||||
class NextCloud extends WebDAV_NextCloud
|
class NextCloud extends WebDAV_NextCloud
|
||||||
{
|
{
|
||||||
protected Users $users;
|
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->users = $users;
|
||||||
|
$this->temporary_chunks_path = $temporary_chunks_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function auth(?string $login, ?string $password): bool
|
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);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Server extends WebDAV_Server
|
||||||
|
|
||||||
public function route(?string $uri = null): bool
|
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 = $nc->route($uri)) {
|
||||||
if ($r['route'] == 'direct') {
|
if ($r['route'] == 'direct') {
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace KaraDAV;
|
||||||
|
|
||||||
use KD2\WebDAV\AbstractStorage;
|
use KD2\WebDAV\AbstractStorage;
|
||||||
use KD2\WebDAV\Server as WebDAV_Server;
|
use KD2\WebDAV\Server as WebDAV_Server;
|
||||||
|
use KD2\WebDAV\Exception as WebDAV_Exception;
|
||||||
|
|
||||||
class Storage extends AbstractStorage
|
class Storage extends AbstractStorage
|
||||||
{
|
{
|
||||||
|
@ -91,7 +92,7 @@ class Storage extends AbstractStorage
|
||||||
return is_dir($target) ? 'collection' : '';
|
return is_dir($target) ? 'collection' : '';
|
||||||
case 'DAV::getlastmodified':
|
case 'DAV::getlastmodified':
|
||||||
if (!$uri && $depth == 0 && is_dir($target)) {
|
if (!$uri && $depth == 0 && is_dir($target)) {
|
||||||
$mtime = get_directory_mtime($target);
|
$mtime = self::getDirectoryMTime($target);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$mtime = filemtime($target);
|
$mtime = filemtime($target);
|
||||||
|
@ -108,7 +109,7 @@ class Storage extends AbstractStorage
|
||||||
return basename($target)[0] == '.';
|
return basename($target)[0] == '.';
|
||||||
case 'DAV::getetag':
|
case 'DAV::getetag':
|
||||||
if (!$uri && !$depth) {
|
if (!$uri && !$depth) {
|
||||||
$hash = get_directory_size($target) . get_directory_mtime($target);
|
$hash = self::getDirectorySize($target) . self::getDirectoryMTime($target);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$hash = filemtime($target) . filesize($target);
|
$hash = filemtime($target) . filesize($target);
|
||||||
|
@ -123,12 +124,13 @@ class Storage extends AbstractStorage
|
||||||
return md5_file($target);
|
return md5_file($target);
|
||||||
// NextCloud stuff
|
// NextCloud stuff
|
||||||
case Nextcloud::PROP_OC_ID:
|
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:
|
case Nextcloud::PROP_OC_PERMISSIONS:
|
||||||
return implode('', [NextCloud::PERM_READ, NextCloud::PERM_WRITE, NextCloud::PERM_CREATE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME_MOVE]);
|
return implode('', [NextCloud::PERM_READ, NextCloud::PERM_WRITE, NextCloud::PERM_CREATE, NextCloud::PERM_DELETE, NextCloud::PERM_RENAME_MOVE]);
|
||||||
case Nextcloud::PROP_OC_SIZE:
|
case Nextcloud::PROP_OC_SIZE:
|
||||||
if (is_dir($target)) {
|
if (is_dir($target)) {
|
||||||
return get_directory_size($target);
|
return self::getDirectorySize($target);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return filesize($target);
|
return filesize($target);
|
||||||
|
@ -137,12 +139,11 @@ class Storage extends AbstractStorage
|
||||||
break;
|
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 null;
|
return $this->getResourceProperties($uri)->get($name);
|
||||||
//return $this->getResourceProperties($uri)->get($name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function properties(string $uri, ?array $properties, int $depth): ?array
|
public function properties(string $uri, ?array $properties, int $depth): ?array
|
||||||
|
@ -248,7 +249,7 @@ class Storage extends AbstractStorage
|
||||||
unlink($target);
|
unlink($target);
|
||||||
}
|
}
|
||||||
|
|
||||||
//$this->getResourceProperties($uri)->clear();
|
$this->getResourceProperties($uri)->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function copymove(bool $move, string $uri, string $destination): bool
|
public function copymove(bool $move, string $uri, string $destination): bool
|
||||||
|
@ -296,7 +297,7 @@ class Storage extends AbstractStorage
|
||||||
else {
|
else {
|
||||||
$method($source, $target);
|
$method($source, $target);
|
||||||
|
|
||||||
//$this->getResourceProperties($uri)->move($destination);
|
$this->getResourceProperties($uri)->move($destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $overwritten;
|
return $overwritten;
|
||||||
|
@ -365,7 +366,28 @@ class Storage extends AbstractStorage
|
||||||
throw new WebDAV_Exception('Empty xmlns', 400);
|
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) {
|
foreach ($xml->remove as $prop) {
|
||||||
$prop = $prop->prop->children();
|
$prop = $prop->prop->children();
|
||||||
$ns = $prop->getNamespaces();
|
$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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,7 +247,7 @@ class Users
|
||||||
$used = $total = $free = 0;
|
$used = $total = $free = 0;
|
||||||
|
|
||||||
if ($user) {
|
if ($user) {
|
||||||
$used = get_directory_size($user->path);
|
$used = Storage::getDirectorySize($user->path);
|
||||||
$total = $user->quota;
|
$total = $user->quota;
|
||||||
$free = $user->quota - $used;
|
$free = $user->quota - $used;
|
||||||
}
|
}
|
||||||
|
|
43
www/_inc.php
43
www/_inc.php
|
@ -35,47 +35,6 @@ if (!file_exists(DB_FILE)) {
|
||||||
$db->exec('END;');
|
$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
|
function html_head(string $title): void
|
||||||
{
|
{
|
||||||
$title = htmlspecialchars($title);
|
$title = htmlspecialchars($title);
|
||||||
|
@ -97,7 +56,7 @@ function html_foot(): void
|
||||||
{
|
{
|
||||||
echo '
|
echo '
|
||||||
<footer>
|
<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>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>';
|
</html>';
|
||||||
|
|
Loading…
Reference in a new issue