diff --git a/Containers/borgbackup/backupscript.sh b/Containers/borgbackup/backupscript.sh index 269c9312..26c9b42c 100644 --- a/Containers/borgbackup/backupscript.sh +++ b/Containers/borgbackup/backupscript.sh @@ -149,6 +149,54 @@ if [ "$BORG_MODE" = backup ]; then exit 1 fi + # Back up additional directories of the host + if [ "$ADDITIONAL_DIRECTORIES_BACKUP" = 'yes' ]; then + if [ -d "/docker_volumes/" ]; then + DOCKER_VOLUME_DIRS="$(find /docker_volumes -mindepth 1 -maxdepth 1 -type d)" + mapfile -t DOCKER_VOLUME_DIRS <<< "$DOCKER_VOLUME_DIRS" + for directory in "${DOCKER_VOLUME_DIRS[@]}"; do + if [ -z "$(ls -A "$directory")" ]; then + echo "$directory is empty which is not allowed." + exit 1 + fi + done + if ! borg create "${BORG_OPTS[@]}" "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-docker-volumes" "/docker_volumes/"; then + echo "Deleting the failed backup archive..." + borg delete --stats --progress "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-docker-volumes" + echo "Backup of additional docker-volumes failed!" + exit 1 + fi + + if ! borg prune --prefix '*_*-additional-docker-volumes' "${BORG_PRUNE_OPTS[@]}"; then + echo "Failed to prune additional docker-volumes archives!" + exit 1 + fi + fi + if [ -d "/host_mounts/" ]; then + EXCLUDED_DIRECTORIES=(home/*/.cache root/.cache var/cache lost+found run var/run dev tmp sys proc) + # Exclude borg backup cache + EXCLUDED_DIRECTORIES+=(var/lib/docker/volumes/nextcloud_aio_backup_cache/_data) + # Exclude target directory + if [ -n "$BORGBACKUP_HOST_LOCATION" ] && [ "$BORGBACKUP_HOST_LOCATION" != "nextcloud_aio_backupdir" ]; then + EXCLUDED_DIRECTORIES+=("$BORGBACKUP_HOST_LOCATION") + fi + for directory in "${EXCLUDED_DIRECTORIES[@]}" + do + EXCLUDE_DIRS+=(--exclude "/host_mounts/$directory/") + done + if ! borg create "${BORG_OPTS[@]}" "${EXCLUDED_DIRECTORIES[@]}" "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-host-mounts" "/host_mounts/"; then + echo "Deleting the failed backup archive..." + borg delete --stats --progress "$BORG_BACKUP_DIRECTORY::$CURRENT_DATE-additional-host-mounts" + echo "Backup of additional host-mounts failed!" + exit 1 + fi + if ! borg prune --prefix '*_*-additional-host-mounts' "${BORG_PRUNE_OPTS[@]}"; then + echo "Failed to prune additional host-mount archives!" + exit 1 + fi + fi + fi + # Inform user get_expiration_time echo "Backup finished successfully on $END_DATE_READABLE ($DURATION_READABLE)" @@ -195,6 +243,11 @@ if [ "$BORG_MODE" = restore ]; then # Save current path BORG_LOCATION="$(jq '.borg_backup_host_location' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + # Save Additional Backup dirs + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" ]; then + ADDITIONAL_BACKUP_DIRECTORIES="$(cat /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories)" + fi + # Save current nextcloud datadir if grep -q '"nextcloud_datadir":' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then NEXTCLOUD_DATADIR="$(jq '.nextcloud_datadir' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" @@ -227,6 +280,13 @@ if [ "$BORG_MODE" = restore ]; then CONTENTS="$(jq ".nextcloud_datadir = $NEXTCLOUD_DATADIR" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + # Reset the additional backup directories + if [ -n "$ADDITIONAL_BACKUP_DIRECTORIES" ]; then + echo "$ADDITIONAL_BACKUP_DIRECTORIES" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + chown 33:0 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + chmod 770 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + fi + umount /tmp/borg # Inform user diff --git a/php/containers.json b/php/containers.json index d06299dd..c92699be 100644 --- a/php/containers.json +++ b/php/containers.json @@ -233,7 +233,9 @@ "BORG_PASSWORD=%BORGBACKUP_PASSWORD%", "BORG_MODE=%BORGBACKUP_MODE%", "SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%", - "BACKUP_RESTORE_PASSWORD=%BACKUP_RESTORE_PASSWORD%" + "BACKUP_RESTORE_PASSWORD=%BACKUP_RESTORE_PASSWORD%", + "ADDITIONAL_DIRECTORIES_BACKUP=%ADDITIONAL_DIRECTORIES_BACKUP%", + "BORGBACKUP_HOST_LOCATION=%BORGBACKUP_HOST_LOCATION%" ], "volumes": [ { diff --git a/php/public/index.php b/php/public/index.php index 6136f2d5..3829017a 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -104,6 +104,7 @@ $app->get('/containers', function ($request, $response, $args) use ($container) 'is_backup_section_enabled' => $configurationManager->isBackupSectionEnabled(), 'is_imaginary_enabled' => $configurationManager->isImaginaryEnabled(), 'is_fulltextsearch_enabled' => $configurationManager->isFulltextsearchEnabled(), + 'additional_backup_directories' => $configurationManager->GetAdditionalBackupDirectoriesString(), ]); })->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 ad2697d7..a06cc6b9 100644 --- a/php/src/Controller/ConfigurationController.php +++ b/php/src/Controller/ConfigurationController.php @@ -57,6 +57,11 @@ class ConfigurationController $this->configurationManager->DeleteDailyBackupTime(); } + if (isset($request->getParsedBody()['additional_backup_directories'])) { + $additionalBackupDirectories = $request->getParsedBody()['additional_backup_directories'] ?? ''; + $this->configurationManager->SetAdditionalBackupDirectories($additionalBackupDirectories); + } + if (isset($request->getParsedBody()['delete_timezone'])) { $this->configurationManager->DeleteTimezone(); } diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php index 1dec7615..826fa3ea 100644 --- a/php/src/Data/ConfigurationManager.php +++ b/php/src/Data/ConfigurationManager.php @@ -584,6 +584,45 @@ class ConfigurationManager } } + /** + * @throws InvalidSettingConfigurationException + */ + public function SetAdditionalBackupDirectories(string $additionalBackupDirectories) : void { + $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); + $validDirectories = ''; + foreach($additionalBackupDirectoriesArray as $entry) { + // Trim all unwanted chars on both sites + $entry = trim($entry); + if ($entry !== "") { + if (!preg_match("#^/[0-1a-zA-Z/-_]$#", $entry) && !preg_match("#^[0-1a-zA-Z_-]$#", $entry)) { + throw new InvalidSettingConfigurationException("You entered unallowed characters! Problematic is " . $entry); + } + $validDirectories .= rtrim($entry, '/') . PHP_EOL; + } + } + + if ($validDirectories === '') { + unlink(DataConst::GetAdditionalBackupDirectoriesFile()); + } else { + file_put_contents(DataConst::GetAdditionalBackupDirectoriesFile(), $validDirectories); + } + } + + public function GetAdditionalBackupDirectoriesString() : string { + if (!file_exists(DataConst::GetAdditionalBackupDirectoriesFile())) { + return ''; + } + $additionalBackupDirectories = file_get_contents(DataConst::GetAdditionalBackupDirectoriesFile()); + return $additionalBackupDirectories; + } + + public function GetAdditionalBackupDirectoriesArray() : array { + $additionalBackupDirectories = $this->GetAdditionalBackupDirectoriesString(); + $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); + $additionalBackupDirectoriesArray = array_unique($additionalBackupDirectoriesArray, SORT_REGULAR); + return $additionalBackupDirectoriesArray; + } + public function isDailyBackupRunning() : bool { if (file_exists(DataConst::GetDailyBackupBlockFile())) { return true; diff --git a/php/src/Data/DataConst.php b/php/src/Data/DataConst.php index 5ffaf403..5e671c11 100644 --- a/php/src/Data/DataConst.php +++ b/php/src/Data/DataConst.php @@ -31,6 +31,10 @@ class DataConst { return self::GetDataDirectory() . '/daily_backup_time'; } + public static function GetAdditionalBackupDirectoriesFile() : string { + return self::GetDataDirectory() . '/additional_backup_directories'; + } + public static function GetDailyBackupBlockFile() : string { return self::GetDataDirectory() . '/daily_backup_running'; } diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index cb76aac9..85c9cd21 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -314,6 +314,14 @@ class DockerActionManager $replacements[1] = $this->configurationManager->GetNextcloudUploadLimit(); } elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') { $replacements[1] = $this->configurationManager->GetNextcloudMaxTime(); + } elseif ($out[1] === 'ADDITIONAL_DIRECTORIES_BACKUP') { + if ($this->configurationManager->GetAdditionalBackupDirectoriesString() !== '') { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'BORGBACKUP_HOST_LOCATION') { + $replacements[1] = $this->configurationManager->GetBorgBackupHostLocation(); } else { $replacements[1] = $this->configurationManager->GetSecret($out[1]); } @@ -354,6 +362,22 @@ class DockerActionManager $requestBody['HostConfig']['CapAdd'] = ["SYS_ADMIN"]; $requestBody['HostConfig']['Devices'] = [["PathOnHost" => "/dev/fuse", "PathInContainer" => "/dev/fuse", "CgroupPermissions" => "rwm"]]; $requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined"]; + + // Additional backup directories + $mounts = []; + foreach ($this->configurationManager->GetAdditionalBackupDirectoriesArray() as $additionalBackupDirectories) { + if ($additionalBackupDirectories !== '') { + if (!str_starts_with($additionalBackupDirectories, '/')) { + $target = '/docker_volumes/'; + } else { + $target = '/host_mounts'; + } + $mounts[] = ["Type" => "bind", "Source" => $additionalBackupDirectories, "Target" => $target . $additionalBackupDirectories, "ReadOnly" => true, "BindOptions" => ["NonRecursive" => true]]; + } + } + if(count($mounts) > 0) { + $requestBody['HostConfig']['Mounts'] = $mounts; + } } $url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier()); diff --git a/php/templates/containers.twig b/php/templates/containers.twig index be4d3a7d..6cb573d1 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -371,7 +371,7 @@

