diff --git a/lib/private/cache/file.php b/lib/private/cache/file.php index 4e7c065678e3..9f644ac2854e 100644 --- a/lib/private/cache/file.php +++ b/lib/private/cache/file.php @@ -9,9 +9,19 @@ namespace OC\Cache; +use OC\Files\View; +use OCP\Files\Storage; + class File { + /** + * @var View|Storage + */ protected $storage; + public function __construct($storage = null) { + $this->storage = $storage; + } + /** * Returns the cache storage for the logged in user * @return \OC\Files\View cache storage diff --git a/lib/private/connector/sabre/file.php b/lib/private/connector/sabre/file.php index d93b8e68eb65..3cee78edd104 100644 --- a/lib/private/connector/sabre/file.php +++ b/lib/private/connector/sabre/file.php @@ -238,45 +238,67 @@ private function createFileChunked($data) if (empty($info)) { throw new \Sabre\DAV\Exception\NotImplemented(); } - $chunk_handler = new OC_FileChunking($info); - $bytesWritten = $chunk_handler->store($info['index'], $data); + + // we first assembly the target file as a part file + $targetPath = $path . '/' . $info['name']; + if (isset($_SERVER['CONTENT_LENGTH'])) { + $expected = $_SERVER['CONTENT_LENGTH']; + } else { + $expected = -1; + } + $partFilePath = $path . '/' . $info['name'] . '.ocTransferId' . $info['transferid'] . '.part'; + /** @var \OC\Files\Storage\Storage $storage */ + list($storage,) = $this->fileView->resolvePath($partFilePath); + $storeData = $storage->getChunkHandler()->storeChunk($partFilePath, $info['index'], $info['chunkcount'], $expected, $data, $info['transferid']); + $bytesWritten = $storeData['bytesWritten']; //detect aborted upload if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT' ) { if (isset($_SERVER['CONTENT_LENGTH'])) { - $expected = $_SERVER['CONTENT_LENGTH']; if ($bytesWritten != $expected) { - $chunk_handler->remove($info['index']); throw new \Sabre\DAV\Exception\BadRequest( 'expected filesize ' . $expected . ' got ' . $bytesWritten); } } } - if ($chunk_handler->isComplete()) { + if ($storeData['complete']) { try { - // we first assembly the target file as a part file - $partFile = $path . '/' . $info['name'] . '.ocTransferId' . $info['transferid'] . '.part'; - $chunk_handler->file_assemble($partFile); // here is the final atomic rename - $targetPath = $path . '/' . $info['name']; - $renameOkay = $this->fileView->rename($partFile, $targetPath); - $fileExists = $this->fileView->file_exists($targetPath); + // trigger hooks for post processing + $targetFileExists = $storage->file_exists('/files' . $targetPath); + $run = $this->runPreHooks($targetFileExists, '/files' . $targetPath); + if (!$run) { + \OC::$server->getLogger()->error('Hook execution on {file} failed', array('app' => 'webdav', 'file' => $targetPath)); + // delete part file + $storage->unlink('/files' . $partFilePath); + throw new \Sabre\DAV\Exception('Upload rejected'); + } + $renameOkay = $storage->rename('/files' . $partFilePath, '/files' . $targetPath); + $fileExists = $storage->file_exists('/files' . $targetPath); if ($renameOkay === false || $fileExists === false) { - \OC_Log::write('webdav', '\OC\Files\Filesystem::rename() failed', \OC_Log::ERROR); + \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', array('app'=>'webdav')); // only delete if an error occurred and the target file was already created if ($fileExists) { - $this->fileView->unlink($targetPath); + $storage->unlink('/files' . $targetPath); + } + $partFileExists = $storage->file_exists('/files' . $partFilePath); + if ($partFileExists) { + $storage->unlink('/files' . $partFilePath); } throw new \Sabre\DAV\Exception('Could not rename part file assembled from chunks'); } + // trigger hooks for post processing + $this->runPostHooks($targetFileExists, '/files' . $targetPath); + // allow sync clients to send the mtime along in a header $mtime = OC_Request::hasModificationTime(); if ($mtime !== false) { - if($this->fileView->touch($targetPath, $mtime)) { + // TODO: will this update the cache properly - e.g. smb where we cannot change the mtime ??? + if($storage->touch('/files' . $targetPath, $mtime)) { header('X-OC-MTime: accepted'); } } @@ -290,4 +312,43 @@ private function createFileChunked($data) return null; } + + private function runPreHooks($fileExists, $path) { + $run = true; + if(!$fileExists) { + 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_run => &$run + ) + ); + } + OC_Hook::emit( + \OC\Files\Filesystem::CLASSNAME, + \OC\Files\Filesystem::signal_write, + array( + \OC\Files\Filesystem::signal_param_path => $path, + \OC\Files\Filesystem::signal_param_run => &$run + ) + ); + + return $run; + } + + private function runPostHooks($fileExists, $path) { + if(!$fileExists) { + OC_Hook::emit( + \OC\Files\Filesystem::CLASSNAME, + \OC\Files\Filesystem::signal_post_create, + array( \OC\Files\Filesystem::signal_param_path => $path) + ); + } + OC_Hook::emit( + \OC\Files\Filesystem::CLASSNAME, + \OC\Files\Filesystem::signal_post_write, + array( \OC\Files\Filesystem::signal_param_path => $path) + ); + } } diff --git a/lib/private/filechunking.php b/lib/private/filechunking.php index 990499e40b49..006f31008010 100644 --- a/lib/private/filechunking.php +++ b/lib/private/filechunking.php @@ -19,15 +19,16 @@ static public function decodeName($name) { /** * @param string[] $info */ - public function __construct($info) { + public function __construct($info, $cache = null) { $this->info = $info; + $this->cache = $cache; } public function getPrefix() { $name = $this->info['name']; - $transferid = $this->info['transferid']; + $transferId = $this->info['transferid']; - return $name.'-chunking-'.$transferid.'-'; + return $name.'-chunking-'.$transferId.'-'; } protected function getCache() { @@ -72,7 +73,7 @@ public function isComplete() { * * @return integer assembled file size * - * @throws \OC\InsufficientStorageException when file could not be fully + * @throws \OCP\Files\NotEnoughSpaceException when file could not be fully * assembled due to lack of free space */ public function assemble($f) { @@ -123,99 +124,4 @@ public function remove($index) { $prefix = $this->getPrefix(); $cache->remove($prefix.$index); } - - public function signature_split($orgfile, $input) { - $info = unpack('n', fread($input, 2)); - $blocksize = $info[1]; - $this->info['transferid'] = mt_rand(); - $count = 0; - $needed = array(); - $cache = $this->getCache(); - $prefix = $this->getPrefix(); - while (!feof($orgfile)) { - $new_md5 = fread($input, 16); - if (feof($input)) { - break; - } - $data = fread($orgfile, $blocksize); - $org_md5 = md5($data, true); - if ($org_md5 == $new_md5) { - $cache->set($prefix.$count, $data); - } else { - $needed[] = $count; - } - $count++; - } - return array( - 'transferid' => $this->info['transferid'], - 'needed' => $needed, - 'count' => $count, - ); - } - - /** - * Assembles the chunks into the file specified by the path. - * Also triggers the relevant hooks and proxies. - * - * @param string $path target path - * - * @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 - */ - public function file_assemble($path) { - $absolutePath = \OC\Files\Filesystem::normalizePath(\OC\Files\Filesystem::getView()->getAbsolutePath($path)); - $data = ''; - // use file_put_contents as method because that best matches what this function does - if (OC_FileProxy::runPreProxies('file_put_contents', $absolutePath, $data) - && \OC\Files\Filesystem::isValidPath($path)) { - $path = \OC\Files\Filesystem::getView()->getRelativePath($absolutePath); - $exists = \OC\Files\Filesystem::file_exists($path); - $run = true; - 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_run => &$run - ) - ); - } - OC_Hook::emit( - \OC\Files\Filesystem::CLASSNAME, - \OC\Files\Filesystem::signal_write, - array( - \OC\Files\Filesystem::signal_param_path => $path, - \OC\Files\Filesystem::signal_param_run => &$run - ) - ); - if(!$run) { - return false; - } - $target = \OC\Files\Filesystem::fopen($path, 'w'); - if($target) { - $count = $this->assemble($target); - fclose($target); - if(!$exists) { - OC_Hook::emit( - \OC\Files\Filesystem::CLASSNAME, - \OC\Files\Filesystem::signal_post_create, - array( \OC\Files\Filesystem::signal_param_path => $path) - ); - } - OC_Hook::emit( - \OC\Files\Filesystem::CLASSNAME, - \OC\Files\Filesystem::signal_post_write, - array( \OC\Files\Filesystem::signal_param_path => $path) - ); - OC_FileProxy::runPostProxies('file_put_contents', $absolutePath, $count); - return $count > 0; - }else{ - return false; - } - } - return false; - } } diff --git a/lib/private/files/cachingchunkhandler.php b/lib/private/files/cachingchunkhandler.php new file mode 100644 index 000000000000..14e41301c698 --- /dev/null +++ b/lib/private/files/cachingchunkhandler.php @@ -0,0 +1,70 @@ +storage = $storage; + } + + /** + * Write a chunk to a give file. + * + * @param string $fileName + * @param int $index + * @param int $numberOfChunk + * @param int $chunkSize + * @param string $data + * @return array + */ + function storeChunk($fileName, $index, $numberOfChunk, $chunkSize, $data, $transferId) { + $info = array( + 'name' => $transferId, + 'transferid' => $transferId, + 'chunkcount' => $numberOfChunk, + '' + ); + $chunkHandler = new \OC_FileChunking($info, $this->cache); + $bytesWritten = $chunkHandler->store($index, $data); + if ($bytesWritten != $chunkSize) { + $chunkHandler->remove($index); + } + $complete = false; + $actualSize = $chunkHandler->getCurrentSize(); + + if ($chunkHandler->isComplete()) { + $complete = true; + $f = $this->storage->fopen("/files" . $fileName, 'w'); + $chunkHandler->assemble($f); + fclose($f); + } + + return array( + 'complete' => $complete, + 'bytesWritten' => $bytesWritten, + 'actualSize' => $actualSize + ); + } + + public function setFileCache($cache) { + $this->cache = $cache; + } +} diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php index d76c6aa031b7..e51aa3f4b660 100644 --- a/lib/private/files/storage/common.php +++ b/lib/private/files/storage/common.php @@ -8,8 +8,10 @@ namespace OC\Files\Storage; +use OC\Files\CachingChunkHandler; use OC\Files\Filesystem; use OC\Files\Cache\Watcher; +use OCP\Files\IChunkHandler; /** * Storage backend class for providing common filesystem operation methods @@ -437,4 +439,14 @@ protected function removeCachedFile($path) { public function instanceOfStorage($class) { return is_a($this, $class); } + + /** + * Returns the storage specific chunk handler + * + * @return IChunkHandler + */ + public function getChunkHandler() { + return new CachingChunkHandler($this); + } + } diff --git a/lib/private/files/storage/wrapper/wrapper.php b/lib/private/files/storage/wrapper/wrapper.php index d899c88363f2..93575ddc7895 100644 --- a/lib/private/files/storage/wrapper/wrapper.php +++ b/lib/private/files/storage/wrapper/wrapper.php @@ -8,6 +8,8 @@ namespace OC\Files\Storage\Wrapper; +use OCP\Files\IChunkHandler; + class Wrapper implements \OC\Files\Storage\Storage { /** * @var \OC\Files\Storage\Storage $storage @@ -465,4 +467,13 @@ public function instanceOfStorage($class) { public function __call($method, $args) { return call_user_func_array(array($this->storage, $method), $args); } + + /** + * Returns the storage specific chunk handler + * + * @return IChunkHandler + */ + public function getChunkHandler() { + return $this->storage->getChunkHandler(); + } } diff --git a/lib/public/files/ichunkhandler.php b/lib/public/files/ichunkhandler.php new file mode 100644 index 000000000000..facc54eecd8f --- /dev/null +++ b/lib/public/files/ichunkhandler.php @@ -0,0 +1,30 @@ +assertTrue($this->instance->instanceOfStorage(get_class($this->instance))); $this->assertFalse($this->instance->instanceOfStorage('\OC')); } + + public function testChunkedUpload() { + $chunkHandler = $this->instance->getChunkHandler(); + $this->assertInstanceOf('\OCP\Files\IChunkHandler', $chunkHandler); + + if ($chunkHandler instanceof CachingChunkHandler) { + $chunkHandler->setFileCache(new \OC\Cache\File(new Temporary(array()))); + } + + $transferId = uniqid('transfer-'); + $return = $chunkHandler->storeChunk('chunk-test-01.txt', 0, 2, 10, '0123456789', $transferId); + $this->assertTrue(is_array($return)); + $this->assertFalse($return['complete']); + $this->assertEquals(10, $return['bytesWritten']); + $this->assertEquals(10, $return['actualSize']); + + $return = $chunkHandler->storeChunk('chunk-test-01.txt', 1, 2, 10, '0123456789', $transferId); + $this->assertTrue(is_array($return)); + $this->assertTrue($return['complete']); + $this->assertEquals(10, $return['bytesWritten']); + $this->assertEquals(20, $return['actualSize']); + + $this->instance->file_exists('chunk-test-01.txt'); + } }