diff options
Diffstat (limited to 'lib/private/Migration')
-rw-r--r-- | lib/private/Migration/BackgroundRepair.php | 69 | ||||
-rw-r--r-- | lib/private/Migration/ConsoleOutput.php | 80 | ||||
-rw-r--r-- | lib/private/Migration/Exceptions/AttributeException.php | 17 | ||||
-rw-r--r-- | lib/private/Migration/MetadataManager.php | 168 | ||||
-rw-r--r-- | lib/private/Migration/NullOutput.php | 36 | ||||
-rw-r--r-- | lib/private/Migration/SimpleOutput.php | 68 |
6 files changed, 438 insertions, 0 deletions
diff --git a/lib/private/Migration/BackgroundRepair.php b/lib/private/Migration/BackgroundRepair.php new file mode 100644 index 00000000000..d542b82d5e1 --- /dev/null +++ b/lib/private/Migration/BackgroundRepair.php @@ -0,0 +1,69 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OC\Repair; +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; + +/** + * Class BackgroundRepair + * + * @package OC\Migration + */ +class BackgroundRepair extends TimedJob { + public function __construct( + private Repair $repair, + ITimeFactory $time, + private LoggerInterface $logger, + private IJobList $jobList, + private IAppManager $appManager, + ) { + parent::__construct($time); + $this->setInterval(15 * 60); + } + + /** + * @param array $argument + * @throws \Exception + */ + protected function run($argument): void { + if (!isset($argument['app']) || !isset($argument['step'])) { + // remove the job - we can never execute it + $this->jobList->remove($this, $this->argument); + return; + } + $app = $argument['app']; + + $this->appManager->loadApp($app); + + $step = $argument['step']; + $this->repair->setRepairSteps([]); + try { + $this->repair->addStep($step); + } catch (\Exception $ex) { + $this->logger->error($ex->getMessage(), [ + 'app' => 'migration', + 'exception' => $ex, + ]); + + // remove the job - we can never execute it + $this->jobList->remove($this, $this->argument); + return; + } + + // execute the repair step + $this->repair->run(); + + // remove the job once executed successfully + $this->jobList->remove($this, $this->argument); + } +} diff --git a/lib/private/Migration/ConsoleOutput.php b/lib/private/Migration/ConsoleOutput.php new file mode 100644 index 00000000000..31412bf4ff0 --- /dev/null +++ b/lib/private/Migration/ConsoleOutput.php @@ -0,0 +1,80 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OCP\Migration\IOutput; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class SimpleOutput + * + * Just a simple IOutput implementation with writes messages to the log file. + * Alternative implementations will write to the console or to the web ui (web update case) + * + * @package OC\Migration + */ +class ConsoleOutput implements IOutput { + private ?ProgressBar $progressBar = null; + + public function __construct( + private OutputInterface $output, + ) { + } + + public function debug(string $message): void { + $this->output->writeln($message, OutputInterface::VERBOSITY_VERBOSE); + } + + /** + * @param string $message + */ + public function info($message): void { + $this->output->writeln("<info>$message</info>"); + } + + /** + * @param string $message + */ + public function warning($message): void { + $this->output->writeln("<comment>$message</comment>"); + } + + /** + * @param int $max + */ + public function startProgress($max = 0): void { + if (!is_null($this->progressBar)) { + $this->progressBar->finish(); + } + $this->progressBar = new ProgressBar($this->output); + $this->progressBar->start($max); + } + + /** + * @param int $step + * @param string $description + */ + public function advance($step = 1, $description = ''): void { + if (is_null($this->progressBar)) { + $this->progressBar = new ProgressBar($this->output); + $this->progressBar->start(); + } + $this->progressBar->advance($step); + if (!is_null($description)) { + $this->output->write(" $description"); + } + } + + public function finishProgress(): void { + if (is_null($this->progressBar)) { + return; + } + $this->progressBar->finish(); + } +} diff --git a/lib/private/Migration/Exceptions/AttributeException.php b/lib/private/Migration/Exceptions/AttributeException.php new file mode 100644 index 00000000000..3daf99032ad --- /dev/null +++ b/lib/private/Migration/Exceptions/AttributeException.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Migration\Exceptions; + +use Exception; + +/** + * @since 30.0.0 + */ +class AttributeException extends Exception { +} diff --git a/lib/private/Migration/MetadataManager.php b/lib/private/Migration/MetadataManager.php new file mode 100644 index 00000000000..f4cb95342b4 --- /dev/null +++ b/lib/private/Migration/MetadataManager.php @@ -0,0 +1,168 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OC\DB\Connection; +use OC\DB\MigrationService; +use OC\Migration\Exceptions\AttributeException; +use OCP\App\IAppManager; +use OCP\Migration\Attributes\GenericMigrationAttribute; +use OCP\Migration\Attributes\MigrationAttribute; +use Psr\Log\LoggerInterface; +use ReflectionClass; + +/** + * Helps managing DB Migrations' Metadata + * + * @since 30.0.0 + */ +class MetadataManager { + public function __construct( + private readonly IAppManager $appManager, + private readonly Connection $connection, + private readonly LoggerInterface $logger, + ) { + } + + /** + * We get all migrations from an app (or 'core'), and + * for each migration files we extract its attributes + * + * @param string $appId + * + * @return array<string, MigrationAttribute[]> + * @since 30.0.0 + */ + public function extractMigrationAttributes(string $appId): array { + $ms = new MigrationService($appId, $this->connection); + + $metadata = []; + foreach ($ms->getAvailableVersions() as $version) { + $metadata[$version] = []; + $class = new ReflectionClass($ms->createInstance($version)); + $attributes = $class->getAttributes(); + foreach ($attributes as $attribute) { + $item = $attribute->newInstance(); + if ($item instanceof MigrationAttribute) { + $metadata[$version][] = $item; + } + } + } + + return $metadata; + } + + /** + * convert direct data from release metadata into a list of Migrations' Attribute + * + * @param array<array-key, array<array-key, array>> $metadata + * @param bool $filterKnownMigrations ignore metadata already done in local instance + * + * @return array{apps: array<array-key, array<string, MigrationAttribute[]>>, core: array<string, MigrationAttribute[]>} + * @since 30.0.0 + */ + public function getMigrationsAttributesFromReleaseMetadata( + array $metadata, + bool $filterKnownMigrations = false, + ): array { + $appsAttributes = []; + foreach (array_keys($metadata['apps']) as $appId) { + if ($filterKnownMigrations && !$this->appManager->isEnabledForAnyone($appId)) { + continue; // if not interested and app is not installed + } + + $done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : []; + $appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done); + } + + $done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : []; + return [ + 'core' => $this->parseMigrations($metadata['core'] ?? [], $done), + 'apps' => $appsAttributes + ]; + } + + /** + * returns list of installed apps that does not support migrations metadata (yet) + * + * @param array<array-key, array<array-key, array>> $metadata + * + * @return string[] + * @since 30.0.0 + */ + public function getUnsupportedApps(array $metadata): array { + return array_values(array_diff($this->appManager->getEnabledApps(), array_keys($metadata['apps']))); + } + + /** + * convert raw data to a list of MigrationAttribute + * + * @param array $migrations + * @param array $ignoreMigrations + * + * @return array<string, MigrationAttribute[]> + */ + private function parseMigrations(array $migrations, array $ignoreMigrations = []): array { + $parsed = []; + foreach (array_keys($migrations) as $entry) { + if (in_array($entry, $ignoreMigrations)) { + continue; + } + + $parsed[$entry] = []; + foreach ($migrations[$entry] as $item) { + try { + $parsed[$entry][] = $this->createAttribute($item); + } catch (AttributeException $e) { + $this->logger->warning('exception while trying to create attribute', ['exception' => $e, 'item' => json_encode($item)]); + $parsed[$entry][] = new GenericMigrationAttribute($item); + } + } + } + + return $parsed; + } + + /** + * returns migrations already done + * + * @param string $appId + * + * @return array + * @throws \Exception + */ + private function getKnownMigrations(string $appId): array { + $ms = new MigrationService($appId, $this->connection); + return $ms->getMigratedVersions(); + } + + /** + * generate (deserialize) a MigrationAttribute from a serialized version + * + * @param array $item + * + * @return MigrationAttribute + * @throws AttributeException + */ + private function createAttribute(array $item): MigrationAttribute { + $class = $item['class'] ?? ''; + $namespace = 'OCP\Migration\Attributes\\'; + if (!str_starts_with($class, $namespace) + || !ctype_alpha(substr($class, strlen($namespace)))) { + throw new AttributeException('class name does not looks valid'); + } + + try { + $attribute = new $class($item['table'] ?? ''); + return $attribute->import($item); + } catch (\Error) { + throw new AttributeException('cannot import Attribute'); + } + } +} diff --git a/lib/private/Migration/NullOutput.php b/lib/private/Migration/NullOutput.php new file mode 100644 index 00000000000..8db7b950af8 --- /dev/null +++ b/lib/private/Migration/NullOutput.php @@ -0,0 +1,36 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OCP\Migration\IOutput; + +/** + * Class NullOutput + * + * A simple IOutput that discards all output + * + * @package OC\Migration + */ +class NullOutput implements IOutput { + public function debug(string $message): void { + } + + public function info($message): void { + } + + public function warning($message): void { + } + + public function startProgress($max = 0): void { + } + + public function advance($step = 1, $description = ''): void { + } + + public function finishProgress(): void { + } +} diff --git a/lib/private/Migration/SimpleOutput.php b/lib/private/Migration/SimpleOutput.php new file mode 100644 index 00000000000..b7a07cc6ff2 --- /dev/null +++ b/lib/private/Migration/SimpleOutput.php @@ -0,0 +1,68 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2015 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Migration; + +use OCP\Migration\IOutput; +use Psr\Log\LoggerInterface; + +/** + * Class SimpleOutput + * + * Just a simple IOutput implementation with writes messages to the log file. + * Alternative implementations will write to the console or to the web ui (web update case) + * + * @package OC\Migration + */ +class SimpleOutput implements IOutput { + public function __construct( + private LoggerInterface $logger, + private $appName, + ) { + } + + public function debug(string $message): void { + $this->logger->debug($message, ['app' => $this->appName]); + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function info($message): void { + $this->logger->info($message, ['app' => $this->appName]); + } + + /** + * @param string $message + * @since 9.1.0 + */ + public function warning($message): void { + $this->logger->warning($message, ['app' => $this->appName]); + } + + /** + * @param int $max + * @since 9.1.0 + */ + public function startProgress($max = 0): void { + } + + /** + * @param int $step + * @param string $description + * @since 9.1.0 + */ + public function advance($step = 1, $description = ''): void { + } + + /** + * @since 9.1.0 + */ + public function finishProgress(): void { + } +} |