Backup restore

- Choose the backup that you want to restore and click on the button below to restore the selected backup. This will overwrite all your files with the state of the backup so you should consider creating a backup first. It also makes sense to run an integrity check before restoring your files but is not mandatory since it shouldn't be needed in most situations.

+ Choose the backup that you want to restore and click on the button below to restore the selected backup. This will overwrite all your files with the state of the backup so you should consider creating a backup first. It also makes sense to run an integrity check before restoring your files but is not mandatory since it shouldn't be needed in most situations. Please note that this will not restore additionally chosen backup directories!

@@ -406,6 +406,21 @@
{% endif %} + +

Back up additional directories and volumes of your host

+ Below, you can enter directories and docker volumes of your host that will backed up additionally into the same borg backup archive.

+
+ + + +
+
+ Each line and entry needs to start with a slash or letter/digit. Allowed are only a-z, A-Z, 0-9, _, -, and /. If the entry begins with a letter/digit are slashes not supported. Two valid entries are /directory/on/the/host and my_custom_docker_volume

+ Make sure to specify all storages that you want to back up separately since storages will not be mounted recursively. E.g. providing / as additional backup directory will only back up files and folders that are stored on the root partition and not on the EFI partition or any other. Excluded by the backup will be caches and a few other directories. You should make sure to stop all services before the backup can run correctly if you want to back up the root partition. For automating this see this documentation

+ Please note that the chosen directories/volumes will not be restored when you restore your instance, so this would need to be done manually.

+ {% if additional_backup_directories != "" %} + This option is currently set. You can disable it again by clearing the field and submitting your changes.

+ {% endif %} {% endif %} {% endif %} {% if has_backup_run_once == false %} diff --git a/tests/QA/020-backup-and-restore.md b/tests/QA/020-backup-and-restore.md index dad3b45d..8c8996e6 100644 --- a/tests/QA/020-backup-and-restore.md +++ b/tests/QA/020-backup-and-restore.md @@ -7,5 +7,6 @@ - [ ] Submitting a time here should reload the page and reveal at the same place the option to delete the setting again. - [ ] When the time of the automatic backup has come (you can test it by choosing a time that is e.g. only a minute away), it should automatically log you out (you can verify by reloading) and after you log in again you should see that the automatic backup is currently running. - [ ] After a while you should see that your container are starting and in the Backup and restore section you should see that the backup was successful +- [ ] When entering additional backup directories, it should allow e.g. `/etc` and `nextcloud_aio_mastercontainer` but not `nextcloud/test`. Running a backup with this should back up these directories/volumes successfully. You can now continue with [030-aio-password-change.md](./030-aio-password-change.md) \ No newline at end of file