Skip to content

Commit 62e3aea

Browse files
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 <[email protected]>
1 parent f4753cc commit 62e3aea

File tree

1 file changed

+54
-35
lines changed

1 file changed

+54
-35
lines changed

core/Command/App/Remove.php

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace OC\Core\Command\App;
1010

1111
use OC\Installer;
12+
use OCP\App\AppPathNotFoundException;
1213
use OCP\App\IAppManager;
1314
use Psr\Log\LoggerInterface;
1415
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
@@ -32,7 +33,14 @@ public function __construct(
3233
protected function configure(): void {
3334
$this
3435
->setName('app:remove')
35-
->setDescription('remove an app')
36+
->setDescription('Remove an app from this Nextcloud instance')
37+
->setHelp(
38+
"Removes the specified app and, if present, runs the app's uninstall steps.\n" .
39+
"\n" .
40+
"By default, this command runs the app's uninstall steps (which may delete data) and then removes the app files.\n" .
41+
"Use `--keep-data` to skip uninstall steps and preserve app data (database tables, configuration, and stored files).\n" .
42+
"Note: Some apps may still preserve data either way, depending on their uninstall implementation.\n"
43+
)
3644
->addArgument(
3745
'app-id',
3846
InputArgument::REQUIRED,
@@ -42,63 +50,73 @@ protected function configure(): void {
4250
'keep-data',
4351
null,
4452
InputOption::VALUE_NONE,
45-
'keep app data and do not remove them'
53+
'Do not run uninstall tasks; preserve app data and configuration'
4654
);
4755
}
4856

4957
protected function execute(InputInterface $input, OutputInterface $output): int {
50-
$appId = $input->getArgument('app-id');
58+
$appId = (string) $input->getArgument('app-id');
59+
$keepData = (bool) $input->getOption('keep-data');
5160

52-
// Check if the app is enabled
53-
if (!$this->manager->isEnabledForAnyone($appId)) {
54-
$output->writeln($appId . ' is not enabled');
55-
return 1;
61+
// Prevent removal of shipped/core apps
62+
if ($this->manager->isShipped($appId)) {
63+
$output->writeln("App '$appId' is a shipped/core app and cannot be removed.");
64+
return self::FAILURE;
5665
}
5766

58-
// Removing shipped apps is not possible, therefore we pre-check that
59-
// before trying to remove it
60-
if ($this->manager->isShipped($appId)) {
61-
$output->writeln($appId . ' could not be removed as it is a shipped app');
62-
return 1;
67+
// Prevent removal of apps that aren't even installed (note: don't use isInstalled(); it's a misnomer)
68+
try {
69+
$this->manager->getAppPath($appId);
70+
} catch (AppPathNotFoundException $e) {
71+
$output->writeln("App '$appId' is not installed. Nothing to remove.");
72+
return self::FAILURE; // one could argue this a no-op and should be considered a success (?)
6373
}
6474

65-
// If we want to keep the data of the app, we simply don't disable it here.
66-
// App uninstall tasks are being executed when disabled. More info: PR #11627.
67-
if (!$input->getOption('keep-data')) {
75+
$appVersion = $this->manager->getAppVersion($appId);
76+
77+
// Do not run the specified app's uninstall tasks -- preserving app data/config -- if requested
78+
if ($keepData) {
79+
$message = "Removing app '$appId' but keeping app data (uninstall hooks skipped).";
80+
$output->writeln($message);
81+
$this->logger->info($message, [ 'app' => 'CLI', ]);
82+
} else {
83+
// Disable the app before removing to trigger uninstall steps
6884
try {
6985
$this->manager->disableApp($appId);
70-
$output->writeln($appId . ' disabled');
86+
$message = "Disabled app '$appId' (uninstall steps executed).";
87+
$output->writeln($message);
88+
$this->logger->info($message, [ 'app' => 'CLI', ]);
7189
} catch (Throwable $e) {
90+
$message = "Failed to disable app '$appId' (version $appVersion) - app removal skipped.";
7291
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
73-
$this->logger->error($e->getMessage(), [
74-
'app' => 'CLI',
75-
'exception' => $e,
76-
]);
77-
return 1;
92+
$output->writeln("\n" . $message);
93+
$this->logger->error($message, [ 'app' => 'CLI', 'exception' => $e, ]);
94+
return self::FAILURE;
7895
}
7996
}
8097

81-
// Let's try to remove the app...
98+
// Remove the specified app
8299
try {
83-
$result = $this->installer->removeApp($appId);
100+
$removeSuccess = $this->installer->removeApp($appId);
84101
} catch (Throwable $e) {
102+
$removeSuccess = false;
85103
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
86-
$this->logger->error($e->getMessage(), [
87-
'app' => 'CLI',
88-
'exception' => $e,
89-
]);
90-
return 1;
104+
$this->logger->error("Failed to remove app '$appId': " . $e->getMessage(), [ 'app' => 'CLI', 'exception' => $e, ]);
91105
}
92106

93-
if ($result === false) {
94-
$output->writeln($appId . ' could not be removed');
95-
return 1;
107+
// Something went wrong during removeApp(); probably no removal took place or incomplete
108+
if (!$removeSuccess) {
109+
$message = "\nFailed to remove app '$appId' (version $appVersion) - app files/registration were not removed.";
110+
$output->writeln($message);
111+
$this->logger->error($message, [ 'app' => 'CLI', ]);
112+
return self::FAILURE;
96113
}
114+
115+
$message = "Removed app '$appId' (version $appVersion).";
116+
$output->writeln($message);
117+
$this->logger->info($message, [ 'app' => 'CLI', ]);
97118

98-
$appVersion = $this->manager->getAppVersion($appId);
99-
$output->writeln($appId . ' ' . $appVersion . ' removed');
100-
101-
return 0;
119+
return self::SUCCESS;
102120
}
103121

104122
/**
@@ -117,6 +135,7 @@ public function completeOptionValues($optionName, CompletionContext $context): a
117135
*/
118136
public function completeArgumentValues($argumentName, CompletionContext $context): array {
119137
if ($argumentName === 'app-id') {
138+
// TODO: Include disabled apps too
120139
return $this->manager->getEnabledApps();
121140
}
122141
return [];

0 commit comments

Comments
 (0)