From 121e60491d075df782ba1aac081a3ffbff1b30a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:21:21 +0000 Subject: [PATCH] Extract Nextcloud major upgrade logic to script and add UI button Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/8cd11b09-5073-4e27-8e59-9afffaf96c1f Rename sendNotification to execCommandInContainer and reuse for upgrade method Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/88744552-9d64-4de2-9f64-5a98a5e3b200 Add $cmd array validation to execCommandInContainer Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/45d5228c-7834-404e-ba54-90b5c8c207c8 Apply suggestion from @szaimen Signed-off-by: Simon L. Apply suggestion from @szaimen Signed-off-by: Simon L. Apply suggestion from @szaimen Signed-off-by: Simon L. Apply suggestion from @szaimen Signed-off-by: Simon L. Set installLatestMajor when upgrade-to-latest-major button is clicked Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/7b977c85-9b74-4027-a536-152e49a01976 Extract getLatestMajorVersion() to avoid duplicating the version string Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/d5ec921f-8629-4f6e-949a-e8f89f1eb85f Address PR review comments and hardcode updater channel to stable Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/c40941ff-2bf8-4a57-82be-2a0bd22b19a2 Restore sendNotification(), update cron files, extract getPlainStreamingCallback() Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/a5b6cd86-d278-4771-8a11-976c4a862966 Remove getPlainStreamingCallback, unify on getAddToStreamingResponseBody Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/15a4b815-076b-469f-95b2-c61df688a28d Revert "Remove getPlainStreamingCallback, unify on getAddToStreamingResponseBody" This reverts commit 6846c3a99549703121461f910cc26e6c116e0dc4. --- Containers/nextcloud/config/aio.config.php | 1 + Containers/nextcloud/entrypoint.sh | 33 +------ Containers/nextcloud/upgrade-latest-major.sh | 38 ++++++++ php/public/index.php | 1 + php/src/Controller/DockerController.php | 25 ++++- php/src/Docker/DockerActionManager.php | 96 ++++++++++++++------ php/templates/containers.twig | 7 +- 7 files changed, 137 insertions(+), 64 deletions(-) create mode 100644 Containers/nextcloud/upgrade-latest-major.sh diff --git a/Containers/nextcloud/config/aio.config.php b/Containers/nextcloud/config/aio.config.php index 7c80b6ba..497b2cf0 100644 --- a/Containers/nextcloud/config/aio.config.php +++ b/Containers/nextcloud/config/aio.config.php @@ -2,4 +2,5 @@ $CONFIG = array ( 'one-click-instance' => true, 'one-click-instance.user-limit' => 100, + 'update_channel' => 'stable', ); diff --git a/Containers/nextcloud/entrypoint.sh b/Containers/nextcloud/entrypoint.sh index 07e0ae24..61692a28 100644 --- a/Containers/nextcloud/entrypoint.sh +++ b/Containers/nextcloud/entrypoint.sh @@ -419,41 +419,12 @@ EOF # AIO update to latest start # Do not remove or change this line! if [ "$INSTALL_LATEST_MAJOR" = yes ]; then - php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" - INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)" - if [ -n "${INSTALLED_AT}" ]; then - # Set the installdat to 00 which will allow to skip staging and install the next major directly - # shellcheck disable=SC2001 - INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")" - php /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}" - fi - php /var/www/html/updater/updater.phar --no-interaction --no-backup - if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then - echo "Installation of Nextcloud failed!" - touch "$NEXTCLOUD_DATA_DIR/install.failed" + if ! bash /upgrade-latest-major.sh; then + echo "Upgrade to latest major version failed! Check the output above for details." exit 1 fi # shellcheck disable=SC2016 installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" - INSTALLED_MAJOR="${installed_version%%.*}" - IMAGE_MAJOR="${image_version%%.*}" - # If a valid upgrade path, trigger the Nextcloud built-in Updater - if ! [ "$INSTALLED_MAJOR" -gt "$IMAGE_MAJOR" ]; then - php /var/www/html/updater/updater.phar --no-interaction --no-backup - if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then - echo "Installation of Nextcloud failed!" - # TODO: Add a hint here about what to do / where to look / updater.log? - touch "$NEXTCLOUD_DATA_DIR/install.failed" - exit 1 - fi - # shellcheck disable=SC2016 - installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" - fi - php /var/www/html/occ config:system:set updatechecker --type=bool --value=true - php /var/www/html/occ app:enable nextcloud-aio --force - php /var/www/html/occ db:add-missing-columns - php /var/www/html/occ db:add-missing-primary-keys - yes | php /var/www/html/occ db:convert-filecache-bigint fi # AIO update to latest end # Do not remove or change this line! diff --git a/Containers/nextcloud/upgrade-latest-major.sh b/Containers/nextcloud/upgrade-latest-major.sh new file mode 100644 index 00000000..20fceb89 --- /dev/null +++ b/Containers/nextcloud/upgrade-latest-major.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# shellcheck disable=SC2016 +image_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" +IMAGE_MAJOR="${image_version%%.*}" + +php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" +INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)" +if [ -n "${INSTALLED_AT}" ]; then + # Set the installedat to 00 which will allow to skip staging and install the next major directly + # shellcheck disable=SC2001 + INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")" + php /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}" +fi +php /var/www/html/updater/updater.phar --no-interaction --no-backup +if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 +fi +# shellcheck disable=SC2016 +installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" +INSTALLED_MAJOR="${installed_version%%.*}" +# If a valid upgrade path, trigger the Nextcloud built-in Updater +if ! [ "$INSTALLED_MAJOR" -gt "$IMAGE_MAJOR" ]; then + php /var/www/html/updater/updater.phar --no-interaction --no-backup + if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + # TODO: Add a hint here about what to do / where to look / updater.log? + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi +fi +php /var/www/html/occ config:system:set updatechecker --type=bool --value=true +php /var/www/html/occ app:enable nextcloud-aio --force +php /var/www/html/occ db:add-missing-columns +php /var/www/html/occ db:add-missing-primary-keys +yes | php /var/www/html/occ db:convert-filecache-bigint diff --git a/php/public/index.php b/php/public/index.php index 8d109a7f..84819a68 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -104,6 +104,7 @@ $app->post('/api/docker/backup-test', AIO\Controller\DockerController::class . ' $app->post('/api/docker/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore'); $app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer'); $app->post('/api/docker/backup-reset-location', AIO\Controller\DockerController::class . ':DeleteBorgBackupConfig'); +$app->post('/api/docker/nextcloud-upgrade-to-latest-major', AIO\Controller\DockerController::class . ':RunNextcloudUpgradeToLatestMajor'); $app->post('/api/docker/prune', AIO\Controller\DockerController::class . ':SystemPrune'); $app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs'); $app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin'); diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 66e8a3e2..bae9bfd6 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -14,6 +14,7 @@ use Slim\Psr7\NonBufferedBody; readonly class DockerController { private const string TOP_CONTAINER = 'nextcloud-aio-apache'; + private const string LATEST_MAJOR_VERSION = '33'; public function __construct( private DockerActionManager $dockerActionManager, @@ -221,7 +222,7 @@ readonly class DockerController { } if (isset($request->getParsedBody()['install_latest_major'])) { - $installLatestMajor = '33'; + $installLatestMajor = self::LATEST_MAJOR_VERSION; } else { $installLatestMajor = ''; } @@ -333,6 +334,20 @@ readonly class DockerController { return $response->withStatus(201)->withHeader('Location', '.'); } + public function RunNextcloudUpgradeToLatestMajor(Request $request, Response $response, array $args) : Response { + $this->configurationManager->installLatestMajor = self::LATEST_MAJOR_VERSION; + + // Get streaming response start and closure + $nonbufResp = $this->startStreamingResponse($response); + $addToStreamingResponseBody = $this->getPlainStreamingCallback($nonbufResp); + + $this->dockerActionManager->RunNextcloudUpgradeToLatestMajor($addToStreamingResponseBody); + + // End streaming response + $this->finalizeStreamingResponse($nonbufResp); + return $nonbufResp; + } + public function SystemPrune(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); @@ -430,12 +445,18 @@ readonly class DockerController { // if it'll actually pull an image), but which should not need to know anything about the // wanted markup or formatting. $addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void { - $nonbufResp->getBody()->write("
{$container->displayName}: {$message}
"); + $nonbufResp->getBody()->write("
" . htmlspecialchars("{$container->displayName}: {$message}", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "
"); }; return $addToStreamingResponseBody; } + private function getPlainStreamingCallback(Response $nonbufResp) : \Closure { + return function (string $message) use ($nonbufResp) : void { + $nonbufResp->getBody()->write("
" . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "
"); + }; + } + private function finalizeStreamingResponse(Response $nonbufResp) : void { $nonbufResp->getBody()->write($this->getStreamingResponseHtmlEnd()); } diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index ca6a4d72..24edbd40 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -761,48 +761,79 @@ readonly class DockerActionManager { } public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void { - if ($this->GetContainerStartingState($container) === ContainerState::Running) { + $this->execCommandInContainer($container, ['bash', $file, $subject, $message]); + } - $containerName = $container->identifier; + public function execCommandInContainer(Container $container, array $cmd, ?\Closure $outputCallback = null): void { + if ($cmd === []) { + throw new \InvalidArgumentException('$cmd must not be empty.'); + } + foreach ($cmd as $arg) { + if (!is_string($arg) || $arg === '') { + throw new \InvalidArgumentException('Every element of $cmd must be a non-empty string.'); + } + } - // schedule the exec - $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); - $response = json_decode( - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'AttachStdout' => true, - 'Tty' => true, - 'Cmd' => [ - 'bash', - $file, - $subject, - $message - ], - ], - ] - )->getBody()->getContents(), - true, - 512, - JSON_THROW_ON_ERROR, - ); + if ($this->GetContainerStartingState($container) !== ContainerState::Running) { + return; + } - $id = $response['Id']; + $containerName = $container->identifier; - // start the exec - $url = $this->BuildApiUrl(sprintf('exec/%s/start', $id)); + // Create exec instance + $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); + $response = json_decode( $this->guzzleClient->request( 'POST', $url, [ 'json' => [ - 'Detach' => false, + 'AttachStdout' => true, + 'AttachStderr' => true, 'Tty' => true, + 'Cmd' => $cmd, ], ] - ); + )->getBody()->getContents(), + true, + 512, + JSON_THROW_ON_ERROR, + ); + + $execId = $response['Id']; + + // Start exec + $url = $this->BuildApiUrl(sprintf('exec/%s/start', $execId)); + $requestOptions = [ + 'json' => [ + 'Detach' => false, + 'Tty' => true, + ], + ]; + if ($outputCallback !== null) { + $requestOptions['stream'] = true; + } + + $startResponse = $this->guzzleClient->request('POST', $url, $requestOptions); + + if ($outputCallback !== null) { + $body = $startResponse->getBody(); + $buffer = ''; + while (!$body->eof()) { + $chunk = $body->read(1024); + $buffer .= $chunk; + while (($pos = strpos($buffer, "\n")) !== false) { + $line = substr($buffer, 0, $pos); + $buffer = substr($buffer, $pos + 1); + $line = rtrim($line, "\r"); + if ($line !== '') { + $outputCallback($line); + } + } + } + if (trim($buffer) !== '') { + $outputCallback(trim($buffer)); + } } } @@ -1027,6 +1058,11 @@ readonly class DockerActionManager { } } + public function RunNextcloudUpgradeToLatestMajor(\Closure $addToStreamingResponseBody): void { + $container = $this->containerDefinitionFetcher->GetContainerById('nextcloud-aio-nextcloud'); + $this->execCommandInContainer($container, ['bash', '/upgrade-latest-major.sh'], $addToStreamingResponseBody); + } + public function SystemPrune(?\Closure $addToStreamingResponseBody = null): void { $endpoints = [ // Remove stopped containers diff --git a/php/templates/containers.twig b/php/templates/containers.twig index 8faa4474..53951c8d 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -298,7 +298,12 @@ {% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %}
Note about Nextcloud Hub {{ newMajorVersionString }} -

If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to follow this documentation

+

If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to click the button below. ⚠️ Warning: make sure to create a backup before clicking the button as the update can go wrong and will leave your instance in a broken state!

+
+ + + +
{% endif %} {% endif %}