From 5d7bd8be422d9e9904500869182d344f18aa8710 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 28 Sep 2014 17:09:07 +0200 Subject: [PATCH] Add EtagPropagator to handle etag changes when external storages are changed --- apps/files_external/lib/etagpropagator.php | 107 ++++++ apps/files_external/tests/etagpropagator.php | 328 +++++++++++++++++++ 2 files changed, 435 insertions(+) create mode 100644 apps/files_external/lib/etagpropagator.php create mode 100644 apps/files_external/tests/etagpropagator.php diff --git a/apps/files_external/lib/etagpropagator.php b/apps/files_external/lib/etagpropagator.php new file mode 100644 index 0000000000..0057165750 --- /dev/null +++ b/apps/files_external/lib/etagpropagator.php @@ -0,0 +1,107 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace OCA\Files_External; + +use OC\Files\Filesystem; + +class EtagPropagator { + /** + * @var \OCP\IUser + */ + protected $user; + + /** + * @var \OC\Files\Cache\ChangePropagator + */ + protected $changePropagator; + + /** + * @var \OCP\IConfig + */ + protected $config; + + /** + * @param \OCP\IUser $user + * @param \OC\Files\Cache\ChangePropagator $changePropagator + * @param \OCP\IConfig $config + */ + public function __construct($user, $changePropagator, $config) { + $this->user = $user; + $this->changePropagator = $changePropagator; + $this->config = $config; + } + + public function propagateDirtyMountPoints($time = null) { + if ($time === null) { + $time = time(); + } + $mountPoints = $this->getDirtyMountPoints(); + foreach ($mountPoints as $mountPoint) { + $this->changePropagator->addChange($mountPoint); + $this->config->setUserValue($this->user->getUID(), 'files_external', $mountPoint, $time); + } + if (count($mountPoints)) { + $this->changePropagator->propagateChanges($time); + } + } + + /** + * Get all mountpoints we need to update the etag for + * + * @return string[] + */ + protected function getDirtyMountPoints() { + $dirty = array(); + $mountPoints = $this->config->getAppKeys('files_external'); + foreach ($mountPoints as $mountPoint) { + if (substr($mountPoint, 0, 1) === '/') { + $updateTime = $this->config->getAppValue('files_external', $mountPoint); + $userTime = $this->config->getUserValue($this->user->getUID(), 'files_external', $mountPoint); + if ($updateTime > $userTime) { + $dirty[] = $mountPoint; + } + } + } + return $dirty; + } + + /** + * @param string $mountPoint + * @param int $time + */ + protected function markDirty($mountPoint, $time = null) { + if ($time === null) { + $time = time(); + } + $this->config->setAppValue('files_external', $mountPoint, $time); + } + + /** + * Update etags for mount points for known user + * For global or group mount points, updating the etag for every user is not feasible + * instead we mark the mount point as dirty and update the etag when the filesystem is loaded for the user + * + * @param array $params + * @param int $time + */ + public function updateHook($params, $time = null) { + if ($time === null) { + $time = time(); + } + $users = $params[Filesystem::signal_param_users]; + $type = $params[Filesystem::signal_param_mount_type]; + $mountPoint = $params[Filesystem::signal_param_path]; + if ($type === \OC_Mount_Config::MOUNT_TYPE_GROUP or $users === 'all') { + $this->markDirty($mountPoint, $time); + } else { + $this->changePropagator->addChange($mountPoint); + $this->changePropagator->propagateChanges($time); + } + } +} diff --git a/apps/files_external/tests/etagpropagator.php b/apps/files_external/tests/etagpropagator.php new file mode 100644 index 0000000000..7fa1863f96 --- /dev/null +++ b/apps/files_external/tests/etagpropagator.php @@ -0,0 +1,328 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Tests\Files_External; + +use OC\Files\Filesystem; +use OC\User\User; + +class EtagPropagator extends \PHPUnit_Framework_TestCase { + protected function getUser() { + return new User(uniqid(), null); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject | \OC\Files\Cache\ChangePropagator + */ + protected function getChangePropagator() { + return $this->getMockBuilder('\OC\Files\Cache\ChangePropagator') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject | \OCP\IConfig + */ + protected function getConfig() { + $appConfig = array(); + $userConfig = array(); + $mock = $this->getMockBuilder('\OCP\IConfig') + ->disableOriginalConstructor() + ->getMock(); + + $mock->expects($this->any()) + ->method('getAppValue') + ->will($this->returnCallback(function ($appId, $key, $default = null) use (&$appConfig) { + if (isset($appConfig[$appId]) and isset($appConfig[$appId][$key])) { + return $appConfig[$appId][$key]; + } else { + return $default; + } + })); + $mock->expects($this->any()) + ->method('setAppValue') + ->will($this->returnCallback(function ($appId, $key, $value) use (&$appConfig) { + if (!isset($appConfig[$appId])) { + $appConfig[$appId] = array(); + } + $appConfig[$appId][$key] = $value; + })); + $mock->expects($this->any()) + ->method('getAppKeys') + ->will($this->returnCallback(function ($appId) use (&$appConfig) { + if (!isset($appConfig[$appId])) { + $appConfig[$appId] = array(); + } + return array_keys($appConfig[$appId]); + })); + + $mock->expects($this->any()) + ->method('getUserValue') + ->will($this->returnCallback(function ($userId, $appId, $key, $default = null) use (&$userConfig) { + if (isset($userConfig[$userId]) and isset($userConfig[$userId][$appId]) and isset($userConfig[$userId][$appId][$key])) { + return $userConfig[$userId][$appId][$key]; + } else { + return $default; + } + })); + $mock->expects($this->any()) + ->method('setUserValue') + ->will($this->returnCallback(function ($userId, $appId, $key, $value) use (&$userConfig) { + if (!isset($userConfig[$userId])) { + $userConfig[$userId] = array(); + } + if (!isset($userConfig[$userId][$appId])) { + $userConfig[$userId][$appId] = array(); + } + $userConfig[$userId][$appId][$key] = $value; + })); + + return $mock; + } + + public function testSingleUserMount() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $changePropagator->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator->updateHook(array( + Filesystem::signal_param_path => '/test', + Filesystem::signal_param_mount_type => \OC_Mount_Config::MOUNT_TYPE_USER, + Filesystem::signal_param_users => $user->getUID(), + ), $time); + } + + public function testGlobalMountNoDirectUpdate() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + // not updated directly + $changePropagator->expects($this->never()) + ->method('addChange'); + $changePropagator->expects($this->never()) + ->method('propagateChanges'); + + $propagator->updateHook(array( + Filesystem::signal_param_path => '/test', + Filesystem::signal_param_mount_type => \OC_Mount_Config::MOUNT_TYPE_USER, + Filesystem::signal_param_users => 'all', + ), $time); + + // mount point marked as dirty + $this->assertEquals(array('/test'), $config->getAppKeys('files_external')); + $this->assertEquals($time, $config->getAppValue('files_external', '/test')); + } + + public function testGroupMountNoDirectUpdate() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + // not updated directly + $changePropagator->expects($this->never()) + ->method('addChange'); + $changePropagator->expects($this->never()) + ->method('propagateChanges'); + + $propagator->updateHook(array( + Filesystem::signal_param_path => '/test', + Filesystem::signal_param_mount_type => \OC_Mount_Config::MOUNT_TYPE_GROUP, + Filesystem::signal_param_users => 'test', + ), $time); + + // mount point marked as dirty + $this->assertEquals(array('/test'), $config->getAppKeys('files_external')); + $this->assertEquals($time, $config->getAppValue('files_external', '/test')); + } + + public function testGlobalMountNoDirtyMountPoint() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $changePropagator->expects($this->never()) + ->method('addChange'); + $changePropagator->expects($this->never()) + ->method('propagateChanges'); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals(0, $config->getUserValue($user->getUID(), 'files_external', '/test', 0)); + } + + public function testGlobalMountDirtyMountPointFirstTime() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + + $changePropagator->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user->getUID(), 'files_external', '/test')); + } + + public function testGlobalMountNonDirtyMountPoint() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + $config->setUserValue($user->getUID(), 'files_external', '/test', $time - 10); + + $changePropagator->expects($this->never()) + ->method('addChange'); + $changePropagator->expects($this->never()) + ->method('propagateChanges'); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals($time - 10, $config->getUserValue($user->getUID(), 'files_external', '/test')); + } + + public function testGlobalMountNonDirtyMountPointOtherUser() { + $time = time(); + $user = $this->getUser(); + $user2 = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + $config->setUserValue($user2->getUID(), 'files_external', '/test', $time - 10); + + $changePropagator->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user->getUID(), 'files_external', '/test')); + } + + public function testGlobalMountDirtyMountPointSecondTime() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + $config->setUserValue($user->getUID(), 'files_external', '/test', $time - 20); + + $changePropagator->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user->getUID(), 'files_external', '/test')); + } + + public function testGlobalMountMultipleUsers() { + $time = time(); + $config = $this->getConfig(); + $user1 = $this->getUser(); + $user2 = $this->getUser(); + $user3 = $this->getUser(); + $changePropagator1 = $this->getChangePropagator(); + $changePropagator2 = $this->getChangePropagator(); + $changePropagator3 = $this->getChangePropagator(); + $propagator1 = new \OCA\Files_External\EtagPropagator($user1, $changePropagator1, $config); + $propagator2 = new \OCA\Files_External\EtagPropagator($user2, $changePropagator2, $config); + $propagator3 = new \OCA\Files_External\EtagPropagator($user3, $changePropagator3, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + + $changePropagator1->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator1->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator1->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user1->getUID(), 'files_external', '/test')); + $this->assertEquals(0, $config->getUserValue($user2->getUID(), 'files_external', '/test', 0)); + $this->assertEquals(0, $config->getUserValue($user3->getUID(), 'files_external', '/test', 0)); + + $changePropagator2->expects($this->once()) + ->method('addChange') + ->with('/test'); + $changePropagator2->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator2->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user1->getUID(), 'files_external', '/test')); + $this->assertEquals($time, $config->getUserValue($user2->getUID(), 'files_external', '/test', 0)); + $this->assertEquals(0, $config->getUserValue($user3->getUID(), 'files_external', '/test', 0)); + } + + public function testGlobalMountMultipleDirtyMountPoints() { + $time = time(); + $user = $this->getUser(); + $config = $this->getConfig(); + $changePropagator = $this->getChangePropagator(); + $propagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, $config); + + $config->setAppValue('files_external', '/test', $time - 10); + $config->setAppValue('files_external', '/foo', $time - 50); + $config->setAppValue('files_external', '/bar', $time - 70); + + $config->setUserValue($user->getUID(), 'files_external', '/foo', $time - 70); + $config->setUserValue($user->getUID(), 'files_external', '/bar', $time - 70); + + $changePropagator->expects($this->exactly(2)) + ->method('addChange'); + $changePropagator->expects($this->once()) + ->method('propagateChanges') + ->with($time); + + $propagator->propagateDirtyMountPoints($time); + + $this->assertEquals($time, $config->getUserValue($user->getUID(), 'files_external', '/test')); + $this->assertEquals($time, $config->getUserValue($user->getUID(), 'files_external', '/foo')); + $this->assertEquals($time - 70, $config->getUserValue($user->getUID(), 'files_external', '/bar')); + } +}