containerDefinitionFetcher->GetContainerById($id); // Start all dependencies first and then itself foreach($container->dependsOn as $dependency) { $this->PerformRecursiveContainerStart($dependency, $pullImage, $addToStreamingResponseBody); } // Don't start if container is already running // This is expected to happen if a container is defined in depends_on of multiple containers if ($container->GetRunningState() === ContainerState::Running) { error_log('Not starting ' . $id . ' because it was already started.'); return; } $this->dockerActionManager->DeleteContainer($container); $this->dockerActionManager->CreateVolumes($container); $this->dockerActionManager->PullImage($container, $pullImage, $addToStreamingResponseBody); $this->dockerActionManager->CreateContainer($container); $this->dockerActionManager->StartContainer($container, $addToStreamingResponseBody); $this->dockerActionManager->ConnectContainerToNetwork($container); } private function PerformRecursiveImagePull(string $id) : void { $container = $this->containerDefinitionFetcher->GetContainerById($id); // Pull all dependencies first and then itself foreach($container->dependsOn as $dependency) { $this->PerformRecursiveImagePull($dependency); } $this->dockerActionManager->PullImage($container, true); } public function PullAllContainerImages(): void { $id = self::TOP_CONTAINER; $this->PerformRecursiveImagePull($id); } public function GetLogs(Request $request, Response $response, array $args) : Response { $requestParams = $request->getQueryParams(); $id = ''; if (isset($requestParams['id']) && is_string($requestParams['id'])) { $id = $requestParams['id']; } if (str_starts_with($id, 'nextcloud-aio-')) { $since = $this->getTimestampForDockerLogsApiSince($requestParams['since'] ?? ''); $logs = $this->dockerActionManager->GetLogs($id, $since); } else { $logs = 'Container not found.'; } $body = $response->getBody(); $body->write($logs); return $response ->withStatus(200) ->withHeader('Content-Type', 'text/plain; charset=utf-8') ->withHeader('Content-Disposition', 'inline'); } public function StartBackupContainerBackup(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $forceStopNextcloud = true; $this->startBackup($forceStopNextcloud, $addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function startBackup(bool $forceStopNextcloud = false, ?\Closure $addToStreamingResponseBody = null) : void { $this->configurationManager->backupMode = 'backup'; $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStop($id, $forceStopNextcloud, $addToStreamingResponseBody); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); } public function StartBackupContainerCheck(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $this->checkBackup($addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function StartBackupContainerList(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $this->listBackup($addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function checkBackup(?\Closure $addToStreamingResponseBody = null) : void { $this->configurationManager->backupMode = 'check'; $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); } private function listBackup(?\Closure $addToStreamingResponseBody = null) : void { $this->configurationManager->backupMode = 'list'; $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); } public function StartBackupContainerRestore(Request $request, Response $response, array $args) : Response { $this->configurationManager->startTransaction(); $this->configurationManager->backupMode = 'restore'; $this->configurationManager->selectedRestoreTime = $request->getParsedBody()['selected_restore_time'] ?? ''; $this->configurationManager->restoreExcludePreviews = isset($request->getParsedBody()['restore-exclude-previews']); $this->configurationManager->commitTransaction(); // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $id = self::TOP_CONTAINER; $forceStopNextcloud = true; $this->PerformRecursiveContainerStop($id, $forceStopNextcloud, $addToStreamingResponseBody); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function StartBackupContainerCheckRepair(Request $request, Response $response, array $args) : Response { $this->configurationManager->backupMode = 'check-repair'; // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); // Restore to backup check which is needed to make the UI logic work correctly $this->configurationManager->backupMode = 'check'; // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function StartBackupContainerTest(Request $request, Response $response, array $args) : Response { $this->configurationManager->startTransaction(); $this->configurationManager->backupMode = 'test'; $this->configurationManager->instanceRestoreAttempt = false; $this->configurationManager->commitTransaction(); // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStop($id, true, $addToStreamingResponseBody); $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function StartContainer(Request $request, Response $response, array $args) : Response { $uri = $request->getUri(); $host = $uri->getHost(); $port = $uri->getPort(); $path = $request->getParsedBody()['base_path'] ?? ''; if ($port === 8000) { error_log('The AIO_URL-port was discovered to be 8000 which is not expected. It is now set to 443.'); $port = 443; } if (isset($request->getParsedBody()['install_latest_major'])) { $installLatestMajor = '33'; } else { $installLatestMajor = ''; } $this->configurationManager->startTransaction(); $this->configurationManager->installLatestMajor = $installLatestMajor; // set AIO_URL $this->configurationManager->aioUrl = $host . ':' . (string)$port . $path; // set wasStartButtonClicked $this->configurationManager->wasStartButtonClicked = true; $this->configurationManager->commitTransaction(); // Do not pull container images in case 'bypass_container_update' is set via url params // Needed for local testing $pullImage = !isset($request->getParsedBody()['bypass_container_update']); if ($pullImage === false) { error_log('WARNING: Not pulling container images. Instead, using local ones.'); } // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); // Start container $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(); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } 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 $this->StopDomaincheckContainer(); $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStart($id, $pullImage, $addToStreamingResponseBody); } public function StartWatchtowerContainer(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $this->startWatchtower($addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function startWatchtower(?\Closure $addToStreamingResponseBody = null) : void { $id = 'nextcloud-aio-watchtower'; $this->PerformRecursiveContainerStart($id, true, $addToStreamingResponseBody); } private function PerformRecursiveContainerStop(string $id, bool $forceStopNextcloud = false, ?\Closure $addToStreamingResponseBody = null) : void { $container = $this->containerDefinitionFetcher->GetContainerById($id); // This is a hack but no better solution was found for the meantime // Stop Collabora first to make sure it force-saves // See https://github.com/nextcloud/richdocuments/issues/3799 if ($id === self::TOP_CONTAINER && $this->configurationManager->isCollaboraEnabled) { $this->PerformRecursiveContainerStop('nextcloud-aio-collabora', false, $addToStreamingResponseBody); } if ($addToStreamingResponseBody !== null) { $addToStreamingResponseBody($container, "Stopping container"); } // Stop itself first and then all the dependencies if ($id !== 'nextcloud-aio-nextcloud') { $this->dockerActionManager->StopContainer($container); } else { // We want to stop the Nextcloud container after 10s and not wait for the configured stop_grace_period $this->dockerActionManager->StopContainer($container, $forceStopNextcloud); } foreach($container->dependsOn as $dependency) { $this->PerformRecursiveContainerStop($dependency, $forceStopNextcloud, $addToStreamingResponseBody); } } public function StopContainer(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $addToStreamingResponseBody = $this->getAddToStreamingResponseBody($nonbufResp); $id = self::TOP_CONTAINER; $forceStopNextcloud = true; $this->PerformRecursiveContainerStop($id, $forceStopNextcloud, $addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function DeleteBorgBackupConfig(Request $request, Response $response, array $args) : Response { $this->dockerActionManager->deleteBorgBackupConfig(); return $response->withStatus(201)->withHeader('Location', '.'); } public function SystemPrune(Request $request, Response $response, array $args) : Response { // Get streaming response start and closure $nonbufResp = $this->startStreamingResponse($response); $body = $nonbufResp->getBody(); $addToStreamingResponseBody = function (string $message) use ($body) : void { $body->write("
$message
"); }; $this->dockerActionManager->SystemPrune($addToStreamingResponseBody); // End streaming response $this->finalizeStreamingResponse($nonbufResp); return $nonbufResp; } public function stopTopContainer() : void { $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStop($id); } public function StartDomaincheckContainer() : void { # Don't start if domain is already set if ($this->configurationManager->domain !== '' || $this->configurationManager->wasStartButtonClicked) { return; } $id = 'nextcloud-aio-domaincheck'; $cacheKey = 'domaincheckWasStarted'; $domaincheckContainer = $this->containerDefinitionFetcher->GetContainerById($id); $apacheContainer = $this->containerDefinitionFetcher->GetContainerById(self::TOP_CONTAINER); // Don't start if apache is already running if ($apacheContainer->GetRunningState() === ContainerState::Running) { return; // Don't start if domaincheck is already running } elseif ($domaincheckContainer->GetRunningState() === ContainerState::Running) { $domaincheckWasStarted = apcu_fetch($cacheKey); // Start domaincheck again when 10 minutes are over by not returning here if($domaincheckWasStarted !== false && is_string($domaincheckWasStarted)) { return; } } $this->StopDomaincheckContainer(); try { $this->PerformRecursiveContainerStart($id); } catch (\Exception $e) { error_log('Could not start domaincheck container: ' . $e->getMessage()); } // Cache the start for 10 minutes apcu_add($cacheKey, '1', 600); } private function StopDomaincheckContainer() : void { $id = 'nextcloud-aio-domaincheck'; $this->PerformRecursiveContainerStop($id); } private function getStreamingResponseHtmlStart() : string { return << END; } private function startStreamingResponse(Response $response) : Response { $nonbufResp = $response ->withBody(new NonBufferedBody()) ->withHeader('Content-Type', 'text/html; charset=utf-8') ->withHeader('X-Accel-Buffering', 'no') ->withHeader('Content-Length', '-1') ->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()); return $nonbufResp; } 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 // 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}
"); }; return $addToStreamingResponseBody; } private function finalizeStreamingResponse(Response $nonbufResp) : void { $nonbufResp->getBody()->write($this->getStreamingResponseHtmlEnd()); } private function getStreamingResponseHtmlEnd() : string { return "\n \n"; } private function getTimestampForDockerLogsApiSince(string $input) : string { if ($input === '') { return ''; } // We expect an RFC3339Nano string with Timezone UTC here, as docker will put out. // Unfortunately PHP doesn't support this format with nanoseconds, so we have to help // ourselves a little bit. // First we split off the nanoseconds. preg_match('/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.(\d{9}).*/', $input, $match); if (count($match) !== 3) { // The input doesn't match our expectations, it might be manipulated, we ignore it. return ''; } $datetime = \DateTimeImmutable::createFromFormat("Y-m-d\\TH:i:s", $match[1]); $nanoseconds = $match[2]; if ($datetime === false) { // Input was not parseable, it might be manipulated, we ignore it. return ''; } // Format the datetime as unix timestamp. $timestamp = $datetime->format('U'); // Increase the nanoseconds by 1, so we don't get the line with exactly the original datetime again. $nanoseconds = strval(intval($nanoseconds) + 1); // Now append the nanoseconds to the timestamp-string. return "{$timestamp}.{$nanoseconds}"; } }