mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
feat(app): allow removing installed-but-disabled apps; improve help & logging
- Allow removal of apps that are installed but currently disabled - Add explicit installed check (getAppPath) with clear message when not installed - Expand help text to document uninstall behavior and --keep-data - Mirror console messages to logs for key events and consolidate exception logging - miscellaneous code tidying Signed-off-by: Josh <josh.t.richards@gmail.com>
This commit is contained in:
parent
60a1e725e0
commit
e97cf02574
1 changed files with 64 additions and 45 deletions
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
namespace OC\Core\Command\App;
|
||||
|
||||
use OC\Installer;
|
||||
use OCP\App\AppPathNotFoundException;
|
||||
use OCP\App\IAppManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
|
||||
|
|
@ -32,7 +33,14 @@ class Remove extends Command implements CompletionAwareInterface {
|
|||
protected function configure(): void {
|
||||
$this
|
||||
->setName('app:remove')
|
||||
->setDescription('remove an app')
|
||||
->setDescription('Remove an app from this Nextcloud instance')
|
||||
->setHelp(
|
||||
"Removes the specified app and, if present, runs the app's uninstall steps.\n" .
|
||||
"\n" .
|
||||
"By default, this command runs the app's uninstall steps (which may delete data) and then removes the app files.\n" .
|
||||
"Use `--keep-data` to skip uninstall steps and preserve app data (database tables, configuration, and stored files).\n" .
|
||||
"Note: Some apps may still preserve data either way, depending on their uninstall implementation.\n"
|
||||
)
|
||||
->addArgument(
|
||||
'app-id',
|
||||
InputArgument::REQUIRED,
|
||||
|
|
@ -42,63 +50,73 @@ class Remove extends Command implements CompletionAwareInterface {
|
|||
'keep-data',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'keep app data and do not remove them'
|
||||
'Do not run uninstall tasks; preserve app data and configuration'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$appId = $input->getArgument('app-id');
|
||||
$appId = (string) $input->getArgument('app-id');
|
||||
$keepData = (bool) $input->getOption('keep-data');
|
||||
|
||||
// Check if the app is enabled
|
||||
if (!$this->manager->isEnabledForAnyone($appId)) {
|
||||
$output->writeln($appId . ' is not enabled');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Removing shipped apps is not possible, therefore we pre-check that
|
||||
// before trying to remove it
|
||||
// Prevent removal of shipped/core apps
|
||||
if ($this->manager->isShipped($appId)) {
|
||||
$output->writeln($appId . ' could not be removed as it is a shipped app');
|
||||
return 1;
|
||||
$output->writeln("App '$appId' is a shipped/core app and cannot be removed.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// If we want to keep the data of the app, we simply don't disable it here.
|
||||
// App uninstall tasks are being executed when disabled. More info: PR #11627.
|
||||
if (!$input->getOption('keep-data')) {
|
||||
try {
|
||||
$this->manager->disableApp($appId);
|
||||
$output->writeln($appId . ' disabled');
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
|
||||
$this->logger->error($e->getMessage(), [
|
||||
'app' => 'CLI',
|
||||
'exception' => $e,
|
||||
]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's try to remove the app...
|
||||
// Prevent removal of apps that aren't even installed (note: don't use isInstalled(); it's a misnomer)
|
||||
try {
|
||||
$result = $this->installer->removeApp($appId);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
|
||||
$this->logger->error($e->getMessage(), [
|
||||
'app' => 'CLI',
|
||||
'exception' => $e,
|
||||
]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
$output->writeln($appId . ' could not be removed');
|
||||
return 1;
|
||||
$this->manager->getAppPath($appId);
|
||||
} catch (AppPathNotFoundException $e) {
|
||||
$output->writeln("App '$appId' is not installed. Nothing to remove.");
|
||||
return self::FAILURE; // one could argue this a no-op and should be considered a success (?)
|
||||
}
|
||||
|
||||
$appVersion = $this->manager->getAppVersion($appId);
|
||||
$output->writeln($appId . ' ' . $appVersion . ' removed');
|
||||
|
||||
return 0;
|
||||
// Do not run the specified app's uninstall tasks -- preserving app data/config -- if requested
|
||||
if ($keepData) {
|
||||
$message = "Removing app '$appId' but keeping app data (uninstall hooks skipped).";
|
||||
$output->writeln($message);
|
||||
$this->logger->info($message, [ 'app' => 'CLI', ]);
|
||||
} else {
|
||||
// Disable the app before removing to trigger uninstall steps
|
||||
try {
|
||||
$this->manager->disableApp($appId);
|
||||
$message = "Disabled app '$appId' (uninstall steps executed).";
|
||||
$output->writeln($message);
|
||||
$this->logger->info($message, [ 'app' => 'CLI', ]);
|
||||
} catch (Throwable $e) {
|
||||
$message = "Failed to disable app '$appId' (version $appVersion) - app removal skipped.";
|
||||
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
|
||||
$output->writeln("\n" . $message);
|
||||
$this->logger->error($message, [ 'app' => 'CLI', 'exception' => $e, ]);
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the specified app
|
||||
try {
|
||||
$removeSuccess = $this->installer->removeApp($appId);
|
||||
} catch (Throwable $e) {
|
||||
$removeSuccess = false;
|
||||
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
|
||||
$this->logger->error("Failed to remove app '$appId': " . $e->getMessage(), [ 'app' => 'CLI', 'exception' => $e, ]);
|
||||
}
|
||||
|
||||
// Something went wrong during removeApp(); probably no removal took place or incomplete
|
||||
if (!$removeSuccess) {
|
||||
$message = "\nFailed to remove app '$appId' (version $appVersion) - app files/registration were not removed.";
|
||||
$output->writeln($message);
|
||||
$this->logger->error($message, [ 'app' => 'CLI', ]);
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$message = "Removed app '$appId' (version $appVersion).";
|
||||
$output->writeln($message);
|
||||
$this->logger->info($message, [ 'app' => 'CLI', ]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,6 +135,7 @@ class Remove extends Command implements CompletionAwareInterface {
|
|||
*/
|
||||
public function completeArgumentValues($argumentName, CompletionContext $context): array {
|
||||
if ($argumentName === 'app-id') {
|
||||
// TODO: Include disabled apps too
|
||||
return $this->manager->getEnabledApps();
|
||||
}
|
||||
return [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue