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