summaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Connector/Sabre
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2023-02-21 07:36:43 +0100
committerGitHub <noreply@github.com>2023-02-21 07:36:43 +0100
commit98ed72b3ed7e81a75d9a323c70a5e7f5af265a23 (patch)
treea92d3ab78f2fa52969139448a8aa7c509ed97af5 /apps/dav/lib/Connector/Sabre
parent93e703bbfc7c8ef654b7b0185474397ec1bbaa6b (diff)
downloadnextcloud-server-98ed72b3ed7e81a75d9a323c70a5e7f5af265a23.tar.gz
nextcloud-server-98ed72b3ed7e81a75d9a323c70a5e7f5af265a23.zip
Revert "fix(performance): Do not set up filesystem on every call"
Diffstat (limited to 'apps/dav/lib/Connector/Sabre')
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php75
-rw-r--r--apps/dav/lib/Connector/Sabre/File.php135
-rw-r--r--apps/dav/lib/Connector/Sabre/FilesPlugin.php9
-rw-r--r--apps/dav/lib/Connector/Sabre/LockPlugin.php4
-rw-r--r--apps/dav/lib/Connector/Sabre/Node.php3
-rw-r--r--apps/dav/lib/Connector/Sabre/ObjectTree.php38
-rw-r--r--apps/dav/lib/Connector/Sabre/QuotaPlugin.php17
7 files changed, 246 insertions, 35 deletions
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index 4459daea869..f4b1ee62190 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -48,7 +48,6 @@ use OCP\Files\StorageNotAvailableException;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Psr\Log\LoggerInterface;
-use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Locked;
use Sabre\DAV\Exception\NotFound;
@@ -103,19 +102,33 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* @param string $name Name of the file
* @param resource|string $data Initial payload
* @return null|string
+ * @throws Exception\EntityTooLarge
* @throws Exception\UnsupportedMediaType
* @throws FileLocked
* @throws InvalidPath
- * @throws Exception
- * @throws BadRequest
- * @throws Exception\Forbidden
- * @throws ServiceUnavailable
+ * @throws \Sabre\DAV\Exception
+ * @throws \Sabre\DAV\Exception\BadRequest
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function createFile($name, $data = null) {
try {
- // For non-chunked upload it is enough to check if we can create a new file
- if (!$this->fileView->isCreatable($this->path)) {
- throw new Exception\Forbidden();
+ // for chunked upload also updating a existing file is a "createFile"
+ // because we create all the chunks before re-assemble them to the existing file.
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+
+ // exit if we can't create a new file and we don't updatable existing file
+ $chunkInfo = \OC_FileChunking::decodeName($name);
+ if (!$this->fileView->isCreatable($this->path) &&
+ !$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
+ ) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ } else {
+ // For non-chunked upload it is enough to check if we can create a new file
+ if (!$this->fileView->isCreatable($this->path)) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
}
$this->fileView->verifyPath($this->path, $name);
@@ -140,8 +153,8 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
$node->releaseLock(ILockingProvider::LOCK_SHARED);
return $result;
- } catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage(), false, $ex);
} catch (ForbiddenException $ex) {
@@ -157,22 +170,22 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* @param string $name
* @throws FileLocked
* @throws InvalidPath
- * @throws Exception\Forbidden
- * @throws ServiceUnavailable
+ * @throws \Sabre\DAV\Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function createDirectory($name) {
try {
if (!$this->info->isCreatable()) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
$this->fileView->verifyPath($this->path, $name);
$newPath = $this->path . '/' . $name;
if (!$this->fileView->mkdir($newPath)) {
- throw new Exception\Forbidden('Could not create directory ' . $newPath);
+ throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
}
- } catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
} catch (ForbiddenException $ex) {
@@ -190,7 +203,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* @return \Sabre\DAV\INode
* @throws InvalidPath
* @throws \Sabre\DAV\Exception\NotFound
- * @throws ServiceUnavailable
+ * @throws \Sabre\DAV\Exception\ServiceUnavailable
*/
public function getChild($name, $info = null) {
if (!$this->info->isReadable()) {
@@ -203,12 +216,12 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
try {
$this->fileView->verifyPath($this->path, $name);
$info = $this->fileView->getFileInfo($path);
- } catch (StorageNotAvailableException $e) {
- throw new ServiceUnavailable($e->getMessage());
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
} catch (InvalidPathException $ex) {
throw new InvalidPath($ex->getMessage());
} catch (ForbiddenException $e) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
}
@@ -285,17 +298,17 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
*
* @return void
* @throws FileLocked
- * @throws Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\Forbidden
*/
public function delete() {
if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
try {
if (!$this->fileView->rmdir($this->path)) {
// assume it wasn't possible to remove due to permission issue
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
} catch (ForbiddenException $ex) {
throw new Forbidden($ex->getMessage(), $ex->getRetry());
@@ -330,7 +343,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
} catch (\OCP\Files\NotFoundException $e) {
$logger->warning("error while getting quota into", ['exception' => $e]);
return [0, 0];
- } catch (StorageNotAvailableException $e) {
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
$logger->warning("error while getting quota into", ['exception' => $e]);
return [0, 0];
} catch (NotPermittedException $e) {
@@ -362,7 +375,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
* @throws ServiceUnavailable
* @throws Forbidden
* @throws FileLocked
- * @throws Exception\Forbidden
+ * @throws \Sabre\DAV\Exception\Forbidden
*/
public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
if (!$sourceNode instanceof Node) {
@@ -386,7 +399,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
// at getNodeForPath we also check the path for isForbiddenFileOrDir
// with that we have covered both source and destination
if ($sourceNode instanceof Directory && $targetNodeExists) {
- throw new Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
+ throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
}
[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
@@ -407,11 +420,11 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
if ($targetNodeExists || $sameFolder) {
// note that renaming a share mount point is always allowed
if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
} else {
if (!$this->fileView->isCreatable($destinationDir)) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
}
@@ -419,7 +432,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
// moving to a different folder, source will be gone, like a deletion
// note that moving a share mount point is always allowed
if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
}
@@ -432,7 +445,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
if (!$renameOkay) {
- throw new Exception\Forbidden('');
+ throw new \Sabre\DAV\Exception\Forbidden('');
}
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($e->getMessage());
@@ -452,7 +465,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol
$sourcePath = $sourceNode->getPath();
if (!$this->fileView->isCreatable($this->getPath())) {
- throw new Exception\Forbidden();
+ throw new \Sabre\DAV\Exception\Forbidden();
}
try {
diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php
index 075026142ec..b0f17417d21 100644
--- a/apps/dav/lib/Connector/Sabre/File.php
+++ b/apps/dav/lib/Connector/Sabre/File.php
@@ -148,6 +148,15 @@ class File extends Node implements IFile {
// verify path of the target
$this->verifyPath();
+ // chunked handling
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ try {
+ return $this->createFileChunked($data);
+ } catch (\Exception $e) {
+ $this->convertToSabreException($e);
+ }
+ }
+
/** @var Storage $partStorage */
[$partStorage] = $this->fileView->resolvePath($this->path);
$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
@@ -569,6 +578,132 @@ class File extends Node implements IFile {
}
/**
+ * @param resource $data
+ * @return null|string
+ * @throws Exception
+ * @throws BadRequest
+ * @throws NotImplemented
+ * @throws ServiceUnavailable
+ */
+ private function createFileChunked($data) {
+ [$path, $name] = \Sabre\Uri\split($this->path);
+
+ $info = \OC_FileChunking::decodeName($name);
+ if (empty($info)) {
+ throw new NotImplemented($this->l10n->t('Invalid chunk name'));
+ }
+
+ $chunk_handler = new \OC_FileChunking($info);
+ $bytesWritten = $chunk_handler->store($info['index'], $data);
+
+ //detect aborted upload
+ if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
+ if (isset($_SERVER['CONTENT_LENGTH'])) {
+ $expected = (int)$_SERVER['CONTENT_LENGTH'];
+ if ($bytesWritten !== $expected) {
+ $chunk_handler->remove($info['index']);
+ throw new BadRequest(
+ $this->l10n->t(
+ 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
+ [
+ $this->l10n->n('%n byte', '%n bytes', $expected),
+ $this->l10n->n('%n byte', '%n bytes', $bytesWritten),
+ ],
+ )
+ );
+ }
+ }
+ }
+
+ if ($chunk_handler->isComplete()) {
+ /** @var Storage $storage */
+ [$storage,] = $this->fileView->resolvePath($path);
+ $needsPartFile = $storage->needsPartFile();
+ $partFile = null;
+
+ $targetPath = $path . '/' . $info['name'];
+ /** @var \OC\Files\Storage\Storage $targetStorage */
+ [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
+
+ $exists = $this->fileView->file_exists($targetPath);
+
+ try {
+ $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
+
+ $this->emitPreHooks($exists, $targetPath);
+ $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
+ /** @var \OC\Files\Storage\Storage $targetStorage */
+ [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
+
+ if ($needsPartFile) {
+ // we first assembly the target file as a part file
+ $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
+ /** @var \OC\Files\Storage\Storage $targetStorage */
+ [$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
+
+
+ $chunk_handler->file_assemble($partStorage, $partInternalPath);
+
+ // here is the final atomic rename
+ $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
+ $fileExists = $targetStorage->file_exists($targetInternalPath);
+ if ($renameOkay === false || $fileExists === false) {
+ \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
+ // only delete if an error occurred and the target file was already created
+ if ($fileExists) {
+ // set to null to avoid double-deletion when handling exception
+ // stray part file
+ $partFile = null;
+ $targetStorage->unlink($targetInternalPath);
+ }
+ $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
+ throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
+ }
+ } else {
+ // assemble directly into the final file
+ $chunk_handler->file_assemble($targetStorage, $targetInternalPath);
+ }
+
+ // allow sync clients to send the mtime along in a header
+ if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
+ $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
+ if ($targetStorage->touch($targetInternalPath, $mtime)) {
+ $this->header('X-OC-MTime: accepted');
+ }
+ }
+
+ // since we skipped the view we need to scan and emit the hooks ourselves
+ $targetStorage->getUpdater()->update($targetInternalPath);
+
+ $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
+
+ $this->emitPostHooks($exists, $targetPath);
+
+ // FIXME: should call refreshInfo but can't because $this->path is not the of the final file
+ $info = $this->fileView->getFileInfo($targetPath);
+
+ if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
+ $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
+ $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
+ } elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
+ $this->fileView->putFileInfo($this->path, ['checksum' => '']);
+ }
+
+ $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
+
+ return $info->getEtag();
+ } catch (\Exception $e) {
+ if ($partFile !== null) {
+ $targetStorage->unlink($targetInternalPath);
+ }
+ $this->convertToSabreException($e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Convert the given exception to a SabreException instance
*
* @param \Exception $e
diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index 4a5c071848c..a6c9b8b4ebe 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -577,6 +577,15 @@ class FilesPlugin extends ServerPlugin {
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
+ // chunked upload handling
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ [$path, $name] = \Sabre\Uri\split($filePath);
+ $info = \OC_FileChunking::decodeName($name);
+ if (!empty($info)) {
+ $filePath = $path . '/' . $info['name'];
+ }
+ }
+
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
if (!$this->server->tree->nodeExists($filePath)) {
return;
diff --git a/apps/dav/lib/Connector/Sabre/LockPlugin.php b/apps/dav/lib/Connector/Sabre/LockPlugin.php
index 1f3c5211986..6305b0ec138 100644
--- a/apps/dav/lib/Connector/Sabre/LockPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/LockPlugin.php
@@ -61,7 +61,7 @@ class LockPlugin extends ServerPlugin {
public function getLock(RequestInterface $request) {
// we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
// so instead we limit ourselves to the PUT method manually
- if ($request->getMethod() !== 'PUT') {
+ if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
return;
}
try {
@@ -84,7 +84,7 @@ class LockPlugin extends ServerPlugin {
if ($this->isLocked === false) {
return;
}
- if ($request->getMethod() !== 'PUT') {
+ if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
return;
}
try {
diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php
index b4855eaf341..ee159cef1d6 100644
--- a/apps/dav/lib/Connector/Sabre/Node.php
+++ b/apps/dav/lib/Connector/Sabre/Node.php
@@ -38,7 +38,6 @@ namespace OCA\DAV\Connector\Sabre;
use OC\Files\Mount\MoveableMount;
use OC\Files\Node\File;
use OC\Files\Node\Folder;
-use OC\Files\Node\LazyFolder;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
use OCP\Files\DavUtil;
@@ -89,7 +88,7 @@ abstract class Node implements \Sabre\DAV\INode {
} else {
$this->shareManager = \OC::$server->getShareManager();
}
- if ($info instanceof Folder || $info instanceof File || $info instanceof LazyFolder) {
+ if ($info instanceof Folder || $info instanceof File) {
$this->node = $info;
} else {
$root = \OC::$server->get(IRootFolder::class);
diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php
index 8a147d7396f..c129371e376 100644
--- a/apps/dav/lib/Connector/Sabre/ObjectTree.php
+++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php
@@ -68,6 +68,35 @@ class ObjectTree extends CachingTree {
}
/**
+ * If the given path is a chunked file name, converts it
+ * to the real file name. Only applies if the OC-CHUNKED header
+ * is present.
+ *
+ * @param string $path chunk file path to convert
+ *
+ * @return string path to real file
+ */
+ private function resolveChunkFile($path) {
+ if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
+ // resolve to real file name to find the proper node
+ [$dir, $name] = \Sabre\Uri\split($path);
+ if ($dir === '/' || $dir === '.') {
+ $dir = '';
+ }
+
+ $info = \OC_FileChunking::decodeName($name);
+ // only replace path if it was really the chunked file
+ if (isset($info['transferid'])) {
+ // getNodePath is called for multiple nodes within a chunk
+ // upload call
+ $path = $dir . '/' . $info['name'];
+ $path = ltrim($path, '/');
+ }
+ }
+ return $path;
+ }
+
+ /**
* Returns the INode object for the requested path
*
* @param string $path
@@ -118,6 +147,9 @@ class ObjectTree extends CachingTree {
$info = null;
}
} else {
+ // resolve chunk file name to real name, if applicable
+ $path = $this->resolveChunkFile($path);
+
// read from cache
try {
$info = $this->fileView->getFileInfo($path);
@@ -127,6 +159,12 @@ class ObjectTree extends CachingTree {
}
} catch (StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable('Storage is temporarily not available', 0, $e);
+ } catch (StorageInvalidException $e) {
+ throw new \Sabre\DAV\Exception\NotFound('Storage ' . $path . ' is invalid');
+ } catch (LockedException $e) {
+ throw new \Sabre\DAV\Exception\Locked();
+ } catch (ForbiddenException $e) {
+ throw new \Sabre\DAV\Exception\Forbidden();
}
}
diff --git a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
index 2b233d00437..ddf4b2773e0 100644
--- a/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/QuotaPlugin.php
@@ -193,14 +193,31 @@ class QuotaPlugin extends \Sabre\DAV\ServerPlugin {
$parentPath = '';
}
$req = $this->server->httpRequest;
+ if ($req->getHeader('OC-Chunked')) {
+ $info = \OC_FileChunking::decodeName($newName);
+ $chunkHandler = $this->getFileChunking($info);
+ // subtract the already uploaded size to see whether
+ // there is still enough space for the remaining chunks
+ $length -= $chunkHandler->getCurrentSize();
+ // use target file name for free space check in case of shared files
+ $path = rtrim($parentPath, '/') . '/' . $info['name'];
+ }
$freeSpace = $this->getFreeSpace($path);
if ($freeSpace >= 0 && $length > $freeSpace) {
+ if (isset($chunkHandler)) {
+ $chunkHandler->cleanup();
+ }
throw new InsufficientStorage("Insufficient space in $path, $length required, $freeSpace available");
}
}
return true;
}
+ public function getFileChunking($info) {
+ // FIXME: need a factory for better mocking support
+ return new \OC_FileChunking($info);
+ }
+
public function getLength() {
$req = $this->server->httpRequest;
$length = $req->getHeader('X-Expected-Entity-Length');