diff options
-rw-r--r-- | apps/files_encryption/hooks/hooks.php | 10 | ||||
-rw-r--r-- | apps/files_encryption/lib/proxy.php | 9 | ||||
-rw-r--r-- | apps/files_encryption/tests/stream.php | 6 | ||||
-rwxr-xr-x | apps/files_encryption/tests/webdav.php | 9 | ||||
-rw-r--r-- | lib/private/connector/sabre/exception/filelocked.php | 28 | ||||
-rw-r--r-- | lib/private/connector/sabre/file.php | 21 | ||||
-rw-r--r-- | lib/private/files/filesystem.php | 4 | ||||
-rw-r--r-- | lib/private/files/lock.php | 309 | ||||
-rw-r--r-- | lib/private/files/storage/loader.php | 4 | ||||
-rw-r--r-- | lib/private/files/storage/wrapper/lockingwrapper.php | 182 | ||||
-rw-r--r-- | lib/private/helper.php | 2 | ||||
-rw-r--r-- | lib/private/log/owncloud.php | 17 | ||||
-rwxr-xr-x | lib/private/util.php | 12 | ||||
-rw-r--r-- | lib/public/files/lock.php | 63 | ||||
-rw-r--r-- | lib/public/files/locknotacquiredexception.php | 47 | ||||
-rw-r--r-- | tests/lib/files/filesystem.php | 10 | ||||
-rw-r--r-- | tests/lib/files/mount/mount.php | 2 | ||||
-rw-r--r-- | tests/lib/util.php | 2 |
18 files changed, 703 insertions, 34 deletions
diff --git a/apps/files_encryption/hooks/hooks.php b/apps/files_encryption/hooks/hooks.php index 8fae901fe63..d1ee4a97d15 100644 --- a/apps/files_encryption/hooks/hooks.php +++ b/apps/files_encryption/hooks/hooks.php @@ -99,12 +99,14 @@ class Hooks { // Set legacy encryption key if it exists, to support
// depreciated encryption system
- $encLegacyKey = $userView->file_get_contents('encryption.key');
- if ($encLegacyKey) {
+ if ($userView->file_exists('encryption.key')) {
+ $encLegacyKey = $userView->file_get_contents('encryption.key');
+ if ($encLegacyKey) {
- $plainLegacyKey = Crypt::legacyDecrypt($encLegacyKey, $params['password']);
+ $plainLegacyKey = Crypt::legacyDecrypt($encLegacyKey, $params['password']);
- $session->setLegacyKey($plainLegacyKey);
+ $session->setLegacyKey($plainLegacyKey);
+ }
}
// Encrypt existing user files
diff --git a/apps/files_encryption/lib/proxy.php b/apps/files_encryption/lib/proxy.php index ae3df834e9f..fd91073b8de 100644 --- a/apps/files_encryption/lib/proxy.php +++ b/apps/files_encryption/lib/proxy.php @@ -275,7 +275,7 @@ class Proxy extends \OC_FileProxy { \OC_FileProxy::$enabled = false; // get file size - $data['size'] = self::postFileSize($path, $data['size']); + $data['size'] = self::postFileSize($path, $data['size'], $data); // Re-enable the proxy \OC_FileProxy::$enabled = $proxyStatus; @@ -289,7 +289,7 @@ class Proxy extends \OC_FileProxy { * @param int $size * @return int|bool */ - public function postFileSize($path, $size) { + public function postFileSize($path, $size, $fileInfo = null) { $view = new \OC\Files\View('/'); @@ -323,9 +323,8 @@ class Proxy extends \OC_FileProxy { return $size; } - $fileInfo = false; // get file info from database/cache if not .part file - if (!Helper::isPartialFilePath($path)) { + if (empty($fileInfo) && !Helper::isPartialFilePath($path)) { $proxyState = \OC_FileProxy::$enabled; \OC_FileProxy::$enabled = false; $fileInfo = $view->getFileInfo($path); @@ -333,7 +332,7 @@ class Proxy extends \OC_FileProxy { } // if file is encrypted return real file size - if ($fileInfo && $fileInfo['encrypted'] === true) { + if (isset($fileInfo['encrypted']) && $fileInfo['encrypted'] === true) { // try to fix unencrypted file size if it doesn't look plausible if ((int)$fileInfo['size'] > 0 && (int)$fileInfo['unencrypted_size'] === 0 ) { $fixSize = $util->getFileSize($path); diff --git a/apps/files_encryption/tests/stream.php b/apps/files_encryption/tests/stream.php index 5df9cdbe1f1..254c5e87ed1 100644 --- a/apps/files_encryption/tests/stream.php +++ b/apps/files_encryption/tests/stream.php @@ -136,6 +136,8 @@ class Test_Encryption_Stream extends \PHPUnit_Framework_TestCase { // set stream options $this->assertTrue(stream_set_blocking($handle, 1)); + fclose($handle); + // tear down $view->unlink($filename); } @@ -158,6 +160,8 @@ class Test_Encryption_Stream extends \PHPUnit_Framework_TestCase { // set stream options $this->assertFalse(stream_set_timeout($handle, 1)); + fclose($handle); + // tear down $view->unlink($filename); } @@ -177,6 +181,8 @@ class Test_Encryption_Stream extends \PHPUnit_Framework_TestCase { // set stream options $this->assertEquals(0, stream_set_write_buffer($handle, 1024)); + fclose($handle); + // tear down $view->unlink($filename); } diff --git a/apps/files_encryption/tests/webdav.php b/apps/files_encryption/tests/webdav.php index d33dc58cf92..f299116ff23 100755 --- a/apps/files_encryption/tests/webdav.php +++ b/apps/files_encryption/tests/webdav.php @@ -49,7 +49,7 @@ class Test_Encryption_Webdav extends \PHPUnit_Framework_TestCase { public $dataShort; public $stateFilesTrashbin; - private static $storage; + private $storage; public static function setUpBeforeClass() { // reset backend @@ -69,7 +69,6 @@ class Test_Encryption_Webdav extends \PHPUnit_Framework_TestCase { // create test user \Test_Encryption_Util::loginHelper(\Test_Encryption_Webdav::TEST_ENCRYPTION_WEBDAV_USER1, true); - self::$storage = new \OC\Files\Storage\Temporary(array()); } function setUp() { @@ -83,7 +82,7 @@ class Test_Encryption_Webdav extends \PHPUnit_Framework_TestCase { // init filesystem view $this->view = new \OC\Files\View('/'); - + list($this->storage, $intPath) = $this->view->resolvePath('/'); // init short data $this->dataShort = 'hats'; @@ -200,6 +199,9 @@ class Test_Encryption_Webdav extends \PHPUnit_Framework_TestCase { $_SERVER['HTTP_AUTHORIZATION'] = 'Basic dGVzdC13ZWJkYXYtdXNlcjE6dGVzdC13ZWJkYXYtdXNlcjE='; $_SERVER['PATH_INFO'] = '/webdav' . $filename; + // at the beginning the file should exist + $this->assertTrue($this->view->file_exists('/' . $this->userId . '/files' . $filename)); + // handle webdav request $content = $this->handleWebdavRequest(); @@ -230,7 +232,6 @@ class Test_Encryption_Webdav extends \PHPUnit_Framework_TestCase { // Create ownCloud Dir $root = '/' . $this->userId . '/files'; - \OC\Files\Filesystem::mount(self::$storage, array(), $root); $view = new \OC\Files\View($root); $publicDir = new OC_Connector_Sabre_Directory($view, $view->getFileInfo('')); $objectTree = new \OC\Connector\Sabre\ObjectTree(); diff --git a/lib/private/connector/sabre/exception/filelocked.php b/lib/private/connector/sabre/exception/filelocked.php new file mode 100644 index 00000000000..ec200f847e3 --- /dev/null +++ b/lib/private/connector/sabre/exception/filelocked.php @@ -0,0 +1,28 @@ +<?php +/** + * ownCloud + * + * @author Owen Winkler + * @copyright 2013 Owen Winkler <owen@owncloud.com> + * + */ + +class OC_Connector_Sabre_Exception_FileLocked extends Sabre_DAV_Exception { + + public function __construct($message = "", $code = 0, Exception $previous = null) { + if($previous instanceof \OCP\Files\LockNotAcquiredException) { + $message = sprintf('Target file %s is locked by another process.', $previous->path); + } + parent::__construct($message, $code, $previous); + } + + /** + * Returns the HTTP status code for this exception + * + * @return int + */ + public function getHTTPCode() { + + return 503; + } +} diff --git a/lib/private/connector/sabre/file.php b/lib/private/connector/sabre/file.php index 8a16ba55e7a..48287b1e503 100644 --- a/lib/private/connector/sabre/file.php +++ b/lib/private/connector/sabre/file.php @@ -97,15 +97,24 @@ class OC_Connector_Sabre_File extends OC_Connector_Sabre_Node implements Sabre_D // the path for the file was not valid // TODO: find proper http status code for this case throw new Sabre_DAV_Exception_Forbidden($e->getMessage()); + } catch (\OCP\Files\LockNotAcquiredException $e) { + // the file is currently being written to by another process + throw new OC_Connector_Sabre_Exception_FileLocked($e->getMessage(), $e->getCode(), $e); } // rename to correct path - $renameOkay = $this->fileView->rename($partpath, $this->path); - $fileExists = $this->fileView->file_exists($this->path); - if ($renameOkay === false || $fileExists === false) { - \OC_Log::write('webdav', '\OC\Files\Filesystem::rename() failed', \OC_Log::ERROR); - $this->fileView->unlink($partpath); - throw new Sabre_DAV_Exception('Could not rename part file to final file'); + try { + $renameOkay = $this->fileView->rename($partpath, $this->path); + $fileExists = $this->fileView->file_exists($this->path); + if ($renameOkay === false || $fileExists === false) { + \OC_Log::write('webdav', '\OC\Files\Filesystem::rename() failed', \OC_Log::ERROR); + $this->fileView->unlink($partpath); + throw new Sabre_DAV_Exception('Could not rename part file to final file'); + } + } + catch (\OCP\Files\LockNotAcquiredException $e) { + // the file is currently being written to by another process + throw new OC_Connector_Sabre_Exception_FileLocked($e->getMessage(), $e->getCode(), $e); } // allow sync clients to send the mtime along in a header diff --git a/lib/private/files/filesystem.php b/lib/private/files/filesystem.php index ad7213d2368..2cc4a2130eb 100644 --- a/lib/private/files/filesystem.php +++ b/lib/private/files/filesystem.php @@ -168,8 +168,8 @@ class Filesystem { /** * @param callable $wrapper */ - public static function addStorageWrapper($wrapper) { - self::getLoader()->addStorageWrapper($wrapper); + public static function addStorageWrapper($wrapperName, $wrapper) { + self::getLoader()->addStorageWrapper($wrapperName, $wrapper); $mounts = self::getMountManager()->getAll(); foreach ($mounts as $mount) { diff --git a/lib/private/files/lock.php b/lib/private/files/lock.php new file mode 100644 index 00000000000..39138fe0248 --- /dev/null +++ b/lib/private/files/lock.php @@ -0,0 +1,309 @@ +<?php +/** + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files; + +use OCP\Config; +use OC\Files\Filesystem; +use OCP\Files\LockNotAcquiredException; + +/** + * Class Lock + * @package OC\Files + */ +class Lock implements \OCP\Files\Lock { + + /** @var int $retries Number of lock retries to attempt */ + public static $retries = 40; + + /** @var int $retryInterval Milliseconds between retries */ + public static $retryInterval = 50; + + /** @var string $path Filename of the file as represented in storage */ + protected $path; + + /** @var array $stack A stack of lock data */ + protected $stack = array(); + + /** @var resource $handle A file handle used to maintain a lock */ + protected $handle; + + /** @var string $lockFile Filename of the lock file */ + protected $lockFile; + + /** @var resource $lockFileHandle The file handle used to maintain a lock on the lock file */ + protected $lockFileHandle; + + /** + * Constructor for the lock instance + * @param string $path Absolute pathname for a local file on which to obtain a lock + */ + public function __construct($path) { + $this->path = Filesystem::normalizePath($path); + } + + protected function obtainReadLock($existingHandle = null) { + \OC_Log::write('lock', sprintf('INFO: Read lock requested for %s', $this->path), \OC_Log::DEBUG); + $timeout = Lock::$retries; + + // Re-use an existing handle or get a new one + if(empty($existingHandle)) { + $handle = fopen($this->path, 'r'); + } + else { + $handle = $existingHandle; + } + + do { + if($this->isLockFileLocked($this->getLockFile($this->path))) { + \OC_Log::write('lock', sprintf('INFO: Read lock has locked lock file %s for %s', $this->getLockFile($this->path), $this->path), \OC_Log::DEBUG); + do { + usleep(Lock::$retryInterval); + $timeout--; + } while($this->isLockFileLocked($this->getLockFile($this->path)) && $timeout > 0); + \OC_Log::write('lock', sprintf('INFO: Lock file %s has become unlocked for %s', $this->getLockFile($this->path), $this->path), \OC_Log::DEBUG); + } + } while ((!$lockReturn = flock($handle, LOCK_SH | LOCK_NB, $wouldBlock)) && $timeout > 0); + if ($wouldBlock == true || $lockReturn == false || $timeout <= 0) { + \OC_Log::write('lock', sprintf('FAIL: Failed to acquire read lock for %s', $this->path), \OC_Log::DEBUG); + return false; + } + $this->handle = $handle; + \OC_Log::write('lock', sprintf('PASS: Acquired read lock for %s', $this->path), \OC_Log::DEBUG); + return true; + } + + protected function obtainWriteLock($existingHandle = null) { + \OC_Log::write('lock', sprintf('INFO: Write lock requested for %s', $this->path), \OC_Log::DEBUG); + + // Re-use an existing handle or get a new one + if (empty($existingHandle)) { + $handle = fopen($this->path, 'c'); + } + else { + $handle = $existingHandle; + } + + // If the file doesn't exist, but we can create a lock for it + if (!file_exists($this->path) && $this->lockLockFile($this->path)) { + $lockReturn = flock($handle, LOCK_EX | LOCK_NB, $wouldBlock); + if ($lockReturn == false || $wouldBlock == true) { + \OC_Log::write('lock', sprintf('FAIL: Write lock failed, unable to exclusively lock new file %s', $this->path), \OC_Log::DEBUG); + return false; + } + $this->handle = $handle; + return true; + } + + + // Since this file does exist, wait for locks to release to get an exclusive lock + $timeout = Lock::$retries; + $haveBlock = false; + while ((!$lockReturn = flock($handle, LOCK_EX | LOCK_NB, $wouldBlock)) && $timeout > 0) { + // We don't have a lock on the original file, try to get a lock on its lock file + if ($haveBlock || $haveBlock = $this->lockLockFile($this->lockFile)) { + usleep(Lock::$retryInterval); + } + else { + \OC_Log::write('lock', sprintf('FAIL: Write lock failed, unable to lock original %s or lock file', $this->path), \OC_Log::DEBUG); + return false; + } + $timeout--; + } + if ($wouldBlock == true || $lockReturn == false) { + \OC_Log::write('lock', sprintf('FAIL: Write lock failed due to timeout on %s', $this->path), \OC_Log::DEBUG); + return false; + } + \OC_Log::write('lock', sprintf('PASS: Write lock succeeded on %s', $this->path), \OC_Log::DEBUG); + + return true; + } + + /** + * Create a lock file and lock it + * Sets $this->lockFile to the specified lock file, indicating that the lock file is IN USE for this lock instance + * Also sets $this->lockFileHandle to a file handle of the lock file + * @param string $filename The name of the file to lock + * @return bool False if lock can't be acquired, true if it can. + */ + protected function lockLockFile ( $filename ) { + $lockFile = $this->getLockFile($filename); + \OC_Log::write('lock', sprintf('INFO: Locking lock file %s for %s', $lockFile, $filename), \OC_Log::DEBUG); + + // If we already manage the lockfile, success + if(!empty($this->lockFile)) { + \OC_Log::write('lock', sprintf('PASS: Lock file %s was locked by this request for %s', $lockFile, $filename), \OC_Log::DEBUG); + return true; + } + + // Check if the lockfile exists, and if not, try to create it + \OC_Log::write('lock', sprintf('INFO: Does lock file %s already exist? %s', $lockFile, file_exists($lockFile) ? 'yes' : 'no'), \OC_Log::DEBUG); + $handle = fopen($lockFile, 'c'); + if(!$handle) { + \OC_Log::write('lock', sprintf('FAIL: Could not create lock file %s', $lockFile), \OC_Log::DEBUG); + return false; + } + + // Attempt to acquire lock on lock file + $wouldBlock = false; + $timeout = self::$retries; + // Wait for lock over timeout + while((!$lockReturn = flock($handle, LOCK_EX | LOCK_NB, $wouldBlock)) && $timeout > 0) { + \OC_Log::write('lock', sprintf('FAIL: Could not acquire lock on lock file %s, %s timeout increments remain.', $lockFile, $timeout), \OC_Log::DEBUG); + usleep(self::$retryInterval); + $timeout--; + } + if ($wouldBlock == true || $lockReturn == false) { + \OC_Log::write('lock', sprintf('FAIL: Could not acquire lock on lock file %s', $lockFile), \OC_Log::DEBUG); + return false; + } + fwrite($handle, $filename); + \OC_Log::write('lock', sprintf('PASS: Wrote filename to lock lock file %s', $lockFile), \OC_Log::DEBUG); + + $this->lockFile = $lockFile; + $this->lockFileHandle = $handle; + + return true; + } + + /** + * Add a lock of a specific type to the stack + * @param integer $lockType A constant representing the type of lock to queue + * @param null|resource $existingHandle An existing file handle from an fopen() + * @throws LockNotAcquiredException + */ + public function addLock($lockType, $existingHandle = null) { + if(!isset($this->stack[$lockType])) { + switch($lockType) { + case Lock::READ: + $result = $this->obtainReadLock($existingHandle); + break; + case Lock::WRITE: + $result = $this->obtainWriteLock($existingHandle); + break; + default: + $result = false; + break; + } + if($result) { + $this->stack[$lockType] = 0; + } + else { + throw new LockNotAcquiredException($this->path, $lockType); + } + } + + \OC_Log::write('lock', sprintf('INFO: Incrementing lock type %d count for %s', $lockType, $this->path), \OC_Log::DEBUG); + $this->stack[$lockType]++; + + } + + /** + * Release locks on handles and files + */ + public function release($lockType) { + if(isset($this->stack[$lockType])) { + $this->stack[$lockType]--; + if($this->stack[$lockType] <= 0) { + unset($this->stack[$lockType]); + } + } + + if(count($this->stack) == 0) { + // No more locks needed on this file, release the handle and/or lockfile + $this->releaseAll(); + } + + return true; + } + + + /** + * Get the lock file associated to a file + * @param string $filename The filename of the file to create a lock file for + * @return string The filename of the lock file + */ + public static function getLockFile($filename) { + static $locksDir = false; + if(!$locksDir) { + $dataDir = Config::getSystemValue('datadirectory'); + $locksDir = $dataDir . '/.locks'; + if(!file_exists($locksDir)) { + mkdir($locksDir); + } + } + $filename = Filesystem::normalizePath($filename); + return $locksDir . '/' . sha1($filename) . '.lock'; + } + + /** + * Determine if a file has an associated and flocked lock file + * @param string $lockFile The filename of the lock file to check + * @return bool True if the lock file is flocked + */ + protected function isLockFileLocked($lockFile) { + if(file_exists($lockFile)) { + if($handle = fopen($lockFile, 'c')) { + if($lock = flock($handle, LOCK_EX | LOCK_NB)) { + // Got lock, not blocking, release and unlink + unlink($lockFile); + fclose($handle); + flock($lock, LOCK_UN); + return false; + } + else { + return true; + } + } + else { + return true; + } + } + return false; + } + + /** + * Release all queued locks on the file + * @return bool + */ + public function releaseAll() { + $this->stack = array(); + \OC_Log::write('lock', sprintf('INFO: Releasing locks on %s', $this->path), \OC_Log::DEBUG); + if (!empty($this->handle) && is_resource($this->handle)) { + flock($this->handle, LOCK_UN); + \OC_Log::write('lock', sprintf('INFO: Released lock handle %s on %s', $this->handle, $this->path), \OC_Log::DEBUG); + $this->handle = null; + } + if (!empty($this->lockFile) && file_exists($this->lockFile)) { + unlink($this->lockFile); + \OC_Log::write('lock', sprintf('INFO: Released lock file %s on %s', $this->lockFile, $this->path), \OC_Log::DEBUG); + $this->lockFile = null; + } + \OC_Log::write('lock', sprintf('FREE: Released locks on %s', $this->path), \OC_Log::DEBUG); + return true; + } + + public function __destruct() { + // Only releaseAll if we have locks to release + if(!empty($this->handle) || !empty($this->lockFile)) { + \OC_Log::write('lock', sprintf('INFO: Destroying locks on %s', $this->path), \OC_Log::DEBUG); + $this->releaseAll(); + } + } + +}
\ No newline at end of file diff --git a/lib/private/files/storage/loader.php b/lib/private/files/storage/loader.php index 966234cb04d..c75a0a976a7 100644 --- a/lib/private/files/storage/loader.php +++ b/lib/private/files/storage/loader.php @@ -21,8 +21,8 @@ class Loader { * * @param callable $callback */ - public function addStorageWrapper($callback) { - $this->storageWrappers[] = $callback; + public function addStorageWrapper($wrapperName, $callback) { + $this->storageWrappers[$wrapperName] = $callback; } /** diff --git a/lib/private/files/storage/wrapper/lockingwrapper.php b/lib/private/files/storage/wrapper/lockingwrapper.php new file mode 100644 index 00000000000..b785046bc3c --- /dev/null +++ b/lib/private/files/storage/wrapper/lockingwrapper.php @@ -0,0 +1,182 @@ +<?php + +/** + * Copyright (c) 2013 ownCloud, Inc. + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OC\Files\Storage\Wrapper; + +use OC\Files\Filesystem; +use OC\Files\Lock; + +/** + * Class LockingWrapper + * A Storage Wrapper used to lock files at the system level + * @package OC\Files\Storage\Wrapper + * + * Notes: Does the $locks array need to be global to all LockingWrapper instances, such as in the case of two paths + * that point to the same physical file? Otherwise accessing the file from a different path the second time would show + * the file as locked, even though this process is the one locking it. + */ +class LockingWrapper extends Wrapper { + + /** @var \OCP\Files\Lock[] $locks Holds an array of lock instances indexed by path for this storage */ + protected $locks = array(); + + /** + * Acquire a lock on a file + * @param string $path Path to file, relative to this storage + * @param integer $lockType A Lock class constant, Lock::READ/Lock::WRITE + * @param null|resource $existingHandle An existing file handle from an fopen() + * @return bool|\OCP\Files\Lock Lock instance on success, false on failure + */ + protected function getLock($path, $lockType, $existingHandle = null){ + $path = Filesystem::normalizePath($this->storage->getLocalFile($path)); + if(!isset($this->locks[$path])) { + $this->locks[$path] = new Lock($path); + } + $this->locks[$path]->addLock($lockType, $existingHandle); + return $this->locks[$path]; + } + + /** + * Release an existing lock + * @param string $path Path to file, relative to this storage + * @param integer $lockType The type of lock to release + * @param bool $releaseAll If true, release all outstanding locks + * @return bool true on success, false on failure + */ + protected function releaseLock($path, $lockType, $releaseAll = false){ + $path = Filesystem::normalizePath($this->storage->getLocalFile($path)); + if(isset($this->locks[$path])) { + if($releaseAll) { + return $this->locks[$path]->releaseAll(); + } + else { + return $this->locks[$path]->release($lockType); + } + } + return true; + } + + /** + * see http://php.net/manual/en/function.file_get_contents.php + * @param string $path + * @return string + * @throws \Exception + */ + public function file_get_contents($path) { + try { + $this->getLock($path, Lock::READ); + $result = $this->storage->file_get_contents($path); + } + catch(\Exception $originalException) { + // Need to release the lock before more operations happen in upstream exception handlers + $this->releaseLock($path, Lock::READ); + throw $originalException; + } + $this->releaseLock($path, Lock::READ); + return $result; + } + + public function file_put_contents($path, $data) { + try { + $this->getLock($path, Lock::WRITE); + $result = $this->storage->file_put_contents($path, $data); + } + catch(\Exception $originalException) { + // Release lock, throw original exception + $this->releaseLock($path, Lock::WRITE); + throw $originalException; + } + $this->releaseLock($path, Lock::WRITE); + return $result; + } + + + public function copy($path1, $path2) { + try { + $this->getLock($path1, Lock::READ); + $this->getLock($path2, Lock::WRITE); + $result = $this->storage->copy($path1, $path2); + } + catch(\Exception $originalException) { + // Release locks, throw original exception + $this->releaseLock($path1, Lock::READ); + $this->releaseLock($path2, Lock::WRITE); + throw $originalException; + } + $this->releaseLock($path1, Lock::READ); + $this->releaseLock($path2, Lock::WRITE); + return $result; + } + + public function rename($path1, $path2) { + try { + $this->getLock($path1, Lock::READ); + $this->getLock($path2, Lock::WRITE); + $result = $this->storage->rename($path1, $path2); + } + catch(\Exception $originalException) { + // Release locks, throw original exception + $this->releaseLock($path1, Lock::READ); + $this->releaseLock($path2, Lock::WRITE); + throw $originalException; + } + $this->releaseLock($path1, Lock::READ); + $this->releaseLock($path2, Lock::WRITE); + return $result; + } + + public function fopen($path, $mode) { + $lockType = Lock::READ; + switch ($mode) { + case 'r+': + case 'rb+': + case 'w+': + case 'wb+': + case 'x+': + case 'xb+': + case 'a+': + case 'ab+': + case 'c+': + case 'w': + case 'wb': + case 'x': + case 'xb': + case 'a': + case 'ab': + case 'c': + $lockType = Lock::WRITE; + break; + } + // The handle for $this->fopen() is used outside of this class, so the handle/lock can't be closed + // Instead, it will be closed when the request goes out of scope + // Storage doesn't have an fclose() + if($result = $this->storage->fopen($path, $mode)) { + $this->getLock($path, $lockType, $result); + } + return $result; + } + + public function unlink($path) { + try { + if (\OC\Files\Filesystem::is_file($path)) { + $this->getLock($path, Lock::WRITE); + } + $result = $this->storage->unlink($path); + } + catch(\Exception $originalException) { + // Need to release the lock before more operations happen in upstream exception handlers + $this->releaseLock($path, Lock::WRITE); + throw $originalException; + } + $this->releaseLock($path, Lock::WRITE); + return $result; + } + + +}
\ No newline at end of file diff --git a/lib/private/helper.php b/lib/private/helper.php index e9ca036a32c..9ac07bbd3b1 100644 --- a/lib/private/helper.php +++ b/lib/private/helper.php @@ -946,7 +946,7 @@ class OC_Helper { $quota = 0; // TODO: need a better way to get total space from storage $storage = $rootInfo->getStorage(); - if ($storage instanceof \OC\Files\Storage\Wrapper\Quota) { + if ($storage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) { $quota = $storage->getQuota(); } $free = $storage->free_space(''); diff --git a/lib/private/log/owncloud.php b/lib/private/log/owncloud.php index 3590bbd436d..08d0b7d5f93 100644 --- a/lib/private/log/owncloud.php +++ b/lib/private/log/owncloud.php @@ -28,6 +28,7 @@ class OC_Log_Owncloud { static protected $logFile; + static protected $reqId; /** * Init class data @@ -68,8 +69,20 @@ class OC_Log_Owncloud { $timezone = new DateTimeZone('UTC'); } $time = new DateTime(null, $timezone); - // remove username/passswords from URLs before writing the to the log file - $entry=array('app'=>$app, 'message'=>$message, 'level'=>$level, 'time'=> $time->format($format)); + // remove username/passwords from URLs before writing the to the log file + $time = $time->format($format); + if($minLevel == OC_Log::DEBUG) { + if(empty(self::$reqId)) { + self::$reqId = uniqid(); + } + $reqId = self::$reqId; + $url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '--'; + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '--'; + $entry = compact('reqId', 'app', 'message', 'level', 'time', 'method', 'url'); + } + else { + $entry = compact('app', 'message', 'level', 'time'); + } $entry = json_encode($entry); $handle = @fopen(self::$logFile, 'a'); @chmod(self::$logFile, 0640); diff --git a/lib/private/util.php b/lib/private/util.php index 306e37b9478..da67dbcee54 100755 --- a/lib/private/util.php +++ b/lib/private/util.php @@ -53,7 +53,7 @@ class OC_Util { //if we aren't logged in, there is no use to set up the filesystem if( $user != "" ) { - \OC\Files\Filesystem::addStorageWrapper(function($mountPoint, $storage){ + \OC\Files\Filesystem::addStorageWrapper('oc_quota', function($mountPoint, $storage){ // set up quota for home storages, even for other users // which can happen when using sharing @@ -71,6 +71,16 @@ class OC_Util { return $storage; }); + // Set up flock + \OC\Files\Filesystem::addStorageWrapper('oc_flock', function($mountPoint, /** @var \OC\Files\Storage\Storage|null $storage */ $storage){ + // lock files on all local storage + if ($storage instanceof \OC\Files\Storage\Storage && $storage->isLocal()) { + return new \OC\Files\Storage\Wrapper\LockingWrapper(array('storage' => $storage)); + } else { + return $storage; + } + }); + $userDir = '/'.$user.'/files'; $userRoot = OC_User::getHome($user); $userDirectory = $userRoot . '/files'; diff --git a/lib/public/files/lock.php b/lib/public/files/lock.php new file mode 100644 index 00000000000..34d307e8939 --- /dev/null +++ b/lib/public/files/lock.php @@ -0,0 +1,63 @@ +<?php +/** + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Files; + +/** + * Class Lock + * @package OC\Files + */ +interface Lock { + const READ = 1; + const WRITE = 2; + + /** + * Constructor for the lock instance + * @param string $path Absolute pathname for a local file on which to obtain a lock + */ + public function __construct($path); + + + /** + * Add a lock of a specific type to the stack + * @param integer $lockType A constant representing the type of lock to queue + * @param null|resource $existingHandle An existing file handle from an fopen() + * @throws LockNotAcquiredException + */ + public function addLock($lockType, $existingHandle = null); + + /** + * Release locks on handles and files + */ + public function release($lockType); + + + /** + * Get the lock file associated to a file + * @param string $filename The filename of the file to create a lock file for + * @return string The filename of the lock file + */ + public static function getLockFile($filename); + + /** + * Release all queued locks on the file + * @return bool + */ + public function releaseAll(); + +}
\ No newline at end of file diff --git a/lib/public/files/locknotacquiredexception.php b/lib/public/files/locknotacquiredexception.php new file mode 100644 index 00000000000..9fb70e7cbe2 --- /dev/null +++ b/lib/public/files/locknotacquiredexception.php @@ -0,0 +1,47 @@ +<?php +/** + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * Public interface of ownCloud for apps to use. + * Files/LockNotAcquiredException class + */ + +// use OCP namespace for all classes that are considered public. +// This means that they should be used by apps instead of the internal ownCloud classes +namespace OCP\Files; + +/** + * Exception for a file that is locked + */ +class LockNotAcquiredException extends \Exception { + /** @var string $path The path that could not be locked */ + public $path; + + /** @var integer $lockType The type of the lock that was attempted */ + public $lockType; + + public function __construct($path, $lockType, $code = 0, \Exception $previous = null) { + $message = \OC_L10N::get('core')->t('Could not obtain lock type %d on "%s".', array($lockType, $path)); + parent::__construct($message, $code, $previous); + } + + // custom string representation of object + public function __toString() { + return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; + } +}
\ No newline at end of file diff --git a/tests/lib/files/filesystem.php b/tests/lib/files/filesystem.php index 53f528af793..930a252bcb2 100644 --- a/tests/lib/files/filesystem.php +++ b/tests/lib/files/filesystem.php @@ -173,7 +173,7 @@ class Filesystem extends \PHPUnit_Framework_TestCase { $homeMount = \OC\Files\Filesystem::getStorage('/' . $userId . '/'); - $this->assertInstanceOf('\OC\Files\Storage\Local', $homeMount); + $this->assertTrue($homeMount->instanceOfStorage('\OC\Files\Storage\Local')); $this->assertEquals('local::' . $datadir . '/' . $userId . '/', $homeMount->getId()); } @@ -189,7 +189,7 @@ class Filesystem extends \PHPUnit_Framework_TestCase { $homeMount = \OC\Files\Filesystem::getStorage('/' . $userId . '/'); - $this->assertInstanceOf('\OC\Files\Storage\Home', $homeMount); + $this->assertTrue($homeMount->instanceOfStorage('\OC\Files\Storage\Home')); $this->assertEquals('home::' . $userId, $homeMount->getId()); \OC_User::deleteUser($userId); @@ -214,7 +214,7 @@ class Filesystem extends \PHPUnit_Framework_TestCase { $homeMount = \OC\Files\Filesystem::getStorage('/' . $userId . '/'); - $this->assertInstanceOf('\OC\Files\Storage\Home', $homeMount); + $this->assertTrue($homeMount->instanceOfStorage('\OC\Files\Storage\Home')); $this->assertEquals('local::' . $datadir . '/' . $userId . '/', $homeMount->getId()); \OC_User::deleteUser($userId); @@ -244,7 +244,7 @@ class Filesystem extends \PHPUnit_Framework_TestCase { \OC\Files\Filesystem::getMountPoint('/' . $userId . '/cache') ); list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath('/' . $userId . '/cache'); - $this->assertInstanceOf('\OC\Files\Storage\Home', $storage); + $this->assertTrue($storage->instanceOfStorage('\OC\Files\Storage\Home')); $this->assertEquals('cache', $internalPath); \OC_User::deleteUser($userId); @@ -271,7 +271,7 @@ class Filesystem extends \PHPUnit_Framework_TestCase { \OC\Files\Filesystem::getMountPoint('/' . $userId . '/cache') ); list($storage, $internalPath) = \OC\Files\Filesystem::resolvePath('/' . $userId . '/cache'); - $this->assertInstanceOf('\OC\Files\Storage\Local', $storage); + $this->assertTrue($storage->instanceOfStorage('\OC\Files\Storage\Local')); $this->assertEquals('', $internalPath); \OC_User::deleteUser($userId); diff --git a/tests/lib/files/mount/mount.php b/tests/lib/files/mount/mount.php index b057204ad35..c3d33e0870b 100644 --- a/tests/lib/files/mount/mount.php +++ b/tests/lib/files/mount/mount.php @@ -35,7 +35,7 @@ class Mount extends \PHPUnit_Framework_TestCase { }; $loader = new Loader(); - $loader->addStorageWrapper($wrapper); + $loader->addStorageWrapper('test_wrapper', $wrapper); $storage = $this->getMockBuilder('\OC\Files\Storage\Temporary') ->disableOriginalConstructor() diff --git a/tests/lib/util.php b/tests/lib/util.php index 0bafb96cabd..aaa47f033de 100644 --- a/tests/lib/util.php +++ b/tests/lib/util.php @@ -154,7 +154,7 @@ class Test_Util extends PHPUnit_Framework_TestCase { $userMount = \OC\Files\Filesystem::getMountManager()->find('/' . $user1 . '/'); $this->assertNotNull($userMount); - $this->assertInstanceOf('\OC\Files\Storage\Wrapper\Quota', $userMount->getStorage()); + $this->assertTrue($userMount->getStorage()->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')); // ensure that root wasn't wrapped $rootMount = \OC\Files\Filesystem::getMountManager()->find('/'); |