setName('backup:user:restore') ->setDescription('Restore a user from a user_migration export (see backup:user:backup)') ->addArgument('target', InputArgument::REQUIRED, 'Location name or id') ->addArgument('manifest', InputArgument::REQUIRED, 'Manifest path printed by backup:user:backup (users//), or a user id with exactly one export') ->addOption('mode', 'm', InputOption::VALUE_REQUIRED, 'replace or as-backup', UserRestoreService::MODE_REPLACE) ->addOption('force', null, InputOption::VALUE_NONE, 'Replace an existing account without asking (required when not interactive)') ->addOption('install-user-migration', null, InputOption::VALUE_NONE, 'Install and enable user_migration if it is missing (non-interactive consent)') ->setHelp(<<<'HELP' Never imports over a live account. Modes: replace (default) safety-exports the current account first, then deletes and recreates it fresh from the backup, under the same user id as-backup restores into a new account "-bak" (or "-bak-2", ...) instead, so an admin can compare and transfer by hand; Nextcloud cannot rename a user id, so the backup gets the new name, never the existing account occ backup:user:list shows the exports on a location. A user id alone works when that user has exactly one export there; with more, the command lists them and you pass one. Example: occ backup:user:restore offsite users/alice/20261005T120000Z-ab12cd34 occ backup:user:restore offsite alice occ backup:user:restore offsite users/alice/20261005T120000Z-ab12cd34 --mode=as-backup HELP); } protected function execute(InputInterface $input, OutputInterface $output): int { $mode = (string)$input->getOption('mode'); try { $target = $this->targets->get($input->getArgument('target')); $ref = (string)$input->getArgument('manifest'); $manifest = $this->targets->repository($target)->resolveUserExport($ref); } catch (\Throwable $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } if ($manifest !== $ref) { $output->writeln("Using $manifest."); } $uid = explode('/', $manifest)[1]; if ($mode === UserRestoreService::MODE_REPLACE && $this->userManager->userExists($uid) && !$input->getOption('force')) { // Replace deletes the live account (after a safety export). Shares to others and data of // apps without a user_migration migrator do not come back, so never do it unasked. if (!$input->isInteractive()) { $output->writeln("$uid exists: --mode=replace deletes and recreates it. Add --force, or use --mode=as-backup."); return 1; } $q = new ConfirmationQuestion("$uid exists and will be deleted and recreated from the backup (a safety export is made first; its shares to others and data of apps without a migrator are not restored). Continue? [y/N] ", false); if (!(new QuestionHelper())->ask($input, $output, $q)) { $output->writeln('Cancelled.'); return 1; } } try { $output->writeln('Note: on restore, user_migration only imports the app settings on its own allowlist (e.g. calendar view and reminder settings); other app settings of this user are skipped. Reason: NG Backup uses user_migration\'s importer; widening the list would mean building a settings importer of our own.'); $this->installer->ensureEnabled($input, $output); $user = $this->userRestore->restoreUser($target, $manifest, $mode); } catch (\Throwable $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } $output->writeln('Restored ' . $user->getUID() . '.'); return 0; } }