diff --git a/apps/files_sharing/lib/sharedstorage.php b/apps/files_sharing/lib/sharedstorage.php index ee86787c18..bf61dda371 100644 --- a/apps/files_sharing/lib/sharedstorage.php +++ b/apps/files_sharing/lib/sharedstorage.php @@ -31,6 +31,9 @@ namespace OC\Files\Storage; use OC\Files\Filesystem; use OCA\Files_Sharing\ISharedStorage; +use OCA\Files_Sharing\Propagator; +use OCA\Files_Sharing\SharedMount; +use OCP\Lock\ILockingProvider; /** * Convert target path to source path and pass the function call to the correct storage provider @@ -608,4 +611,38 @@ class Shared extends \OC\Files\Storage\Common implements ISharedStorage { list($targetStorage, $targetInternalPath) = $this->resolvePath($targetInternalPath); return $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider) { + /** @var \OCP\Files\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->resolvePath($path); + $targetStorage->acquireLock($targetInternalPath, $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider) { + /** @var \OCP\Files\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->resolvePath($path); + $targetStorage->releaseLock($targetInternalPath, $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function changeLock($path, $type, ILockingProvider $provider) { + /** @var \OCP\Files\Storage $targetStorage */ + list($targetStorage, $targetInternalPath) = $this->resolvePath($path); + $targetStorage->changeLock($targetInternalPath, $type, $provider); + } } diff --git a/config/config.sample.php b/config/config.sample.php index ed86dd9413..23a27fa3ec 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1026,6 +1026,25 @@ $CONFIG = array( */ 'max_filesize_animated_gifs_public_sharing' => 10, + +/** + * Enables the EXPERIMENTAL file locking. + * This is disabled by default as it is experimental. + * + * Prevents concurrent processes to access the same files + * at the same time. Can help prevent side effects that would + * be caused by concurrent operations. + * + * WARNING: EXPERIMENTAL + */ +'filelocking.enabled' => false, + +/** + * Memory caching backend for file locking + * Because most memcache backends can clean values without warning using redis is recommended + */ +'memcache.locking' => '\OC\Memcache\Redis', + /** * This entry is just here to show a warning in case somebody copied the sample * configuration. DO NOT ADD THIS SWITCH TO YOUR CONFIGURATION! diff --git a/lib/base.php b/lib/base.php index b7f19c9640..09159dc22a 100644 --- a/lib/base.php +++ b/lib/base.php @@ -666,6 +666,8 @@ class OC { //make sure temporary files are cleaned up $tmpManager = \OC::$server->getTempManager(); register_shutdown_function(array($tmpManager, 'clean')); + $lockProvider = \OC::$server->getLockingProvider(); + register_shutdown_function(array($lockProvider, 'releaseAll')); if ($systemConfig->getValue('installed', false) && !self::checkUpgrade(false)) { if (\OC::$server->getConfig()->getAppValue('core', 'backgroundjobs_mode', 'ajax') == 'ajax') { diff --git a/lib/private/connector/sabre/directory.php b/lib/private/connector/sabre/directory.php index ef35b300ea..82e1b55d76 100644 --- a/lib/private/connector/sabre/directory.php +++ b/lib/private/connector/sabre/directory.php @@ -28,6 +28,8 @@ namespace OC\Connector\Sabre; use OC\Connector\Sabre\Exception\InvalidPath; +use OC\Connector\Sabre\Exception\FileLocked; +use OCP\Lock\LockedException; class Directory extends \OC\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota { @@ -102,6 +104,8 @@ class Directory extends \OC\Connector\Sabre\Node return $node->put($data); } catch (\OCP\Files\StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } } @@ -127,6 +131,8 @@ class Directory extends \OC\Connector\Sabre\Node throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); } catch (\OCP\Files\InvalidPathException $ex) { throw new InvalidPath($ex->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } } @@ -211,11 +217,14 @@ class Directory extends \OC\Connector\Sabre\Node throw new \Sabre\DAV\Exception\Forbidden(); } - if (!$this->fileView->rmdir($this->path)) { - // assume it wasn't possible to remove due to permission issue - 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 \Sabre\DAV\Exception\Forbidden(); + } + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } - } /** diff --git a/lib/private/connector/sabre/exception/filelocked.php b/lib/private/connector/sabre/exception/filelocked.php index 2405059bfb..1657a7ae37 100644 --- a/lib/private/connector/sabre/exception/filelocked.php +++ b/lib/private/connector/sabre/exception/filelocked.php @@ -42,6 +42,6 @@ class FileLocked extends \Sabre\DAV\Exception { */ public function getHTTPCode() { - return 503; + return 423; } } diff --git a/lib/private/connector/sabre/file.php b/lib/private/connector/sabre/file.php index 8e4460ef3b..3e1b29a4f2 100644 --- a/lib/private/connector/sabre/file.php +++ b/lib/private/connector/sabre/file.php @@ -45,6 +45,8 @@ use OCP\Files\InvalidPathException; use OCP\Files\LockNotAcquiredException; use OCP\Files\NotPermittedException; use OCP\Files\StorageNotAvailableException; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; use Sabre\DAV\Exception; use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Forbidden; @@ -79,6 +81,7 @@ class File extends Node implements IFile { * @throws Exception * @throws EntityTooLarge * @throws ServiceUnavailable + * @throws FileLocked * @return string|null */ public function put($data) { @@ -110,6 +113,12 @@ class File extends Node implements IFile { $partFilePath = $this->path; } + try { + $this->fileView->lockFile($this->path, ILockingProvider::LOCK_EXCLUSIVE); + } 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); @@ -232,6 +241,8 @@ class File extends Node implements IFile { throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage()); } + $this->fileView->unlockFile($this->path, ILockingProvider::LOCK_EXCLUSIVE); + return '"' . $this->info->getEtag() . '"'; } @@ -252,6 +263,8 @@ class File extends Node implements IFile { throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage()); } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable("Failed to open file: " . $e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } } @@ -273,6 +286,8 @@ class File extends Node implements IFile { } } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } } @@ -378,6 +393,8 @@ class File extends Node implements IFile { return $info->getEtag(); } catch (StorageNotAvailableException $e) { throw new ServiceUnavailable("Failed to put file: " . $e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } } diff --git a/lib/private/connector/sabre/filesplugin.php b/lib/private/connector/sabre/filesplugin.php index 3c79f5a7a2..09d931be60 100644 --- a/lib/private/connector/sabre/filesplugin.php +++ b/lib/private/connector/sabre/filesplugin.php @@ -89,6 +89,12 @@ class FilesPlugin extends \Sabre\DAV\ServerPlugin { $this->server->on('afterBind', array($this, 'sendFileIdHeader')); $this->server->on('afterWriteContent', array($this, 'sendFileIdHeader')); $this->server->on('afterMethod:GET', [$this,'httpGet']); + $this->server->on('afterResponse', function($request, ResponseInterface $response) { + $body = $response->getBody(); + if (is_resource($body)) { + fclose($body); + } + }); } /** diff --git a/lib/private/connector/sabre/objecttree.php b/lib/private/connector/sabre/objecttree.php index 17d9aff8f6..c56fd7ee4d 100644 --- a/lib/private/connector/sabre/objecttree.php +++ b/lib/private/connector/sabre/objecttree.php @@ -26,10 +26,12 @@ namespace OC\Connector\Sabre; use OC\Connector\Sabre\Exception\InvalidPath; +use OC\Connector\Sabre\Exception\FileLocked; use OC\Files\FileInfo; use OC\Files\Mount\MoveableMount; use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; +use OCP\Lock\LockedException; class ObjectTree extends \Sabre\DAV\Tree { @@ -221,8 +223,10 @@ class ObjectTree extends \Sabre\DAV\Tree { if (!$renameOkay) { throw new \Sabre\DAV\Exception\Forbidden(''); } - } catch (\OCP\Files\StorageNotAvailableException $e) { + } catch (StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } $this->markDirty($sourceDir); @@ -258,8 +262,10 @@ class ObjectTree extends \Sabre\DAV\Tree { try { $this->fileView->copy($source, $destination); - } catch (\OCP\Files\StorageNotAvailableException $e) { + } catch (StorageNotAvailableException $e) { throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage()); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); } list($destinationDir,) = \Sabre\HTTP\URLUtil::splitPath($destination); diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php index 1257a14dd0..847cb8492f 100644 --- a/lib/private/files/storage/common.php +++ b/lib/private/files/storage/common.php @@ -43,6 +43,7 @@ use OCP\Files\FileNameTooLongException; use OCP\Files\InvalidCharacterInPathException; use OCP\Files\InvalidPathException; use OCP\Files\ReservedWordException; +use OCP\Lock\ILockingProvider; /** * Storage backend class for providing common filesystem operation methods @@ -621,4 +622,32 @@ abstract class Common implements Storage { return $data; } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider) { + $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider) { + $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function changeLock($path, $type, ILockingProvider $provider) { + $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); + } } diff --git a/lib/private/files/storage/storage.php b/lib/private/files/storage/storage.php index 07b5633c90..bd809099e1 100644 --- a/lib/private/files/storage/storage.php +++ b/lib/private/files/storage/storage.php @@ -21,6 +21,7 @@ */ namespace OC\Files\Storage; +use OCP\Lock\ILockingProvider; /** * Provide a common interface to all different storage options @@ -76,4 +77,26 @@ interface Storage extends \OCP\Files\Storage { */ public function getMetaData($path); + /** + * @param string $path The path of the file to acquire the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider); + + /** + * @param string $path The path of the file to release the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider); + + /** + * @param string $path The path of the file to change the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function changeLock($path, $type, ILockingProvider $provider); } diff --git a/lib/private/files/storage/wrapper/jail.php b/lib/private/files/storage/wrapper/jail.php index b86b4e6405..2857e74de4 100644 --- a/lib/private/files/storage/wrapper/jail.php +++ b/lib/private/files/storage/wrapper/jail.php @@ -23,6 +23,7 @@ namespace OC\Files\Storage\Wrapper; use OC\Files\Cache\Wrapper\CacheJail; +use OCP\Lock\ILockingProvider; /** * Jail to a subdirectory of the wrapped storage @@ -424,4 +425,32 @@ class Jail extends Wrapper { public function getETag($path) { return $this->storage->getETag($this->getSourcePath($path)); } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider) { + $this->storage->acquireLock($this->getSourcePath($path), $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider) { + $this->storage->releaseLock($this->getSourcePath($path), $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function changeLock($path, $type, ILockingProvider $provider) { + $this->storage->changeLock($this->getSourcePath($path), $type, $provider); + } } diff --git a/lib/private/files/storage/wrapper/wrapper.php b/lib/private/files/storage/wrapper/wrapper.php index 14024addec..d1414880be 100644 --- a/lib/private/files/storage/wrapper/wrapper.php +++ b/lib/private/files/storage/wrapper/wrapper.php @@ -25,6 +25,7 @@ namespace OC\Files\Storage\Wrapper; use OCP\Files\InvalidPathException; +use OCP\Lock\ILockingProvider; class Wrapper implements \OC\Files\Storage\Storage { /** @@ -541,4 +542,32 @@ class Wrapper implements \OC\Files\Storage\Storage { public function getMetaData($path) { return $this->storage->getMetaData($path); } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider) { + $this->storage->acquireLock($path, $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider) { + $this->storage->releaseLock($path, $type, $provider); + } + + /** + * @param string $path + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function changeLock($path, $type, ILockingProvider $provider) { + $this->storage->changeLock($path, $type, $provider); + } } diff --git a/lib/private/files/view.php b/lib/private/files/view.php index 63af2b616c..b98842f5eb 100644 --- a/lib/private/files/view.php +++ b/lib/private/files/view.php @@ -41,12 +41,15 @@ namespace OC\Files; +use Icewind\Streams\CallbackWrapper; use OC\Files\Cache\Updater; use OC\Files\Mount\MoveableMount; use OCP\Files\FileNameTooLongException; use OCP\Files\InvalidCharacterInPathException; use OCP\Files\InvalidPathException; use OCP\Files\ReservedWordException; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; /** * Class to provide access to ownCloud filesystem via a "view", and methods for @@ -71,6 +74,11 @@ class View { /** @var \OC\Files\Cache\Updater */ protected $updater; + /** + * @var \OCP\Lock\ILockingProvider + */ + private $lockingProvider; + /** * @param string $root * @throws \Exception If $root contains an invalid path @@ -79,12 +87,13 @@ class View { if (is_null($root)) { throw new \InvalidArgumentException('Root can\'t be null'); } - if(!Filesystem::isValidPath($root)) { + if (!Filesystem::isValidPath($root)) { throw new \Exception(); } $this->fakeRoot = $root; $this->updater = new Updater($this); + $this->lockingProvider = \OC::$server->getLockingProvider(); } public function getAbsolutePath($path = '/') { @@ -137,7 +146,7 @@ class View { return $path; } - if (rtrim($path,'/') === rtrim($this->fakeRoot, '/')) { + if (rtrim($path, '/') === rtrim($this->fakeRoot, '/')) { return '/'; } @@ -525,6 +534,7 @@ class View { and !Filesystem::isFileBlacklisted($path) ) { $path = $this->getRelativePath($absolutePath); + $exists = $this->file_exists($path); $run = true; if ($this->shouldEmitHooks($path)) { @@ -604,6 +614,10 @@ class View { if ($path1 == null or $path2 == null) { return false; } + + $this->lockFile($path1, ILockingProvider::LOCK_SHARED); + $this->lockFile($path2, ILockingProvider::LOCK_SHARED); + $run = true; if ($this->shouldEmitHooks() && (Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2))) { // if it was a rename from a part file to a regular file it was a write and not a rename operation @@ -629,6 +643,9 @@ class View { $internalPath1 = $mount1->getInternalPath($absolutePath1); $internalPath2 = $mount2->getInternalPath($absolutePath2); + $this->changeLock($path1, ILockingProvider::LOCK_EXCLUSIVE); + $this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE); + if ($internalPath1 === '' and $mount1 instanceof MoveableMount) { if ($this->isTargetAllowed($absolutePath2)) { /** @@ -649,6 +666,15 @@ class View { } else { $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); } + + $this->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); + $this->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); + + if ($internalPath1 === '' and $mount1 instanceof MoveableMount) { + // since $path2 now points to a different storage we need to unlock the path on the old storage separately + $storage2->releaseLock($internalPath2, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider); + } + if ((Cache\Scanner::isPartialFile($path1) && !Cache\Scanner::isPartialFile($path2)) && $result !== false) { // if it was a rename from a part file to a regular file it was a write and not a rename operation $this->updater->update($path2); @@ -676,6 +702,8 @@ class View { } return $result; } else { + $this->unlockFile($path1, ILockingProvider::LOCK_SHARED); + $this->unlockFile($path2, ILockingProvider::LOCK_SHARED); return false; } } else { @@ -707,6 +735,10 @@ class View { return false; } $run = true; + + $this->lockFile($path2, ILockingProvider::LOCK_SHARED); + $this->lockFile($path1, ILockingProvider::LOCK_SHARED); + $exists = $this->file_exists($path2); if ($this->shouldEmitHooks()) { \OC_Hook::emit( @@ -727,6 +759,9 @@ class View { $internalPath1 = $mount1->getInternalPath($absolutePath1); $storage2 = $mount2->getStorage(); $internalPath2 = $mount2->getInternalPath($absolutePath2); + + $this->changeLock($path2, ILockingProvider::LOCK_EXCLUSIVE); + if ($mount1->getMountPoint() == $mount2->getMountPoint()) { if ($storage1) { $result = $storage1->copy($internalPath1, $internalPath2); @@ -737,6 +772,10 @@ class View { $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2); } $this->updater->update($path2); + + $this->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); + $this->unlockFile($path1, ILockingProvider::LOCK_SHARED); + if ($this->shouldEmitHooks() && $result !== false) { \OC_Hook::emit( Filesystem::CLASSNAME, @@ -750,6 +789,8 @@ class View { } return $result; } else { + $this->unlockFile($path2, ILockingProvider::LOCK_SHARED); + $this->unlockFile($path1, ILockingProvider::LOCK_SHARED); return false; } } else { @@ -929,13 +970,30 @@ class View { return false; } + if(in_array('write', $hooks) || in_array('delete', $hooks) || in_array('read', $hooks)) { + // always a shared lock during pre-hooks so the hook can read the file + $this->lockFile($path, ILockingProvider::LOCK_SHARED); + } + $run = $this->runHooks($hooks, $path); list($storage, $internalPath) = Filesystem::resolvePath($absolutePath . $postFix); if ($run and $storage) { - if (!is_null($extraParam)) { - $result = $storage->$operation($internalPath, $extraParam); - } else { - $result = $storage->$operation($internalPath); + if (in_array('write', $hooks) || in_array('delete', $hooks)) { + $this->changeLock($path, ILockingProvider::LOCK_EXCLUSIVE); + } + try { + if (!is_null($extraParam)) { + $result = $storage->$operation($internalPath, $extraParam); + } else { + $result = $storage->$operation($internalPath); + } + } catch (\Exception $e) { + if (in_array('write', $hooks) || in_array('delete', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE); + } else if (in_array('read', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_SHARED); + } + throw $e; } if (in_array('delete', $hooks) and $result) { @@ -948,12 +1006,31 @@ class View { $this->updater->update($path, $extraParam); } + if ($operation === 'fopen' and $result) { + $result = CallbackWrapper::wrap($result, null, null, function () use ($hooks, $path) { + if (in_array('write', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE); + } else if (in_array('read', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_SHARED); + } + }); + } else { + if (in_array('write', $hooks) || in_array('delete', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_EXCLUSIVE); + } else if (in_array('read', $hooks)) { + $this->unlockFile($path, ILockingProvider::LOCK_SHARED); + } + } + + if ($this->shouldEmitHooks($path) && $result !== false) { if ($operation != 'fopen') { //no post hooks for fopen, the file stream is still open $this->runHooks($hooks, $path, true); } } return $result; + } else { + $this->unlockFile($path, ILockingProvider::LOCK_SHARED); } } return null; @@ -1553,4 +1630,114 @@ class View { throw new InvalidPathException($l10n->t('File name is too long')); } } + + /** + * get all parent folders of $path + * + * @param string $path + * @return string[] + */ + private function getParents($path) { + $path = trim($path, '/'); + if (!$path) { + return []; + } + + $parts = explode('/', $path); + + // remove the single file + array_pop($parts); + $result = array('/'); + $resultPath = ''; + foreach ($parts as $part) { + if ($part) { + $resultPath .= '/' . $part; + $result[] = $resultPath; + } + } + return $result; + } + + /** + * @param string $path the path of the file to lock, relative to the view + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + private function lockPath($path, $type) { + $mount = $this->getMount($path); + if ($mount) { + $mount->getStorage()->acquireLock( + $mount->getInternalPath( + $this->getAbsolutePath($path) + ), + $type, + $this->lockingProvider + ); + } + } + + /** + * @param string $path the path of the file to lock, relative to the view + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + private function changeLock($path, $type) { + $mount = $this->getMount($path); + if ($mount) { + $mount->getStorage()->changeLock( + $mount->getInternalPath( + $this->getAbsolutePath($path) + ), + $type, + $this->lockingProvider + ); + } + } + + /** + * @param string $path the path of the file to unlock, relative to the view + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + private function unlockPath($path, $type) { + $mount = $this->getMount($path); + if ($mount) { + $mount->getStorage()->releaseLock( + $mount->getInternalPath( + $this->getAbsolutePath($path) + ), + $type, + $this->lockingProvider + ); + } + } + + /** + * Lock a path and all its parents up to the root of the view + * + * @param string $path the path of the file to lock relative to the view + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + public function lockFile($path, $type) { + $path = '/' . trim($path, '/'); + $this->lockPath($path, $type); + + $parents = $this->getParents($path); + foreach ($parents as $parent) { + $this->lockPath($parent, ILockingProvider::LOCK_SHARED); + } + } + + /** + * Unlock a path and all its parents up to the root of the view + * + * @param string $path the path of the file to lock relative to the view + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + */ + public function unlockFile($path, $type) { + $path = rtrim($path, '/'); + $this->unlockPath($path, $type); + + $parents = $this->getParents($path); + foreach ($parents as $parent) { + $this->unlockPath($parent, ILockingProvider::LOCK_SHARED); + } + } } diff --git a/lib/private/lock/memcachelockingprovider.php b/lib/private/lock/memcachelockingprovider.php index 9c8c723546..3f32ab1d8c 100644 --- a/lib/private/lock/memcachelockingprovider.php +++ b/lib/private/lock/memcachelockingprovider.php @@ -31,6 +31,11 @@ class MemcacheLockingProvider implements ILockingProvider { */ private $memcache; + private $acquiredLocks = [ + 'shared' => [], + 'exclusive' => [] + ]; + /** * @param \OCP\IMemcache $memcache */ @@ -64,11 +69,16 @@ class MemcacheLockingProvider implements ILockingProvider { if (!$this->memcache->inc($path)) { throw new LockedException($path); } + if (!isset($this->acquiredLocks['shared'][$path])) { + $this->acquiredLocks['shared'][$path] = 0; + } + $this->acquiredLocks['shared'][$path]++; } else { $this->memcache->add($path, 0); if (!$this->memcache->cas($path, 0, 'exclusive')) { throw new LockedException($path); } + $this->acquiredLocks['exclusive'][$path] = true; } } @@ -78,9 +88,55 @@ class MemcacheLockingProvider implements ILockingProvider { */ public function releaseLock($path, $type) { if ($type === self::LOCK_SHARED) { - $this->memcache->dec($path); + if (isset($this->acquiredLocks['shared'][$path]) and $this->acquiredLocks['shared'][$path] > 0) { + $this->memcache->dec($path); + $this->acquiredLocks['shared'][$path]--; + } } else if ($type === self::LOCK_EXCLUSIVE) { $this->memcache->cas($path, 'exclusive', 0); + unset($this->acquiredLocks['exclusive'][$path]); + } + } + + /** + * Change the type of an existing lock + * + * @param string $path + * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE + * @throws \OCP\Lock\LockedException + */ + public function changeLock($path, $targetType) { + if ($targetType === self::LOCK_SHARED) { + if (!$this->memcache->cas($path, 'exclusive', 1)) { + throw new LockedException($path); + } + unset($this->acquiredLocks['exclusive'][$path]); + if (!isset($this->acquiredLocks['shared'][$path])) { + $this->acquiredLocks['shared'][$path] = 0; + } + $this->acquiredLocks['shared'][$path]++; + } else if ($targetType === self::LOCK_EXCLUSIVE) { + // we can only change a shared lock to an exclusive if there's only a single owner of the shared lock + if (!$this->memcache->cas($path, 1, 'exclusive')) { + throw new LockedException($path); + } + $this->acquiredLocks['exclusive'][$path] = true; + $this->acquiredLocks['shared'][$path]--; + } + } + + /** + * release all lock acquired by this instance + */ + public function releaseAll() { + foreach ($this->acquiredLocks['shared'] as $path => $count) { + for ($i = 0; $i < $count; $i++) { + $this->releaseLock($path, self::LOCK_SHARED); + } + } + + foreach ($this->acquiredLocks['exclusive'] as $path => $hasLock) { + $this->releaseLock($path, self::LOCK_EXCLUSIVE); } } } diff --git a/lib/private/lock/nooplockingprovider.php b/lib/private/lock/nooplockingprovider.php new file mode 100644 index 0000000000..4f33b88155 --- /dev/null +++ b/lib/private/lock/nooplockingprovider.php @@ -0,0 +1,67 @@ + + * + * @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 + * + */ + +namespace OC\Lock; + +use OCP\Lock\ILockingProvider; + +/** + * Locking provider that does nothing. + * + * To be used when locking is disabled. + */ +class NoopLockingProvider implements ILockingProvider { + + /** + * {@inheritdoc} + */ + public function isLocked($path, $type) { + return false; + } + + /** + * {@inheritdoc} + */ + public function acquireLock($path, $type) { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function releaseLock($path, $type) { + // do nothing + } + + /**1 + * {@inheritdoc} + */ + public function releaseAll() { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function changeLock($path, $targetType) { + // do nothing + } +} diff --git a/lib/private/memcache/factory.php b/lib/private/memcache/factory.php index bbfd775c77..6e9aaa11d8 100644 --- a/lib/private/memcache/factory.php +++ b/lib/private/memcache/factory.php @@ -46,13 +46,19 @@ class Factory implements ICacheFactory { */ private $distributedCacheClass; + /** + * @var string $lockingCacheClass + */ + private $lockingCacheClass; + /** * @param string $globalPrefix * @param string|null $localCacheClass * @param string|null $distributedCacheClass + * @param string|null $lockingCacheClass */ public function __construct($globalPrefix, - $localCacheClass = null, $distributedCacheClass = null) + $localCacheClass = null, $distributedCacheClass = null, $lockingCacheClass = null) { $this->globalPrefix = $globalPrefix; @@ -62,8 +68,23 @@ class Factory implements ICacheFactory { if (!($distributedCacheClass && $distributedCacheClass::isAvailable())) { $distributedCacheClass = $localCacheClass; } + if (!($lockingCacheClass && $lockingCacheClass::isAvailable())) { + // dont fallback since the fallback might not be suitable for storing lock + $lockingCacheClass = '\OC\Memcache\Null'; + } $this->localCacheClass = $localCacheClass; $this->distributedCacheClass = $distributedCacheClass; + $this->lockingCacheClass = $lockingCacheClass; + } + + /** + * create a cache instance for storing locks + * + * @param string $prefix + * @return \OCP\IMemcache + */ + public function createLocking($prefix = '') { + return new $this->lockingCacheClass($this->globalPrefix . '/' . $prefix); } /** diff --git a/lib/private/memcache/null.php b/lib/private/memcache/null.php index 3b8ce0b42d..982f7edc80 100644 --- a/lib/private/memcache/null.php +++ b/lib/private/memcache/null.php @@ -22,7 +22,7 @@ namespace OC\Memcache; -class Null extends Cache { +class Null extends Cache implements \OCP\IMemcache { public function get($key) { return null; } @@ -39,6 +39,22 @@ class Null extends Cache { return true; } + public function add($key, $value, $ttl = 0) { + return true; + } + + public function inc($key, $step = 1) { + return true; + } + + public function dec($key, $step = 1) { + return true; + } + + public function cas($key, $old, $new) { + return true; + } + public function clear($prefix = '') { return true; } diff --git a/lib/private/server.php b/lib/private/server.php index aeea4a6485..aea5be5afa 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -43,6 +43,8 @@ use OC\Command\AsyncBus; use OC\Diagnostics\NullQueryLogger; use OC\Diagnostics\EventLogger; use OC\Diagnostics\QueryLogger; +use OC\Lock\MemcacheLockingProvider; +use OC\Lock\NoopLockingProvider; use OC\Mail\Mailer; use OC\Memcache\ArrayCache; use OC\Http\Client\ClientService; @@ -223,7 +225,7 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('MemCacheFactory', function (Server $c) { $config = $c->getConfig(); - if($config->getSystemValue('installed', false)) { + if($config->getSystemValue('installed', false) && !(defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { $v = \OC_App::getAppVersions(); $v['core'] = implode('.', \OC_Util::getVersion()); $version = implode(',', $v); @@ -232,11 +234,13 @@ class Server extends SimpleContainer implements IServerContainer { $prefix = md5($instanceId.'-'.$version.'-'.$path); return new \OC\Memcache\Factory($prefix, $config->getSystemValue('memcache.local', null), - $config->getSystemValue('memcache.distributed', null) + $config->getSystemValue('memcache.distributed', null), + $config->getSystemValue('memcache.locking', null) ); } return new \OC\Memcache\Factory('', + new ArrayCache(), new ArrayCache(), new ArrayCache() ); @@ -420,6 +424,17 @@ class Server extends SimpleContainer implements IServerContainer { $this->getLogger() ); }); + $this->registerService('LockingProvider', function (Server $c) { + if ($c->getConfig()->getSystemValue('filelocking.enabled', false) or (defined('PHPUNIT_RUN') && PHPUNIT_RUN)) { + /** @var \OC\Memcache\Factory $memcacheFactory */ + $memcacheFactory = $c->getMemCacheFactory(); + $memcache = $memcacheFactory->createLocking('lock'); + if (!($memcache instanceof \OC\Memcache\Null)) { + return new MemcacheLockingProvider($memcache); + } + } + return new NoopLockingProvider(); + }); } /** @@ -908,4 +923,14 @@ class Server extends SimpleContainer implements IServerContainer { public function getTrustedDomainHelper() { return $this->query('TrustedDomainHelper'); } + + /** + * Get the locking provider + * + * @return \OCP\Lock\ILockingProvider + * @since 8.1.0 + */ + public function getLockingProvider() { + return $this->query('LockingProvider'); + } } diff --git a/lib/public/files/storage.php b/lib/public/files/storage.php index b89fb49a4b..ee160c50e8 100644 --- a/lib/public/files/storage.php +++ b/lib/public/files/storage.php @@ -33,6 +33,7 @@ // This means that they should be used by apps instead of the internal ownCloud classes namespace OCP\Files; use OCP\Files\InvalidPathException; +use OCP\Lock\ILockingProvider; /** * Provide a common interface to all different storage options @@ -413,4 +414,27 @@ interface Storage { * @since 8.1.0 */ public function moveFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath); + + /** + * @param string $path The path of the file to acquire the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function acquireLock($path, $type, ILockingProvider $provider); + + /** + * @param string $path The path of the file to acquire the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + */ + public function releaseLock($path, $type, ILockingProvider $provider); + + /** + * @param string $path The path of the file to change the lock for + * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE + * @param \OCP\Lock\ILockingProvider $provider + * @throws \OCP\Lock\LockedException + */ + public function changeLock($path, $type, ILockingProvider $provider); } diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php index 5dd545aed3..8f0bede6cc 100644 --- a/lib/public/iservercontainer.php +++ b/lib/public/iservercontainer.php @@ -413,4 +413,12 @@ interface IServerContainer { * @since 8.1.0 */ public function getMailer(); + + /** + * Get the locking provider + * + * @return \OCP\Lock\ILockingProvider + * @since 8.1.0 + */ + public function getLockingProvider(); } diff --git a/lib/public/lock/ilockingprovider.php b/lib/public/lock/ilockingprovider.php index 0b17580faa..6a963b9d7a 100644 --- a/lib/public/lock/ilockingprovider.php +++ b/lib/public/lock/ilockingprovider.php @@ -44,4 +44,18 @@ interface ILockingProvider { * @param int $type self::LOCK_SHARED or self::LOCK_EXCLUSIVE */ public function releaseLock($path, $type); + + /** + * Change the type of an existing lock + * + * @param string $path + * @param int $targetType self::LOCK_SHARED or self::LOCK_EXCLUSIVE + * @throws \OCP\Lock\LockedException + */ + public function changeLock($path, $targetType); + + /** + * release all lock acquired by this instance + */ + public function releaseAll(); } diff --git a/settings/admin.php b/settings/admin.php index 5720bd9f99..f2e01adab1 100644 --- a/settings/admin.php +++ b/settings/admin.php @@ -29,6 +29,8 @@ * */ +use OC\Lock\NoopLockingProvider; + OC_Util::checkAdminUser(); OC_App::setActiveNavigationEntry("admin"); @@ -175,6 +177,11 @@ $template->assign('fileSharingSettings', $fileSharingSettings); $template->assign('filesExternal', $filesExternal); $template->assign('updaterAppPanel', $updaterAppPanel); $template->assign('ocDefaultEncryptionModulePanel', $ocDefaultEncryptionModulePanel); +if (\OC::$server->getLockingProvider() instanceof NoopLockingProvider) { + $template->assign('fileLockingEnabled', false); +} else { + $template->assign('fileLockingEnabled', true); +} $formsMap = array_map(function ($form) { if (preg_match('%(]*>.*?)%i', $form, $regs)) { @@ -200,6 +207,7 @@ $formsAndMore = array_merge($formsAndMore, $formsMap); $formsAndMore[] = ['anchor' => 'backgroundjobs', 'section-name' => $l->t('Cron')]; $formsAndMore[] = ['anchor' => 'mail_general_settings', 'section-name' => $l->t('Email server')]; $formsAndMore[] = ['anchor' => 'log-section', 'section-name' => $l->t('Log')]; +$formsAndMore[] = ['anchor' => 'server-status', 'section-name' => $l->t('Server Status')]; $formsAndMore[] = ['anchor' => 'admin-tips', 'section-name' => $l->t('Tips & tricks')]; if ($updaterAppPanel) { $formsAndMore[] = ['anchor' => 'updater', 'section-name' => $l->t('Updates')]; diff --git a/settings/templates/admin.php b/settings/templates/admin.php index f9a99b589a..3d253d4cbb 100644 --- a/settings/templates/admin.php +++ b/settings/templates/admin.php @@ -7,6 +7,7 @@ /** * @var array $_ * @var \OCP\IL10N $l + * @var OC_Defaults $theme */ style('settings', 'settings'); @@ -15,32 +16,32 @@ script('core', ['multiselect', 'setupchecks']); vendor_script('select2/select2'); vendor_style('select2/select2'); -$levels = array('Debug', 'Info', 'Warning', 'Error', 'Fatal'); -$levelLabels = array( +$levels = ['Debug', 'Info', 'Warning', 'Error', 'Fatal']; +$levelLabels = [ $l->t( 'Everything (fatal issues, errors, warnings, info, debug)' ), $l->t( 'Info, warnings, errors and fatal issues' ), $l->t( 'Warnings, errors and fatal issues' ), $l->t( 'Errors and fatal issues' ), $l->t( 'Fatal issues only' ), -); +]; -$mail_smtpauthtype = array( +$mail_smtpauthtype = [ '' => $l->t('None'), 'LOGIN' => $l->t('Login'), 'PLAIN' => $l->t('Plain'), 'NTLM' => $l->t('NT LAN Manager'), -); +]; -$mail_smtpsecure = array( +$mail_smtpsecure = [ '' => $l->t('None'), 'ssl' => $l->t('SSL'), 'tls' => $l->t('TLS'), -); +]; -$mail_smtpmode = array( +$mail_smtpmode = [ 'php', 'smtp', -); +]; if ($_['sendmail_is_available']) { $mail_smtpmode[] = 'sendmail'; } @@ -137,7 +138,7 @@ if (!$_['isLocaleWorking']) { ?>
t('We strongly suggest installing the required packages on your system to support one of the following locales: %s.', array($locales))); + p($l->t('We strongly suggest installing the required packages on your system to support one of the following locales: %s.', [$locales])); ?> - t("Last cron job execution: %s.", array($relative_time)));?> + t("Last cron job execution: %s.", [$relative_time]));?> - t("Last cron job execution: %s. Something seems wrong.", array($relative_time)));?> + t("Last cron job execution: %s. Something seems wrong.", [$relative_time]));?> @@ -527,6 +528,18 @@ if ($_['cronErrors']) {
  • t('Hardening and security guidance'));?> ↗
  • +
    +

    t('Server Status'));?>

    +
      +
    • + t('Experimental File Lock is enabled.')); + } else { + p($l->t('Experimental File Lock is disabled.')); + } ?> +
    • +
    +

    t('Version'));?>

    diff --git a/tests/lib/files/view.php b/tests/lib/files/view.php index 6bc6355713..06a42d6343 100644 --- a/tests/lib/files/view.php +++ b/tests/lib/files/view.php @@ -11,6 +11,7 @@ use OC\Files\Cache\Watcher; use OC\Files\Storage\Common; use OC\Files\Mount\MountPoint; use OC\Files\Storage\Temporary; +use OCP\Lock\ILockingProvider; class TemporaryNoTouch extends \OC\Files\Storage\Temporary { public function touch($path, $mtime = null) { @@ -1080,4 +1081,30 @@ class View extends \Test\TestCase { public function testNullAsRoot() { new \OC\Files\View(null); } + + /** + * e.g. reading from a folder that's being renamed + * + * @expectedException \OCP\Lock\LockedException + */ + public function testReadFromWriteLockedPath() { + $view = new \OC\Files\View(); + $storage = new Temporary(array()); + \OC\Files\Filesystem::mount($storage, [], '/'); + $view->lockFile('/foo/bar', ILockingProvider::LOCK_EXCLUSIVE); + $view->lockFile('/foo/bar/asd', ILockingProvider::LOCK_SHARED); + } + + /** + * e.g. writing a file that's being downloaded + * + * @expectedException \OCP\Lock\LockedException + */ + public function testWriteToReadLockedFile() { + $view = new \OC\Files\View(); + $storage = new Temporary(array()); + \OC\Files\Filesystem::mount($storage, [], '/'); + $view->lockFile('/foo/bar', ILockingProvider::LOCK_SHARED); + $view->lockFile('/foo/bar', ILockingProvider::LOCK_EXCLUSIVE); + } } diff --git a/tests/lib/lock/lockingprovider.php b/tests/lib/lock/lockingprovider.php index 08d879da8b..efd6e1939f 100644 --- a/tests/lib/lock/lockingprovider.php +++ b/tests/lib/lock/lockingprovider.php @@ -107,6 +107,30 @@ abstract class LockingProvider extends TestCase { $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); } + public function testReleaseAll() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->acquireLock('bar', ILockingProvider::LOCK_SHARED); + $this->instance->acquireLock('asd', ILockingProvider::LOCK_EXCLUSIVE); + + $this->instance->releaseAll(); + + $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); + $this->assertFalse($this->instance->isLocked('bar', ILockingProvider::LOCK_SHARED)); + $this->assertFalse($this->instance->isLocked('asd', ILockingProvider::LOCK_EXCLUSIVE)); + } + + public function testReleaseAfterReleaseAll() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + + $this->instance->releaseAll(); + + $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); + + $this->instance->releaseLock('foo', ILockingProvider::LOCK_SHARED); + } + /** * @expectedException \OCP\Lock\LockedException @@ -134,4 +158,57 @@ abstract class LockingProvider extends TestCase { $this->assertEquals('foo', $e->getPath()); } } + + public function testChangeLockToExclusive() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); + $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); + } + + public function testChangeLockToShared() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); + $this->assertFalse($this->instance->isLocked('foo', ILockingProvider::LOCK_EXCLUSIVE)); + $this->assertTrue($this->instance->isLocked('foo', ILockingProvider::LOCK_SHARED)); + } + + /** + * @expectedException \OCP\Lock\LockedException + */ + public function testChangeLockToExclusiveDoubleShared() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + } + + /** + * @expectedException \OCP\Lock\LockedException + */ + public function testChangeLockToExclusiveNoShared() { + $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + } + + /** + * @expectedException \OCP\Lock\LockedException + */ + public function testChangeLockToExclusiveFromExclusive() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + $this->instance->changeLock('foo', ILockingProvider::LOCK_EXCLUSIVE); + } + + /** + * @expectedException \OCP\Lock\LockedException + */ + public function testChangeLockToSharedNoExclusive() { + $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); + } + + /** + * @expectedException \OCP\Lock\LockedException + */ + public function testChangeLockToSharedFromShared() { + $this->instance->acquireLock('foo', ILockingProvider::LOCK_SHARED); + $this->instance->changeLock('foo', ILockingProvider::LOCK_SHARED); + } } diff --git a/tests/lib/testcase.php b/tests/lib/testcase.php index c1ebfb025c..d8595c83a9 100644 --- a/tests/lib/testcase.php +++ b/tests/lib/testcase.php @@ -43,6 +43,7 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase { protected function tearDown() { $hookExceptions = \OC_Hook::$thrownExceptions; \OC_Hook::$thrownExceptions = []; + \OC::$server->getLockingProvider()->releaseAll(); if(!empty($hookExceptions)) { throw $hookExceptions[0]; }