99namespace OC \Core \Command \App ;
1010
1111use OC \Installer ;
12+ use OCP \App \AppPathNotFoundException ;
1213use OCP \App \IAppManager ;
1314use Psr \Log \LoggerInterface ;
1415use 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