MetadataManager.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Migration;
  8. use OC\DB\Connection;
  9. use OC\DB\MigrationService;
  10. use OC\Migration\Exceptions\AttributeException;
  11. use OCP\App\IAppManager;
  12. use OCP\Migration\Attributes\GenericMigrationAttribute;
  13. use OCP\Migration\Attributes\MigrationAttribute;
  14. use Psr\Log\LoggerInterface;
  15. use ReflectionClass;
  16. /**
  17. * Helps managing DB Migrations' Metadata
  18. *
  19. * @since 30.0.0
  20. */
  21. class MetadataManager {
  22. public function __construct(
  23. private readonly IAppManager $appManager,
  24. private readonly Connection $connection,
  25. private readonly LoggerInterface $logger,
  26. ) {
  27. }
  28. /**
  29. * We get all migrations from an app (or 'core'), and
  30. * for each migration files we extract its attributes
  31. *
  32. * @param string $appId
  33. *
  34. * @return array<string, MigrationAttribute[]>
  35. * @since 30.0.0
  36. */
  37. public function extractMigrationAttributes(string $appId): array {
  38. $ms = new MigrationService($appId, $this->connection);
  39. $metadata = [];
  40. foreach ($ms->getAvailableVersions() as $version) {
  41. $metadata[$version] = [];
  42. $class = new ReflectionClass($ms->createInstance($version));
  43. $attributes = $class->getAttributes();
  44. foreach ($attributes as $attribute) {
  45. $item = $attribute->newInstance();
  46. if ($item instanceof MigrationAttribute) {
  47. $metadata[$version][] = $item;
  48. }
  49. }
  50. }
  51. return $metadata;
  52. }
  53. /**
  54. * convert direct data from release metadata into a list of Migrations' Attribute
  55. *
  56. * @param array<array-key, array<array-key, array>> $metadata
  57. * @param bool $filterKnownMigrations ignore metadata already done in local instance
  58. *
  59. * @return array{apps: array<array-key, array<string, MigrationAttribute[]>>, core: array<string, MigrationAttribute[]>}
  60. * @since 30.0.0
  61. */
  62. public function getMigrationsAttributesFromReleaseMetadata(
  63. array $metadata,
  64. bool $filterKnownMigrations = false,
  65. ): array {
  66. $appsAttributes = [];
  67. foreach (array_keys($metadata['apps']) as $appId) {
  68. if ($filterKnownMigrations && !$this->appManager->isInstalled($appId)) {
  69. continue; // if not interested and app is not installed
  70. }
  71. $done = ($filterKnownMigrations) ? $this->getKnownMigrations($appId) : [];
  72. $appsAttributes[$appId] = $this->parseMigrations($metadata['apps'][$appId] ?? [], $done);
  73. }
  74. $done = ($filterKnownMigrations) ? $this->getKnownMigrations('core') : [];
  75. return [
  76. 'core' => $this->parseMigrations($metadata['core'] ?? [], $done),
  77. 'apps' => $appsAttributes
  78. ];
  79. }
  80. /**
  81. * returns list of installed apps that does not support migrations metadata (yet)
  82. *
  83. * @param array<array-key, array<array-key, array>> $metadata
  84. *
  85. * @return string[]
  86. * @since 30.0.0
  87. */
  88. public function getUnsupportedApps(array $metadata): array {
  89. return array_values(array_diff($this->appManager->getInstalledApps(), array_keys($metadata['apps'])));
  90. }
  91. /**
  92. * convert raw data to a list of MigrationAttribute
  93. *
  94. * @param array $migrations
  95. * @param array $ignoreMigrations
  96. *
  97. * @return array<string, MigrationAttribute[]>
  98. */
  99. private function parseMigrations(array $migrations, array $ignoreMigrations = []): array {
  100. $parsed = [];
  101. foreach (array_keys($migrations) as $entry) {
  102. if (in_array($entry, $ignoreMigrations)) {
  103. continue;
  104. }
  105. $parsed[$entry] = [];
  106. foreach ($migrations[$entry] as $item) {
  107. try {
  108. $parsed[$entry][] = $this->createAttribute($item);
  109. } catch (AttributeException $e) {
  110. $this->logger->warning('exception while trying to create attribute', ['exception' => $e, 'item' => json_encode($item)]);
  111. $parsed[$entry][] = new GenericMigrationAttribute($item);
  112. }
  113. }
  114. }
  115. return $parsed;
  116. }
  117. /**
  118. * returns migrations already done
  119. *
  120. * @param string $appId
  121. *
  122. * @return array
  123. * @throws \Exception
  124. */
  125. private function getKnownMigrations(string $appId): array {
  126. $ms = new MigrationService($appId, $this->connection);
  127. return $ms->getMigratedVersions();
  128. }
  129. /**
  130. * generate (deserialize) a MigrationAttribute from a serialized version
  131. *
  132. * @param array $item
  133. *
  134. * @return MigrationAttribute
  135. * @throws AttributeException
  136. */
  137. private function createAttribute(array $item): MigrationAttribute {
  138. $class = $item['class'] ?? '';
  139. $namespace = 'OCP\Migration\Attributes\\';
  140. if (!str_starts_with($class, $namespace)
  141. || !ctype_alpha(substr($class, strlen($namespace)))) {
  142. throw new AttributeException('class name does not looks valid');
  143. }
  144. try {
  145. $attribute = new $class($item['table'] ?? '');
  146. return $attribute->import($item);
  147. } catch (\Error) {
  148. throw new AttributeException('cannot import Attribute');
  149. }
  150. }
  151. }