diff --git a/Containers/borgbackup/backupscript.sh b/Containers/borgbackup/backupscript.sh index ae378635..a0374cf4 100644 --- a/Containers/borgbackup/backupscript.sh +++ b/Containers/borgbackup/backupscript.sh @@ -178,6 +178,7 @@ if [ "$BORG_MODE" = restore ]; then if ! rsync --stats --archive --human-readable -vv --delete \ --exclude "nextcloud_aio_mastercontainer/session/"** \ --exclude "nextcloud_aio_mastercontainer/certs/"** \ + --exclude "nextcloud_aio_mastercontainer/data/daily_backup_running" \ --exclude "nextcloud_aio_mastercontainer/data/configuration.json" \ /tmp/borg/nextcloud_aio_volumes/ /nextcloud_aio_volumes; then echo "Something failed while restoring from backup." diff --git a/Containers/mastercontainer/Dockerfile b/Containers/mastercontainer/Dockerfile index 18a44082..284fce66 100644 --- a/Containers/mastercontainer/Dockerfile +++ b/Containers/mastercontainer/Dockerfile @@ -26,6 +26,7 @@ RUN apt-get update; \ openssl \ sudo \ dpkg-dev \ + netcat \ ; \ rm -rf /var/lib/apt/lists/* @@ -84,10 +85,12 @@ RUN mkdir /var/log/supervisord; \ COPY Caddyfile / COPY start.sh /usr/bin/ +COPY backup-time-file-watcher.sh / COPY cron.sh / COPY supervisord.conf / RUN chmod +x /usr/bin/start.sh; \ - chmod +x /cron.sh + chmod +x /cron.sh; \ + chmod +x /backup-time-file-watcher.sh USER root diff --git a/Containers/mastercontainer/backup-time-file-watcher.sh b/Containers/mastercontainer/backup-time-file-watcher.sh new file mode 100644 index 00000000..69b40ef6 --- /dev/null +++ b/Containers/mastercontainer/backup-time-file-watcher.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +restart_process() { + set -x + pkill cron.sh + set +x +} + +file_present() { + if [ -f "/mnt/docker-aio-config/data/daily_backup_time" ]; then + if [ "$FILE_PRESENT" = 0 ]; then + restart_process + fi + FILE_PRESENT=1 + else + if [ "$FILE_PRESENT" = 1 ]; then + restart_process + fi + FILE_PRESENT=0 + fi +} + +while true; do + file_present + sleep 5 +done diff --git a/Containers/mastercontainer/cron.sh b/Containers/mastercontainer/cron.sh index 58fecf03..88b8d631 100644 --- a/Containers/mastercontainer/cron.sh +++ b/Containers/mastercontainer/cron.sh @@ -1,12 +1,93 @@ -#!/bin/sh -set -eux +#!/bin/bash while true; do + if [ -f "/mnt/docker-aio-config/data/daily_backup_time" ]; then + set -x + BACKUP_TIME="$(cat "/mnt/docker-aio-config/data/daily_backup_time")" + DAILY_BACKUP=1 + set +x + else + BACKUP_TIME="04:00" + DAILY_BACKUP=0 + fi + + if [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then + LOCK_FILE_PRESENT=1 + else + LOCK_FILE_PRESENT=0 + fi + + # Allow to continue directly if e.g. the mastercontainer was updated. Otherwise wait for the next execution + if [ "$LOCK_FILE_PRESENT" = 0 ]; then + while [ "$(date +%H:%M)" != "$BACKUP_TIME" ]; do + sleep 1 + done + fi + + if [ "$DAILY_BACKUP" = 1 ]; then + echo "Daily backup has started" + + # Delete all active sessions and create a lock file + rm -f "/mnt/docker-aio-config/session/"* + sudo -u www-data touch "/mnt/docker-aio-config/data/daily_backup_running" + + # Check if apache is running/stopped, watchtower is stopped and backupcontainer is stopped + APACHE_PORT="$(docker inspect nextcloud-aio-apache --format "{{.HostConfig.PortBindings}}" | grep -oP '[0-9]+' | head -1)" + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-apache$" && ! nc -z nextcloud-aio-apache "$APACHE_PORT"; do + echo "Waiting for apache to become available" + sleep 30 + done + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do + echo "Waiting for watchtower to stop" + sleep 30 + done + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; do + echo "Waiting for borgbackup to stop" + sleep 30 + done + + # Update the mastercontainer + sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateMastercontainer.php + + # Wait for watchtower to stop + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; then + echo "Something seems to be wrong: Watchtower should be started at this step." + else + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do + echo "Waiting for watchtower to stop" + sleep 30 + done + fi + + # Execute the backup itself and some related tasks + sudo -u www-data php /var/www/docker-aio/php/src/Cron/DailyBackup.php + + # Delete the lock file + rm -f "/mnt/docker-aio-config/data/daily_backup_running" + + # Wait for the nextcloud container to start and send if the backup was successful + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-nextcloud$"; then + echo "Something seems to be wrong: Nextcloud should be started at this step." + else + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-nextcloud$" && ! nc -z nextcloud-aio-nextcloud 9000; do + echo "Waiting for the Nextcloud container to start" + sleep 30 + done + fi + sudo -u www-data php /var/www/docker-aio/php/src/Cron/BackupNotification.php + + echo "Daily backup has finished" + fi + + # Make sure to delete the lock file always + rm -f "/mnt/docker-aio-config/data/daily_backup_running" + # Check for updates and send notification if yes - sudo -u www-data php /var/www/docker-aio/php/src/Cron/cron.php - # Remove dangling images - sudo -u www-data docker image prune -f + sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateNotification.php + # Remove sessions older than 24h find "/mnt/docker-aio-config/session/" -mindepth 1 -mmin +1440 -delete - sleep 1d + + # Remove dangling images + sudo -u www-data docker image prune -f done diff --git a/Containers/mastercontainer/supervisord.conf b/Containers/mastercontainer/supervisord.conf index 7ed7eaf5..7d108cb2 100644 --- a/Containers/mastercontainer/supervisord.conf +++ b/Containers/mastercontainer/supervisord.conf @@ -28,3 +28,10 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 command=/cron.sh + +[program:backup-time-file-watcher] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/backup-time-file-watcher.sh diff --git a/Containers/nextcloud/cron.sh b/Containers/nextcloud/cron.sh index bfdfafbe..0fe5f589 100644 --- a/Containers/nextcloud/cron.sh +++ b/Containers/nextcloud/cron.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -eu while true; do diff --git a/Containers/nextcloud/entrypoint.sh b/Containers/nextcloud/entrypoint.sh index e3aa667e..b7492c5f 100644 --- a/Containers/nextcloud/entrypoint.sh +++ b/Containers/nextcloud/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # version_greater A B returns whether A > B version_greater() { @@ -230,6 +230,11 @@ if ! [ -f "/mnt/ncdata/skip.update" ]; then php /var/www/html/occ maintenance:mimetype:update-db fi fi + + # Performing update of all apps if daily backups are enabled, running and successful + if [ "$DAILY_BACKUP_RUNNING" = 'yes' ]; then + php /var/www/html/occ app:update --all + fi fi # Check if appdata is present diff --git a/php/containers.json b/php/containers.json index 25ee579e..3147bb28 100644 --- a/php/containers.json +++ b/php/containers.json @@ -132,7 +132,8 @@ "ONLYOFFICE_ENABLED=%ONLYOFFICE_ENABLED%", "COLLABORA_ENABLED=%COLLABORA_ENABLED%", "TALK_ENABLED=%TALK_ENABLED%", - "ONLYOFFICE_HOST=nextcloud-aio-onlyoffice" + "ONLYOFFICE_HOST=nextcloud-aio-onlyoffice", + "DAILY_BACKUP_RUNNING=%DAILY_BACKUP_RUNNING%" ], "maxShutdownTime": 10, "restartPolicy": "unless-stopped" diff --git a/php/public/index.php b/php/public/index.php index e2a1e641..eddd71ca 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -93,6 +93,8 @@ $app->get('/containers', function ($request, $response, $args) use ($container) 'is_collabora_enabled' => $configurationManager->isCollaboraEnabled(), 'is_talk_enabled' => $configurationManager->isTalkEnabled(), 'borg_restore_password' => $configurationManager->GetBorgRestorePassword(), + 'daily_backup_time' => $configurationManager->GetDailyBackupTime(), + 'is_daily_backup_running' => $configurationManager->isDailyBackupRunning(), ]); })->setName('profile'); $app->get('/login', function ($request, $response, $args) use ($container) { diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php index dbb67ffa..70dd6c54 100644 --- a/php/src/Controller/ConfigurationController.php +++ b/php/src/Controller/ConfigurationController.php @@ -43,6 +43,15 @@ class ConfigurationController $this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword); } + if (isset($request->getParsedBody()['daily_backup_time'])) { + $dailyBackupTime = $request->getParsedBody()['daily_backup_time'] ?? ''; + $this->configurationManager->SetDailyBackupTime($dailyBackupTime); + } + + if (isset($request->getParsedBody()['delete_daily_backup_time'])) { + $this->configurationManager->DeleteDailyBackupTime(); + } + if (isset($request->getParsedBody()['options-form'])) { if (isset($request->getParsedBody()['collabora']) && isset($request->getParsedBody()['onlyoffice'])) { throw new InvalidSettingConfigurationException("Collabora and Onlyoffice are not allowed to be enabled at the same time!"); diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 6afbfdd4..5257be19 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -70,6 +70,11 @@ class DockerController } public function StartBackupContainerBackup(Request $request, Response $response, $args) : Response { + $this->startBackup(); + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startBackup() : void { $config = $this->configurationManager->GetConfig(); $config['backup-mode'] = 'backup'; $this->configurationManager->WriteConfig($config); @@ -79,8 +84,6 @@ class DockerController $id = 'nextcloud-aio-borgbackup'; $this->PerformRecursiveContainerStart($id); - - return $response->withStatus(201)->withHeader('Location', '/'); } public function StartBackupContainerCheck(Request $request, Response $response, $args) : Response { @@ -134,6 +137,16 @@ class DockerController $config['AIO_URL'] = $host . ':' . $port; // set wasStartButtonClicked $config['wasStartButtonClicked'] = 1; + $this->configurationManager->WriteConfig($config); + + // Start container + $this->startTopContainer(); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startTopContainer() : void { + $config = $this->configurationManager->GetConfig(); // set AIO_TOKEN $config['AIO_TOKEN'] = bin2hex(random_bytes(24)); $this->configurationManager->WriteConfig($config); @@ -144,14 +157,17 @@ class DockerController $id = self::TOP_CONTAINER; $this->PerformRecursiveContainerStart($id); - return $response->withStatus(201)->withHeader('Location', '/'); } public function StartWatchtowerContainer(Request $request, Response $response, $args) : Response { + $this->startWatchtower(); + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startWatchtower() : void { $id = 'nextcloud-aio-watchtower'; $this->PerformRecursiveContainerStart($id); - return $response->withStatus(201)->withHeader('Location', '/'); } private function PerformRecursiveContainerStop(string $id) : void diff --git a/php/src/Cron/BackupNotification.php b/php/src/Cron/BackupNotification.php new file mode 100644 index 00000000..a570031b --- /dev/null +++ b/php/src/Cron/BackupNotification.php @@ -0,0 +1,29 @@ +get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ +$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); + +$id = 'nextcloud-aio-nextcloud'; +$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); + +$backupExitCode = $dockerActionManger->GetBackupcontainerExitCode(); + +if ($backupExitCode === 0) { + $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup successful!', 'You can get further info by looking at the backup logs in the AIO interface.'); +} + +if ($backupExitCode > 0) { + $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup failed!', 'You can get further info by looking at the backup logs in the AIO interface.'); +} diff --git a/php/src/Cron/DailyBackup.php b/php/src/Cron/DailyBackup.php new file mode 100644 index 00000000..932bd04f --- /dev/null +++ b/php/src/Cron/DailyBackup.php @@ -0,0 +1,20 @@ +get(\AIO\Controller\DockerController::class); + +// Stop container and start backup +$dockerController->startBackup(); + +// Start apache +$dockerController->startTopContainer(); diff --git a/php/src/Cron/UpdateMastercontainer.php b/php/src/Cron/UpdateMastercontainer.php new file mode 100644 index 00000000..01c28e59 --- /dev/null +++ b/php/src/Cron/UpdateMastercontainer.php @@ -0,0 +1,17 @@ +get(\AIO\Controller\DockerController::class); + +# Update the mastercontainer +$dockerController->startWatchtower(); diff --git a/php/src/Cron/cron.php b/php/src/Cron/UpdateNotification.php similarity index 100% rename from php/src/Cron/cron.php rename to php/src/Cron/UpdateNotification.php diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php index e13e1e67..afe30b5a 100644 --- a/php/src/Data/ConfigurationManager.php +++ b/php/src/Data/ConfigurationManager.php @@ -444,4 +444,39 @@ class ConfigurationManager $defaultValue = 'nextcloud_aio_nextcloud_data'; return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetDailyBackupTime(string $time) : void { + if ($time === "") { + throw new InvalidSettingConfigurationException("The daily backup time must not be empty!"); + } + + if (!preg_match("#^[0-1][0-9]:[0-5][0-9]$#", $time) && !preg_match("#^2[0-3]:[0-5][0-9]$#", $time)) { + throw new InvalidSettingConfigurationException("You did not enter a correct time! One correct example is '04:00'!"); + } + + file_put_contents(DataConst::GetDailyBackupTimeFile(), $time); + } + + public function GetDailyBackupTime() : string { + if (!file_exists(DataConst::GetDailyBackupTimeFile())) { + return ''; + } + return file_get_contents(DataConst::GetDailyBackupTimeFile()); + } + + public function DeleteDailyBackupTime() : void { + if (file_exists(DataConst::GetDailyBackupTimeFile())) { + unlink(DataConst::GetDailyBackupTimeFile()); + } + } + + public function isDailyBackupRunning() : bool { + if (file_exists(DataConst::GetDailyBackupBlockFile())) { + return true; + } + return false; + } } diff --git a/php/src/Data/DataConst.php b/php/src/Data/DataConst.php index 4c7643d8..5ffaf403 100644 --- a/php/src/Data/DataConst.php +++ b/php/src/Data/DataConst.php @@ -27,6 +27,14 @@ class DataConst { return self::GetDataDirectory() . '/backupsecret'; } + public static function GetDailyBackupTimeFile() : string { + return self::GetDataDirectory() . '/daily_backup_time'; + } + + public static function GetDailyBackupBlockFile() : string { + return self::GetDataDirectory() . '/daily_backup_running'; + } + public static function GetBackupKeyFile() : string { return self::GetDataDirectory() . '/borg.config'; } diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 70e65b48..d7ff2aed 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -267,6 +267,12 @@ class DockerActionManager } else { $replacements[1] = ''; } + } elseif ($out[1] === 'DAILY_BACKUP_RUNNING') { + if ($this->configurationManager->isDailyBackupRunning()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } } else { $replacements[1] = $this->configurationManager->GetSecret($out[1]); } diff --git a/php/templates/containers.twig b/php/templates/containers.twig index adc26f71..13728612 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -50,7 +50,19 @@ {% endif %} {% endfor %} - {% if isWatchtowerRunning == true %} + {% if is_daily_backup_running == true %} + Daily backup currently running. (Logs)

+ It will update all containers and all apps if the backup is successful.

+ {% if is_mastercontainer_update_available == true %} + Since the mastercontainer gets updated, it will restart the container which will make it unavailable for a moment. (Logs)

+ {% endif %} + {% if has_update_available == false %} + The whole process should not take more than a few minutes.

+ {% else %} + The whole process can take a while because your containers get updated.

+ {% endif %} + Reload ↻
+ {% elseif isWatchtowerRunning == true %} Mastercontainer update currently running. It will restart the mastercontainer soon which will make it unavailable for a moment. Please wait until that's done. (Logs)

Reload ↻
{% else %} @@ -335,6 +347,26 @@ + +

Daily backup creation

+ {% if daily_backup_time == "" %} + By entering a time below, you can enable daily backups. It will create them at the entered time in 24h format. E.g. 04:00 will create backups at 4 am CT and 16:00 at 4 pm CT.

+
+ + + + +
+ This option will also automatically update your containers and apps and will send a notification about the result of the backup.

+ {% else %} + Daily backups will be created at {{ daily_backup_time }} CT which includes a notification about the result of the backup and automatic updates of your containers and apps. You can disable this option again by clicking on the button below.

+
+ + + + +
+ {% endif %} {% endif %} {% endif %} {% if has_backup_run_once == false %} @@ -402,7 +434,7 @@ {% endif %} {% endif %} - {% if isApacheStarting == true or is_backup_container_running == true or isWatchtowerRunning == true %} + {% if isApacheStarting == true or is_backup_container_running == true or isWatchtowerRunning == true or is_daily_backup_running == true %} {% else %}