Commit 9dea79e3 authored by Vincent Petry's avatar Vincent Petry
Browse files

Merge pull request #11305 from owncloud/ext-updateetagonmount

Update etag of parent dir when adding/removing ext storage mount points
parents c82e310e 26e242a8
......@@ -118,6 +118,22 @@ class OC_Mount_Config {
}
$manager->addMount($mount);
}
if ($data['user']) {
$user = \OC::$server->getUserManager()->get($data['user']);
$userView = new \OC\Files\View('/' . $user->getUID() . '/files');
$changePropagator = new \OC\Files\Cache\ChangePropagator($userView);
$etagPropagator = new \OCA\Files_External\EtagPropagator($user, $changePropagator, \OC::$server->getConfig());
$etagPropagator->propagateDirtyMountPoints();
\OCP\Util::connectHook(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_create_mount,
$etagPropagator, 'updateHook');
\OCP\Util::connectHook(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_delete_mount,
$etagPropagator, 'updateHook');
}
}
/**
......@@ -463,6 +479,7 @@ class OC_Mount_Config {
$priority = null) {
$backends = self::getBackends();
$mountPoint = OC\Files\Filesystem::normalizePath($mountPoint);
$relMountPoint = $mountPoint;
if ($mountPoint === '' || $mountPoint === '/') {
// can't mount at root folder
return false;
......@@ -495,6 +512,10 @@ class OC_Mount_Config {
}
$mountPoints = self::readData($isPersonal ? OCP\User::getUser() : NULL);
// who else loves multi-dimensional array ?
$isNew = !isset($mountPoints[$mountType]) ||
!isset($mountPoints[$mountType][$applicable]) ||
!isset($mountPoints[$mountType][$applicable][$mountPoint]);
$mountPoints = self::mergeMountPoints($mountPoints, $mount, $mountType);
// Set default priority if none set
......@@ -510,7 +531,19 @@ class OC_Mount_Config {
self::writeData($isPersonal ? OCP\User::getUser() : NULL, $mountPoints);
return self::getBackendStatus($class, $classOptions, $isPersonal);
$result = self::getBackendStatus($class, $classOptions, $isPersonal);
if ($result && $isNew) {
\OC_Hook::emit(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_create_mount,
array(
\OC\Files\Filesystem::signal_param_path => $relMountPoint,
\OC\Files\Filesystem::signal_param_mount_type => $mountType,
\OC\Files\Filesystem::signal_param_users => $applicable,
)
);
}
return $result;
}
/**
......@@ -523,6 +556,7 @@ class OC_Mount_Config {
*/
public static function removeMountPoint($mountPoint, $mountType, $applicable, $isPersonal = false) {
// Verify that the mount point applies for the current user
$relMountPoints = $mountPoint;
if ($isPersonal) {
if ($applicable != OCP\User::getUser()) {
return false;
......@@ -543,6 +577,15 @@ class OC_Mount_Config {
}
}
self::writeData($isPersonal ? OCP\User::getUser() : NULL, $mountPoints);
\OC_Hook::emit(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_delete_mount,
array(
\OC\Files\Filesystem::signal_param_path => $relMountPoints,
\OC\Files\Filesystem::signal_param_mount_type => $mountType,
\OC\Files\Filesystem::signal_param_users => $applicable,
)
);
return true;
}
......
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* 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;
/**
* Updates the etag of parent folders whenever a new external storage mount
* point has been created or deleted. Updates need to be triggered using
* the updateHook() method.
*
* There are two modes of operation:
* - for personal mount points, the etag is propagated directly
* - for system mount points, a dirty flag is saved in the configuration and
* the etag will be updated the next time propagateDirtyMountPoints() is called
*/
class EtagPropagator {
/**
* @var \OCP\IUser
*/
protected $user;
/**
* @var \OC\Files\Cache\ChangePropagator
*/
protected $changePropagator;
/**
* @var \OCP\IConfig
*/
protected $config;
/**
* @param \OCP\IUser $user current user, must match the propagator's
* user
* @param \OC\Files\Cache\ChangePropagator $changePropagator change propagator
* initialized with a view for $user
* @param \OCP\IConfig $config
*/
public function __construct($user, $changePropagator, $config) {
$this->user = $user;
$this->changePropagator = $changePropagator;
$this->config = $config;
}
/**
* Propagate the etag changes for all mountpoints marked as dirty and mark the mountpoints as clean
*
* @param int $time
*/
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
* For personal mount points, the change is propagated directly
*
* @param array $params hook parameters
* @param int $time update time to use when marking a mount point as dirty
*/
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];
$mountPoint = Filesystem::normalizePath($mountPoint);
if ($type === \OC_Mount_Config::MOUNT_TYPE_GROUP or $users === 'all') {
$this->markDirty($mountPoint, $time);
} else {
$this->changePropagator->addChange($mountPoint);
$this->changePropagator->propagateChanges($time);
}
}
}
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* 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'));
}
}
......@@ -26,6 +26,42 @@ class Test_Mount_Config_Dummy_Storage {
}
}
class Test_Mount_Config_Hook_Test {
static $signal;
static $params;
public static function setUpHooks() {
self::clear();
\OCP\Util::connectHook(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_create_mount,
'\Test_Mount_Config_Hook_Test', 'createHookCallback');
\OCP\Util::connectHook(
\OC\Files\Filesystem::CLASSNAME,
\OC\Files\Filesystem::signal_delete_mount,
'\Test_Mount_Config_Hook_Test', 'deleteHookCallback');
}
public static function clear() {
self::$signal = null;
self::$params = null;
}
public static function createHookCallback($params) {
self::$signal = \OC\Files\Filesystem::signal_create_mount;
self::$params = $params;
}
public static function deleteHookCallback($params) {
self::$signal = \OC\Files\Filesystem::signal_delete_mount;
self::$params = $params;
}
public static function getLastCall() {
return array(self::$signal, self::$params);
}
}
/**
* Class Test_Mount_Config
*/
......@@ -77,9 +113,11 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase {
);
OC_Mount_Config::$skipTest = true;
Test_Mount_Config_Hook_Test::setupHooks();
}
public function tearDown() {
Test_Mount_Config_Hook_Test::clear();
OC_Mount_Config::$skipTest = false;
\OC_User::deleteUser(self::TEST_USER2);
......@@ -337,6 +375,102 @@ class Test_Mount_Config extends \PHPUnit_Framework_TestCase {
$this->assertEquals(array_keys($options), array_keys($savedOptions));
}