Signed-off-by: Lukas Reschke <lukas@statuscode.ch>pull/2765/head
parent
1afdce9ef8
commit
252eddadd9
@ -1,257 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* @copyright Copyright (c) 2016, ownCloud, Inc. |
||||
* |
||||
* @author Aaron Wood <aaronjwood@gmail.com> |
||||
* @author Joas Schilling <coding@schilljs.com> |
||||
* @author Morris Jobke <hey@morrisjobke.de> |
||||
* @author Thomas Müller <thomas.mueller@tmit.eu> |
||||
* @author Vincent Petry <pvince81@owncloud.com> |
||||
* |
||||
* @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 <http://www.gnu.org/licenses/> |
||||
* |
||||
*/ |
||||
|
||||
namespace OC\Repair; |
||||
|
||||
use OC\Files\Cache\Storage; |
||||
use OC\RepairException; |
||||
use OCP\Migration\IOutput; |
||||
use OCP\Migration\IRepairStep; |
||||
|
||||
class RepairLegacyStorages implements IRepairStep{ |
||||
/** |
||||
* @var \OCP\IConfig |
||||
*/ |
||||
protected $config; |
||||
|
||||
/** |
||||
* @var \OCP\IDBConnection |
||||
*/ |
||||
protected $connection; |
||||
|
||||
protected $findStorageInCacheStatement; |
||||
protected $renameStorageStatement; |
||||
|
||||
/** |
||||
* @param \OCP\IConfig $config |
||||
* @param \OCP\IDBConnection $connection |
||||
*/ |
||||
public function __construct($config, $connection) { |
||||
$this->connection = $connection; |
||||
$this->config = $config; |
||||
|
||||
$this->findStorageInCacheStatement = $this->connection->prepare( |
||||
'SELECT DISTINCT `storage` FROM `*PREFIX*filecache`' |
||||
. ' WHERE `storage` in (?, ?)' |
||||
); |
||||
$this->renameStorageStatement = $this->connection->prepare( |
||||
'UPDATE `*PREFIX*storages`' |
||||
. ' SET `id` = ?' |
||||
. ' WHERE `id` = ?' |
||||
); |
||||
} |
||||
|
||||
public function getName() { |
||||
return 'Repair legacy storages'; |
||||
} |
||||
|
||||
/** |
||||
* Extracts the user id from a legacy storage id |
||||
* |
||||
* @param string $storageId legacy storage id in the |
||||
* format "local::/path/to/datadir/userid" |
||||
* @return string user id extracted from the storage id |
||||
*/ |
||||
private function extractUserId($storageId) { |
||||
$storageId = rtrim($storageId, '/'); |
||||
$pos = strrpos($storageId, '/'); |
||||
return substr($storageId, $pos + 1); |
||||
} |
||||
|
||||
/** |
||||
* Fix the given legacy storage by renaming the old id |
||||
* to the new id. If the new id already exists, whichever |
||||
* storage that has data in the file cache will be used. |
||||
* If both have data, nothing will be done and false is |
||||
* returned. |
||||
* |
||||
* @param string $oldId old storage id |
||||
* @param int $oldNumericId old storage numeric id |
||||
* @param string $userId |
||||
* @return bool true if fixed, false otherwise |
||||
* @throws RepairException |
||||
*/ |
||||
private function fixLegacyStorage($oldId, $oldNumericId, $userId = null) { |
||||
// check whether the new storage already exists |
||||
if (is_null($userId)) { |
||||
$userId = $this->extractUserId($oldId); |
||||
} |
||||
$newId = 'home::' . $userId; |
||||
|
||||
// check if target id already exists |
||||
$newNumericId = Storage::getNumericStorageId($newId); |
||||
if (!is_null($newNumericId)) { |
||||
$newNumericId = (int)$newNumericId; |
||||
// try and resolve the conflict |
||||
// check which one of "local::" or "home::" needs to be kept |
||||
$this->findStorageInCacheStatement->execute(array($oldNumericId, $newNumericId)); |
||||
$row1 = $this->findStorageInCacheStatement->fetch(); |
||||
$row2 = $this->findStorageInCacheStatement->fetch(); |
||||
$this->findStorageInCacheStatement->closeCursor(); |
||||
if ($row2 !== false) { |
||||
// two results means both storages have data, not auto-fixable |
||||
throw new RepairException( |
||||
'Could not automatically fix legacy storage ' |
||||
. '"' . $oldId . '" => "' . $newId . '"' |
||||
. ' because they both have data.' |
||||
); |
||||
} |
||||
if ($row1 === false || (int)$row1['storage'] === $oldNumericId) { |
||||
// old storage has data, then delete the empty new id |
||||
$toDelete = $newId; |
||||
} else if ((int)$row1['storage'] === $newNumericId) { |
||||
// new storage has data, then delete the empty old id |
||||
$toDelete = $oldId; |
||||
} else { |
||||
// unknown case, do not continue |
||||
return false; |
||||
} |
||||
|
||||
// delete storage including file cache |
||||
Storage::remove($toDelete); |
||||
|
||||
// if we deleted the old id, the new id will be used |
||||
// automatically |
||||
if ($toDelete === $oldId) { |
||||
// nothing more to do |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
// rename old id to new id |
||||
$newId = Storage::adjustStorageId($newId); |
||||
$oldId = Storage::adjustStorageId($oldId); |
||||
$rowCount = $this->renameStorageStatement->execute(array($newId, $oldId)); |
||||
$this->renameStorageStatement->closeCursor(); |
||||
return ($rowCount === 1); |
||||
} |
||||
|
||||
/** |
||||
* Converts legacy home storage ids in the format |
||||
* "local::/data/dir/path/userid/" to the new format "home::userid" |
||||
*/ |
||||
public function run(IOutput $out) { |
||||
// only run once |
||||
if ($this->config->getAppValue('core', 'repairlegacystoragesdone') === 'yes') { |
||||
return; |
||||
} |
||||
|
||||
$dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/'); |
||||
$dataDir = rtrim($dataDir, '/') . '/'; |
||||
$dataDirId = 'local::' . $dataDir; |
||||
|
||||
$count = 0; |
||||
$hasWarnings = false; |
||||
|
||||
$this->connection->beginTransaction(); |
||||
|
||||
// note: not doing a direct UPDATE with the REPLACE function |
||||
// because regexp search/extract is needed and it is not guaranteed |
||||
// to work on all database types |
||||
$sql = 'SELECT `id`, `numeric_id` FROM `*PREFIX*storages`' |
||||
. ' WHERE `id` LIKE ?' |
||||
. ' ORDER BY `id`'; |
||||
$result = $this->connection->executeQuery($sql, array($this->connection->escapeLikeParameter($dataDirId) . '%')); |
||||
|
||||
while ($row = $result->fetch()) { |
||||
$currentId = $row['id']; |
||||
// one entry is the datadir itself |
||||
if ($currentId === $dataDirId) { |
||||
continue; |
||||
} |
||||
|
||||
try { |
||||
if ($this->fixLegacyStorage($currentId, (int)$row['numeric_id'])) { |
||||
$count++; |
||||
} |
||||
} |
||||
catch (RepairException $e) { |
||||
$hasWarnings = true; |
||||
$out->warning('Could not repair legacy storage ' . $currentId . ' automatically.'); |
||||
} |
||||
} |
||||
|
||||
// check for md5 ids, not in the format "prefix::" |
||||
$sql = 'SELECT COUNT(*) AS "c" FROM `*PREFIX*storages`' |
||||
. ' WHERE `id` NOT LIKE \'%::%\''; |
||||
$result = $this->connection->executeQuery($sql); |
||||
$row = $result->fetch(); |
||||
|
||||
// find at least one to make sure it's worth |
||||
// querying the user list |
||||
if ((int)$row['c'] > 0) { |
||||
$userManager = \OC::$server->getUserManager(); |
||||
|
||||
// use chunks to avoid caching too many users in memory |
||||
$limit = 30; |
||||
$offset = 0; |
||||
|
||||
do { |
||||
// query the next page of users |
||||
$results = $userManager->search('', $limit, $offset); |
||||
$storageIds = array(); |
||||
foreach ($results as $uid => $userObject) { |
||||
$storageId = $dataDirId . $uid . '/'; |
||||
if (strlen($storageId) <= 64) { |
||||
// skip short storage ids as they were handled in the previous section |
||||
continue; |
||||
} |
||||
$storageIds[$uid] = $storageId; |
||||
} |
||||
|
||||
if (count($storageIds) > 0) { |
||||
// update the storages of these users |
||||
foreach ($storageIds as $uid => $storageId) { |
||||
$numericId = Storage::getNumericStorageId($storageId); |
||||
try { |
||||
if (!is_null($numericId) && $this->fixLegacyStorage($storageId, (int)$numericId)) { |
||||
$count++; |
||||
} |
||||
} |
||||
catch (RepairException $e) { |
||||
$hasWarnings = true; |
||||
$out->warning('Could not repair legacy storage ' . $storageId . ' automatically.'); |
||||
} |
||||
} |
||||
} |
||||
$offset += $limit; |
||||
} while (count($results) >= $limit); |
||||
} |
||||
|
||||
$out->info('Updated ' . $count . ' legacy home storage ids'); |
||||
|
||||
$this->connection->commit(); |
||||
|
||||
Storage::getGlobalCache()->clearCache(); |
||||
|
||||
if ($hasWarnings) { |
||||
$out->warning('Some legacy storages could not be repaired. Please manually fix them then re-run ./occ maintenance:repair'); |
||||
} else { |
||||
// if all were done, no need to redo the repair during next upgrade |
||||
$this->config->setAppValue('core', 'repairlegacystoragesdone', 'yes'); |
||||
} |
||||
} |
||||
} |
||||
@ -1,321 +0,0 @@ |
||||
<?php |
||||
/** |
||||
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> |
||||
* This file is licensed under the Affero General Public License version 3 or |
||||
* later. |
||||
* See the COPYING-README file. |
||||
*/ |
||||
|
||||
namespace Test\Repair; |
||||
|
||||
use OC\Files\Cache\Cache; |
||||
use OC\Files\Cache\Storage; |
||||
use OCP\Migration\IOutput; |
||||
use PHPUnit_Framework_MockObject_MockObject; |
||||
use Test\TestCase; |
||||
|
||||
/** |
||||
* Tests for the converting of legacy storages to home storages. |
||||
* |
||||
* @group DB |
||||
* |
||||
* @see \OC\Repair\RepairLegacyStorages |
||||
*/ |
||||
class RepairLegacyStoragesTest extends TestCase { |
||||
/** @var \OCP\IDBConnection */ |
||||
private $connection; |
||||
/** @var \OCP\IConfig */ |
||||
private $config; |
||||
private $user; |
||||
/** @var \OC\Repair\RepairLegacyStorages */ |
||||
private $repair; |
||||
|
||||
private $dataDir; |
||||
private $oldDataDir; |
||||
|
||||
private $legacyStorageId; |
||||
private $newStorageId; |
||||
|
||||
/** @var IOutput | PHPUnit_Framework_MockObject_MockObject */ |
||||
private $outputMock; |
||||
|
||||
protected function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->config = \OC::$server->getConfig(); |
||||
$this->connection = \OC::$server->getDatabaseConnection(); |
||||
$this->oldDataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/'); |
||||
|
||||
$this->repair = new \OC\Repair\RepairLegacyStorages($this->config, $this->connection); |
||||
|
||||
$this->outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') |
||||
->disableOriginalConstructor() |
||||
->getMock(); |
||||
} |
||||
|
||||
protected function tearDown() { |
||||
$user = \OC::$server->getUserManager()->get($this->user); |
||||
if ($user) { |
||||
$user->delete(); |
||||
} |
||||
|
||||
$sql = 'DELETE FROM `*PREFIX*storages`'; |
||||
$this->connection->executeQuery($sql); |
||||
$sql = 'DELETE FROM `*PREFIX*filecache`'; |
||||
$this->connection->executeQuery($sql); |
||||
$this->config->setSystemValue('datadirectory', $this->oldDataDir); |
||||
$this->config->setAppValue('core', 'repairlegacystoragesdone', 'no'); |
||||
|
||||
parent::tearDown(); |
||||
} |
||||
|
||||
/** |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
* @throws \Exception |
||||
*/ |
||||
function prepareSettings($dataDir, $userId) { |
||||
// hard-coded string as we want a predictable fixed length |
||||
// no data will be written there |
||||
$this->dataDir = $dataDir; |
||||
$this->config->setSystemValue('datadirectory', $this->dataDir); |
||||
|
||||
$this->user = $userId; |
||||
$this->legacyStorageId = 'local::' . $this->dataDir . $this->user . '/'; |
||||
$this->newStorageId = 'home::' . $this->user; |
||||
\OC::$server->getUserManager()->createUser($this->user, $this->user); |
||||
} |
||||
|
||||
/** |
||||
* Create a storage entry |
||||
* |
||||
* @param string $storageId |
||||
* @return int |
||||
*/ |
||||
private function createStorage($storageId) { |
||||
$sql = 'INSERT INTO `*PREFIX*storages` (`id`)' |
||||
. ' VALUES (?)'; |
||||
|
||||
$storageId = Storage::adjustStorageId($storageId); |
||||
$numRows = $this->connection->executeUpdate($sql, array($storageId)); |
||||
$this->assertSame(1, $numRows); |
||||
|
||||
return (int)\OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages'); |
||||
} |
||||
|
||||
/** |
||||
* Create dummy data in the filecache for the given storage numeric id |
||||
* |
||||
* @param string $storageId storage id |
||||
*/ |
||||
private function createData($storageId) { |
||||
$cache = new Cache($storageId); |
||||
$cache->put( |
||||
'dummyfile.txt', |
||||
array('size' => 5, 'mtime' => 12, 'mimetype' => 'text/plain') |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Test that existing home storages are left alone when valid. |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testNoopWithExistingHomeStorage($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$newStorageNumId = $this->createStorage($this->newStorageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); |
||||
$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that legacy storages are converted to home storages when |
||||
* the latter does not exist. |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testConvertLegacyToHomeStorage($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$legacyStorageNumId = $this->createStorage($this->legacyStorageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); |
||||
$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that legacy storages are converted to home storages |
||||
* when home storage already exists but has no data. |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testConvertLegacyToExistingEmptyHomeStorage($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$legacyStorageNumId = $this->createStorage($this->legacyStorageId); |
||||
$this->createStorage($this->newStorageId); |
||||
|
||||
$this->createData($this->legacyStorageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); |
||||
$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that legacy storages are converted to home storages |
||||
* when home storage already exists and the legacy storage |
||||
* has no data. |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testConvertEmptyLegacyToHomeStorage($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$this->createStorage($this->legacyStorageId); |
||||
$newStorageNumId = $this->createStorage($this->newStorageId); |
||||
|
||||
$this->createData($this->newStorageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); |
||||
$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that nothing is done when both conflicting legacy |
||||
* and home storage have data. |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testConflictNoop($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$legacyStorageNumId = $this->createStorage($this->legacyStorageId); |
||||
$newStorageNumId = $this->createStorage($this->newStorageId); |
||||
|
||||
$this->createData($this->legacyStorageId); |
||||
$this->createData($this->newStorageId); |
||||
|
||||
$this->outputMock->expects($this->exactly(2))->method('warning'); |
||||
$this->repair->run($this->outputMock); |
||||
|
||||
// storages left alone |
||||
$this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->legacyStorageId)); |
||||
$this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); |
||||
|
||||
// do not set the done flag |
||||
$this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone')); |
||||
} |
||||
|
||||
/** |
||||
* Test that the data dir local entry is left alone |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testDataDirEntryNoop($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$storageId = 'local::' . $this->dataDir; |
||||
$numId = $this->createStorage($storageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertSame($numId, Storage::getNumericStorageId($storageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that external local storages are left alone |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testLocalExtStorageNoop($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$storageId = 'local::/tmp/somedir/' . $this->user; |
||||
$numId = $this->createStorage($storageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertSame($numId, Storage::getNumericStorageId($storageId)); |
||||
} |
||||
|
||||
/** |
||||
* Test that other external storages are left alone |
||||
* |
||||
* @dataProvider settingsProvider |
||||
* |
||||
* @param string $dataDir |
||||
* @param string $userId |
||||
*/ |
||||
public function testExtStorageNoop($dataDir, $userId) { |
||||
$this->prepareSettings($dataDir, $userId); |
||||
$storageId = 'smb::user@password/tmp/somedir/' . $this->user; |
||||
$numId = $this->createStorage($storageId); |
||||
|
||||
$this->repair->run($this->outputMock); |
||||
|
||||
$this->assertSame($numId, Storage::getNumericStorageId($storageId)); |
||||
} |
||||
|
||||
/** |
||||
* Provides data dir and user name |
||||
*/ |
||||
function settingsProvider() { |
||||
return array( |
||||
// regular data dir |
||||
array( |
||||
'/tmp/oc-autotest/datadir/', |
||||
$this->getUniqueID('user_'), |
||||
), |
||||
// long datadir / short user |
||||
array( |
||||
'/tmp/oc-autotest/datadir01234567890123456789012345678901234567890123456789END/', |
||||
$this->getUniqueID('user_'), |
||||
), |
||||
// short datadir / long user |
||||
array( |
||||
'/tmp/oc-autotest/datadir/', |
||||
'u123456789012345678901234567890123456789012345678901234567890END', // 64 chars |
||||
), |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Only run the repair once |
||||
*/ |
||||
public function testOnlyRunOnce() { |
||||
$this->outputMock->expects($this->exactly(1))->method('info'); |
||||
|
||||
$this->prepareSettings('/tmp/oc-autotest/datadir', $this->getUniqueID('user_')); |
||||
$this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone')); |
||||
$this->repair->run($this->outputMock); |
||||
$this->assertEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone')); |
||||
|
||||
$this->outputMock->expects($this->never())->method('info'); |
||||
$this->repair->run($this->outputMock); |
||||
$this->assertEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone')); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue