mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-21 10:50:10 +00:00
Merge pull request #7458 from nextcloud/enh/6877/show-sub-steps
aio-interface: show sub-steps for starting containers
This commit is contained in:
@@ -21,6 +21,11 @@ Listen 8080 https
|
||||
<FilesMatch "\.php$">
|
||||
SetHandler "proxy:fcgi://127.0.0.1:9000"
|
||||
</FilesMatch>
|
||||
|
||||
# Disable output buffering to enable streaming responses.
|
||||
<Proxy "fcgi://127.0.0.1:9000/" flushpackets=on>
|
||||
</Proxy>
|
||||
|
||||
# Master dir
|
||||
DocumentRoot /var/www/docker-aio/php/public/
|
||||
<Directory /var/www/docker-aio/php/public/>
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
"slim/twig-view": "^3.3",
|
||||
"slim/csrf": "^1.3",
|
||||
"ext-apcu": "*"
|
||||
"ext-apcu": "*",
|
||||
"slim/psr7": "^1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"sserbin/twig-linter": "@dev",
|
||||
|
||||
139
php/composer.lock
generated
139
php/composer.lock
generated
@@ -4,8 +4,64 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "19598625395cc28e64f15d2719f8f98f",
|
||||
"content-hash": "a47e950885b06f1a4b631a1eea56c57e",
|
||||
"packages": [
|
||||
{
|
||||
"name": "fig/http-message-util",
|
||||
"version": "1.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message-util.git",
|
||||
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765",
|
||||
"reference": "9d94dc0154230ac39e5bf89398b324a86f63f765",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3 || ^7.0 || ^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/http-message": "The package containing the PSR-7 interfaces"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Fig\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Utility classes and constants for use with PSR-7 (psr/http-message)",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/http-message-util/issues",
|
||||
"source": "https://github.com/php-fig/http-message-util/tree/1.1.5"
|
||||
},
|
||||
"time": "2020-11-24T22:02:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "7.10.0",
|
||||
@@ -1146,6 +1202,85 @@
|
||||
},
|
||||
"time": "2025-11-02T14:58:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "slim/psr7",
|
||||
"version": "1.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/slimphp/Slim-Psr7.git",
|
||||
"reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/76e7e3b1cdfd583e9035c4c966c08e01e45ce959",
|
||||
"reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"fig/http-message-util": "^1.1.5",
|
||||
"php": "^8.0",
|
||||
"psr/http-factory": "^1.1",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"ralouphie/getallheaders": "^3.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-factory-implementation": "^1.0",
|
||||
"psr/http-message-implementation": "^1.0 || ^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"adriansuter/php-autoload-override": "^1.5|| ^2.0",
|
||||
"ext-json": "*",
|
||||
"http-interop/http-factory-tests": "^1.0 || ^2.0",
|
||||
"php-http/psr7-integration-tests": "^1.5",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpunit/phpunit": "^9.6 || ^10",
|
||||
"squizlabs/php_codesniffer": "^3.13"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Slim\\Psr7\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Josh Lockhart",
|
||||
"email": "hello@joshlockhart.com",
|
||||
"homepage": "https://joshlockhart.com"
|
||||
},
|
||||
{
|
||||
"name": "Andrew Smith",
|
||||
"email": "a.smith@silentworks.co.uk",
|
||||
"homepage": "https://silentworks.co.uk"
|
||||
},
|
||||
{
|
||||
"name": "Rob Allen",
|
||||
"email": "rob@akrabat.com",
|
||||
"homepage": "https://akrabat.com"
|
||||
},
|
||||
{
|
||||
"name": "Pierre Berube",
|
||||
"email": "pierre@lgse.com",
|
||||
"homepage": "https://www.lgse.com"
|
||||
}
|
||||
],
|
||||
"description": "Strict PSR-7 implementation",
|
||||
"homepage": "https://www.slimframework.com",
|
||||
"keywords": [
|
||||
"http",
|
||||
"psr-7",
|
||||
"psr7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/slimphp/Slim-Psr7/issues",
|
||||
"source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0"
|
||||
},
|
||||
"time": "2025-11-02T17:51:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "slim/slim",
|
||||
"version": "4.15.1",
|
||||
@@ -4825,5 +4960,5 @@
|
||||
"ext-apcu": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Don't run if the expected form isn't present.
|
||||
if (document.getElementById('options-form') === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide submit button initially
|
||||
const optionsFormSubmit = document.querySelectorAll(".options-form-submit");
|
||||
optionsFormSubmit.forEach(element => {
|
||||
|
||||
@@ -70,15 +70,24 @@ function showPassword(id) {
|
||||
}
|
||||
|
||||
form.onsubmit = submit;
|
||||
console.info(form);
|
||||
}
|
||||
|
||||
function initForms() {
|
||||
const forms = document.querySelectorAll('form.xhr')
|
||||
console.info("Making " + forms.length + " form(s) use XHR.");
|
||||
for (const form of forms) {
|
||||
initForm(form);
|
||||
}
|
||||
const overlayLogForms = document.querySelectorAll('form[target="overlay-log"]')
|
||||
for (const form of overlayLogForms) {
|
||||
form.onsubmit = function() {
|
||||
enableSpinner();
|
||||
document.getElementById('overlay-log')?.classList.add('visible');
|
||||
// Reload the page after the response was fully loaded into the iframe.
|
||||
document.querySelector('iframe[name="overlay-log"]').addEventListener('load', () => {
|
||||
location.reload();
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@@ -468,7 +468,29 @@ input[type="checkbox"]:disabled:not(:checked) + label {
|
||||
}
|
||||
|
||||
#overlay.loading {
|
||||
display: block;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
row-gap: 2rem;
|
||||
}
|
||||
|
||||
#overlay #overlay-log.visible {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity 1s ease-in;
|
||||
}
|
||||
|
||||
#overlay #overlay-log {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
align-self: start;
|
||||
width: 20%;
|
||||
height: 7rem;
|
||||
border-radius: var(--border-radius-large);
|
||||
border: solid thin rgb(192, 192, 192);
|
||||
}
|
||||
|
||||
.overlay-iframe {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@@ -479,9 +501,7 @@ input[type="checkbox"]:disabled:not(:checked) + label {
|
||||
height: 120px;
|
||||
-webkit-animation: spin 2s linear infinite; /* Safari */
|
||||
animation: spin 2s linear infinite;
|
||||
position: absolute;
|
||||
top: calc(50% - 60px);
|
||||
left: calc(50% - 60px);
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Safari */
|
||||
@@ -705,4 +725,4 @@ input[type="checkbox"]:disabled:not(:checked) + label {
|
||||
.office-suite-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,26 @@
|
||||
function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const newTheme = (currentTheme === 'dark') ? '' : 'dark'; // Toggle between no theme and dark theme
|
||||
document.documentElement.setAttribute('data-theme', newTheme);
|
||||
setThemeToDOM(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Change the icon based on the current theme
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙'; // Switch between moon and sun icons
|
||||
setThemeIcon(newTheme);
|
||||
}
|
||||
|
||||
// Function to immediately apply saved theme without icon update
|
||||
function applySavedThemeImmediately() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme'); // Default to light theme
|
||||
}
|
||||
function setThemeToDOM(value) {
|
||||
// Set the theme to the root document and all possible iframe documents (so they can adapt their styling, too).
|
||||
const documents = [document, Array.from(document.querySelectorAll('iframe')).map((iframe) => iframe.contentDocument)].flat()
|
||||
documents.forEach((doc) => doc.documentElement.setAttribute('data-theme', value));
|
||||
}
|
||||
|
||||
function getSavedTheme() {
|
||||
return localStorage.getItem('theme') ?? '';
|
||||
}
|
||||
|
||||
// Function to apply theme-icon update
|
||||
function setThemeIcon() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'dark') {
|
||||
function setThemeIcon(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.getElementById('theme-icon').textContent = '☀️'; // Sun icon for dark mode
|
||||
} else {
|
||||
document.getElementById('theme-icon').textContent = '🌙'; // Moon icon for light mode
|
||||
@@ -31,7 +29,7 @@ function setThemeIcon() {
|
||||
}
|
||||
|
||||
// Immediately apply the saved theme to avoid flickering
|
||||
applySavedThemeImmediately();
|
||||
setThemeToDOM(getSavedTheme());
|
||||
|
||||
// Apply theme when the page loads
|
||||
document.addEventListener('DOMContentLoaded', setThemeIcon);
|
||||
document.addEventListener('DOMContentLoaded', () => setThemeIcon(getSavedTheme()));
|
||||
|
||||
@@ -3,12 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace AIO\Controller;
|
||||
|
||||
use AIO\Container\Container;
|
||||
use AIO\Container\ContainerState;
|
||||
use AIO\ContainerDefinitionFetcher;
|
||||
use AIO\Docker\DockerActionManager;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use AIO\Data\ConfigurationManager;
|
||||
use Slim\Psr7\NonBufferedBody;
|
||||
|
||||
readonly class DockerController {
|
||||
private const string TOP_CONTAINER = 'nextcloud-aio-apache';
|
||||
@@ -20,12 +22,12 @@ readonly class DockerController {
|
||||
) {
|
||||
}
|
||||
|
||||
private function PerformRecursiveContainerStart(string $id, bool $pullImage = true) : void {
|
||||
private function PerformRecursiveContainerStart(string $id, bool $pullImage = true, ?\Closure $addToStreamingResponseBody = null) : void {
|
||||
$container = $this->containerDefinitionFetcher->GetContainerById($id);
|
||||
|
||||
// Start all dependencies first and then itself
|
||||
foreach($container->dependsOn as $dependency) {
|
||||
$this->PerformRecursiveContainerStart($dependency, $pullImage);
|
||||
$this->PerformRecursiveContainerStart($dependency, $pullImage, $addToStreamingResponseBody);
|
||||
}
|
||||
|
||||
// Don't start if container is already running
|
||||
@@ -37,9 +39,9 @@ readonly class DockerController {
|
||||
|
||||
$this->dockerActionManager->DeleteContainer($container);
|
||||
$this->dockerActionManager->CreateVolumes($container);
|
||||
$this->dockerActionManager->PullImage($container, $pullImage);
|
||||
$this->dockerActionManager->PullImage($container, $pullImage, $addToStreamingResponseBody);
|
||||
$this->dockerActionManager->CreateContainer($container);
|
||||
$this->dockerActionManager->StartContainer($container);
|
||||
$this->dockerActionManager->StartContainer($container, $addToStreamingResponseBody);
|
||||
$this->dockerActionManager->ConnectContainerToNetwork($container);
|
||||
}
|
||||
|
||||
@@ -198,17 +200,37 @@ readonly class DockerController {
|
||||
if ($pullImage === false) {
|
||||
error_log('WARNING: Not pulling container images. Instead, using local ones.');
|
||||
}
|
||||
|
||||
$nonbufResp = $response
|
||||
->withBody(new NonBufferedBody())
|
||||
->withHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
->withHeader('X-Accel-Buffering', 'no')
|
||||
->withHeader('Cache-Control', 'no-cache');
|
||||
|
||||
// Text written into this body is immediately sent to the client, without waiting for later content.
|
||||
$streamingResponseBody = $nonbufResp->getBody();
|
||||
|
||||
$streamingResponseBody->write($this->getStreamingResponseHtmlStart());
|
||||
|
||||
// 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
|
||||
// wanted markup or formatting.
|
||||
$addToStreamingResponseBody = function (Container $container, string $message) use ($streamingResponseBody) : void {
|
||||
$streamingResponseBody->write("<div>{$container->displayName}: {$message}</div>");
|
||||
};
|
||||
|
||||
// Start container
|
||||
$this->startTopContainer($pullImage);
|
||||
$this->startTopContainer($pullImage, $addToStreamingResponseBody);
|
||||
|
||||
// Clear apcu cache in order to check if container updates are available
|
||||
// Temporarily disabled as it leads much faster to docker rate limits
|
||||
// apcu_clear_cache();
|
||||
|
||||
return $response->withStatus(201)->withHeader('Location', '.');
|
||||
$streamingResponseBody->write($this->getStreamingResponseHtmlEnd());
|
||||
return $nonbufResp;
|
||||
}
|
||||
|
||||
public function startTopContainer(bool $pullImage) : void {
|
||||
public function startTopContainer(bool $pullImage, ?\Closure $addToStreamingResponseBody = null) : void {
|
||||
$this->configurationManager->aioToken = bin2hex(random_bytes(24));
|
||||
|
||||
// Stop domaincheck since apache would not be able to start otherwise
|
||||
@@ -216,7 +238,7 @@ readonly class DockerController {
|
||||
|
||||
$id = self::TOP_CONTAINER;
|
||||
|
||||
$this->PerformRecursiveContainerStart($id, $pullImage);
|
||||
$this->PerformRecursiveContainerStart($id, $pullImage, $addToStreamingResponseBody);
|
||||
}
|
||||
|
||||
public function StartWatchtowerContainer(Request $request, Response $response, array $args) : Response {
|
||||
@@ -308,4 +330,31 @@ readonly class DockerController {
|
||||
$id = 'nextcloud-aio-domaincheck';
|
||||
$this->PerformRecursiveContainerStop($id);
|
||||
}
|
||||
|
||||
private function getStreamingResponseHtmlStart() : string {
|
||||
return <<<END
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="overlay-iframe">
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../style.css?v8" media="all" />
|
||||
<script>
|
||||
const observer = new MutationObserver((records) => {
|
||||
const node = records[0]?.addedNodes[0];
|
||||
// Text nodes also appear here but can't be scrolled to, so we have to check for the
|
||||
// function being present.
|
||||
if (node && typeof(node.scrollIntoView) === 'function') {
|
||||
node.scrollIntoView();
|
||||
}
|
||||
});
|
||||
observer.observe(document, {childList: true, subtree: true});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
END;
|
||||
}
|
||||
|
||||
private function getStreamingResponseHtmlEnd() : string {
|
||||
return "\n </body>\n</html>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,9 +166,12 @@ readonly class DockerActionManager {
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function StartContainer(Container $container): void {
|
||||
public function StartContainer(Container $container, ?\Closure $addToStreamingResponseBody = null): void {
|
||||
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->identifier)));
|
||||
try {
|
||||
if ($addToStreamingResponseBody !== null) {
|
||||
$addToStreamingResponseBody($container, "Starting container");
|
||||
}
|
||||
$this->guzzleClient->post($url);
|
||||
} catch (RequestException $e) {
|
||||
throw new \Exception("Could not start container " . $container->identifier . ": " . $e->getResponse()?->getBody()->getContents());
|
||||
@@ -473,8 +476,7 @@ readonly class DockerActionManager {
|
||||
}
|
||||
}
|
||||
|
||||
public function PullImage(Container $container, bool $pullImage = true): void {
|
||||
|
||||
public function PullImage(Container $container, bool $pullImage = true, ?\Closure $addToStreamingResponseBody = null): void {
|
||||
// Skip database image pull if the last shutdown was not clean
|
||||
if ($container->identifier === 'nextcloud-aio-database') {
|
||||
if ($this->GetDatabasecontainerExitCode() > 0) {
|
||||
@@ -502,6 +504,9 @@ readonly class DockerActionManager {
|
||||
$url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName));
|
||||
$imageIsThere = true;
|
||||
try {
|
||||
if ($addToStreamingResponseBody) {
|
||||
$addToStreamingResponseBody($container, "Pulling image");
|
||||
}
|
||||
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName));
|
||||
$this->guzzleClient->get($imageUrl)->getBody()->getContents();
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<script type="text/javascript" src="timezone.js"></script>
|
||||
|
||||
{# js for optional containers and additional containers forms #}
|
||||
<script type="text/javascript" src="containers-form-submit.js?v5"></script>
|
||||
<script type="text/javascript" src="containers-form-submit.js?v6"></script>
|
||||
|
||||
{% set hasBackupLocation = borg_backup_host_location or borg_remote_repo %}
|
||||
{% set isAnyRunning = false %}
|
||||
@@ -339,7 +339,7 @@
|
||||
</form>
|
||||
{% else %}
|
||||
{% if was_start_button_clicked == false %}
|
||||
<form method="POST" action="api/docker/start" class="xhr">
|
||||
<form method="POST" action="api/docker/start" target="overlay-log">
|
||||
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
|
||||
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
|
||||
<input id="base_path" type="hidden" name="base_path" value="">
|
||||
@@ -356,7 +356,7 @@
|
||||
<input type="submit" value="Start containers" />
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST" action="api/docker/start" class="xhr">
|
||||
<form method="POST" action="api/docker/start" target="overlay-log">
|
||||
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
|
||||
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
|
||||
<input id="base_path" type="hidden" name="base_path" value="">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>AIO</title>
|
||||
<link rel="stylesheet" href="style.css?v7" media="all" />
|
||||
<link rel="stylesheet" href="style.css?v8" media="all" />
|
||||
<link rel="icon" href="img/favicon.png">
|
||||
<script type="text/javascript" src="forms.js"></script>
|
||||
<script type="text/javascript" src="toggle-dark-mode.js"></script>
|
||||
<script type="text/javascript" src="forms.js?v1"></script>
|
||||
<script type="text/javascript" src="toggle-dark-mode.js?v1"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -13,6 +13,7 @@
|
||||
</div>
|
||||
<div id="overlay">
|
||||
<div class="loader"></div>
|
||||
<iframe name="overlay-log" id="overlay-log"></iframe>
|
||||
</div>
|
||||
<button id="theme-toggle" onclick="toggleTheme()">
|
||||
<span id="theme-icon"></span>
|
||||
|
||||
Reference in New Issue
Block a user