From 7f9fdfbe46583492b76b598b9e444d00c7003a72 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Tue, 14 Apr 2026 11:30:34 +0200 Subject: [PATCH] fix(upgrade): restore missing apps on upgrade Signed-off-by: Julien Veyssier --- lib/private/Updater.php | 14 ++++++++++++++ tests/lib/UpdaterTest.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 8c0a3f363d3..ab154041ce0 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -23,6 +23,7 @@ use OC\Repair\Events\RepairInfoEvent; use OC\Repair\Events\RepairStartEvent; use OC\Repair\Events\RepairStepEvent; use OC\Repair\Events\RepairWarningEvent; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -391,6 +392,8 @@ class Updater extends BasicEmitter { $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]); if (isset($previousEnableStates[$app])) { + $this->restoreMissingAppStoreApp($app); + if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) { $this->appManager->enableAppForGroups($app, $previousEnableStates[$app]); } elseif ($previousEnableStates[$app] === 'yes') { @@ -405,6 +408,17 @@ class Updater extends BasicEmitter { } } + private function restoreMissingAppStoreApp(string $appId): void { + try { + $this->appManager->getAppPath($appId, true); + } catch (AppPathNotFoundException) { + // the app was not found locally but we know it was previously enabled + // so we automatically download it from the appstore and run its missing migrations + $this->installer->downloadApp($appId); + $this->installer->installApp($appId); + } + } + private function logAllEvents(): void { $log = $this->log; diff --git a/tests/lib/UpdaterTest.php b/tests/lib/UpdaterTest.php index 23a4d5329c8..c7717327ca9 100644 --- a/tests/lib/UpdaterTest.php +++ b/tests/lib/UpdaterTest.php @@ -11,6 +11,7 @@ namespace Test; use OC\Installer; use OC\IntegrityCheck\Checker; use OC\Updater; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\IAppConfig; use OCP\IConfig; @@ -107,4 +108,36 @@ class UpdaterTest extends TestCase { $this->assertSame($result, $this->updater->isUpgradePossible($oldVersion, $newVersion, $allowedVersions)); } + + public function testUpgradeAppStoreAppsRestoresMissingAutoDisabledAppBeforeEnabling(): void { + $this->installer->expects($this->once()) + ->method('isUpdateAvailable') + ->with('mailroundcube') + ->willReturn(false); + + $this->installer->expects($this->once()) + ->method('downloadApp') + ->with('mailroundcube'); + + $this->installer->expects($this->once()) + ->method('installApp') + ->with('mailroundcube'); + + $this->appManager->expects($this->once()) + ->method('getAppPath') + ->with('mailroundcube', true) + ->willThrowException(new AppPathNotFoundException('missing')); + + $this->appManager->expects($this->once()) + ->method('enableApp') + ->with('mailroundcube'); + + $this->appManager->expects($this->never()) + ->method('enableAppForGroups'); + + self::invokePrivate($this->updater, 'upgradeAppStoreApps', [ + ['mailroundcube'], + ['mailroundcube' => 'yes'], + ]); + } }