Implement chunk upload
This commit is contained in:
parent
b93f8fa553
commit
d38e45b015
|
@ -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/)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
43
www/_inc.php
43
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 '
|
||||
<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>';
|
||||
|
|
Loading…
Reference in a new issue