From 56fbcdad74f46dd00093f704f8bf50dab34dd82e Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Thu, 10 Sep 2026 14:31:32 +0200 Subject: [PATCH] Support home-based mailboxes during automated upgrades --- Dokumentation.md | 16 +++++++- README.md | 3 ++ UPDATE_ANLEITUNG.md | 42 +++++++++++++++++++- etc/app-backup/app-backup.conf | 3 ++ lib/common.sh | 14 +++++-- tests/test_backup.py | 70 ++++++++++++++++++++++++++++++++++ tools/build-update-package.py | 2 +- update.sh | 53 ++++++++++++++++++++++++- 8 files changed, 194 insertions(+), 9 deletions(-) diff --git a/Dokumentation.md b/Dokumentation.md index 722e6b0..60a42b4 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -83,7 +83,7 @@ Ein neuer Lauf heißt beispielsweise `appbackup_2026-09-10_02-30-00_a1b2c3d4`. Z | `nextcloud-data` | `NC_DATA_DIR`, sofern aktiviert | | `gitea`, `gitea-etc` | `GITEA_DATA_DIR`, `GITEA_ETC_DIR` | | `gitea-repos`, `gitea-custom` | Optionale explizite externe Repository-/Custom-Pfade | -| `mail`, `postfix`, `dovecot` | Mailstore und beide Konfigurationsverzeichnisse | +| `mail`, `postfix`, `dovecot` | Zentraler Mailstore (bei `ENABLE_MAIL_STORE=true`) und beide Konfigurationsverzeichnisse | | `mail-home-0`, `mail-home-1`, … | Reihenfolge von `MAIL_HOME_DIRS`, mit Excludes und Dateisystemgrenze | | `extra-0`, `extra-1`, … | Reihenfolge von `EXTRA_PATHS` | @@ -156,6 +156,17 @@ Gitea: `APP_DATA_PATH`, `[repository] ROOT`, Custom-Verzeichnis und `app.ini` pr `MAIL_BACKUP_MODE=live` und abgeschaltete Stopps sind bewusste Ausnahmen mit Konsistenzrisiko. Das Log weist darauf hin. Home-Verzeichnisse werden mit `--one-file-system` kopiert: Daten anderer Mounts fehlen dann absichtlich. Mailqueues, zusätzliche Spamfilter-/DKIM-Verzeichnisse oder Maildatenbanken sind nur gesichert, wenn sie explizit im Umfang stehen. +`ENABLE_MAIL_STORE=true` bleibt der kompatible Default: `MAIL_DIR` muss existieren. +Bei Mailboxen ausschließlich in Benutzerverzeichnissen kann explizit +`ENABLE_MAIL_STORE=false` gesetzt werden. Dann sind aktivierte, nicht leere +`MAIL_HOME_DIRS` zwingend; fehlende Homes lassen die Vorprüfung scheitern. +Postfix/Dovecot-Konfigurationen und Dienststopps bleiben erhalten. Der Restore +verwendet dieselbe Komponentenzuordnung und stellt Home-Mailboxen über +`mail-home-N` wieder her. Die Vollständigkeit der Benutzerliste sowie Excludes, +Symlinks und Mountgrenzen muss der Betreiber prüfen. Der Paket-Updater bietet +bei fehlendem zentralem Verzeichnis einen Pfaddialog oder eine ausdrückliche +Homes-Bestätigung; Details stehen in [UPDATE_ANLEITUNG.md](UPDATE_ANLEITUNG.md). + ### Speicher, Retention und Protokolle `MIN_FREE_GB` ist eine Reserveprüfung auf den Staging- und Archivdateisystemen, keine genaue Prognose. Die Kopie braucht Platz für unkomprimierte Quelldaten und Dumps; Archive benötigen zusätzlich komprimierten Platz. Beim Restore kommen die private Kopie des Sicherungssatzes und entpackte Daten hinzu. Eine Platte kann zwischen Prüfungen volllaufen; der Lauf muss dann scheitern. Ausreichend Reserve und ein externes Kapazitätsmonitoring einplanen. @@ -293,10 +304,11 @@ Die Entfernung der drei DB-Dateien aus dem aktuellen Baum löscht deren Historie Reproduzierbar mit `tests/Dockerfile` und `tests/run.sh`. Der Container bekommt das Repository nur lesbar und benötigt während der Tests kein Netzwerk. Die MariaDB läuft ohne TCP-Port in einem temporären Verzeichnis. systemd/occ werden simuliert; alle eigentlichen Archive, Dateikopien, Dumps/Imports und Crypt-Operationen sind real. -Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **30 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion. +Ergebnis des lokalen Abschlusslaufs am 10.09.2026: **34 Tests erfolgreich**, Shell-Syntax und ShellCheck ohne Befund. Testumgebung: Debian 13.6, MariaDB 11.8.6, rclone 1.60.1-DEV (Distributionspaket), ShellCheck 0.10.0. Diese Versionsangaben beschreiben die Testumgebung, keine Empfehlung für die Produktivversion. Geprüfte Fälle umfassen: +- Fehlender Mailstore ohne Live-Änderung, explizite Pfadkorrektur, Homes-only-Update samt tatsächlichem Maildatei-Restore und Ablehnung unvollständiger Home-Konfigurationen. - Shell-Syntax und ShellCheck der Paketdateien. - MyISAM-Dump und tatsächlicher SQL-Import samt Dateiwiederherstellung. - gzip/zstd, korrekte Excludes, vorhandener Wartungsmodus und inaktive Dienste. diff --git a/README.md b/README.md index d698c6c..3076a9b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,9 @@ RCLONE_REMOTE_BASE="OneDriveCrypt:gemini" # vorhandenes Crypt-Ziel beibehalten! RETENTION_KEEP_LAST=3 ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus MAIL_BACKUP_MODE="stop" +# Zentrale Mailablage muss existieren; bei ausschließlich in Homes liegenden +# Mailboxen ENABLE_MAIL_STORE=false und MAIL_HOME_DIRS vollständig konfigurieren. +ENABLE_MAIL_STORE="true" # Tatsächliche Timer-/Service-Namen einsetzen; Timer vor seinem Service nennen: QUIESCE_SERVICES=() # Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert: diff --git a/UPDATE_ANLEITUNG.md b/UPDATE_ANLEITUNG.md index 0033d63..0f29143 100644 --- a/UPDATE_ANLEITUNG.md +++ b/UPDATE_ANLEITUNG.md @@ -55,6 +55,19 @@ für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch. ## Gezielte Rückfragen und Grenzen +- **Zentrales Mailverzeichnis fehlt, etwa `/var/vmail`:** Der Installer fragt vor + der Installation nach dem tatsächlichen absoluten Pfad. Alternativ lässt sich + dieser mit `--mail-dir /tatsaechliche/mailablage` übergeben. Kein leeres + Ersatzverzeichnis anlegen: Das würde fehlende Maildaten nur verdecken. + Liegen **alle** Mailboxen in den angezeigten `MAIL_HOME_DIRS`, im Dialog `homes` + wählen und die vollständige Abdeckung bestätigen. `--mail-homes-only` erteilt + dieselbe ausdrückliche Bestätigung ohne Dialog. Dabei müssen die Home-Sicherung + aktiviert und alle konfigurierten Homes vorhanden sein. Excludes und + Dateisystemgrenzen gelten weiterhin; ausgeschlossene Verzeichnisse, verlinkte + externe Mailablagen und weitere Mounts sind dadurch nicht abgedeckt. + Postfix/Dovecot-Konfigurationen und Maildienst-Stopps bleiben aktiv. + Bei unbekannter Mailablage abbrechen und zuerst die Mailserver-Konfiguration + prüfen. Ein fehlender Pfad wird nie automatisch übersprungen. - **Rclone-Datei nicht eindeutig:** Pfad im Dialog oder mit `--rclone-config FILE` angeben. Eine user-eigene Datei wird in eine neue root-eigene Datei kopiert; die vom Benutzer verwendete Originaldatei bleibt unverändert. @@ -94,6 +107,33 @@ bleiben erhalten; beim Aktivieren kann `Persistent=true` einen Lauf nachholen. ## Fehler und Rückweg +Ein Abbruch `Missing source: /var/vmail` bei der **Vorprüfung**, wie beim Paket +`8e6c9a058fa9`, erfordert kein Rollback. Mit dem korrigierten Paket erneut starten +und die Mailablage klären. Die bisherige Konfiguration und der Timer sind erhalten; +der abgebrochene Versuch enthält noch kein erfolgreiches neues Backup. +Bereits bewusst getroffene Entscheidungen können als Optionen mitgegeben werden: + +```bash +sudo bash app-backup-update-VERSION.run --enable-gitea --cron-services crond.service +``` + +Diese Optionen passen nur, wenn Gitea aufgenommen werden soll und das Pausieren +aller Cronjobs über `crond.service` ausdrücklich akzeptiert wurde. + +Für Mailablagen wie `/home/johannes/Mail` unter den konfigurierten Benutzerhomes +zusätzlich `--mail-homes-only` verwenden, sofern damit alle Postfächer erfasst sind: + +```bash +sudo bash app-backup-update-VERSION.run \ + --enable-gitea --cron-services crond.service --mail-homes-only +``` + +Der Installer zeigt die übernommenen Home-Verzeichnisse an. `Mail/` einschließlich +`cur`, `new`, `tmp` und Dovecot-Dateien wird mit den normalen Home-Excludes der +Vorlage vollständig kopiert, sofern es auf demselben Dateisystem liegt und kein +Verweis auf eine externe Ablage ist. Weitere Benutzer müssen in `MAIL_HOME_DIRS` +stehen; das Vorhandensein eines einzelnen Postfachs belegt nicht deren Abdeckung. + Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt. Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls @@ -125,7 +165,7 @@ Das ausführbare Paket ausschließlich über euren vertrauenswürdigen Kanal bez ## Verifikation der Lieferung -30 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich. +34 Container-Tests sowie ShellCheck und Shell-Syntaxprüfung erfolgreich. Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips; systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore diff --git a/etc/app-backup/app-backup.conf b/etc/app-backup/app-backup.conf index dddeead..1467499 100644 --- a/etc/app-backup/app-backup.conf +++ b/etc/app-backup/app-backup.conf @@ -82,6 +82,9 @@ GITEA_DB_CNF="/etc/app-backup/db-gitea.cnf" ENABLE_MAIL="true" MAIL_BACKUP_MODE="stop" +# Nur false setzen, wenn alle Mailboxen durch MAIL_HOME_DIRS abgedeckt sind. +# Postfix/Dovecot und deren Dienststopps bleiben auch dann aktiviert. +ENABLE_MAIL_STORE="true" MAIL_DIR="/var/vmail" POSTFIX_DIR="/etc/postfix" DOVECOT_DIR="/etc/dovecot" diff --git a/lib/common.sh b/lib/common.sh index 6ea96b9..0b07c11 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -41,7 +41,7 @@ load_config() { : "${GITEA_DATA_DIR:=/var/lib/gitea/data}" "${GITEA_ETC_DIR:=/etc/gitea}" : "${ENABLE_HTTPD_STOP:=true}" "${HTTPD_SERVICE_NAME:=httpd}" : "${ENABLE_PHPFPM_STOP:=true}" "${PHPFPM_SERVICE_NAME:=php-fpm}" - : "${MAIL_BACKUP_MODE:=stop}" "${ENABLE_MAIL_HOME_DIRS:=true}" + : "${MAIL_BACKUP_MODE:=stop}" "${ENABLE_MAIL_HOME_DIRS:=true}" "${ENABLE_MAIL_STORE:=true}" : "${MAIL_DIR:=/var/vmail}" "${POSTFIX_DIR:=/etc/postfix}" "${DOVECOT_DIR:=/etc/dovecot}" : "${RESTORE_STRICT_DELETE:=false}" # Preserve old space-separated lists; arrays additionally support spaces in paths. @@ -55,7 +55,7 @@ load_config() { read -r -a MAIL_HOME_EXCLUDES <<< "${MAIL_HOME_EXCLUDES[0]}" fi for v in ENABLE_UPLOAD ENABLE_REMOTE_RETENTION ENABLE_MAIL_REPORT ENABLE_WORDPRESS ENABLE_NEXTCLOUD \ - ENABLE_NEXTCLOUD_DATA ENABLE_GITEA ENABLE_MAIL ENABLE_DB_DUMPS ENABLE_NEXTCLOUD_MAINTENANCE \ + ENABLE_NEXTCLOUD_DATA ENABLE_GITEA ENABLE_MAIL ENABLE_MAIL_STORE ENABLE_DB_DUMPS ENABLE_NEXTCLOUD_MAINTENANCE \ ENABLE_GITEA_SERVICE_STOP ENABLE_HTTPD_STOP ENABLE_PHPFPM_STOP ENABLE_MAIL_HOME_DIRS RESTORE_STRICT_DELETE; do [[ ${!v} == true || ${!v} == false ]] || die "$v must be true or false" done @@ -67,6 +67,10 @@ load_config() { [[ $ARCHIVE_PREFIX =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || die "Unsafe ARCHIVE_PREFIX" [[ $COMPRESSOR == zstd || $COMPRESSOR == gzip ]] || die "COMPRESSOR must be zstd or gzip" [[ $MAIL_BACKUP_MODE == stop || $MAIL_BACKUP_MODE == live ]] || die "MAIL_BACKUP_MODE must be stop or live" + # A homes-only setup must still contain mailbox data, not just server config. + if [[ $ENABLE_MAIL == true && $ENABLE_MAIL_STORE == false ]]; then + [[ $ENABLE_MAIL_HOME_DIRS == true && ${#MAIL_HOME_DIRS[@]} -gt 0 ]] || die 'Mail without a central store requires enabled, nonempty MAIL_HOME_DIRS' + fi if [[ -z ${DB_DUMP_BIN:-} ]]; then DB_DUMP_BIN=$(command -v mariadb-dump || command -v mysqldump || true); fi if [[ -z ${DB_CLIENT_BIN:-} ]]; then DB_CLIENT_BIN=$(command -v mariadb || command -v mysql || true); fi } @@ -78,7 +82,8 @@ guard_workspaces() { if [[ $ENABLE_NEXTCLOUD == true ]]; then sources+=("${NC_DIR:-}" "${NC_DATA_DIR:-}"); fi if [[ $ENABLE_GITEA == true ]]; then sources+=("$GITEA_DATA_DIR" "$GITEA_ETC_DIR" "${GITEA_REPOS_DIR:-}" "${GITEA_CUSTOM_DIR:-}"); fi if [[ $ENABLE_MAIL == true ]]; then - sources+=("$MAIL_DIR" "$POSTFIX_DIR" "$DOVECOT_DIR") + if [[ $ENABLE_MAIL_STORE == true ]]; then sources+=("$MAIL_DIR"); fi + sources+=("$POSTFIX_DIR" "$DOVECOT_DIR") if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then sources+=("${MAIL_HOME_DIRS[@]}"); fi fi # Validate the WHOLE plan before mkdir/chmod touches any configured directory. @@ -149,7 +154,8 @@ build_plan() { add_db gitea GITEA fi if [[ $ENABLE_MAIL == true ]]; then - add_source mail "$MAIL_DIR"; add_source postfix "$POSTFIX_DIR"; add_source dovecot "$DOVECOT_DIR" + if [[ $ENABLE_MAIL_STORE == true ]]; then add_source mail "$MAIL_DIR"; fi + add_source postfix "$POSTFIX_DIR"; add_source dovecot "$DOVECOT_DIR" if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then local i=0 for p in "${MAIL_HOME_DIRS[@]}"; do add_source "mail-home-$i" "$p"; i=$((i+1)); done diff --git a/tests/test_backup.py b/tests/test_backup.py index 8bda708..b15c9fb 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -440,6 +440,76 @@ QUIESCE_SERVICES=(nc-cron) self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active') self.assertNotIn('systemctl stop',self.events.read_text()) + def test_missing_mail_store_requires_explicit_resolution(self): + canonical=self.configure_upgrade() + with canonical.open('a') as f: f.write(f'MAIL_DIR="{self.src}/missing-mail"\n') + original=canonical.read_bytes() + package=self.build_package() + result=subprocess.run(['bash',str(package),'--foreground'],env=self.env,capture_output=True,text=True,timeout=60) + self.assertNotEqual(result.returncode,0) + self.assertIn('--mail-dir DIR',result.stdout+result.stderr) + self.assertEqual(canonical.read_bytes(),original) + self.assertFalse((self.src/'missing-mail').exists()) + self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active') + self.assertNotIn('systemctl stop',self.events.read_text()) + + def test_package_mail_directory_override(self): + canonical=self.configure_upgrade() + with canonical.open('a') as f: f.write(f'MAIL_DIR="{self.src}/missing-mail"\n') + package=self.build_package() + result=subprocess.run(['bash',str(package),'--foreground','--mail-dir',str(self.src/'mail')],env=self.env,capture_output=True,text=True,timeout=120) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + self.assertIn('ENABLE_MAIL_STORE=true',canonical.read_text()) + run=next((self.base/'work/archives').glob('appbackup_*')) + self.assertTrue((run/'mail.tar.zst').is_file()) + self.assertTrue((run/'UPLOADED').is_file()) + + def test_package_homes_only_keeps_mail_configs_and_service_stops(self): + canonical=self.configure_upgrade() + home=self.src/'johannes' + for folder in ['cur','new','tmp']: + (home/'Mail'/folder).mkdir(parents=True) + mailbox=home/'Mail/cur/test-message:2,S' + mailbox.write_bytes(b'Subject: retained mail\n\nMailbox content\n') + mailbox.chmod(0o600) + original=mailbox.read_bytes() + with canonical.open('a') as f: + f.write(f'MAIL_DIR="{self.src}/missing-mail"\nENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=("{home}")\n') + package=self.build_package() + result=subprocess.run(['bash',str(package),'--foreground','--mail-homes-only'],env=self.env,capture_output=True,text=True,timeout=120) + self.assertEqual(result.returncode,0,result.stdout+result.stderr) + self.assertIn('ENABLE_MAIL_STORE=false',canonical.read_text()) + run=next((self.base/'work/archives').glob('appbackup_*')) + for name in ['mail-home-0','postfix','dovecot']: + self.assertTrue((run/f'{name}.tar.zst').is_file()) + self.assertFalse((run/'mail.tar.zst').exists()) + self.assertTrue((run/'UPLOADED').is_file()) + for unit in ['postfix','dovecot']: + self.assertIn(f'systemctl stop {unit}',self.events.read_text()) + self.assertEqual(json.loads(self.state.read_text())['services'][unit],'active') + mailbox.write_text('changed after backup') + restored=subprocess.run(['bash',str(REPO/'usr-local-sbin/app-restore.sh'),'--config',str(canonical), + '--local-run',str(run),'--components','mail-home-0','--apply','--yes'], + env=self.env,capture_output=True,text=True,timeout=60) + self.assertEqual(restored.returncode,0,restored.stdout+restored.stderr) + self.assertEqual(mailbox.read_bytes(),original) + self.assertEqual(mailbox.stat().st_mode & 0o777,0o600) + + def test_homes_only_rejects_disabled_empty_and_missing_homes(self): + canonical=self.configure_upgrade() + original=canonical.read_text() + package=self.build_package() + for settings in ['ENABLE_MAIL_HOME_DIRS=false', 'ENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=()', + f'ENABLE_MAIL_HOME_DIRS=true\nMAIL_HOME_DIRS=("{self.src}/missing-home")']: + with self.subTest(settings=settings): + canonical.write_text(original+'\n'+settings+'\n') + before=canonical.read_bytes() + result=subprocess.run(['bash',str(package),'--foreground','--mail-homes-only'],env=self.env,capture_output=True,text=True,timeout=60) + self.assertNotEqual(result.returncode,0,result.stdout+result.stderr) + self.assertEqual(canonical.read_bytes(),before) + self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active') + self.assertNotIn('systemctl stop',self.events.read_text()) + def test_package_failed_backup_keeps_timer_stopped_and_rollback_available(self): canonical=self.configure_upgrade() original=canonical.read_bytes() diff --git a/tools/build-update-package.py b/tools/build-update-package.py index 6c595b7..5637e64 100644 --- a/tools/build-update-package.py +++ b/tools/build-update-package.py @@ -46,7 +46,7 @@ def build(output, version): set -Eeuo pipefail umask 077 if [[ ${{1:-}} == --help ]]; then - echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--install-deps]' + echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--mail-dir DIR | --mail-homes-only] [--install-deps]' exit 0 fi [[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }} diff --git a/update.sh b/update.sh index d61c7a2..d957fa9 100755 --- a/update.sh +++ b/update.sh @@ -15,6 +15,8 @@ UP_INSTALL_DEPS=false UP_GITEA='' UP_RCLONE='' UP_CRON='' +UP_MAIL_DIR='' +UP_MAIL_HOMES=false UP_STATE='' UP_WORKER=false while (( $# )); do @@ -26,11 +28,14 @@ while (( $# )); do --no-gitea) UP_GITEA=false; shift ;; --rclone-config) UP_RCLONE=${2:?Missing rclone configuration}; shift 2 ;; --cron-services) UP_CRON=${2:?Missing comma-separated systemd units}; shift 2 ;; + --mail-dir) UP_MAIL_DIR=${2:?Missing mailbox directory}; shift 2 ;; + --mail-homes-only) UP_MAIL_HOMES=true; shift ;; --worker) UP_WORKER=true; UP_STATE=${2:?Missing state}; shift 2 ;; - --help) echo 'Usage: sudo bash update.sh [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--install-deps]'; exit 0 ;; + --help) echo 'Usage: sudo bash update.sh [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--mail-dir DIR | --mail-homes-only] [--install-deps]'; exit 0 ;; *) die "Unknown argument: $1" ;; esac done +[[ -z $UP_MAIL_DIR || $UP_MAIL_HOMES == false ]] || die 'Choose --mail-dir OR --mail-homes-only' [[ $EUID == 0 ]] || die 'Run with sudo bash' need systemctl; need flock # This lock excludes two coordinators, independently from ordinary backup jobs. @@ -46,6 +51,51 @@ prompt() { append_setting() { printf '%s=%q\n' "$1" "$2" >> "$UP_STATE/candidate.conf"; } discover() { python3 "$LIB_DIR/update-discover.py" "$@"; } +# Do not infer mailbox coverage from an existing directory. In particular, +# creating an empty /var/vmail would make preflight pass without saving mail. +prepare_mail() { + local choice home + if [[ $ENABLE_MAIL != true ]]; then + [[ -z $UP_MAIL_DIR && $UP_MAIL_HOMES == false ]] || die 'Mail options require ENABLE_MAIL=true' + return 0 + fi + if [[ -n $UP_MAIL_DIR ]]; then + MAIL_DIR=$UP_MAIL_DIR + ENABLE_MAIL_STORE=true + elif [[ $UP_MAIL_HOMES == true ]]; then + ENABLE_MAIL_STORE=false + elif [[ $ENABLE_MAIL_STORE == true && ! -d $MAIL_DIR ]]; then + echo "Konfiguriertes zentrales Mailverzeichnis fehlt: $MAIL_DIR" + printf 'Konfigurierte Mail-Homes: '; printf '%q ' "${MAIL_HOME_DIRS[@]}"; echo + echo 'Absoluten Pfad zur tatsaechlichen zentralen Mailablage angeben.' + echo 'Nur wenn ALLE Mailboxen in den konfigurierten Homes liegen: homes eingeben.' + echo 'Bei unbekannter Ablage abbrechen; kein leeres Ersatzverzeichnis anlegen.' + [[ -t 0 ]] || die 'Missing mail source: supply --mail-dir DIR or explicitly confirm coverage with --mail-homes-only' + read -r -p 'Mailverzeichnis / homes / Enter zum Abbrechen: ' choice + [[ -n $choice ]] || die 'Mail storage unresolved; installed package/config/timer unchanged' + if [[ $choice == homes ]]; then + printf 'Home-Excludes: '; printf '%q ' "${MAIL_HOME_EXCLUDES[@]}"; echo + prompt 'Alle Mailboxen liegen in diesen Homes, ohne ausgeschlossene Pfade oder weitere Mounts? [j/N]' || die 'Mail coverage not confirmed' + ENABLE_MAIL_STORE=false + else MAIL_DIR=$choice; fi + fi + if [[ $ENABLE_MAIL_STORE == true ]]; then + [[ $MAIL_DIR == /* ]] || die 'MAIL_DIR must be absolute' + check_source_path "$MAIL_DIR" + append_setting MAIL_DIR "$MAIL_DIR" + echo "Zentrales Mailverzeichnis: $MAIL_DIR" + else + [[ $ENABLE_MAIL_HOME_DIRS == true && ${#MAIL_HOME_DIRS[@]} -gt 0 ]] || die 'Homes-only mail requires enabled, nonempty MAIL_HOME_DIRS' + for home in "${MAIL_HOME_DIRS[@]}"; do + [[ $home == /* ]] || die 'MAIL_HOME_DIRS must be absolute' + check_source_path "$home" + done + printf 'Mail ausschliesslich in Homes (Excludes und Mountgrenzen gelten): ' + printf '%q ' "${MAIL_HOME_DIRS[@]}"; echo + fi + append_setting ENABLE_MAIL_STORE "$ENABLE_MAIL_STORE" +} + if [[ $UP_WORKER == false ]]; then systemctl is-active --quiet app-backup-upgrade.service && die 'Upgrade service is already running' # Only install missing, small prerequisites when requested. Never upgrade PHP, @@ -87,6 +137,7 @@ if [[ $UP_WORKER == false ]]; then append_setting ENABLE_UPLOAD true append_setting ENABLE_NEXTCLOUD_MAINTENANCE true append_setting MAIL_BACKUP_MODE stop + prepare_mail if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then for pair in "ENABLE_HTTPD_STOP:$HTTPD_SERVICE_NAME" "ENABLE_PHPFPM_STOP:$PHPFPM_SERVICE_NAME"; do name=${pair%%:*}; unit=${pair#*:}