diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 16236fd6bc..f1bf586b49 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -48,7 +48,6 @@ use OC\Repair\SqliteAutoincrement; use OC\Repair\DropOldTables; use OC\Repair\FillETags; use OC\Repair\InnoDB; -use OC\Repair\RepairLegacyStorages; use OC\Repair\RepairMimeTypes; use OC\Repair\SearchLuceneTables; use OC\Repair\UpdateOutdatedOcsIds; @@ -133,7 +132,6 @@ class Repair implements IOutput{ return [ new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), \OC::$server->getDatabaseConnection(), false), new RepairMimeTypes(\OC::$server->getConfig()), - new RepairLegacyStorages(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()), new AssetCache(), new FillETags(\OC::$server->getDatabaseConnection()), new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()), diff --git a/lib/private/Repair/RepairLegacyStorages.php b/lib/private/Repair/RepairLegacyStorages.php deleted file mode 100644 index 228bdb67fe..0000000000 --- a/lib/private/Repair/RepairLegacyStorages.php +++ /dev/null @@ -1,257 +0,0 @@ - - * @author Joas Schilling - * @author Morris Jobke - * @author Thomas Müller - * @author Vincent Petry - * - * @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\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'); - } - } -} diff --git a/tests/lib/Repair/RepairLegacyStoragesTest.php b/tests/lib/Repair/RepairLegacyStoragesTest.php deleted file mode 100644 index 8d8366dde0..0000000000 --- a/tests/lib/Repair/RepairLegacyStoragesTest.php +++ /dev/null @@ -1,321 +0,0 @@ - - * 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')); - } -}