Compare commits

..

7 Commits

Author SHA1 Message Date
Simon L. 21250f8ea8 talk-recording: adjust listen address back to 0.0.0.0 as talk-recording listen address does not officially support ipv6 yet (#8246) 2026-06-03 13:34:42 +02:00
Simon L. dc69f69e74 talk-recording: adjust listen address back to 0.0.0.0 as talk-recording listen address does not officially support ipv6 yet
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-06-03 13:32:04 +02:00
Simon L. f28b2a7c1e overlay-log: make it a bit less wide
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-06-03 12:59:57 +02:00
Simon L. 1b1a15edba increase to v13.2.0
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-06-03 12:42:21 +02:00
Copilot 1f94bc8af0 aio-interface: extract Nextcloud latest-major upgrade logic to dedicated script and add UI trigger button (#7988)
* 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. <szaimen@e.mail.de>

Apply suggestion from @szaimen

Signed-off-by: Simon L. <szaimen@e.mail.de>

Apply suggestion from @szaimen

Signed-off-by: Simon L. <szaimen@e.mail.de>

Apply suggestion from @szaimen

Signed-off-by: Simon L. <szaimen@e.mail.de>

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.

* Refactor creating and using addToStreamingResponseBody()

This way we stick to having one implementation of the function, not three.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Read streamed output line by line, not via buffer

This way the code doesn't wait for a buffer to be filled, and we don't need to
implement logic ourselves that is provided by a present library already.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Ensure all HTTP requests are proxied, even with streaming

When requesting a streamed response, Guzzle apparently doesn't use curl, and thus we have to specify the unix socket proxy differently.

We can't specify it when creating the client, though (Guzzle complains).

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Fix syntax errors

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Remove broken code

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Fix readline line from streaming response

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Strip ANSI codes from command output before sending it to the browser

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Run PHP commands as www-data

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Properly compare version numbers

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Fix using memory limits from env

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Fix return type spec

This method always returns a closure, never null.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Use more general return type

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Avoid psalm complaint

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Fix namespace of return type

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>

* Apply suggestion from @szaimen

Signed-off-by: Simon L. <szaimen@e.mail.de>

---------

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
Signed-off-by: Simon L. <szaimen@e.mail.de>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Pablo Zmdl <pablo@nextcloud.com>
Co-authored-by: Simon L. <szaimen@e.mail.de>
2026-06-03 12:38:50 +02:00
Simon L. 335db2aac2 try to fix playwright (#8245) 2026-06-02 17:11:46 +02:00
Simon L. f5f19a488f fix playwright
Signed-off-by: Simon L. <szaimen@e.mail.de>
2026-06-02 17:09:45 +02:00
14 changed files with 186 additions and 124 deletions
+4 -7
View File
@@ -32,16 +32,13 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: lts/* node-version: 24.15.0
- name: Install dependencies - name: Install dependencies
run: cd php/tests && npm ci run: cd php/tests && npm ci
- name: Install Playwright system dependencies
run: cd php/tests && ./node_modules/.bin/playwright install-deps chromium
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: cd php/tests && ./node_modules/.bin/playwright install chromium run: cd php/tests && npx playwright install --with-deps chromium
- name: Set up php 8.5 - name: Set up php 8.5
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0 uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
@@ -89,7 +86,7 @@ jobs:
run: | run: |
cd php/tests cd php/tests
export DEBUG=pw:api export DEBUG=pw:api
if ! ./node_modules/.bin/playwright test tests/initial-setup.spec.js; then if ! npx playwright test tests/initial-setup.spec.js; then
docker logs nextcloud-aio-mastercontainer docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup docker logs nextcloud-aio-borgbackup
exit 1 exit 1
@@ -121,7 +118,7 @@ jobs:
run: | run: |
cd php/tests cd php/tests
export DEBUG=pw:api export DEBUG=pw:api
if ! ./node_modules/.bin/playwright test tests/restore-instance.spec.js; then if ! npx playwright test tests/restore-instance.spec.js; then
docker logs nextcloud-aio-mastercontainer docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup docker logs nextcloud-aio-borgbackup
exit 1 exit 1
@@ -17,16 +17,13 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with: with:
node-version: lts/* node-version: 24.15.0
- name: Install dependencies - name: Install dependencies
run: cd php/tests && npm ci run: cd php/tests && npm ci
- name: Install Playwright system dependencies
run: cd php/tests && ./node_modules/.bin/playwright install-deps chromium
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: cd php/tests && ./node_modules/.bin/playwright install chromium run: cd php/tests && npx playwright install --with-deps chromium
- name: Start fresh development server - name: Start fresh development server
run: | run: |
@@ -51,7 +48,7 @@ jobs:
run: | run: |
cd php/tests cd php/tests
export DEBUG=pw:api export DEBUG=pw:api
if ! ./node_modules/.bin/playwright test tests/initial-setup.spec.js; then if ! npx playwright test tests/initial-setup.spec.js; then
docker logs nextcloud-aio-mastercontainer docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup docker logs nextcloud-aio-borgbackup
exit 1 exit 1
@@ -79,7 +76,7 @@ jobs:
run: | run: |
cd php/tests cd php/tests
export DEBUG=pw:api export DEBUG=pw:api
if ! ./node_modules/.bin/playwright test tests/restore-instance.spec.js; then if ! npx playwright test tests/restore-instance.spec.js; then
docker logs nextcloud-aio-mastercontainer docker logs nextcloud-aio-mastercontainer
docker logs nextcloud-aio-borgbackup docker logs nextcloud-aio-borgbackup
exit 1 exit 1
@@ -2,4 +2,5 @@
$CONFIG = array ( $CONFIG = array (
'one-click-instance' => true, 'one-click-instance' => true,
'one-click-instance.user-limit' => 100, 'one-click-instance.user-limit' => 100,
'update_channel' => 'stable',
); );
+2 -31
View File
@@ -419,41 +419,12 @@ EOF
# AIO update to latest start # Do not remove or change this line! # AIO update to latest start # Do not remove or change this line!
if [ "$INSTALL_LATEST_MAJOR" = yes ]; then if [ "$INSTALL_LATEST_MAJOR" = yes ]; then
php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" if ! bash /upgrade-latest-major.sh; then
INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)" echo "Upgrade to latest major version failed! Check the output above for details."
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"
exit 1 exit 1
fi fi
# shellcheck disable=SC2016 # shellcheck disable=SC2016
installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" 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 fi
# AIO update to latest end # Do not remove or change this line! # AIO update to latest end # Do not remove or change this line!
@@ -0,0 +1,43 @@
#!/bin/bash
PHP_CLI="php"
if [[ "$EUID" = 0 ]]; then
PHP_CLI="sudo -u www-data -E $PHP_CLI"
fi
# shellcheck disable=SC2016
image_version="$($PHP_CLI -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
export IMAGE_MAJOR="${image_version%%.*}"
$PHP_CLI /var/www/html/occ config:system:set updatedirectory --value="/nc-updater"
INSTALLED_AT="$($PHP_CLI /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_CLI /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}"
fi
$PHP_CLI /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! $PHP_CLI /var/www/html/occ -V || $PHP_CLI /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_CLI -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')"
export INSTALLED_MAJOR="${installed_version%%.*}"
# If a valid upgrade path, trigger the Nextcloud built-in Updater
if ! $PHP_CLI -r "version_compare(getenv('INSTALLED_MAJOR'), getenv('IMAGE_MAJOR'), '>') || exit(1);"; then
$PHP_CLI /var/www/html/updater/updater.phar --no-interaction --no-backup
if ! $PHP_CLI /var/www/html/occ -V || $PHP_CLI /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_CLI /var/www/html/occ config:system:set updatechecker --type=bool --value=true
$PHP_CLI /var/www/html/occ app:enable nextcloud-aio --force
$PHP_CLI /var/www/html/occ db:add-missing-columns
$PHP_CLI /var/www/html/occ db:add-missing-primary-keys
yes | $PHP_CLI /var/www/html/occ db:convert-filecache-bigint
+1 -1
View File
@@ -4,4 +4,4 @@ if [ "$AIO_LOG_LEVEL" = 'debug' ]; then
set -x set -x
fi fi
nc -z 127.0.0.1 1234 || nc -z ::1 1234 || exit 1 nc -z 127.0.0.1 1234 || exit 1
+1 -9
View File
@@ -58,21 +58,13 @@ extensionaudio = .m4a
extensionvideo = .mp4" extensionvideo = .mp4"
fi fi
# Detect IPv6 availability to choose the right listen address
RECORDING_LISTEN="0.0.0.0:1234"
if ! grep -q "1" /sys/module/ipv6/parameters/disable 2>/dev/null \
&& ! grep -q "1" /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null \
&& ! grep -q "1" /proc/sys/net/ipv6/conf/default/disable_ipv6 2>/dev/null; then
RECORDING_LISTEN="[::]:1234"
fi
cat << RECORDING_CONF > "/conf/recording.conf" cat << RECORDING_CONF > "/conf/recording.conf"
[logs] [logs]
# 30 means Warning # 30 means Warning
level = ${TALK_RECORDING_LOG_LEVEL} level = ${TALK_RECORDING_LOG_LEVEL}
[http] [http]
listen = ${RECORDING_LISTEN} listen = 0.0.0.0:1234
[backend] [backend]
allowall = ${ALLOW_ALL} allowall = ${ALLOW_ALL}
-2
View File
@@ -1,7 +1,5 @@
# PHP Docker Controller # PHP Docker Controller
<!--test-->
This is the code for the PHP Docker controller. This is the code for the PHP Docker controller.
## How to run ## How to run
+1
View File
@@ -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/restore', AIO\Controller\DockerController::class . ':StartBackupContainerRestore');
$app->post('/api/docker/stop', AIO\Controller\DockerController::class . ':StopContainer'); $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/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->post('/api/docker/prune', AIO\Controller\DockerController::class . ':SystemPrune');
$app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs'); $app->get('/api/docker/logs', AIO\Controller\DockerController::class . ':GetLogs');
$app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin'); $app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin');
+1 -1
View File
@@ -483,7 +483,7 @@ input[type="checkbox"]:disabled:not(:checked) + label {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
align-self: start; align-self: start;
width: min(700px, calc(100vw - 4rem)); width: min(600px, calc(100vw - 4rem));
height: min(400px, calc(100vh - 14rem)); height: min(400px, calc(100vh - 14rem));
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
border: solid thin rgb(192, 192, 192); border: solid thin rgb(192, 192, 192);
+30 -8
View File
@@ -14,6 +14,7 @@ use Slim\Psr7\NonBufferedBody;
readonly class DockerController { readonly class DockerController {
private const string TOP_CONTAINER = 'nextcloud-aio-apache'; private const string TOP_CONTAINER = 'nextcloud-aio-apache';
private const string LATEST_MAJOR_VERSION = '34';
public function __construct( public function __construct(
private DockerActionManager $dockerActionManager, private DockerActionManager $dockerActionManager,
@@ -221,7 +222,7 @@ readonly class DockerController {
} }
if (isset($request->getParsedBody()['install_latest_major'])) { if (isset($request->getParsedBody()['install_latest_major'])) {
$installLatestMajor = '34'; $installLatestMajor = self::LATEST_MAJOR_VERSION;
} else { } else {
$installLatestMajor = ''; $installLatestMajor = '';
} }
@@ -298,7 +299,7 @@ readonly class DockerController {
} }
if ($addToStreamingResponseBody !== null) { if ($addToStreamingResponseBody !== null) {
$addToStreamingResponseBody($container, "Stopping container"); $addToStreamingResponseBody("Stopping container", $container);
} }
// Stop itself first and then all the dependencies // Stop itself first and then all the dependencies
@@ -333,14 +334,30 @@ readonly class DockerController {
return $response->withStatus(201)->withHeader('Location', '.'); 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->getAddToStreamingResponseBody($nonbufResp);
$this->dockerActionManager->RunNextcloudUpgradeToLatestMajor($addToStreamingResponseBody);
// We automatically reload after 10s so that the output can be read or copied if necessary
$addToStreamingResponseBody("Automatically reloading the page after 10s.");
sleep(10);
// End streaming response
$this->finalizeStreamingResponse($nonbufResp);
return $nonbufResp;
}
public function SystemPrune(Request $request, Response $response, array $args) : Response { public function SystemPrune(Request $request, Response $response, array $args) : Response {
// Get streaming response start and closure // Get streaming response start and closure
$nonbufResp = $this->startStreamingResponse($response); $nonbufResp = $this->startStreamingResponse($response);
$body = $nonbufResp->getBody(); $body = $nonbufResp->getBody();
$addToStreamingResponseBody = function (string $message) use ($body) : void { $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp);
$body->write("<div>$message</div>");
};
$this->dockerActionManager->SystemPrune($addToStreamingResponseBody); $this->dockerActionManager->SystemPrune($addToStreamingResponseBody);
@@ -426,12 +443,17 @@ readonly class DockerController {
return $nonbufResp; return $nonbufResp;
} }
private function getAddToStreamingResponseBody(Response $nonbufResp) : ?\Closure { private function getAddToStreamingResponseBody(Response $nonbufResp) : \Closure {
// Create a closure to pass around to the code, which should to the logging (because it e.g. decides // Create a closure to pass around to the code, which should to the logging (because it e.g. decides
// if it'll actually pull an image), but which should not need to know anything about the // if it'll actually pull an image), but which should not need to know anything about the
// wanted markup or formatting. // wanted markup or formatting.
$addToStreamingResponseBody = function (Container $container, string $message) use ($nonbufResp) : void { $addToStreamingResponseBody = function (string $message, ?Container $container = null) use ($nonbufResp) : void {
$nonbufResp->getBody()->write("<div>{$container->displayName}: {$message}</div>"); // Strip ANSI codes.
$message = preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $message);
if ($container) {
$message = "{$container->displayName}: {$message}";
}
$nonbufResp->getBody()->write("<div>" . htmlspecialchars("{$message}", ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</div>");
}; };
return $addToStreamingResponseBody; return $addToStreamingResponseBody;
+91 -56
View File
@@ -12,6 +12,7 @@ use AIO\Data\DataConst;
use AIO\Helper\NetworkHelper; use AIO\Helper\NetworkHelper;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Utils;
use http\Env\Response; use http\Env\Response;
readonly class DockerActionManager { readonly class DockerActionManager {
@@ -48,7 +49,7 @@ readonly class DockerActionManager {
public function GetContainerRunningState(Container $container): ContainerState { public function GetContainerRunningState(Container $container): ContainerState {
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier)));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->sendHttpRequest('GET', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() === 404) { if ($e->getCode() === 404) {
return ContainerState::ImageDoesNotExist; return ContainerState::ImageDoesNotExist;
@@ -68,7 +69,7 @@ readonly class DockerActionManager {
public function GetContainerRestartingState(Container $container): ContainerState { public function GetContainerRestartingState(Container $container): ContainerState {
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->identifier)));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->sendHttpRequest('GET', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() === 404) { if ($e->getCode() === 404) {
return ContainerState::ImageDoesNotExist; return ContainerState::ImageDoesNotExist;
@@ -138,7 +139,7 @@ readonly class DockerActionManager {
public function DeleteContainer(Container $container): void { public function DeleteContainer(Container $container): void {
$url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->identifier))); $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->identifier)));
try { try {
$this->guzzleClient->delete($url); $this->sendHttpRequest('DELETE', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() !== 404) { if ($e->getCode() !== 404) {
throw $e; throw $e;
@@ -155,7 +156,7 @@ readonly class DockerActionManager {
// Delete the borg cache volume // Delete the borg cache volume
$url = $this->BuildApiUrl('volumes/nextcloud_aio_backup_cache'); $url = $this->BuildApiUrl('volumes/nextcloud_aio_backup_cache');
try { try {
$this->guzzleClient->delete($url); $this->sendHttpRequest('DELETE', $url);
error_log('nextcloud_aio_backup_cache volume deleted successfully.'); error_log('nextcloud_aio_backup_cache volume deleted successfully.');
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() !== 404) { if ($e->getCode() !== 404) {
@@ -174,7 +175,7 @@ readonly class DockerActionManager {
urlencode($id), urlencode($id),
$since $since
)); ));
$responseBody = (string)$this->guzzleClient->get($url)->getBody(); $responseBody = (string)$this->sendHttpRequest('GET', $url)->getBody();
$response = ""; $response = "";
$separator = "\r\n"; $separator = "\r\n";
@@ -194,9 +195,9 @@ readonly class DockerActionManager {
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->identifier))); $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->identifier)));
try { try {
if ($addToStreamingResponseBody !== null) { if ($addToStreamingResponseBody !== null) {
$addToStreamingResponseBody($container, "Starting container"); $addToStreamingResponseBody("Starting container", $container);
} }
$this->guzzleClient->post($url); $this->sendHttpRequest('POST', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
throw new \Exception("Could not start container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents()); throw new \Exception("Could not start container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents());
} }
@@ -215,7 +216,7 @@ readonly class DockerActionManager {
$firstChar = substr($volume->name, 0, 1); $firstChar = substr($volume->name, 0, 1);
if (!in_array($firstChar, $forbiddenChars)) { if (!in_array($firstChar, $forbiddenChars)) {
$this->guzzleClient->request( $this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
@@ -494,7 +495,7 @@ readonly class DockerActionManager {
$url = $this->BuildApiUrl('containers/create?name=' . $container->identifier); $url = $this->BuildApiUrl('containers/create?name=' . $container->identifier);
try { try {
$this->guzzleClient->request( $this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
@@ -551,10 +552,10 @@ readonly class DockerActionManager {
$imageIsThere = true; $imageIsThere = true;
try { try {
if ($addToStreamingResponseBody) { if ($addToStreamingResponseBody) {
$addToStreamingResponseBody($container, "Pulling image"); $addToStreamingResponseBody("Pulling image", $container);
} }
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName)); $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName));
$this->guzzleClient->get($imageUrl)->getBody()->getContents(); $this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$imageIsThere = false; $imageIsThere = false;
} }
@@ -562,7 +563,7 @@ readonly class DockerActionManager {
$maxRetries = 3; $maxRetries = 3;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try { try {
$this->guzzleClient->post($url); $this->sendHttpRequest('POST', $url);
break; break;
} catch (RequestException $e) { } catch (RequestException $e) {
$message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents(); $message = "Could not pull image " . $imageName . " (attempt $attempt/$maxRetries): " . $e->getResponse()?->getBody()->getContents();
@@ -647,11 +648,11 @@ readonly class DockerActionManager {
private function GetRepoDigestsOfContainer(string $containerName): ?array { private function GetRepoDigestsOfContainer(string $containerName): ?array {
try { try {
$containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
$containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $containerOutput = json_decode($this->sendHttpRequest('GET', $containerUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageName = $containerOutput['Image']; $imageName = $containerOutput['Image'];
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $imageOutput = json_decode($this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (!isset($imageOutput['RepoDigests'])) { if (!isset($imageOutput['RepoDigests'])) {
error_log('RepoDigests is not set of container ' . $containerName); error_log('RepoDigests is not set of container ' . $containerName);
@@ -695,7 +696,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-mastercontainer'; $containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try { try {
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $output = json_decode($this->sendHttpRequest('GET', $url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$imageNameArray = explode(':', $output['Config']['Image']); $imageNameArray = explode(':', $output['Config']['Image']);
if (count($imageNameArray) === 2) { if (count($imageNameArray) === 2) {
$imageName = $imageNameArray[0]; $imageName = $imageNameArray[0];
@@ -722,7 +723,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-mastercontainer'; $containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try { try {
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $output = json_decode($this->sendHttpRequest('GET', $url)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$tagArray = explode(':', $output['Config']['Image']); $tagArray = explode(':', $output['Config']['Image']);
if (count($tagArray) === 2) { if (count($tagArray) === 2) {
$tag = $tagArray[1]; $tag = $tagArray[1];
@@ -763,48 +764,69 @@ readonly class DockerActionManager {
} }
public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh'): void { 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 if ($this->GetContainerStartingState($container) !== ContainerState::Running) {
$url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); return;
$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,
);
$id = $response['Id']; $containerName = $container->identifier;
// start the exec // Create exec instance
$url = $this->BuildApiUrl(sprintf('exec/%s/start', $id)); $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName)));
$this->guzzleClient->request( $response = json_decode(
$this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
'json' => [ 'json' => [
'Detach' => false, 'AttachStdout' => true,
'AttachStderr' => true,
'Tty' => 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->sendHttpRequest('POST', $url, $requestOptions);
if ($outputCallback !== null) {
$body = $startResponse->getBody();
while (!$body->eof()) {
$line = rtrim(Utils::readLine($body), "\r");;
if ($line !== '') {
$outputCallback($line);
}
}
} }
} }
@@ -815,7 +837,7 @@ readonly class DockerActionManager {
); );
try { try {
$this->guzzleClient->request( $this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
@@ -836,7 +858,7 @@ readonly class DockerActionManager {
if ($createNetwork) { if ($createNetwork) {
$url = $this->BuildApiUrl('networks/create'); $url = $this->BuildApiUrl('networks/create');
try { try {
$this->guzzleClient->request( $this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
@@ -865,7 +887,7 @@ readonly class DockerActionManager {
} }
try { try {
$this->guzzleClient->request( $this->sendHttpRequest(
'POST', 'POST',
$url, $url,
[ [
@@ -910,7 +932,7 @@ readonly class DockerActionManager {
} }
$url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->identifier), $maxShutDownTime)); $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->identifier), $maxShutDownTime));
try { try {
$this->guzzleClient->post($url); $this->sendHttpRequest('POST', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() !== 404 && $e->getCode() !== 304) { if ($e->getCode() !== 404 && $e->getCode() !== 304) {
throw $e; throw $e;
@@ -922,7 +944,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-borgbackup'; $containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->sendHttpRequest('GET', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() === 404) { if ($e->getCode() === 404) {
return -1; return -1;
@@ -944,7 +966,7 @@ readonly class DockerActionManager {
$containerName = 'nextcloud-aio-database'; $containerName = 'nextcloud-aio-database';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try { try {
$response = $this->guzzleClient->get($url); $response = $this->sendHttpRequest('GET', $url);
} catch (RequestException $e) { } catch (RequestException $e) {
if ($e->getCode() === 404) { if ($e->getCode() === 404) {
return -1; return -1;
@@ -984,7 +1006,7 @@ readonly class DockerActionManager {
$imageName = $imageName . ':' . $this->GetCurrentChannel(); $imageName = $imageName . ':' . $this->GetCurrentChannel();
try { try {
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); $imageOutput = json_decode($this->sendHttpRequest('GET', $imageUrl)->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
if (!isset($imageOutput['Created'])) { if (!isset($imageOutput['Created'])) {
error_log('Created is not set of image ' . $imageName); error_log('Created is not set of image ' . $imageName);
@@ -1029,6 +1051,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 { public function SystemPrune(?\Closure $addToStreamingResponseBody = null): void {
$endpoints = [ $endpoints = [
// Remove stopped containers // Remove stopped containers
@@ -1057,7 +1084,7 @@ readonly class DockerActionManager {
} }
try { try {
$response = $this->guzzleClient->post($url); $response = $this->sendHttpRequest('POST', $url);
if ($addToStreamingResponseBody !== null) { if ($addToStreamingResponseBody !== null) {
$data = json_decode((string)$response->getBody(), true); $data = json_decode((string)$response->getBody(), true);
$deleted = 0; $deleted = 0;
@@ -1095,4 +1122,12 @@ readonly class DockerActionManager {
sleep(10); sleep(10);
} }
} }
}
protected function sendHttpRequest(string $httpMethod, string $url, array $requestOptions = []): \Psr\Http\Message\ResponseInterface {
if (($requestOptions['stream'] ?? null) === true) {
$requestOptions['proxy'] = 'unix:///var/run/docker.sock';
}
return $this->guzzleClient->request($httpMethod, $url, $requestOptions);
}
}
+6 -1
View File
@@ -298,7 +298,12 @@
{% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %} {% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %}
<details> <details>
<summary>Note about <strong>Nextcloud Hub {{ newMajorVersionString }}</strong></summary> <summary>Note about <strong>Nextcloud Hub {{ newMajorVersionString }}</strong></summary>
<p>If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to follow <strong><a target="_blank" href="https://github.com/nextcloud/all-in-one/discussions/8223">this documentation</a></strong></p> <p>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!</p>
<form method="POST" action="api/docker/nextcloud-upgrade-to-latest-major" target="overlay-log">
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input type="submit" value="Upgrade to Nextcloud Hub {{ newMajorVersionString }}" data-confirm="Upgrade to Nextcloud Hub {{ newMajorVersionString }}? You should consider creating a backup first." />
</form>
</details> </details>
{% endif %} {% endif %}
{% endif %} {% endif %}
+1 -1
View File
@@ -1 +1 @@
13.1.0 13.2.0