diff --git a/composer.json b/composer.json index 7cebbdc3..2457790a 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "cs:check": "php-cs-fixer fix --dry-run --diff", "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -not -path './node_modules/*' -print0 | xargs -0 -n1 php -l", "psalm": "psalm", - "psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType" + "psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType", + "psalm:update-baseline": "psalm --threads=1 --update-baseline" }, "config": { "optimize-autoloader": true, diff --git a/composer.lock b/composer.lock index fd819629..c9739c3a 100644 --- a/composer.lock +++ b/composer.lock @@ -179,16 +179,16 @@ "source": { "type": "git", "url": "https://github.com/ChristophWurst/nextcloud_composer.git", - "reference": "b664d5ed366d7dc1e9d7f81e62f09ec6c923430b" + "reference": "8acf976cf96ca52a720bec554af52e109e7100a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/b664d5ed366d7dc1e9d7f81e62f09ec6c923430b", - "reference": "b664d5ed366d7dc1e9d7f81e62f09ec6c923430b", + "url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/8acf976cf96ca52a720bec554af52e109e7100a1", + "reference": "8acf976cf96ca52a720bec554af52e109e7100a1", "shasum": "" }, "require": { - "php": "^7.3 || ~8.0.0", + "php": "^7.4 || ~8.0 || ~8.1", "psr/container": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.1" @@ -215,7 +215,7 @@ "issues": "https://github.com/ChristophWurst/nextcloud_composer/issues", "source": "https://github.com/ChristophWurst/nextcloud_composer/tree/master" }, - "time": "2022-02-03T01:08:28+00:00" + "time": "2022-02-22T10:38:34+00:00" }, { "name": "composer/package-versions-deprecated", @@ -4694,16 +4694,16 @@ }, { "name": "vimeo/psalm", - "version": "4.20.0", + "version": "4.21.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "f82a70e7edfc6cf2705e9374c8a0b6a974a779ed" + "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/f82a70e7edfc6cf2705e9374c8a0b6a974a779ed", - "reference": "f82a70e7edfc6cf2705e9374c8a0b6a974a779ed", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d8bec4c7aaee111a532daec32fb09de5687053d1", + "reference": "d8bec4c7aaee111a532daec32fb09de5687053d1", "shasum": "" }, "require": { @@ -4794,9 +4794,9 @@ ], "support": { "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.20.0" + "source": "https://github.com/vimeo/psalm/tree/4.21.0" }, - "time": "2022-02-03T17:03:47+00:00" + "time": "2022-02-18T04:34:15+00:00" }, { "name": "webmozart/assert", @@ -4920,5 +4920,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.1.0" } diff --git a/lib/ExportDestination.php b/lib/ExportDestination.php index 97fb18bb..ced3ed2b 100644 --- a/lib/ExportDestination.php +++ b/lib/ExportDestination.php @@ -29,6 +29,7 @@ use OCP\Files\File; use OCP\Files\Folder; use OCP\ITempManager; +use OCP\UserMigration\IExportDestination; use ZipStreamer\COMPR; use ZipStreamer\ZipStreamer; @@ -53,7 +54,7 @@ public function __construct(ITempManager $tempManager, string $uid) { /** * {@inheritDoc} */ - public function addFile(string $path, string $content): bool { + public function addFileContents(string $path, string $content): bool { $stream = fopen('php://temp', 'r+'); fwrite($stream, $content); rewind($stream); @@ -61,6 +62,14 @@ public function addFile(string $path, string $content): bool { return true; } + /** + * {@inheritDoc} + */ + public function addFileAsStream(string $path, $stream): bool { + $this->streamer->addFileFromStream($stream, $path); + return true; + } + /** * {@inheritDoc} */ @@ -83,6 +92,13 @@ public function copyFolder(Folder $folder, string $destinationPath): bool { return true; } + /** + * {@inheritDoc} + */ + public function setMigratorVersions(array $versions): bool { + return $this->addFileContents("migrator_versions.json", json_encode($versions)); + } + /** * {@inheritDoc} */ diff --git a/lib/IExportDestination.php b/lib/IExportDestination.php deleted file mode 100644 index 74912ec4..00000000 --- a/lib/IExportDestination.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * @author Côme Chilliet - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see . - * - */ - -namespace OCA\UserMigration; - -use OCP\Files\Folder; - -interface IExportDestination { - /** - * Adds a file to the export - * - * @param string $path Full path to the file in the export archive. Parent directories will be created if needed. - * @param string $content The full content of the file. - * @return bool whether the file was successfully added. - */ - public function addFile(string $path, string $content): bool; - - /** - * Copy a folder to the export - * - * @param Folder $folder folder to copy to the export archive. - * @param string $destinationPath Full path to the folder in the export archive. Parent directories will be created if needed. - * @return bool whether the folder was successfully added. - */ - public function copyFolder(Folder $folder, string $destinationPath): bool; - - /** - * Called after export is complete - */ - public function close(): void; -} diff --git a/lib/IImportSource.php b/lib/IImportSource.php deleted file mode 100644 index df43144c..00000000 --- a/lib/IImportSource.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * @author Côme Chilliet - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see . - * - */ - -namespace OCA\UserMigration; - -use OCP\Files\Folder; - -interface IImportSource { - /** - * Reads a file from the export - * - * @param string $path Full path to the file in the export archive. - * @return string The full content of the file. - */ - public function getFileContents(string $path): string; - - /** - * Reads a file from the export as a stream - * - * @param string $path Full path to the file in the export archive. - * @return resource A stream resource to read from to get the file content. - */ - public function getFileAsStream(string $path); - - /** - * Copy files from the export to a Folder - * - * Folder $destination folder to copy into - * string $sourcePath path in the export archive - */ - public function copyToFolder(Folder $destination, string $sourcePath): bool; - - /** - * Called after import is complete - */ - public function close(): void; -} diff --git a/lib/ImportSource.php b/lib/ImportSource.php index e2a074ab..9ad3bac4 100644 --- a/lib/ImportSource.php +++ b/lib/ImportSource.php @@ -29,12 +29,18 @@ use OC\Archive\Archive; use OC\Archive\ZIP; use OCP\Files\Folder; +use OCP\UserMigration\IImportSource; class ImportSource implements IImportSource { private Archive $archive; private string $path; + /** + * @var ?array + */ + private ?array $migratorVersions = null; + public function __construct(string $path) { $this->path = $path; $this->archive = new ZIP($this->path); @@ -83,6 +89,24 @@ public function copyToFolder(Folder $destination, string $sourcePath): bool { return true; } + /** + * {@inheritDoc} + */ + public function getMigratorVersions(): array { + if ($this->migratorVersions === null) { + $this->migratorVersions = json_decode($this->getFileContents("migrator_versions.json"), true, 512, JSON_THROW_ON_ERROR); + } + return $this->migratorVersions; + } + + /** + * {@inheritDoc} + */ + public function getMigratorVersion(string $migrator): ?int { + $versions = $this->getMigratorVersions(); + return $versions[$migrator] ?? null; + } + /** * {@inheritDoc} */ diff --git a/lib/Service/UserMigrationService.php b/lib/Service/UserMigrationService.php index 39b5f938..1a3d5a8f 100644 --- a/lib/Service/UserMigrationService.php +++ b/lib/Service/UserMigrationService.php @@ -5,6 +5,7 @@ /** * @copyright Copyright (c) 2022 Côme Chilliet * + * @author Christopher Ng * @author Côme Chilliet * * @license GNU AGPL version 3 or any later version @@ -28,10 +29,7 @@ use OCA\UserMigration\Exception\UserMigrationException; use OCA\UserMigration\ExportDestination; -use OCA\UserMigration\IExportDestination; -use OCA\UserMigration\IImportSource; use OCA\UserMigration\ImportSource; -use OC\Files\Filesystem; use OCP\Accounts\IAccountManager; use OCP\Files\IRootFolder; use OCP\IConfig; @@ -39,10 +37,19 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Security\ISecureRandom; +use OCP\UserMigration\IExportDestination; +use OCP\UserMigration\IImportSource; +use OCP\UserMigration\IMigrator; +use OCP\UserMigration\TMigratorBasicVersionHandling; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Files\Filesystem; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; class UserMigrationService { + use TMigratorBasicVersionHandling; + protected IRootFolder $root; protected IConfig $config; @@ -53,18 +60,29 @@ class UserMigrationService { protected IUserManager $userManager; + protected ContainerInterface $container; + + // Allow use of the private Coordinator class here to get and run registered migrators + protected Coordinator $coordinator; + public function __construct( IRootFolder $rootFolder, IConfig $config, IAccountManager $accountManager, ITempManager $tempManager, - IUserManager $userManager + IUserManager $userManager, + ContainerInterface $container, + Coordinator $coordinator ) { $this->root = $rootFolder; $this->config = $config; $this->accountManager = $accountManager; $this->tempManager = $tempManager; $this->userManager = $userManager; + $this->container = $container; + $this->coordinator = $coordinator; + + $this->mandatory = true; } /** @@ -80,6 +98,12 @@ public function export(IUser $user, ?OutputInterface $output = null): string { \OC::$server->getUserFolder($uid); Filesystem::initMountPoints($uid); + $context = $this->coordinator->getRegistrationContext(); + + if ($context === null) { + throw new UserMigrationException("Failed to get context"); + } + $exportDestination = new ExportDestination($this->tempManager, $uid); // copy the files @@ -113,6 +137,20 @@ public function export(IUser $user, ?OutputInterface $output = null): string { $output ); + // Run exports of registered migrators + $migratorVersions = [ + static::class => $this->getVersion(), + ]; + foreach ($context->getUserMigrators() as $migratorRegistration) { + /** @var IMigrator $migrator */ + $migrator = $this->container->get($migratorRegistration->getService()); + $migrator->export($user, $exportDestination, $output); + $migratorVersions[get_class($migrator)] = $migrator->getVersion(); + } + if ($exportDestination->setMigratorVersions($migratorVersions) === false) { + throw new UserMigrationException("Could not export user information."); + } + $exportDestination->close(); $output->writeln("Export saved in ".$exportDestination->getPath()); return $exportDestination->getPath(); @@ -124,13 +162,39 @@ public function import(string $path, ?OutputInterface $output = null): void { $output->writeln("Importing from ${path}…"); $importSource = new ImportSource($path); + $context = $this->coordinator->getRegistrationContext(); + try { - // TODO check versions + if ($context === null) { + throw new UserMigrationException("Failed to get context"); + } + $migratorVersions = $importSource->getMigratorVersions(); + + if (!$this->canImport($importSource, $migratorVersions[static::class] ?? null)) { + throw new UserMigrationException("Version ${$migratorVersions[static::class]} for main class ".static::class." is not compatible"); + } + + // Check versions + foreach ($context->getUserMigrators() as $migratorRegistration) { + /** @var IMigrator $migrator */ + $migrator = $this->container->get($migratorRegistration->getService()); + if (!$migrator->canImport($importSource)) { + throw new UserMigrationException("Version ".($importSource->getMigratorVersion(get_class($migrator)) ?? 'null')." for migrator ".get_class($migrator)." is not supported"); + } + } $user = $this->importUser($importSource, $output); $this->importAccountInformation($user, $importSource, $output); $this->importAppsSettings($user, $importSource, $output); $this->importFiles($user, $importSource, $output); + + // Run imports of registered migrators + foreach ($context->getUserMigrators() as $migratorRegistration) { + /** @var IMigrator $migrator */ + $migrator = $this->container->get($migratorRegistration->getService()); + $migrator->import($user, $importSource, $output); + } + $uid = $user->getUID(); $output->writeln("Successfully imported $uid from $path"); } finally { @@ -184,7 +248,7 @@ protected function exportUserInformation(IUser $user, 'enabled' => $user->isEnabled(), ]; - if ($exportDestination->addFile("user.json", json_encode($userinfo)) === false) { + if ($exportDestination->addFileContents("user.json", json_encode($userinfo)) === false) { throw new UserMigrationException("Could not export user information."); } } @@ -218,7 +282,7 @@ protected function exportAccountInformation(IUser $user, OutputInterface $output): void { $output->writeln("Exporting account information in account.json…"); - if ($exportDestination->addFile("account.json", json_encode($this->accountManager->getAccount($user))) === false) { + if ($exportDestination->addFileContents("account.json", json_encode($this->accountManager->getAccount($user))) === false) { throw new UserMigrationException("Could not export account information."); } } @@ -247,7 +311,7 @@ protected function exportVersions(string $uid, \OC_App::getAppVersions() ); - if ($exportDestination->addFile("versions.json", json_encode($versions)) === false) { + if ($exportDestination->addFileContents("versions.json", json_encode($versions)) === false) { throw new UserMigrationException("Could not export versions."); } } @@ -262,7 +326,7 @@ protected function exportAppsSettings(string $uid, $data = $this->config->getAllUserValues($uid); - if ($exportDestination->addFile("settings.json", json_encode($data)) === false) { + if ($exportDestination->addFileContents("settings.json", json_encode($data)) === false) { throw new UserMigrationException("Could not export settings."); } } diff --git a/psalm.xml b/psalm.xml index 5ef2b0fe..f56440fa 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,6 +1,5 @@ + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index a176c44e..476b321f 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,10 +1,14 @@ - + - + $read $stream + $stream + + $versions + diff --git a/tests/stubs/stub.phpstub b/tests/stubs/stub.phpstub new file mode 100644 index 00000000..68bb46f7 --- /dev/null +++ b/tests/stubs/stub.phpstub @@ -0,0 +1,55 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +use OCP\UserMigration\IMigrator as IUserMigrator; + +namespace OC\AppFramework\Bootstrap { + class Coordinator { + public function getRegistrationContext(): ?RegistrationContext {} + } + + class RegistrationContext { + /** + * @return ServiceRegistration[] + */ + public function getUserMigrators(): array {} + } + + /** + * @psalm-immutable + * @template T + */ + class ServiceRegistration extends ARegistration { + /** + * @psalm-return class-string + */ + public function getService(): string {} + } + + /** + * @psalm-immutable + */ + abstract class ARegistration { + public function getAppId(): string {} + } +}