diff options
author | provokateurin <kate@provokateurin.de> | 2025-07-21 11:06:42 +0200 |
---|---|---|
committer | provokateurin <kate@provokateurin.de> | 2025-07-21 14:12:56 +0200 |
commit | 00eb35f56c75b6ea9955d372ddb60b40f3eec936 (patch) | |
tree | 2b90f143628f508330271fa320711ad52539c8ed | |
parent | a1f4b59997df99af48e0c58a3a3526e6d9f5e90f (diff) | |
download | nextcloud-server-feat/repair-step-deduplicate-mounts.tar.gz nextcloud-server-feat/repair-step-deduplicate-mounts.zip |
feat: Add repair step for deduplicating mountsfeat/repair-step-deduplicate-mounts
Signed-off-by: provokateurin <kate@provokateurin.de>
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | lib/private/Repair.php | 2 | ||||
-rw-r--r-- | lib/private/Repair/DeduplicateMounts.php | 68 | ||||
-rw-r--r-- | tests/lib/Repair/DeduplicateMountsTest.php | 158 |
5 files changed, 230 insertions, 0 deletions
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 16926e77775..efcaee303da 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1930,6 +1930,7 @@ return array( 'OC\\Repair\\ClearGeneratedAvatarCacheJob' => $baseDir . '/lib/private/Repair/ClearGeneratedAvatarCacheJob.php', 'OC\\Repair\\Collation' => $baseDir . '/lib/private/Repair/Collation.php', 'OC\\Repair\\ConfigKeyMigration' => $baseDir . '/lib/private/Repair/ConfigKeyMigration.php', + 'OC\\Repair\\DeduplicateMounts' => $baseDir . '/lib/private/Repair/DeduplicateMounts.php', 'OC\\Repair\\Events\\RepairAdvanceEvent' => $baseDir . '/lib/private/Repair/Events/RepairAdvanceEvent.php', 'OC\\Repair\\Events\\RepairErrorEvent' => $baseDir . '/lib/private/Repair/Events/RepairErrorEvent.php', 'OC\\Repair\\Events\\RepairFinishEvent' => $baseDir . '/lib/private/Repair/Events/RepairFinishEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index daa1a1940dd..353e78eba53 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1971,6 +1971,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Repair\\ClearGeneratedAvatarCacheJob' => __DIR__ . '/../../..' . '/lib/private/Repair/ClearGeneratedAvatarCacheJob.php', 'OC\\Repair\\Collation' => __DIR__ . '/../../..' . '/lib/private/Repair/Collation.php', 'OC\\Repair\\ConfigKeyMigration' => __DIR__ . '/../../..' . '/lib/private/Repair/ConfigKeyMigration.php', + 'OC\\Repair\\DeduplicateMounts' => __DIR__ . '/../../..' . '/lib/private/Repair/DeduplicateMounts.php', 'OC\\Repair\\Events\\RepairAdvanceEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairAdvanceEvent.php', 'OC\\Repair\\Events\\RepairErrorEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairErrorEvent.php', 'OC\\Repair\\Events\\RepairFinishEvent' => __DIR__ . '/../../..' . '/lib/private/Repair/Events/RepairFinishEvent.php', diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 7fbf776d9a1..4c2e24482b6 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -19,6 +19,7 @@ use OC\Repair\CleanUpAbandonedApps; use OC\Repair\ClearFrontendCaches; use OC\Repair\ClearGeneratedAvatarCache; use OC\Repair\Collation; +use OC\Repair\DeduplicateMounts; use OC\Repair\Events\RepairAdvanceEvent; use OC\Repair\Events\RepairErrorEvent; use OC\Repair\Events\RepairFinishEvent; @@ -215,6 +216,7 @@ class Repair implements IOutput { \OCP\Server::get(IDBConnection::class) ), \OCP\Server::get(DeleteSchedulingObjects::class), + \OCP\Server::get(DeduplicateMounts::class), ]; } diff --git a/lib/private/Repair/DeduplicateMounts.php b/lib/private/Repair/DeduplicateMounts.php new file mode 100644 index 00000000000..c2f087aced3 --- /dev/null +++ b/lib/private/Repair/DeduplicateMounts.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Repair; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class DeduplicateMounts implements IRepairStep { + public function __construct( + private readonly IDBConnection $connection, + private readonly IConfig $config, + ) { + } + + public function getName(): string { + return 'Deduplicate mounts'; + } + + public function run(IOutput $output): void { + $threshold = $this->config->getSystemValueInt('repair_duplicate_mounts_threshold', 10); + if ($threshold < 1) { + $threshold = 1; + } + + $this->connection->beginTransaction(); + + $selectQuery = $this->connection->getQueryBuilder(); + $selectQuery + ->select('root_id', 'user_id', 'mount_point') + ->selectAlias($selectQuery->func()->min('id'), 'min_id') + ->from('mounts') + ->groupBy('root_id', 'user_id', 'mount_point') + ->having($selectQuery->expr()->gt($selectQuery->func()->count('*'), $selectQuery->createNamedParameter($threshold, IQueryBuilder::PARAM_INT))); + + $deleteQuery = $this->connection->getQueryBuilder(); + $deleteQuery + ->delete('mounts') + ->where( + $deleteQuery->expr()->neq('id', $deleteQuery->createParameter('id')), + $deleteQuery->expr()->eq('root_id', $deleteQuery->createParameter('root_id')), + $deleteQuery->expr()->eq('user_id', $deleteQuery->createParameter('user_id')), + $deleteQuery->expr()->eq('mount_point', $deleteQuery->createParameter('mount_point')), + ); + + $result = $selectQuery->executeQuery(); + while ($row = $result->fetch()) { + $deleteQuery + ->setParameter('id', $row['min_id']) + ->setParameter('root_id', $row['root_id']) + ->setParameter('user_id', $row['user_id']) + ->setParameter('mount_point', $row['mount_point']) + ->executeStatement(); + } + $result->closeCursor(); + + $this->connection->commit(); + } +} diff --git a/tests/lib/Repair/DeduplicateMountsTest.php b/tests/lib/Repair/DeduplicateMountsTest.php new file mode 100644 index 00000000000..afb37e4cc27 --- /dev/null +++ b/tests/lib/Repair/DeduplicateMountsTest.php @@ -0,0 +1,158 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Repair; + +use OC\Repair\DeduplicateMounts; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + * + * @see DeduplicateMounts + */ +class DeduplicateMountsTest extends TestCase { + + private DeduplicateMounts $repair; + private IDBConnection $connection; + private IConfig $config; + + protected function setUp(): void { + parent::setUp(); + $this->connection = Server::get(IDBConnection::class); + $this->deleteAllMounts(); + + $this->config = $this->createMock(IConfig::class); + $this->repair = new DeduplicateMounts($this->connection, $this->config); + } + + protected function tearDown(): void { + $this->deleteAllMounts(); + + parent::tearDown(); + } + + protected function deleteAllMounts(): void { + $this->connection->getQueryBuilder()->delete('mounts')->executeStatement(); + } + + public function testDeduplicateMounts(): void { + $rows = [ + // Original mount + [ + 'storage_id' => 1, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + // Duplicate mount 1 + [ + 'storage_id' => 2, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + // Duplicate mount 2 + [ + 'storage_id' => 3, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + // Different root_id + [ + 'storage_id' => 4, + 'root_id' => 2, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + // Different user_id + [ + 'storage_id' => 5, + 'root_id' => 1, + 'user_id' => 'user2', + 'mount_point' => '/user1/files/1.txt/', + ], + // Different mount_point + [ + 'storage_id' => 6, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/2.txt/', + ], + ]; + + $qb = $this->connection->getQueryBuilder(); + $qb->insert('mounts') + ->values([ + 'storage_id' => $qb->createParameter('storage_id'), + 'root_id' => $qb->createParameter('root_id'), + 'user_id' => $qb->createParameter('user_id'), + 'mount_point' => $qb->createParameter('mount_point'), + ]); + + foreach ($rows as $row) { + $qb + ->setParameter('storage_id', $row['storage_id'], IQueryBuilder::PARAM_INT) + ->setParameter('root_id', $row['root_id'], IQueryBuilder::PARAM_INT) + ->setParameter('user_id', $row['user_id'], IQueryBuilder::PARAM_STR) + ->setParameter('mount_point', $row['mount_point'], IQueryBuilder::PARAM_STR) + ->executeStatement(); + } + + $this->config + ->expects($this->once()) + ->method('getSystemValueInt') + ->with('repair_duplicate_mounts_threshold', 10) + ->willReturn(1); + + $output = $this->createMock(IOutput::class); + $this->repair->run($output); + + $result = $this->connection->getQueryBuilder() + ->select('storage_id', 'root_id', 'user_id', 'mount_point') + ->from('mounts') + ->orderBy('storage_id', 'ASC') + ->executeQuery(); + + $this->assertEquals([ + [ + 'storage_id' => 1, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + // Duplicate mount 1 is removed + // Duplicate mount 2 is removed + [ + 'storage_id' => 4, + 'root_id' => 2, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/1.txt/', + ], + [ + 'storage_id' => 5, + 'root_id' => 1, + 'user_id' => 'user2', + 'mount_point' => '/user1/files/1.txt/', + ], + [ + 'storage_id' => 6, + 'root_id' => 1, + 'user_id' => 'user1', + 'mount_point' => '/user1/files/2.txt/', + ], + ], $result->fetchAll()); + + $result->closeCursor(); + } +} |