diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php
index addcb59e68b03..effd064480dd0 100644
--- a/core/Command/Db/Migrations/GenerateMetadataCommand.php
+++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php
@@ -9,22 +9,21 @@
namespace OC\Core\Command\Db\Migrations;
use OC\DB\Connection;
-use OC\DB\MigrationService;
+use OC\Migration\MetadataManager;
use OCP\App\IAppManager;
-use ReflectionClass;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class GenerateMetadataCommand extends Command {
public function __construct(
- private readonly Connection $connection,
+ private readonly MetadataManager $metadataManager,
private readonly IAppManager $appManager,
) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this->setName('migrations:generate-metadata')
->setHidden(true)
->setDescription('Generate metadata from DB migrations - internal and should not be used');
@@ -45,7 +44,9 @@ public function execute(InputInterface $input, OutputInterface $output): int {
return 0;
}
- private function extractMigrationMetadata(): array {
+
+
+ public function extractMigrationMetadata(): array {
return [
'core' => $this->extractMigrationMetadataFromCore(),
'apps' => $this->extractMigrationMetadataFromApps()
@@ -53,7 +54,7 @@ private function extractMigrationMetadata(): array {
}
private function extractMigrationMetadataFromCore(): array {
- return $this->extractMigrationAttributes('core');
+ return $this->metadataManager->extractMigrationAttributes('core');
}
/**
@@ -72,35 +73,11 @@ private function extractMigrationMetadataFromApps(): array {
if (!$alreadyLoaded) {
$this->appManager->loadApp($appId);
}
- $metadata[$appId] = $this->extractMigrationAttributes($appId);
+ $metadata[$appId] = $this->metadataManager->extractMigrationAttributes($appId);
if (!$alreadyLoaded) {
$this->appManager->disableApp($appId);
}
}
return $metadata;
}
-
- /**
- * We get all migrations from an app, and for each migration we extract attributes
- *
- * @param string $appId
- *
- * @return array
- * @throws \Exception
- */
- private 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) {
- $metadata[$version][] = $attribute->newInstance();
- }
- }
-
- return $metadata;
- }
}
diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php
index 17d1d8b01ec61..7ab4c75e19c01 100644
--- a/core/Command/Db/Migrations/PreviewCommand.php
+++ b/core/Command/Db/Migrations/PreviewCommand.php
@@ -8,12 +8,9 @@
*/
namespace OC\Core\Command\Db\Migrations;
-use OC\DB\Connection;
-use OC\DB\MigrationService;
-use OCP\Migration\Attributes\GenericMigrationAttribute;
+use OC\Migration\MetadataManager;
+use OC\Updater\ReleaseMetadata;
use OCP\Migration\Attributes\MigrationAttribute;
-use OCP\Migration\Exceptions\AttributeException;
-use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
@@ -24,14 +21,15 @@
use Symfony\Component\Console\Output\OutputInterface;
class PreviewCommand extends Command {
+ private bool $initiated = false;
public function __construct(
- private readonly Connection $connection,
- private readonly LoggerInterface $logger,
+ private readonly MetadataManager $metadataManager,
+ private readonly ReleaseMetadata $releaseMetadata,
) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('migrations:preview')
->setDescription('Get preview of available DB migrations in case of initiating an upgrade')
@@ -42,21 +40,37 @@ protected function configure() {
public function execute(InputInterface $input, OutputInterface $output): int {
$version = $input->getArgument('version');
+ if (filter_var($version, FILTER_VALIDATE_URL)) {
+ $metadata = $this->releaseMetadata->downloadMetadata($version);
+ } else if (str_starts_with($version, '/')) {
+ $metadata = json_decode(file_get_contents($version), true, flags: JSON_THROW_ON_ERROR);
+ } else {
+ $metadata = $this->releaseMetadata->getMetadata($version);
+ }
- $metadata = $this->getMetadata($version);
- $parsed = $this->getMigrationsAttributes($metadata);
+ $parsed = $this->metadataManager->getMigrationsAttributesFromReleaseMetadata($metadata['migrations'] ?? [], true);
$table = new Table($output);
- $this->displayMigrations($table, 'core', $parsed['core']);
-
+ $this->displayMigrations($table, 'core', $parsed['core'] ?? []);
+ foreach ($parsed['apps'] as $appId => $migrations) {
+ if (!empty($migrations)) {
+ $this->displayMigrations($table, $appId, $migrations);
+ }
+ }
$table->render();
return 0;
}
private function displayMigrations(Table $table, string $appId, array $data): void {
- $done = $this->getDoneMigrations($appId);
- $done = array_diff($done, ['30000Date20240429122720']);
+ if (empty($data)) {
+ return;
+ }
+
+ if ($this->initiated) {
+ $table->addRow(new TableSeparator());
+ }
+ $this->initiated = true;
$table->addRow(
[
@@ -70,13 +84,9 @@ private function displayMigrations(Table $table, string $appId, array $data): vo
]
)->addRow(new TableSeparator());
+ /** @var MigrationAttribute[] $attributes */
foreach($data as $migration => $attributes) {
- if (in_array($migration, $done)) {
- continue;
- }
-
$attributesStr = [];
- /** @var MigrationAttribute[] $attributes */
foreach($attributes as $attribute) {
$definition = '' . $attribute->definition() . "";
$definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription();
@@ -85,78 +95,5 @@ private function displayMigrations(Table $table, string $appId, array $data): vo
}
$table->addRow([$migration, implode("\n", $attributesStr)]);
}
-
- }
-
-
-
-
-
- private function getMetadata(string $version): array {
- $metadata = json_decode(file_get_contents('/tmp/nextcloud-' . $version . '.metadata'), true);
- if (!$metadata) {
- throw new \Exception();
- }
- return $metadata['migrations'] ?? [];
- }
-
- private function getDoneMigrations(string $appId): array {
- $ms = new MigrationService($appId, $this->connection);
- return $ms->getMigratedVersions();
- }
-
- private function getMigrationsAttributes(array $metadata): array {
- $appsAttributes = [];
- foreach (array_keys($metadata['apps']) as $appId) {
- $appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? []);
- }
-
- return [
- 'core' => $this->parseMigrations($metadata['core'] ?? []),
- 'apps' => $appsAttributes
- ];
- }
-
- private function parseMigrations(array $migrations): array {
- $parsed = [];
- foreach (array_keys($migrations) as $entry) {
- $items = $migrations[$entry];
- $parsed[$entry] = [];
- foreach ($items 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;
- }
-
- /**
- * @param array $item
- *
- * @return MigrationAttribute|null
- * @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();
- return $attribute->import($item);
- } catch (\Error) {
- throw new AttributeException('cannot import Attribute');
- }
}
}
diff --git a/lib/private/Migration/MetadataManager.php b/lib/private/Migration/MetadataManager.php
new file mode 100644
index 0000000000000..c5061b6fe0c1c
--- /dev/null
+++ b/lib/private/Migration/MetadataManager.php
@@ -0,0 +1,153 @@
+connection);
+
+ $metadata = [];
+ foreach($ms->getAvailableVersions() as $version) {
+ $metadata[$version] = [];
+ $class = new ReflectionClass($ms->createInstance($version));
+ $attributes = $class->getAttributes();
+ foreach ($attributes as $attribute) {
+ $metadata[$version][] = $attribute->newInstance();
+ }
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * convert direct data from release metadata into a list of Migrations' Attribute
+ *
+ * @param array $metadata
+ * @param bool $filterKnownMigrations ignore metadata already done in local instance
+ *
+ * @return array
+ * @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->isInstalled($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
+ ];
+ }
+
+ /**
+ * convert raw data to a list of MigrationAttribute
+ *
+ * @param array $migrations
+ * @param array $ignoreMigrations
+ *
+ * @return array
+ */
+ 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();
+ return $attribute->import($item);
+ } catch (\Error) {
+ throw new AttributeException('cannot import Attribute');
+ }
+ }
+}
diff --git a/lib/private/Updater/Exceptions/ReleaseMetadataException.php b/lib/private/Updater/Exceptions/ReleaseMetadataException.php
new file mode 100644
index 0000000000000..bc82e4e03df7a
--- /dev/null
+++ b/lib/private/Updater/Exceptions/ReleaseMetadataException.php
@@ -0,0 +1,17 @@
+downloadMetadata($url);
+ }
+
+ /**
+ * download Metadata from a link
+ *
+ * @param string $url
+ *
+ * @return array
+ * @throws ReleaseMetadataException
+ * @since 30.0.0
+ */
+ public function downloadMetadata(string $url): array {
+ $client = $this->clientService->newClient();
+ try {
+ $response = $client->get($url, [
+ 'timeout' => 10,
+ 'connect_timeout' => 10,
+ 'verify' => false,
+ ]);
+ } catch (Exception $e) {
+ throw new ReleaseMetadataException('could not reach metadata at ' . $url, previous: $e);
+ }
+
+ try {
+ return json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR);
+ } catch (JsonException) {
+ throw new ReleaseMetadataException('remote document is not valid');
+ }
+ }
+}