Merge pull request #17811 from owncloud/dav-lock-wide

Wrap the entire dav PUT in a read lock
This commit is contained in:
Robin Appelman 2015-09-15 17:22:00 +02:00
commit e545c2eec5
8 changed files with 287 additions and 68 deletions

View file

@ -30,6 +30,7 @@ namespace OC\Connector\Sabre;
use OC\Connector\Sabre\Exception\InvalidPath;
use OC\Connector\Sabre\Exception\FileLocked;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Sabre\DAV\Exception\Locked;
@ -110,6 +111,7 @@ class Directory extends \OC\Connector\Sabre\Node
// using a dummy FileInfo is acceptable here since it will be refreshed after the put is complete
$info = new \OC\Files\FileInfo($path, null, null, array(), null);
$node = new \OC\Connector\Sabre\File($this->fileView, $info);
$node->acquireLock(ILockingProvider::LOCK_SHARED);
return $node->put($data);
} catch (\OCP\Files\StorageNotAvailableException $e) {
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());

View file

@ -35,6 +35,7 @@ namespace OC\Connector\Sabre;
use OC\Connector\Sabre\Exception\EntityTooLarge;
use OC\Connector\Sabre\Exception\FileLocked;
use OC\Connector\Sabre\Exception\UnsupportedMediaType;
use OC\Files\Filesystem;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\Files\EntityTooLargeException;
use OCP\Files\InvalidContentException;
@ -114,12 +115,6 @@ class File extends Node implements IFile {
$partFilePath = $this->path;
}
try {
$this->fileView->lockFile($this->path, ILockingProvider::LOCK_SHARED);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
/** @var \OC\Files\Storage\Storage $partStorage */
list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
@ -132,7 +127,7 @@ class File extends Node implements IFile {
// because we have no clue about the cause we can only throw back a 500/Internal Server Error
throw new Exception('Could not write file contents');
}
list($count, ) = \OC_Helper::streamCopy($data, $target);
list($count,) = \OC_Helper::streamCopy($data, $target);
fclose($target);
// if content length is sent by client:
@ -154,29 +149,14 @@ class File extends Node implements IFile {
try {
$view = \OC\Files\Filesystem::getView();
$run = true;
if ($view) {
$hookPath = $view->getRelativePath($this->fileView->getAbsolutePath($this->path));
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
$run = $this->emitPreHooks($exists);
} else {
$run = true;
}
try {
$this->fileView->changeLock($this->path, ILockingProvider::LOCK_EXCLUSIVE);
$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
} catch (LockedException $e) {
if ($needsPartFile) {
$partStorage->unlink($internalPartPath);
@ -202,7 +182,7 @@ class File extends Node implements IFile {
}
try {
$this->fileView->changeLock($this->path, ILockingProvider::LOCK_SHARED);
$this->changeLock(ILockingProvider::LOCK_SHARED);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
@ -211,18 +191,7 @@ class File extends Node implements IFile {
$this->fileView->getUpdater()->update($this->path);
if ($view) {
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
$this->emitPostHooks($exists);
}
// allow sync clients to send the mtime along in a header
@ -233,7 +202,6 @@ class File extends Node implements IFile {
}
}
$this->refreshInfo();
$this->fileView->unlockFile($this->path, ILockingProvider::LOCK_SHARED);
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
}
@ -241,6 +209,50 @@ class File extends Node implements IFile {
return '"' . $this->info->getEtag() . '"';
}
private function emitPreHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
$run = true;
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run,
));
return $run;
}
private function emitPostHooks($exists, $path = null) {
if (is_null($path)) {
$path = $this->path;
}
$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
} else {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
}
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
));
}
/**
* Returns the data
*
@ -354,15 +366,30 @@ class File extends Node implements IFile {
$needsPartFile = $this->needsPartFile($storage);
$partFile = null;
$targetPath = $path . '/' . $info['name'];
/** @var \OC\Files\Storage\Storage $targetStorage */
list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
$exists = $this->fileView->file_exists($targetPath);
try {
$targetPath = $path . '/' . $info['name'];
$this->emitPreHooks($exists, $targetPath);
$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
if ($needsPartFile) {
// we first assembly the target file as a part file
$partFile = $path . '/' . $info['name'] . '.ocTransferId' . $info['transferid'] . '.part';
$chunk_handler->file_assemble($partFile);
list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
$chunk_handler->file_assemble($partStorage, $partInternalPath, $this->fileView->getAbsolutePath($targetPath));
// here is the final atomic rename
$renameOkay = $this->fileView->rename($partFile, $targetPath);
$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
$fileExists = $this->fileView->file_exists($targetPath);
if ($renameOkay === false || $fileExists === false) {
\OCP\Util::writeLog('webdav', '\OC\Files\Filesystem::rename() failed', \OCP\Util::ERROR);
@ -371,28 +398,36 @@ class File extends Node implements IFile {
// set to null to avoid double-deletion when handling exception
// stray part file
$partFile = null;
$this->fileView->unlink($targetPath);
$targetStorage->unlink($targetInternalPath);
}
$this->changeLock(ILockingProvider::LOCK_SHARED);
throw new Exception('Could not rename part file assembled from chunks');
}
} else {
// assemble directly into the final file
$chunk_handler->file_assemble($targetPath);
$chunk_handler->file_assemble($targetStorage, $targetInternalPath, $this->fileView->getAbsolutePath($targetPath));
}
// allow sync clients to send the mtime along in a header
$request = \OC::$server->getRequest();
if (isset($request->server['HTTP_X_OC_MTIME'])) {
if ($this->fileView->touch($targetPath, $request->server['HTTP_X_OC_MTIME'])) {
if ($targetStorage->touch($targetInternalPath, $request->server['HTTP_X_OC_MTIME'])) {
header('X-OC-MTime: accepted');
}
}
$this->changeLock(ILockingProvider::LOCK_SHARED);
// since we skipped the view we need to scan and emit the hooks ourselves
$this->fileView->getUpdater()->update($targetPath);
$this->emitPostHooks($exists, $targetPath);
$info = $this->fileView->getFileInfo($targetPath);
return $info->getEtag();
} catch (\Exception $e) {
if ($partFile !== null) {
$this->fileView->unlink($partFile);
$targetStorage->unlink($targetInternalPath);
}
$this->convertToSabreException($e);
}

View file

@ -0,0 +1,97 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @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 <http://www.gnu.org/licenses/>
*
*/
namespace OC\Connector\Sabre;
use OC\Connector\Sabre\Exception\FileLocked;
use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use Sabre\DAV\Exception\NotFound;
use \Sabre\DAV\PropFind;
use \Sabre\DAV\PropPatch;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Tree;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class LockPlugin extends ServerPlugin {
/**
* Reference to main server object
*
* @var \Sabre\DAV\Server
*/
private $server;
/**
* @var \Sabre\DAV\Tree
*/
private $tree;
/**
* @param \Sabre\DAV\Tree $tree tree
*/
public function __construct(Tree $tree) {
$this->tree = $tree;
}
/**
* {@inheritdoc}
*/
public function initialize(\Sabre\DAV\Server $server) {
$this->server = $server;
$this->server->on('beforeMethod', [$this, 'getLock'], 50);
$this->server->on('afterMethod', [$this, 'releaseLock'], 50);
}
public function getLock(RequestInterface $request) {
// we cant 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') {
return;
}
try {
$node = $this->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
if ($node instanceof Node) {
try {
$node->acquireLock(ILockingProvider::LOCK_SHARED);
} catch (LockedException $e) {
throw new FileLocked($e->getMessage(), $e->getCode(), $e);
}
}
}
public function releaseLock(RequestInterface $request) {
if ($request->getMethod() !== 'PUT') {
return;
}
try {
$node = $this->tree->getNodeForPath($request->getPath());
} catch (NotFound $e) {
return;
}
if ($node instanceof Node) {
$node->releaseLock(ILockingProvider::LOCK_SHARED);
}
}
}

View file

@ -67,6 +67,7 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Sets up the node, expects a full path name
*
* @param \OC\Files\View $view
* @param \OCP\Files\FileInfo $info
*/
@ -82,6 +83,7 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the name of the node
*
* @return string
*/
public function getName() {
@ -99,6 +101,7 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Renames the node
*
* @param string $name The new name
* @throws \Sabre\DAV\Exception\BadRequest
* @throws \Sabre\DAV\Exception\Forbidden
@ -131,6 +134,7 @@ abstract class Node implements \Sabre\DAV\INode {
/**
* Returns the last modification time, as a unix timestamp
*
* @return int timestamp as integer
*/
public function getLastModified() {
@ -212,7 +216,7 @@ abstract class Node implements \Sabre\DAV\INode {
* @return string|null
*/
public function getDavPermissions() {
$p ='';
$p = '';
if ($this->info->isShared()) {
$p .= 'S';
}
@ -248,4 +252,25 @@ abstract class Node implements \Sabre\DAV\INode {
throw new InvalidPath($ex->getMessage());
}
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function acquireLock($type) {
$this->fileView->lockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function releaseLock($type) {
$this->fileView->unlockFile($this->path, $type);
}
/**
* @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
*/
public function changeLock($type) {
$this->fileView->changeLock($this->path, $type);
}
}

View file

@ -70,6 +70,7 @@ class ServerFactory {
$server->addPlugin(new \OC\Connector\Sabre\FilesPlugin($objectTree));
$server->addPlugin(new \OC\Connector\Sabre\MaintenancePlugin($this->config));
$server->addPlugin(new \OC\Connector\Sabre\ExceptionLoggerPlugin('webdav', $this->logger));
$server->addPlugin(new \OC\Connector\Sabre\LockPlugin($objectTree));
// wait with registering these until auth is handled and the filesystem is setup
$server->on('beforeMethod', function () use ($server, $objectTree, $viewCallBack) {

View file

@ -178,27 +178,26 @@ class OC_FileChunking {
* Assembles the chunks into the file specified by the path.
* Also triggers the relevant hooks and proxies.
*
* @param string $path target path
* @param \OC\Files\Storage\Storage $storage
* @param string $path target path relative to the storage
* @param string $absolutePath
* @return bool assembled file size or false if file could not be created
*
* @return boolean assembled file size or false if file could not be created
*
* @throws \OC\InsufficientStorageException when file could not be fully
* assembled due to lack of free space
* @throws \OC\ServerNotAvailableException
*/
public function file_assemble($path) {
$absolutePath = \OC\Files\Filesystem::normalizePath(\OC\Files\Filesystem::getView()->getAbsolutePath($path));
public function file_assemble($storage, $path, $absolutePath) {
$data = '';
// use file_put_contents as method because that best matches what this function does
if (\OC\Files\Filesystem::isValidPath($path)) {
$path = \OC\Files\Filesystem::getView()->getRelativePath($absolutePath);
$exists = \OC\Files\Filesystem::file_exists($path);
$exists = $storage->file_exists($path);
$run = true;
$hookPath = \OC\Files\Filesystem::getView()->getRelativePath($absolutePath);
if(!$exists) {
OC_Hook::emit(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_create,
array(
\OC\Files\Filesystem::signal_param_path => $path,
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run
)
);
@ -207,14 +206,14 @@ class OC_FileChunking {
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_write,
array(
\OC\Files\Filesystem::signal_param_path => $path,
\OC\Files\Filesystem::signal_param_path => $hookPath,
\OC\Files\Filesystem::signal_param_run => &$run
)
);
if(!$run) {
return false;
}
$target = \OC\Files\Filesystem::fopen($path, 'w');
$target = $storage->fopen($path, 'w');
if($target) {
$count = $this->assemble($target);
fclose($target);
@ -222,13 +221,13 @@ class OC_FileChunking {
OC_Hook::emit(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_post_create,
array( \OC\Files\Filesystem::signal_param_path => $path)
array( \OC\Files\Filesystem::signal_param_path => $hookPath)
);
}
OC_Hook::emit(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_post_write,
array( \OC\Files\Filesystem::signal_param_path => $path)
array( \OC\Files\Filesystem::signal_param_path => $hookPath)
);
return $count > 0;
}else{

View file

@ -114,7 +114,7 @@ class File extends \Test\TestCase {
$view->expects($this->atLeastOnce())
->method('resolvePath')
->will($this->returnCallback(
function($path) use ($storage){
function ($path) use ($storage) {
return [$storage, $path];
}
));
@ -172,7 +172,7 @@ class File extends \Test\TestCase {
$view->expects($this->atLeastOnce())
->method('resolvePath')
->will($this->returnCallback(
function($path) use ($storage){
function ($path) use ($storage) {
return [$storage, $path];
}
));
@ -210,7 +210,9 @@ class File extends \Test\TestCase {
$caughtException = null;
try {
// last chunk
$file->acquireLock(ILockingProvider::LOCK_SHARED);
$file->put('test data two');
$file->releaseLock(ILockingProvider::LOCK_SHARED);
} catch (\Exception $e) {
$caughtException = $e;
}
@ -249,7 +251,15 @@ class File extends \Test\TestCase {
$file = new \OC\Connector\Sabre\File($view, $info);
return $file->put($this->getStream('test data'));
// beforeMethod locks
$view->lockFile($path, ILockingProvider::LOCK_SHARED);
$result = $file->put($this->getStream('test data'));
// afterMethod unlocks
$view->unlockFile($path, ILockingProvider::LOCK_SHARED);
return $result;
}
/**
@ -431,7 +441,13 @@ class File extends \Test\TestCase {
// action
$thrown = false;
try {
// beforeMethod locks
$view->lockFile('/test.txt', ILockingProvider::LOCK_SHARED);
$file->put($this->getStream('test data'));
// afterMethod unlocks
$view->unlockFile('/test.txt', ILockingProvider::LOCK_SHARED);
} catch (\Sabre\DAV\Exception\BadRequest $e) {
$thrown = true;
}
@ -458,7 +474,13 @@ class File extends \Test\TestCase {
// action
$thrown = false;
try {
// beforeMethod locks
$view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
$file->put($this->getStream('test data'));
// afterMethod unlocks
$view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
} catch (\OC\Connector\Sabre\Exception\FileLocked $e) {
$thrown = true;
}
@ -519,7 +541,13 @@ class File extends \Test\TestCase {
// action
$thrown = false;
try {
// beforeMethod locks
$view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
$file->put($this->getStream('test data'));
// afterMethod unlocks
$view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
} catch (\OC\Connector\Sabre\Exception\InvalidPath $e) {
$thrown = true;
}
@ -577,7 +605,13 @@ class File extends \Test\TestCase {
// action
$thrown = false;
try {
// beforeMethod locks
$view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
$file->put($this->getStream('test data'));
// afterMethod unlocks
$view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED);
} catch (\Sabre\DAV\Exception\BadRequest $e) {
$thrown = true;
}
@ -702,7 +736,7 @@ class File extends \Test\TestCase {
$eventHandler->expects($this->once())
->method('writeCallback')
->will($this->returnCallback(
function() use ($view, $path, &$wasLockedPre){
function () use ($view, $path, &$wasLockedPre) {
$wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
$wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
}
@ -710,7 +744,7 @@ class File extends \Test\TestCase {
$eventHandler->expects($this->once())
->method('postWriteCallback')
->will($this->returnCallback(
function() use ($view, $path, &$wasLockedPost){
function () use ($view, $path, &$wasLockedPost) {
$wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED);
$wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE);
}
@ -729,8 +763,14 @@ class File extends \Test\TestCase {
'postWriteCallback'
);
// beforeMethod locks
$view->lockFile($path, ILockingProvider::LOCK_SHARED);
$this->assertNotEmpty($file->put($this->getStream('test data')));
// afterMethod unlocks
$view->unlockFile($path, ILockingProvider::LOCK_SHARED);
$this->assertTrue($wasLockedPre, 'File was locked during pre-hooks');
$this->assertTrue($wasLockedPost, 'File was locked during post-hooks');

View file

@ -19,18 +19,26 @@ class UploadTest extends RequestTest {
$this->assertEquals(201, $response->getStatus());
$this->assertTrue($view->file_exists('foo.txt'));
$this->assertEquals('asd', $view->file_get_contents('foo.txt'));
$info = $view->getFileInfo('foo.txt');
$this->assertInstanceOf('\OC\Files\FileInfo', $info);
$this->assertEquals(3, $info->getSize());
}
public function testUploadOverWrite() {
$user = $this->getUniqueID();
$view = $this->setupUser($user, 'pass');
$view->file_put_contents('foo.txt', 'bar');
$view->file_put_contents('foo.txt', 'foobar');
$response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd');
$this->assertEquals(204, $response->getStatus());
$this->assertEquals('asd', $view->file_get_contents('foo.txt'));
$info = $view->getFileInfo('foo.txt');
$this->assertInstanceOf('\OC\Files\FileInfo', $info);
$this->assertEquals(3, $info->getSize());
}
public function testChunkedUpload() {
@ -49,6 +57,10 @@ class UploadTest extends RequestTest {
$this->assertTrue($view->file_exists('foo.txt'));
$this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
$info = $view->getFileInfo('foo.txt');
$this->assertInstanceOf('\OC\Files\FileInfo', $info);
$this->assertEquals(6, $info->getSize());
}
public function testChunkedUploadOverWrite() {
@ -66,6 +78,10 @@ class UploadTest extends RequestTest {
$this->assertEquals(201, $response->getStatus());
$this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
$info = $view->getFileInfo('foo.txt');
$this->assertInstanceOf('\OC\Files\FileInfo', $info);
$this->assertEquals(6, $info->getSize());
}
public function testChunkedUploadOutOfOrder() {
@ -84,5 +100,9 @@ class UploadTest extends RequestTest {
$this->assertTrue($view->file_exists('foo.txt'));
$this->assertEquals('asdbar', $view->file_get_contents('foo.txt'));
$info = $view->getFileInfo('foo.txt');
$this->assertInstanceOf('\OC\Files\FileInfo', $info);
$this->assertEquals(6, $info->getSize());
}
}