diff --git a/apps/dav/lib/Capabilities.php b/apps/dav/lib/Capabilities.php
index 3bd15d095e17..efdb92871af0 100644
--- a/apps/dav/lib/Capabilities.php
+++ b/apps/dav/lib/Capabilities.php
@@ -30,6 +30,7 @@ public function getCapabilities() {
return [
'dav' => [
'chunking' => '1.0',
+ 'zsync' => '1.0',
]
];
}
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index e051e78d1f88..c8fc4cd21a86 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -51,6 +51,7 @@
use OC\Files\Mount\MoveableMount;
use Sabre\DAV\IFile;
use OCA\DAV\Upload\FutureFile;
+use OCA\DAV\Upload\FutureFileZsync;
use Sabre\DAV\IQuota;
use Sabre\HTTP\URLUtil;
@@ -147,7 +148,7 @@ public function createFile($name, $data = null) {
) {
throw new SabreForbidden();
}
- } else if (FutureFile::isFutureFile()) {
+ } else if (FutureFile::isFutureFile() or FutureFileZsync::isFutureFile()) {
// Future file (chunked upload) requires fileinfo
$info = $this->fileView->getFileInfo($this->path . '/' . $name);
} else {
diff --git a/apps/dav/lib/Files/ZsyncPlugin.php b/apps/dav/lib/Files/ZsyncPlugin.php
new file mode 100644
index 000000000000..4af4e848ae97
--- /dev/null
+++ b/apps/dav/lib/Files/ZsyncPlugin.php
@@ -0,0 +1,148 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Files;
+
+use OC\AppFramework\Http;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\PropFind;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use OC\Files\View;
+use \Exception;
+
+class ZsyncPlugin extends ServerPlugin {
+
+ // namespace
+ const ZSYNC_PROPERTYNAME = '{http://owncloud.org/ns}zsync';
+
+ /** @var OC\Files\View */
+ private $view;
+
+ public function __construct(View $view) {
+ $this->view = $view;
+ $this->view->mkdir('files_zsync');
+ }
+
+ /**
+ * Initializes the plugin and registers event handlers
+ *
+ * @param Server $server
+ * @return void
+ */
+ function initialize(Server $server) {
+ $server->on('method:GET', [$this, 'httpGet'], 90);
+ $server->on('method:DELETE', [$this, 'httpDelete'], 90);
+ $server->on('propFind', [$this, 'handleGetProperties']);
+ }
+
+ /**
+ * Intercepts GET requests on file urls ending with ?zsync.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ */
+ function httpGet(RequestInterface $request, ResponseInterface $response) {
+
+ $queryParams = $request->getQueryParameters();
+ if (!array_key_exists('zsync', $queryParams)) {
+ return true;
+ }
+
+ $path = ltrim($request->getPath(), '/');
+ /* remove files/$user */
+ $path = implode('/', array_slice(explode('/', $path), 2));
+ /* If basefile not found this is an error */
+ if (!$this->view->file_exists('files/'.$path)) {
+ $response->setStatus(Http::STATUS_NOT_FOUND);
+ return false;
+ }
+
+ $info = $this->view->getFileInfo('files/'.$path);
+ $zsyncMetadataFile = 'files_zsync/'.$info->getId();
+ if ($this->view->file_exists($zsyncMetadataFile)) {
+ $content = $this->view->file_get_contents($zsyncMetadataFile);
+ $response->setStatus(Http::STATUS_OK);
+ $response->setBody($content);
+ } else {
+ $response->setStatus(Http::STATUS_NOT_FOUND);
+ }
+
+ return false;
+ }
+
+ /**
+ * Intercepts DELETE requests on file urls ending with ?zsync.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ */
+ function httpDelete(RequestInterface $request, ResponseInterface $response) {
+
+ $queryParams = $request->getQueryParameters();
+ if (!array_key_exists('zsync', $queryParams)) {
+ return true;
+ }
+
+ $path = ltrim($request->getPath(), '/');
+ /* remove files/$user */
+ $path = implode('/', array_slice(explode('/', $path), 2));
+ /* If basefile not found this is an error */
+ if (!$this->view->file_exists('files/'.$path)) {
+ $response->setStatus(Http::STATUS_NOT_FOUND);
+ return false;
+ }
+
+ $info = $this->view->getFileInfo('files/'.$path);
+ $zsyncMetadataFile = 'files_zsync/'.$info->getId();
+ if ($this->view->file_exists($zsyncMetadataFile)) {
+ $this->view->unlink($zsyncMetadataFile);
+ $response->setStatus(Http::STATUS_OK);
+ } else {
+ $response->setStatus(Http::STATUS_NOT_FOUND);
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds zsync property if metadata exists
+ *
+ * @param PropFind $propFind
+ * @param \Sabre\DAV\INode $node
+ * @return void
+ */
+ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
+ if ($node instanceof \OCA\DAV\Connector\Sabre\File) {
+ if (!$this->view->is_file('files/'.$node->getPath()))
+ return;
+ $info = $this->view->getFileInfo('files/'.$node->getPath());
+ $zsyncMetadataFile = 'files_zsync/'.$info->getId();
+ if ($this->view->file_exists($zsyncMetadataFile)) {
+ $propFind->handle(self::ZSYNC_PROPERTYNAME, function() use ($node) {
+ return 'true';
+ });
+ }
+ }
+ }
+}
diff --git a/apps/dav/lib/HookManager.php b/apps/dav/lib/HookManager.php
index d48d8cccb8de..fe6824902ae5 100644
--- a/apps/dav/lib/HookManager.php
+++ b/apps/dav/lib/HookManager.php
@@ -27,8 +27,10 @@
use OCA\DAV\CardDAV\SyncService;
use OCP\IL10N;
use OCP\IUser;
+use OCP\User;
use OCP\IUserManager;
use OCP\Util;
+use OC\Files\View;
class HookManager {
@@ -85,6 +87,23 @@ public function setup() {
'changeUser',
$this,
'changeUser');
+
+ Util::connectHook('OC_Filesystem',
+ 'post_copy',
+ $this,
+ 'copyZsyncMetadata');
+ Util::connectHook('OC_Filesystem',
+ 'write',
+ $this,
+ 'deleteZsyncMetadata');
+ Util::connectHook('OC_Filesystem',
+ 'delete',
+ $this,
+ 'deleteZsyncMetadata');
+ Util::connectHook('\OCP\Versions',
+ 'rollback',
+ $this,
+ 'deleteZsyncMetadata');
}
public function postCreateUser($params) {
@@ -144,4 +163,66 @@ public function firstLogin(IUser $user = null) {
}
}
}
+
+ public function deleteZsyncMetadata($params) {
+ $view = new View('/'.User::getUser());
+ $path = $params[\OC\Files\Filesystem::signal_param_path];
+ $path = 'files/' . ltrim($path, '/');
+
+ /* if a file then just delete zsync metadata for file */
+ if ($view->is_file($path)) {
+ $info = $view->getFileInfo($path);
+ if ($view->file_exists('files_zsync/'.$info->getId()))
+ $view->unlink('files_zsync/'.$info->getId());
+ } else if ($view->is_dir($path)) {
+ /* if a folder then iteratively delete all zsync metadata for all files in folder, including subdirs */
+ $array[] = $path;
+ while (count($array)) {
+ $current = array_pop($array);
+ $handle = $view->opendir($current);
+ while (($entry = readdir($handle)) !== false) {
+ if($entry[0]!='.' and $view->is_dir($current.'/'.$entry)) {
+ $array[] = $current.'/'.$entry;
+ } else if ($view->is_file($current.'/'.$entry)) {
+ $info = $view->getFileInfo($current.'/'.$entry);
+ if ($view->file_exists('files_zsync/'.$info->getId()))
+ $view->unlink('files_zsync/'.$info->getId());
+ }
+ }
+ }
+ }
+ }
+
+ public function copyZsyncMetadata($params) {
+ $view = new View('/'.User::getUser());
+ $from = $params[\OC\Files\Filesystem::signal_param_oldpath];
+ $from = 'files/' . ltrim($from, '/');
+ $to = $params[\OC\Files\Filesystem::signal_param_newpath];
+ $to = 'files/' . ltrim($to, '/');
+
+ /* if a file then just copy zsync metadata for file */
+ if ($view->is_file($from)) {
+ $info_from = $view->getFileInfo($from);
+ $info_to = $view->getFileInfo($to);
+ if ($view->file_exists('files_zsync/'.$info_from->getId()))
+ $view->copy('files_zsync/'.$info_from->getId(), 'files_zsync/'.$info_to->getId());
+ } else if ($view->is_dir($from)) {
+ /* if a folder then iteratively copy all zsync metadata for all files in folder, including subdirs */
+ $array[] = [$from, $to];
+ while (count($array)) {
+ list($from_current, $to_current) = array_pop($array);
+ $handle = $view->opendir($from_current);
+ while (($entry = readdir($handle)) !== false) {
+ if($entry[0]!='.' and $view->is_dir($from_current.'/'.$entry)) {
+ $array[] = [$from_current.'/'.$entry, $to_current.'/'.$entry];
+ } else if ($view->is_file($from_current.'/'.$entry)) {
+ $info_from = $view->getFileInfo($from_current.'/'.$entry);
+ $info_to = $view->getFileInfo($to_current.'/'.$entry);
+ if ($view->file_exists('files_zsync/'.$info_from->getId()))
+ $view->copy('files_zsync/'.$info_from->getId(), 'files_zsync/'.$info_to->getId());
+ }
+ }
+ }
+ }
+ }
}
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 7fd765b5c0b8..c193faf63049 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -42,10 +42,12 @@
use OCA\DAV\DAV\PublicAuth;
use OCA\DAV\Connector\Sabre\QuotaPlugin;
use OCA\DAV\Files\BrowserErrorPagePlugin;
+use OCA\DAV\Files\ZsyncPlugin;
use OCA\DAV\DAV\FileCustomPropertiesBackend;
use OCA\DAV\DAV\MiscCustomPropertiesBackend;
use OCA\DAV\SystemTag\SystemTagPlugin;
use OCA\DAV\Upload\ChunkingPlugin;
+use OCA\DAV\Upload\ChunkingPluginZsync;
use OCP\IRequest;
use OCP\SabrePluginEvent;
use Sabre\CardDAV\VCFExportPlugin;
@@ -54,6 +56,7 @@
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Connector\Sabre\ValidateRequestPlugin;
+use OC\Files\View;
class Server {
@@ -168,6 +171,15 @@ public function __construct(IRequest $request, $baseUri) {
$userSession = \OC::$server->getUserSession();
$user = $userSession->getUser();
if (!is_null($user)) {
+ $view = new View('/'.$user->getUID());
+ $this->server->addPlugin(
+ new ChunkingPluginZsync($view)
+ );
+
+ $this->server->addPlugin(
+ new ZsyncPlugin($view)
+ );
+
$view = \OC\Files\Filesystem::getView();
$this->server->addPlugin(
new FilesPlugin(
diff --git a/apps/dav/lib/Upload/AssemblyStream.php b/apps/dav/lib/Upload/AssemblyStream.php
index cb757106ae99..e67417eb9cb7 100644
--- a/apps/dav/lib/Upload/AssemblyStream.php
+++ b/apps/dav/lib/Upload/AssemblyStream.php
@@ -37,22 +37,22 @@
class AssemblyStream implements \Icewind\Streams\File {
/** @var resource */
- private $context;
+ protected $context;
/** @var IFile[] */
- private $nodes;
+ protected $nodes;
/** @var int */
- private $pos = 0;
+ protected $pos = 0;
/** @var array */
- private $sortedNodes;
+ protected $sortedNodes;
/** @var int */
- private $size;
+ protected $size;
/** @var resource */
- private $currentStream = null;
+ protected $currentStream = null;
/**
* @param string $path
@@ -78,6 +78,9 @@ public function stream_open($path, $mode, $options, &$opened_path) {
foreach($this->nodes as $node) {
$size = $node->getSize();
$name = $node->getName();
+ // ignore .zsync metadata file
+ if (!strcmp($name,".zsync"))
+ continue;
$this->sortedNodes[$name] = ['node' => $node, 'start' => $start, 'end' => $start + $size];
$start += $size;
$this->size = $start;
@@ -233,7 +236,7 @@ protected function loadContext($name) {
*
* @throws \BadMethodCallException
*/
- public static function wrap(array $nodes) {
+ public static function wrap(array $nodes, IFile $backingFile = null, $length = null) {
$context = stream_context_create([
'assembly' => [
'nodes' => $nodes]
@@ -253,7 +256,7 @@ public static function wrap(array $nodes) {
* @param $pos
* @return IFile | null
*/
- private function getNodeForPosition($pos) {
+ protected function getNodeForPosition($pos) {
foreach($this->sortedNodes as $node) {
if ($pos >= $node['start'] && $pos < $node['end']) {
return [$node['node'], $pos - $node['start']];
@@ -266,7 +269,7 @@ private function getNodeForPosition($pos) {
* @param IFile $node
* @return resource
*/
- private function getStream(IFile $node) {
+ protected function getStream(IFile $node) {
$data = $node->get();
if (is_resource($data)) {
return $data;
diff --git a/apps/dav/lib/Upload/AssemblyStreamZsync.php b/apps/dav/lib/Upload/AssemblyStreamZsync.php
new file mode 100644
index 000000000000..a933d3ec054d
--- /dev/null
+++ b/apps/dav/lib/Upload/AssemblyStreamZsync.php
@@ -0,0 +1,220 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Upload;
+
+use Sabre\DAV\IFile;
+
+/**
+ * Class AssemblyStreamZsync
+ *
+ * The assembly stream is a virtual stream that wraps multiple chunks.
+ * Reading from the stream transparently accessed the underlying chunks and
+ * give a representation as if they were already merged together.
+ *
+ * @package OCA\DAV\Upload
+ */
+class AssemblyStreamZsync extends AssemblyStream {
+
+ /** @var array */
+ private $backingFile = null;
+
+ /** @var array */
+ private $backingNode = null;
+
+ /** @var array */
+ private $currentNode = null;
+
+ /** @var int */
+ private $next = 0;
+
+ /**
+ * @param string $path
+ * @param string $mode
+ * @param int $options
+ * @param string &$opened_path
+ * @return bool
+ */
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ $this->loadContext('assembly');
+
+ // sort the nodes
+ $nodes = $this->nodes;
+ // http://stackoverflow.com/a/10985500
+ @usort($nodes, function(IFile $a, IFile $b) {
+ return strnatcmp($a->getName(), $b->getName());
+ });
+ $this->nodes = $nodes;
+
+ // build additional information
+ $this->sortedNodes = [];
+ foreach($this->nodes as $node) {
+ $size = $node->getSize();
+ $name = $node->getName();
+ // ignore .zsync metadata file
+ if (!strcmp($name,".zsync"))
+ continue;
+ if ($size == 0)
+ continue;
+ $this->sortedNodes[$name] = ['node' => $node, 'start' => (int)$name, 'end' => (int)$name + $size];
+ }
+
+ $this->backingNode = ["node" => $this->backingFile, "start" => 0, "end" => $this->backingFile->getSize()];
+ $this->currentNode = $this->backingNode;
+ return true;
+ }
+
+ /**
+ * @param int $count
+ * @return string
+ */
+ public function stream_read($count) {
+ // we're done if we've reached the end of where we need to be
+ if ($this->pos >= $this->size)
+ return;
+
+ // change the node/stream when we've reached the point we need to be at
+ if ($this->currentStream === null || $this->pos == $this->next) {
+ list($node, $posInNode) = $this->getNodeForPosition($this->pos);
+ $this->currentStream = $this->getStream($node['node']);
+ fseek($this->currentStream, $posInNode);
+ $this->currentNode = $node;
+ }
+
+ // get the next byte offset when we need to change node/stream again
+ if ($this->pos == $this->next)
+ $this->next = $this->getNextNodeStart($this->pos);
+
+ // don't read beyond the expected file size
+ if ($count + $this->pos >= $this->size)
+ $count = $this->size - $this->pos;
+
+ // don't read beyond the next marker
+ if ($count + $this->pos >= $this->next)
+ $count = $this->next - $this->pos;
+
+ // read the data
+ $data = fread($this->currentStream, $count);
+ if (isset($data[$count - 1])) {
+ // we read the full count
+ $read = $count;
+ } else {
+ // reaching end of stream, which happens less often so strlen is ok
+ $read = strlen($data);
+ }
+
+ // update position
+ $this->pos += $read;
+
+ // if we couldn't read as much as expected we are done
+ if ($read != $count) {
+ $this->pos = $this->size;
+ }
+
+ // ensure we close the last stream or else we'll cause a locking issue
+ if ($this->pos == $this->size) {
+ $this->currentStream = null;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Load the source from the stream context and return the context options
+ *
+ * @param string $name
+ * @return array
+ * @throws \Exception
+ */
+ protected function loadContext($name) {
+ $context = stream_context_get_options($this->context);
+ if (isset($context[$name])) {
+ $context = $context[$name];
+ } else {
+ throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
+ }
+ if (isset($context['nodes']) and is_array($context['nodes'])) {
+ $this->nodes = $context['nodes'];
+ } else {
+ throw new \BadMethodCallException('Invalid context, nodes not set');
+ }
+ if (isset($context['backingFile'])) {
+ $this->backingFile = $context['backingFile'];
+ } else {
+ throw new \BadMethodCallException('Invalid context, backingFile not set');
+ }
+ if (isset($context['fileLength'])) {
+ $this->size = $context['fileLength'];
+ } else {
+ throw new \BadMethodCallException('Invalid context, fileLength not set');
+ }
+
+ return $context;
+ }
+
+ /**
+ * @param IFile[] $nodes
+ * @param IFile $backingFile
+ * @param $fileLength
+ * @return resource
+ *
+ * @throws \BadMethodCallException
+ */
+ public static function wrap(array $nodes, IFile $backingFile = null, $fileLength = null) {
+ $context = stream_context_create([
+ 'assembly' => [
+ 'nodes' => $nodes,
+ 'backingFile' => $backingFile,
+ 'fileLength' => $fileLength
+ ]
+ ]);
+ stream_wrapper_register('assembly', '\OCA\DAV\Upload\AssemblyStreamZsync');
+ try {
+ $wrapped = fopen('assembly://', 'r', null, $context);
+ } catch (\BadMethodCallException $e) {
+ stream_wrapper_unregister('assembly');
+ throw $e;
+ }
+ stream_wrapper_unregister('assembly');
+ return $wrapped;
+ }
+
+ protected function getNextNodeStart($current) {
+ foreach($this->sortedNodes as $node) {
+ if ($current >= $node['start'] && $current < $node['end'])
+ return $node['end'];
+ if ($current < $node['start'])
+ return $node['start'];
+ }
+ return $this->currentNode['end'];
+ }
+
+ /**
+ * @param $pos
+ */
+ protected function getNodeForPosition($pos) {
+ foreach($this->sortedNodes as $node) {
+ if ($pos >= $node['start'] && $pos < $node['end']) {
+ return [$node, 0];
+ }
+ }
+ return [$this->backingNode, $pos];
+ }
+}
diff --git a/apps/dav/lib/Upload/ChunkingPluginZsync.php b/apps/dav/lib/Upload/ChunkingPluginZsync.php
new file mode 100644
index 000000000000..6a71468371c5
--- /dev/null
+++ b/apps/dav/lib/Upload/ChunkingPluginZsync.php
@@ -0,0 +1,170 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Upload;
+
+
+use OCA\DAV\Connector\Sabre\File;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Exception\NotFound;
+use OC\Files\View;
+
+class ChunkingPluginZsync extends ServerPlugin {
+
+ /** @var Server */
+ private $server;
+ /** @var FutureFileZsync */
+ private $sourceNode;
+ /** @var OC\Files\View */
+ private $view;
+
+ public function __construct(View $view) {
+ $this->view = $view;
+ $this->view->mkdir('files_zsync');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ function initialize(Server $server) {
+ $server->on('beforeMove', [$this, 'beforeMove']);
+ $this->server = $server;
+ }
+
+ /**
+ * @param string $sourcePath source path
+ * @param string $destination destination path
+ */
+ function beforeMove($sourcePath, $destination) {
+ $this->sourceNode = $this->server->tree->getNodeForPath($sourcePath);
+ if (!$this->sourceNode instanceof FutureFileZsync) {
+ // skip handling as the source is not a chunked FutureFileZsync
+ return;
+ }
+
+ $this->verifySize();
+ return $this->performMove($sourcePath, $destination);
+ }
+
+ /**
+ * Handles the temporary copy of the zsync metadata file
+ *
+ * Will not execute on external storage.
+ *
+ * @param string $path metadata path
+ * @param string $destination destination path
+ */
+ private function preMoveZsync($path, $destination) {
+ try {
+ $node = $this->server->tree->getNodeForPath($destination);
+ } catch (NotFound $e) {
+ $node = $this->server->tree->getNodeForPath(dirname($destination));
+ }
+
+ // Disable if external storage used.
+ if (strpos($node->getDavPermissions(), 'M') === false) {
+ $zsyncMetadataNode = $this->server->tree->getNodeForPath($path);
+ $zsyncMetadataHandle = $zsyncMetadataNode->get();
+
+ // get .zsync contents before its deletion
+ $zsyncMetadata = '';
+ while (!feof($zsyncMetadataHandle)) {
+ $zsyncMetadata .= fread($zsyncMetadataHandle, $zsyncMetadataNode->getSize());
+ }
+ fclose($zsyncMetadataHandle);
+
+ if ($this->server->tree->nodeExists($destination)) {
+ // set backingFile which is needed by AssemblyStreamZsync
+ $backingFile = $this->server->tree->getNodeForPath($destination);
+ $this->sourceNode->setBackingFile($backingFile);
+ }
+
+ $fileLength = $this->server->httpRequest->getHeader('OC-Total-File-Length');
+ $this->sourceNode->setFileLength($fileLength);
+
+ return $zsyncMetadata;
+ }
+ }
+
+ /**
+ * Handles the creation of the zsync metadata file
+ *
+ * @param string &$zsyncMetadata actual metadata
+ * @param string $destination destination path
+ */
+ private function postMoveZsync(&$zsyncMetadata, $destination) {
+ if (!$zsyncMetadata)
+ return;
+ $destination = implode('/', array_slice(explode('/', $destination), 2));
+ $info = $this->view->getFileInfo('files/'.$destination);
+ $zsyncMetadataFile = 'files_zsync/'.$info->getId();
+ $this->view->file_put_contents($zsyncMetadataFile, $zsyncMetadata);
+ }
+
+ /**
+ * Move handler for future file.
+ *
+ * This overrides the default move behavior to prevent Sabre
+ * to delete the target file before moving. Because deleting would
+ * lose the file id and metadata.
+ *
+ * @param string $path source path
+ * @param string $destination destination path
+ * @return bool|void false to stop handling, void to skip this handler
+ */
+ public function performMove($path, $destination) {
+ $response = $this->server->httpResponse;
+ $response->setHeader('Content-Length', '0');
+ $this->server->tree->nodeExists($destination) ? $response->setStatus(204) : $response->setStatus(201);
+
+ // copy the zsync metadata file contents, before it gets removed.
+ $zsyncMetadataPath = dirname($path).'/.zsync';
+ $zsyncMetadata = $this->preMoveZsync($zsyncMetadataPath, $destination);
+
+ // do a move manually, skipping Sabre's default "delete" for existing nodes
+ $this->server->tree->move($path, $destination);
+
+ // create the zsync metadata file
+ $this->postMoveZsync($zsyncMetadata, $destination);
+
+ // trigger all default events (copied from CorePlugin::move)
+ $this->server->emit('afterMove', [$path, $destination]);
+ $this->server->emit('afterUnbind', [$path]);
+ $this->server->emit('afterBind', [$destination]);
+
+ return false;
+ }
+
+ /**
+ * @throws BadRequest
+ */
+ private function verifySize() {
+ $expectedSize = $this->server->httpRequest->getHeader('OC-Total-Length');
+ if ($expectedSize === null) {
+ return;
+ }
+ $actualSize = $this->sourceNode->getSize();
+ if ((int)$expectedSize !== $actualSize) {
+ throw new BadRequest("Chunks on server do not sum up to $expectedSize but to $actualSize");
+ }
+ }
+}
diff --git a/apps/dav/lib/Upload/FutureFile.php b/apps/dav/lib/Upload/FutureFile.php
index 9ef8b98b5c2f..11574a1a79b0 100644
--- a/apps/dav/lib/Upload/FutureFile.php
+++ b/apps/dav/lib/Upload/FutureFile.php
@@ -36,9 +36,9 @@
class FutureFile implements \Sabre\DAV\IFile {
/** @var Directory */
- private $root;
+ protected $root;
/** @var string */
- private $name;
+ protected $name;
static public function getFutureFileName() {
return '.file';
diff --git a/apps/dav/lib/Upload/FutureFileZsync.php b/apps/dav/lib/Upload/FutureFileZsync.php
new file mode 100644
index 000000000000..f78fb6b4ef65
--- /dev/null
+++ b/apps/dav/lib/Upload/FutureFileZsync.php
@@ -0,0 +1,82 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Upload;
+
+use OCA\DAV\Connector\Sabre\Directory;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\IFile;
+
+/**
+ * Class FutureFileZsync
+ *
+ * The FutureFileZsync is a SabreDav IFile which connects the chunked upload directory
+ * with the AssemblyStreamZsync, who does the final assembly job
+ *
+ * @package OCA\DAV\Upload
+ */
+class FutureFileZsync extends FutureFile {
+
+ /** @var IFile */
+ private $backingFile = null;
+ /** @var string */
+ private $fileLength = 0;
+
+ static public function getFutureFileName() {
+ return '.file.zsync';
+ }
+
+ static public function isFutureFile() {
+ $davUploadsTarget = '/dav/uploads';
+
+ // Check if pathinfo starts with dav uploads target and basename is future file basename
+ if (isset($_SERVER['PATH_INFO'])
+ && pathinfo($_SERVER['PATH_INFO'], PATHINFO_BASENAME) === FutureFileZsync::getFutureFileName()
+ && (strpos($_SERVER['PATH_INFO'], $davUploadsTarget) === 0)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ function get() {
+ $nodes = $this->root->getChildren();
+ return $this->root->childExists('.zsync') && $this->backingFile && $this->fileLength ?
+ AssemblyStreamZsync::wrap($nodes, $this->backingFile, $this->fileLength) :
+ AssemblyStream::wrap($nodes);
+ }
+
+ /**
+ * @param IFile $file
+ */
+ function setBackingFile(IFile $file) {
+ $this->backingFile = $file;
+ }
+
+ /**
+ * @param string $fileLength
+ */
+ function setFileLength($fileLength) {
+ $this->fileLength = $fileLength;
+ }
+}
diff --git a/apps/dav/lib/Upload/UploadFolder.php b/apps/dav/lib/Upload/UploadFolder.php
index 19e511f260d6..931e9a057dc2 100644
--- a/apps/dav/lib/Upload/UploadFolder.php
+++ b/apps/dav/lib/Upload/UploadFolder.php
@@ -46,12 +46,16 @@ function getChild($name) {
if ($name === FutureFile::getFutureFileName()) {
return new FutureFile($this->node, FutureFile::getFutureFileName());
}
+ if ($name === FutureFileZsync::getFutureFileName()) {
+ return new FutureFileZsync($this->node, FutureFileZsync::getFutureFileName());
+ }
return $this->node->getChild($name);
}
function getChildren() {
$children = $this->node->getChildren();
$children[] = new FutureFile($this->node, FutureFile::getFutureFileName());
+ $children[] = new FutureFileZsync($this->node, FutureFileZsync::getFutureFileName());
return $children;
}
@@ -59,6 +63,9 @@ function childExists($name) {
if ($name === FutureFile::getFutureFileName()) {
return true;
}
+ if ($name === FutureFileZsync::getFutureFileName()) {
+ return true;
+ }
return $this->node->childExists($name);
}
diff --git a/apps/dav/tests/unit/DAV/HookManagerZsyncTest.php b/apps/dav/tests/unit/DAV/HookManagerZsyncTest.php
new file mode 100644
index 000000000000..ab9757af1829
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/HookManagerZsyncTest.php
@@ -0,0 +1,316 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\DAV\Tests\unit\DAV;
+
+use Test\TestCase;
+use OCP\Files\NotFoundException;
+use OC\L10N\L10N;
+use OCA\DAV\CalDAV\BirthdayService;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\CardDAV\SyncService;
+use OCA\DAV\HookManager;
+use OCP\IUser;
+use OCP\IUserManager;
+
+/**
+ * Class Test_Files_zsynchooks
+ * this class provide basic files zsync hooks test
+ *
+ * @group DB
+ */
+class HookManagerZsyncTest extends TestCase {
+
+ const TEST_ZSYNC_HOOKS_USER = 'test-files-user';
+ const USERS_ZSYNC_ROOT = '/test-files-user/files_zsync';
+
+ /**
+ * @var \OC\Files\View
+ */
+ private $rootView;
+
+ public static function setUpBeforeClass() {
+ parent::setUpBeforeClass();
+
+ $application = new \OCA\Files_Sharing\AppInfo\Application();
+ $application->registerMountProviders();
+
+ // create test user
+ self::loginHelper(self::TEST_ZSYNC_HOOKS_USER, true);
+ }
+
+ public static function tearDownAfterClass() {
+ // cleanup test user
+ $user = \OC::$server->getUserManager()->get(self::TEST_ZSYNC_HOOKS_USER);
+ if ($user !== null) { $user->delete(); }
+
+ parent::tearDownAfterClass();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $config = \OC::$server->getConfig();
+ $mockConfig = $this->createMock('\OCP\IConfig');
+ $mockConfig->expects($this->any())
+ ->method('getSystemValue')
+ ->will($this->returnCallback(function ($key, $default) use ($config) {
+ if ($key === 'filesystem_check_changes') {
+ return \OC\Files\Cache\Watcher::CHECK_ONCE;
+ } else {
+ return $config->getSystemValue($key, $default);
+ }
+ }));
+ $this->overwriteService('AllConfig', $mockConfig);
+
+ // clear hooks
+ \OC_Hook::clear();
+ \OC::registerShareHooks();
+
+ $l10n = $this->getMockBuilder('OC\L10N\L10N')
+ ->disableOriginalConstructor()->getMock();
+
+ $user = $this->getMockBuilder(IUser::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $userManager */
+ $userManager = $this->getMockBuilder(IUserManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ /** @var SyncService | \PHPUnit_Framework_MockObject_MockObject $syncService */
+ $syncService = $this->getMockBuilder(SyncService::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject $cal */
+ $cal = $this->getMockBuilder(CalDavBackend::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $card */
+ $card = $this->getMockBuilder(CardDavBackend::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $hm = new HookManager($userManager, $syncService, $cal, $card, $l10n);
+ $hm->setup();
+
+ self::loginHelper(self::TEST_ZSYNC_HOOKS_USER);
+ $this->rootView = new \OC\Files\View();
+ if (!$this->rootView->file_exists(self::USERS_ZSYNC_ROOT)) {
+ $this->rootView->mkdir(self::USERS_ZSYNC_ROOT);
+ }
+ }
+
+ protected function tearDown() {
+ $this->restoreService('AllConfig');
+
+ if ($this->rootView) {
+ $this->rootView->deleteAll(self::TEST_ZSYNC_HOOKS_USER . '/files/');
+ $this->rootView->deleteAll(self::TEST_ZSYNC_HOOKS_USER . '/files_zsync/');
+ }
+
+ \OC_Hook::clear();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @param string $user
+ * @param bool $create
+ */
+ public static function loginHelper($user, $create = false) {
+
+ if ($create) {
+ \OC::$server->getUserManager()->createUser($user, $user);
+ }
+
+ $storage = new \ReflectionClass('\OCA\Files_Sharing\SharedStorage');
+ $isInitialized = $storage->getProperty('initialized');
+ $isInitialized->setAccessible(true);
+ $isInitialized->setValue($storage, false);
+ $isInitialized->setAccessible(false);
+
+ \OC_Util::tearDownFS();
+ \OC_User::setUserId('');
+ \OC\Files\Filesystem::tearDown();
+ \OC_User::setUserId($user);
+ \OC_Util::setupFS($user);
+ \OC::$server->getUserFolder($user);
+ }
+
+ public function testRenameFile() {
+ \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+
+ \OC\Files\Filesystem::rename("test.txt", "test.txt.renamed");
+
+ $this->assertTrue($this->rootView->file_exists($z1));
+ }
+
+ public function testDeleteFile() {
+ \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+
+ \OC\Files\Filesystem::unlink("test.txt");
+
+ $this->assertFalse($this->rootView->file_exists($z1));
+ }
+
+ public function testCopyFile() {
+ \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+
+ \OC\Files\Filesystem::copy("test.txt", "test.txt.copied");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.txt.copied');
+ $z1Copied = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->assertTrue($this->rootView->file_exists($z1));
+ $this->assertTrue($this->rootView->file_exists($z1Copied));
+ }
+
+ public function testRenameFolder() {
+ \OC\Files\Filesystem::mkdir("test/sub/sub");
+ \OC\Files\Filesystem::file_put_contents("test/test.txt", "test file1");
+ \OC\Files\Filesystem::file_put_contents("test/sub/test.txt", "test file2");
+ \OC\Files\Filesystem::file_put_contents("test/sub/sub/test.txt", "test file3");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+ $this->rootView->file_put_contents($z2, 'zsync');
+ $this->rootView->file_put_contents($z3, 'zsync');
+
+ \OC\Files\Filesystem::rename("test", "test.renamed");
+
+ $this->assertTrue($this->rootView->file_exists($z1));
+ $this->assertTrue($this->rootView->file_exists($z2));
+ $this->assertTrue($this->rootView->file_exists($z3));
+ }
+
+ public function testDeleteFolder() {
+ \OC\Files\Filesystem::mkdir("test/sub/sub");
+ \OC\Files\Filesystem::file_put_contents("test/test.txt", "test file1");
+ \OC\Files\Filesystem::file_put_contents("test/sub/test.txt", "test file2");
+ \OC\Files\Filesystem::file_put_contents("test/sub/sub/test.txt", "test file3");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+ $this->rootView->file_put_contents($z2, 'zsync');
+ $this->rootView->file_put_contents($z3, 'zsync');
+
+ \OC\Files\Filesystem::rmdir("test");
+
+ $this->assertFalse($this->rootView->file_exists($z1));
+ $this->assertFalse($this->rootView->file_exists($z2));
+ $this->assertFalse($this->rootView->file_exists($z3));
+ }
+
+ public function testCopyFolder() {
+ \OC\Files\Filesystem::mkdir("test/sub/sub");
+ \OC\Files\Filesystem::file_put_contents("test/test.txt", "test file1");
+ \OC\Files\Filesystem::file_put_contents("test/sub/test.txt", "test file2");
+ \OC\Files\Filesystem::file_put_contents("test/sub/sub/test.txt", "test file3");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+ $this->rootView->file_put_contents($z2, 'zsync');
+ $this->rootView->file_put_contents($z3, 'zsync');
+
+ \OC\Files\Filesystem::copy("test", "test.copied");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->assertTrue($this->rootView->file_exists($z1));
+ $this->assertTrue($this->rootView->file_exists($z2));
+ $this->assertTrue($this->rootView->file_exists($z3));
+ }
+
+ public function testCopyFileNotExist() {
+ \OCP\Util::writeLog('testCopyFileNotExist', '', \OCP\Util::ERROR);
+
+ \OC\Files\Filesystem::mkdir("test/sub/sub");
+ \OC\Files\Filesystem::file_put_contents("test/test.txt", "test file1");
+ \OC\Files\Filesystem::file_put_contents("test/sub/test.txt", "test file2");
+ \OC\Files\Filesystem::file_put_contents("test/sub/sub/test.txt", "test file3");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->rootView->file_put_contents($z1, 'zsync');
+ $this->rootView->file_put_contents($z3, 'zsync');
+
+ \OC\Files\Filesystem::copy("test", "test.copied");
+
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/test.txt');
+ $z1 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/sub/test.txt');
+ $z2 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+ $info = $this->rootView->getFileInfo(self::TEST_ZSYNC_HOOKS_USER.'/files/test.copied/sub/sub/test.txt');
+ $z3 = self::USERS_ZSYNC_ROOT . '/' . $info->getId();
+
+ $this->assertTrue($this->rootView->file_exists($z1));
+ $this->assertFalse($this->rootView->file_exists($z2));
+ $this->assertTrue($this->rootView->file_exists($z3));
+ }
+}
diff --git a/apps/dav/tests/unit/DAV/ZsyncPluginTest.php b/apps/dav/tests/unit/DAV/ZsyncPluginTest.php
new file mode 100644
index 000000000000..c934e46ab9f5
--- /dev/null
+++ b/apps/dav/tests/unit/DAV/ZsyncPluginTest.php
@@ -0,0 +1,156 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\DAV\Tests\unit\DAV;
+
+use OCA\DAV\Files\ZsyncPlugin;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use Test\TestCase;
+use OCP\IRequest;
+use OCP\AppFramework\Http;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\DAV\Server;
+use OC\Files\View;
+
+class ZsyncPluginTest extends TestCase {
+ /** @var \OCP\IUserSession */
+ private $userSession;
+ /** @var ZsyncPlugin */
+ private $plugin;
+ /** @var \OC\Files\View */
+ private $view;
+ /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject */
+ private $response;
+ /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+
+ public function setUp() {
+ $this->view = $this->getMockBuilder('\OC\Files\View')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->view->expects($this->once())
+ ->method('mkdir')
+ ->with('files_zsync');
+
+ $this->request = $this->getMockBuilder('Sabre\HTTP\RequestInterface')->getMock();
+ $this->response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface')->getMock();
+ $this->server = $this->getMockBuilder('Sabre\DAV\Server')->getMock();
+ $this->plugin = new ZsyncPlugin($this->view);
+
+ $this->plugin->initialize($this->server);
+ }
+
+ /**
+ * @dataProvider providesQueryParams
+ * @param $param
+ */
+ public function testQueryParams($param) {
+ $this->request->expects($this->exactly(2))->method('getQueryParameters')->willReturn($param);
+ $this->assertTrue($this->plugin->httpGet($this->request, $this->response));
+ $this->assertTrue($this->plugin->httpDelete($this->request, $this->response));
+ }
+
+ public function providesQueryParams() {
+ return [
+ [[]],
+ [['1']],
+ [['foo' => 'bar']],
+ ];
+ }
+
+
+ public function testShowRouteWithExistsFile() {
+ $this->view->expects($this->any())->method('file_exists')->willReturn(true);
+
+ $fileInfo = $this->createMock('\OC\Files\FileInfo');
+ $fileInfo->expects($this->exactly(2))->method('getId')->willReturn('13124');
+ $this->view->expects($this->exactly(2))->method('getFileInfo')->with('files/target')->willReturn($fileInfo);
+
+ $this->view->expects($this->once())->method('file_get_contents')->willReturn('bar');
+
+ $this->request->expects($this->exactly(2))->method('getQueryParameters')->willReturn(['zsync' => true]);
+ $this->request->expects($this->exactly(2))->method('getPath')->willReturn('files/testuser1/target');
+
+ $this->response->expects($this->exactly(2))->method('setStatus')->with(Http::STATUS_OK);
+ $this->response->expects($this->once())->method('setBody')->with('bar');
+
+ $this->assertFalse($this->plugin->httpGet($this->request, $this->response));
+ $this->assertFalse($this->plugin->httpDelete($this->request, $this->response));
+ }
+
+
+ public function testShowRouteWithMissingBaseFile() {
+ $this->view->expects($this->any())->method('file_exists')->willReturn(false);
+
+ $this->request->expects($this->exactly(2))->method('getQueryParameters')->willReturn(['zsync' => true]);
+ $this->request->expects($this->exactly(2))->method('getPath')->willReturn('files/testuser1/target');
+
+ $this->response->expects($this->exactly(2))->method('setStatus')->with(Http::STATUS_NOT_FOUND);
+
+ $this->assertFalse($this->plugin->httpGet($this->request, $this->response));
+ $this->assertFalse($this->plugin->httpDelete($this->request, $this->response));
+ }
+
+ public function testShowRouteWithMissingZsyncFile() {
+ $this->view->expects($this->exactly(4))->method('file_exists')
+ ->withConsecutive(['files/target'], ['files_zsync/13124'], ['files/target'], ['files_zsync/13124'])
+ ->willReturnOnConsecutiveCalls(true, false, true, false);
+
+ $fileInfo = $this->createMock('\OC\Files\FileInfo');
+ $fileInfo->expects($this->exactly(2))->method('getId')->willReturn('13124');
+ $this->view->expects($this->exactly(2))->method('getFileInfo')->with('files/target')->willReturn($fileInfo);
+
+ $this->request->expects($this->exactly(2))->method('getQueryParameters')->willReturn(['zsync' => true]);
+ $this->request->expects($this->exactly(2))->method('getPath')->willReturn('files/testuser1/target');
+
+ $this->response->expects($this->exactly(2))->method('setStatus')->with(Http::STATUS_NOT_FOUND);
+
+ $this->assertFalse($this->plugin->httpGet($this->request, $this->response));
+ $this->assertFalse($this->plugin->httpDelete($this->request, $this->response));
+ }
+
+ public function testGetPropertyWithExistsFile() {
+ $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind')->disableOriginalConstructor()->getMock();
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')->disableOriginalConstructor()->getMock();
+ $node->expects($this->any())->method('getPath')->willReturn('target');
+
+ $this->view->expects($this->once())->method('is_file')->with('files/target')->willReturn(true);
+ $fileInfo = $this->createMock('\OC\Files\FileInfo');
+ $fileInfo->expects($this->once())->method('getId')->willReturn('13124');
+ $this->view->expects($this->once())->method('getFileInfo')->with('files/target')->willReturn($fileInfo);
+ $this->view->expects($this->once())->method('file_exists')->with('files_zsync/13124')->willReturn(true);
+
+ $this->assertNull($this->plugin->handleGetProperties($propFind, $node));
+ }
+
+ public function testGetPropertyWithNotExistsFile() {
+ $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind')->disableOriginalConstructor()->getMock();
+ $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File')->disableOriginalConstructor()->getMock();
+ $node->expects($this->any())->method('getPath')->willReturn('target');
+
+ $this->view->expects($this->once())->method('is_file')->with('files/target')->willReturn(false);
+
+ $this->assertNull($this->plugin->handleGetProperties($propFind, $node));
+ }
+
+}
diff --git a/apps/dav/tests/unit/Upload/AssemblyStreamZsyncTest.php b/apps/dav/tests/unit/Upload/AssemblyStreamZsyncTest.php
new file mode 100644
index 000000000000..8e9ef2eb53ea
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/AssemblyStreamZsyncTest.php
@@ -0,0 +1,161 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\DAV\Tests\unit\Upload;
+
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\File;
+
+class AssemblyStreamZsyncTest extends \Test\TestCase {
+
+ /**
+ * @dataProvider providesNodes()
+ */
+ public function testGetContents($expected, $nodes, $backingFile, $length) {
+ $stream = \OCA\DAV\Upload\AssemblyStreamZsync::wrap($nodes, $backingFile, $length);
+ $content = stream_get_contents($stream);
+
+ $this->assertEquals($expected, $content);
+ }
+
+ /**
+ * @dataProvider providesNodes()
+ */
+ public function testGetContentsFread($expected, $nodes, $backingFile, $length) {
+ $stream = \OCA\DAV\Upload\AssemblyStreamZsync::wrap($nodes, $backingFile, $length);
+
+ $content = '';
+ while (!feof($stream)) {
+ $content .= fread($stream, 3);
+ }
+
+ $this->assertEquals($expected, $content);
+ }
+
+ function providesNodes() {
+ $data512k = $this->makeData(512*1024);
+ $data16k = $this->makeData(16*1024);
+ $data8k = $this->makeData(8*1024);
+ $data4k = $this->makeData(4*1024);
+ $dataLess8k = $this->makeData((8*1024)-1);
+
+ $tonofnodes = [];
+ $tonofdata = "";
+ $start = 0;
+ for ($i = 0; $i < 101; $i++) {
+ $thisdata = rand(0,100); // variable length and content
+ $tonofdata .= $thisdata;
+ array_push($tonofnodes, $this->buildNode($start,$thisdata));
+ $start += strlen($thisdata);
+ }
+ array_push($tonofnodes, $this->buildNode('.zsync','zsync metadata'));
+
+ $file4k = $this->buildNode('file4k', $data4k);
+ $file8k = $this->buildNode('file8k', $data8k);
+ $file512k = $this->buildNode('file512k', $data512k);
+
+ return[
+ 'one node zero bytes 4k backing 4k length' => [
+ $data4k, [
+ $this->buildNode('0', '')
+ ], $file4k, 4096],
+ 'one node zero bytes 4k backing 0 length' => [
+ '', [
+ $this->buildNode('0', '')
+ ], $file4k, 0],
+ 'one node with one byte offset' => [
+ $data4k[0].'123456789', [
+ $this->buildNode('1', '1234567890')
+ ], $file4k, 10],
+ 'two nodes multiple splices' => [
+ substr($data8k, 0, 1024).
+ $data4k.
+ substr($data8k, 5120, 214).
+ substr($data4k, -1521).
+ substr($data8k, 6855),
+ [
+ $this->buildNode('1024', $data4k),
+ $this->buildNode('5334', substr($data4k, -1521))
+ ], $file8k, 8*1024],
+ 'two nodes with smaller length' => [
+ substr($data512k, 0, 4).
+ $data8k.
+ substr($data512k, 8196, 7164),
+ [
+ $this->buildNode('16352', $dataLess8k),
+ $this->buildNode('4', $data8k)
+ ], $file512k, 15*1024],
+ 'two nodes with large gaps' => [
+ substr($data512k, 0, 4).
+ $data8k.
+ substr($data512k, (8*1024)+4, (128*1024)-((8*1024)+4)).
+ $data16k.
+ substr($data512k, (8*1024)+4 + (128*1024)-((8*1024)+4) + (16*1024),
+ (512*1024)-((8*1024)+4 + (128*1024)-((8*1024)+4) + (16*1024))),
+ [
+ $this->buildNode(128*1024, $data16k),
+ $this->buildNode('4', $data8k)
+ ], $file512k, 512*1024],
+ 'a ton of nodes' => [
+ $tonofdata, $tonofnodes, $this->buildNode('empty', ''), strlen($tonofdata)
+ ],
+ 'a backing file that is smaller than expected, creating a hole (30k-31k)' => [
+ substr($data512k, 0, 12*1024).
+ $data16k.
+ substr($data512k, (16+12)*1024, 2*1024),
+ [
+ $this->buildNode(12*1024, $data16k),
+ $this->buildNode(31*1024, $data16k)
+ ], $this->buildNode('file30k', substr($data512k, 0, 30*1024)), 32*1024]
+ ];
+ }
+
+ function makeData($count) {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $charactersLength = strlen($characters);
+ $data = '';
+ for ($i = 0; $i < $count; $i++) {
+ $data .= $characters[rand(0, $charactersLength - 1)];
+ }
+ return $data;
+ }
+
+ private function buildNode($name, $data) {
+ $node = $this->getMockBuilder('\Sabre\DAV\File')
+ ->setMethods(['getName', 'get', 'getSize'])
+ ->getMockForAbstractClass();
+
+ $node->expects($this->any())
+ ->method('getName')
+ ->willReturn($name);
+
+ $node->expects($this->any())
+ ->method('get')
+ ->willReturn($data);
+
+ $node->expects($this->any())
+ ->method('getSize')
+ ->willReturn(strlen($data));
+
+ return $node;
+ }
+}
+
diff --git a/apps/dav/tests/unit/Upload/ChunkingPluginZsyncTest.php b/apps/dav/tests/unit/Upload/ChunkingPluginZsyncTest.php
new file mode 100644
index 000000000000..bef13b3fbaba
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/ChunkingPluginZsyncTest.php
@@ -0,0 +1,291 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+
+use OCA\DAV\Upload\ChunkingPluginZsync;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Test\TestCase;
+use OCA\DAV\Upload\FutureFileZsync;
+use OCA\DAV\Connector\Sabre\Directory;
+use OC\Files\View;
+use Sabre\DAV\Exception\NotFound;
+
+class ChunkingPluginZsyncTest extends TestCase {
+ const TEST_CHUNKING_USER1 = "test-chunking-user1";
+
+ /**
+ * @var \Sabre\DAV\Server | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $server;
+
+ /**
+ * @var \Sabre\DAV\Tree | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $tree;
+
+ /**
+ * @var OC\Files\View | \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $view;
+
+ /**
+ * @var ChunkingPluginZsync
+ */
+ private $plugin;
+ /** @var RequestInterface | \PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var ResponseInterface | \PHPUnit_Framework_MockObject_MockObject */
+ private $response;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->server = $this->getMockBuilder('\Sabre\DAV\Server')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->view = $this->getMockBuilder('\OC\Files\View')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->view->expects($this->once())
+ ->method('mkdir')
+ ->with('files_zsync');
+
+ $this->server->tree = $this->tree;
+ $this->plugin = new ChunkingPluginZsync($this->view);
+ $this->request = $this->createMock(RequestInterface::class);
+ $this->response = $this->createMock(ResponseInterface::class);
+ $this->server->httpRequest = $this->request;
+ $this->server->httpResponse = $this->response;
+
+ $this->plugin->initialize($this->server);
+ }
+
+ public function testBeforeMoveFutureFileSkip() {
+ $node = $this->createMock(Directory::class);
+
+ $this->tree->expects($this->any())
+ ->method('getNodeForPath')
+ ->with('source')
+ ->willReturn($node);
+ $this->response->expects($this->never())
+ ->method('setStatus');
+
+ $this->assertNull($this->plugin->beforeMove('source', 'target'));
+ }
+
+ public function testBeforeMoveFutureFileSkipNonExistingBackingFile() {
+ $sourceNode = $this->createMock(FutureFileZsync::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(4);
+
+ $node = $this->createMock(\OCA\DAV\Connector\Sabre\Node::class);
+
+ $target = 'files/'.self::TEST_CHUNKING_USER1.'/target';
+
+ $node->expects($this->once())
+ ->method('getDavPermissions')
+ ->willReturn('');
+
+ $targetNode = $this->createMock(\Sabre\DAV\IFile::class);
+
+ $stream = fopen('php://memory', 'w+');
+ fwrite($stream, 'bar');
+ rewind($stream);
+
+ $targetNode->expects($this->once())
+ ->method('get')
+ ->willReturn($stream);
+ $targetNode->expects($this->any())
+ ->method('getSize')
+ ->willReturn(3);
+
+ $this->tree->expects($this->exactly(4))
+ ->method('getNodeForPath')
+ ->withConsecutive(
+ ['source/.file.zsync'],
+ [$target],
+ [dirname($target)],
+ ['source/.zsync'])
+ ->willReturnOnConsecutiveCalls(
+ $sourceNode,
+ $this->throwException(new NotFound),
+ $node,
+ $targetNode);
+
+ $this->tree->expects($this->exactly(2))
+ ->method('nodeExists')
+ ->with($target)
+ ->willReturn(false);
+
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(201);
+
+ $this->request->expects($this->exactly(2))
+ ->method('getHeader')
+ ->withConsecutive(
+ ['OC-Total-Length'],
+ ['OC-Total-File-Length'])
+ ->willReturn('4');
+
+ $fileInfo = $this->createMock('\OC\Files\FileInfo');
+ $fileInfo->expects($this->once())
+ ->method('getId')
+ ->willReturn('13124');
+
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('files/target')
+ ->willReturn($fileInfo);
+
+ $this->assertFalse($this->plugin->beforeMove('source/.file.zsync', $target));
+ }
+
+ public function testBeforeMoveFutureFileMoveItWithZsync() {
+ $sourceNode = $this->createMock(FutureFileZsync::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(4);
+
+ $node = $this->createMock(\OCA\DAV\Connector\Sabre\Node::class);
+
+ $target = 'files/'.self::TEST_CHUNKING_USER1.'/target';
+
+ $node->expects($this->once())
+ ->method('getDavPermissions')
+ ->willReturn('');
+
+ $targetNode = $this->createMock(\Sabre\DAV\IFile::class);
+
+ $stream = fopen('php://memory', 'w+');
+ fwrite($stream, 'bar');
+ rewind($stream);
+
+ $targetNode->expects($this->once())
+ ->method('get')
+ ->willReturn($stream);
+ $targetNode->expects($this->any())
+ ->method('getSize')
+ ->willReturn(3);
+
+ $this->tree->expects($this->exactly(5))
+ ->method('getNodeForPath')
+ ->withConsecutive(
+ ['source/.file.zsync'],
+ [$target],
+ [dirname($target)],
+ ['source/.zsync'],
+ [$target])
+ ->willReturnOnConsecutiveCalls(
+ $sourceNode,
+ $this->throwException(new NotFound),
+ $node,
+ $targetNode,
+ $sourceNode);
+
+ $this->tree->expects($this->exactly(2))
+ ->method('nodeExists')
+ ->with($target)
+ ->willReturn(true);
+
+ $this->response->expects($this->once())
+ ->method('setStatus')
+ ->with(204);
+
+ $this->request->expects($this->exactly(2))
+ ->method('getHeader')
+ ->withConsecutive(
+ ['OC-Total-Length'],
+ ['OC-Total-File-Length'])
+ ->willReturn('4');
+
+ $fileInfo = $this->createMock('\OC\Files\FileInfo');
+ $fileInfo->expects($this->once())
+ ->method('getId')
+ ->willReturn('13124');
+
+ $this->view->expects($this->once())
+ ->method('getFileInfo')
+ ->with('files/target')
+ ->willReturn($fileInfo);
+
+ $this->assertFalse($this->plugin->beforeMove('source/.file.zsync', $target));
+ }
+
+ /**
+ * @expectedException \Sabre\DAV\Exception\BadRequest
+ * @expectedExceptionMessage Chunks on server do not sum up to 4 but to 3
+ */
+ public function testBeforeMoveSizeIsWrong() {
+ $sourceNode = $this->createMock(FutureFileZsync::class);
+ $sourceNode->expects($this->once())
+ ->method('getSize')
+ ->willReturn(3);
+
+ $this->tree->expects($this->exactly(1))
+ ->method('getNodeForPath')
+ ->with('source')
+ ->willReturn($sourceNode);
+
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('OC-Total-Length')
+ ->willReturn('4');
+
+ $this->assertFalse($this->plugin->beforeMove('source', 'target'));
+ }
+
+ public function testBeforeMoveSizeIsNull() {
+ $sourceNode = $this->createMock(FutureFileZsync::class);
+ $node = $this->createMock(\OCA\DAV\Connector\Sabre\Node::class);
+
+ $target = 'files/'.self::TEST_CHUNKING_USER1.'/target';
+
+ $node->expects($this->once())
+ ->method('getDavPermissions')
+ ->willReturn('M');
+
+ $this->tree->expects($this->exactly(3))
+ ->method('getNodeForPath')
+ ->withConsecutive(
+ ['source/.file.zsync'],
+ [$target],
+ [dirname($target)])
+ ->willReturnOnConsecutiveCalls(
+ $sourceNode,
+ $this->throwException(new NotFound),
+ $node);
+
+ $this->request->expects($this->once())
+ ->method('getHeader')
+ ->with('OC-Total-Length')
+ ->willReturn(null);
+
+ $this->assertFalse($this->plugin->beforeMove('source/.file.zsync', $target));
+ }
+
+}
diff --git a/apps/dav/tests/unit/Upload/FutureFileZsyncTest.php b/apps/dav/tests/unit/Upload/FutureFileZsyncTest.php
new file mode 100644
index 000000000000..86c87bb139bc
--- /dev/null
+++ b/apps/dav/tests/unit/Upload/FutureFileZsyncTest.php
@@ -0,0 +1,122 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\DAV\Tests\unit\Upload;
+
+class FutureFileTestZsync extends \Test\TestCase {
+
+ public function testGetContentType() {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('application/octet-stream', $f->getContentType());
+ }
+
+ public function testGetETag() {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('1234567890', $f->getETag());
+ }
+
+ public function testGetName() {
+ $f = $this->mockFutureFile();
+ $this->assertEquals('foo.txt', $f->getName());
+ }
+
+ public function testGetLastModified() {
+ $f = $this->mockFutureFile();
+ $this->assertEquals(12121212, $f->getLastModified());
+ }
+
+ public function testGetSize() {
+ $f = $this->mockFutureFile();
+ $this->assertEquals(0, $f->getSize());
+ }
+
+ public function testGet() {
+ $f = $this->mockFutureFile();
+ $stream = $f->get();
+ $this->assertTrue(is_resource($stream));
+ }
+
+ public function testGetZsync() {
+ $file = $this->createMock('Sabre\DAV\IFile');
+ $f = $this->mockFutureFile();
+ $f->setBackingFile($file);
+ $f->setFileLength(1231);
+ $stream = $f->get();
+ $this->assertTrue(is_resource($stream));
+ }
+
+ public function testDelete() {
+ $d = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->setMethods(['delete'])
+ ->getMock();
+
+ $d->expects($this->once())
+ ->method('delete');
+
+ $f = new \OCA\DAV\Upload\FutureFileZsync($d, 'foo.txt');
+ $f->delete();
+ }
+
+ /**
+ * @expectedException Sabre\DAV\Exception\Forbidden
+ */
+ public function testPut() {
+ $f = $this->mockFutureFile();
+ $f->put('');
+ }
+
+ /**
+ * @expectedException Sabre\DAV\Exception\Forbidden
+ */
+ public function testSetName() {
+ $f = $this->mockFutureFile();
+ $f->setName('');
+ }
+
+ /**
+ * @return \OCA\DAV\Upload\FutureFile
+ */
+ private function mockFutureFile() {
+ $d = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Directory')
+ ->disableOriginalConstructor()
+ ->setMethods(['getETag', 'getLastModified', 'getChildren', 'childExists'])
+ ->getMock();
+
+ $d->expects($this->any())
+ ->method('getETag')
+ ->willReturn('1234567890');
+
+ $d->expects($this->any())
+ ->method('getLastModified')
+ ->willReturn(12121212);
+
+ $d->expects($this->any())
+ ->method('getChildren')
+ ->willReturn([]);
+
+ $d->expects($this->any())
+ ->method('childExists')
+ ->willReturn(true);
+
+ return new \OCA\DAV\Upload\FutureFileZsync($d, 'foo.txt');
+ }
+}
+