Support home-based mailboxes during automated upgrades

This commit is contained in:
admin committed 2026-09-10 14:31:32 +02:00
1 parent 8e6c9a058f
commit 56fbcdad74
8 files changed
+194 -9

No files matched your search

+14 -2
View File
@@ -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 | | `nextcloud-data` | `NC_DATA_DIR`, sofern aktiviert |
| `gitea`, `gitea-etc` | `GITEA_DATA_DIR`, `GITEA_ETC_DIR` | | `gitea`, `gitea-etc` | `GITEA_DATA_DIR`, `GITEA_ETC_DIR` |
| `gitea-repos`, `gitea-custom` | Optionale explizite externe Repository-/Custom-Pfade | | `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 | | `mail-home-0`, `mail-home-1`, … | Reihenfolge von `MAIL_HOME_DIRS`, mit Excludes und Dateisystemgrenze |
| `extra-0`, `extra-1`, … | Reihenfolge von `EXTRA_PATHS` | | `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. `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 ### 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. `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. 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: 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. - Shell-Syntax und ShellCheck der Paketdateien.
- MyISAM-Dump und tatsächlicher SQL-Import samt Dateiwiederherstellung. - MyISAM-Dump und tatsächlicher SQL-Import samt Dateiwiederherstellung.
- gzip/zstd, korrekte Excludes, vorhandener Wartungsmodus und inaktive Dienste. - gzip/zstd, korrekte Excludes, vorhandener Wartungsmodus und inaktive Dienste.
+3
View File
@@ -89,6 +89,9 @@ RCLONE_REMOTE_BASE="OneDriveCrypt:gemini" # vorhandenes Crypt-Ziel beibehalten!
RETENTION_KEEP_LAST=3 RETENTION_KEEP_LAST=3
ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus ENABLE_REMOTE_RETENTION="false" # während der Migration zunächst aus
MAIL_BACKUP_MODE="stop" 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: # Tatsächliche Timer-/Service-Namen einsetzen; Timer vor seinem Service nennen:
QUIESCE_SERVICES=() QUIESCE_SERVICES=()
# Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert: # Gitea war in der bisherigen mitgelieferten Konfiguration NICHT aktiviert:
+41 -1
View File
@@ -55,6 +55,19 @@ für Tests/Diagnose gedacht; dabei besteht keine Absicherung gegen SSH-Abbruch.
## Gezielte Rückfragen und Grenzen ## 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` - **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; angeben. Eine user-eigene Datei wird in eine neue root-eigene Datei kopiert;
die vom Benutzer verwendete Originaldatei bleibt unverändert. 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 ## 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. Bei einem Fehler nach Beginn der Installation bleibt der Backup-Timer gestoppt.
Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder Es erfolgt kein automatisches Zurücksetzen über möglicherweise noch laufende oder
teilweise wiederhergestellte Anwendungen. Logs und gegebenenfalls 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 ## 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; Die Tests enthalten echte MariaDB-Dumps/Imports und lokale rclone-Crypt-Roundtrips;
systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche systemd/occ und die systemd-run-Übergabe sind simuliert. Die tatsächliche
Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore Server-/OneDrive-Prüfung erfolgt beim ersten Paketlauf. Ein fachlicher Restore
+3
View File
@@ -82,6 +82,9 @@ GITEA_DB_CNF="/etc/app-backup/db-gitea.cnf"
ENABLE_MAIL="true" ENABLE_MAIL="true"
MAIL_BACKUP_MODE="stop" 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" MAIL_DIR="/var/vmail"
POSTFIX_DIR="/etc/postfix" POSTFIX_DIR="/etc/postfix"
DOVECOT_DIR="/etc/dovecot" DOVECOT_DIR="/etc/dovecot"
+10 -4
View File
@@ -41,7 +41,7 @@ load_config() {
: "${GITEA_DATA_DIR:=/var/lib/gitea/data}" "${GITEA_ETC_DIR:=/etc/gitea}" : "${GITEA_DATA_DIR:=/var/lib/gitea/data}" "${GITEA_ETC_DIR:=/etc/gitea}"
: "${ENABLE_HTTPD_STOP:=true}" "${HTTPD_SERVICE_NAME:=httpd}" : "${ENABLE_HTTPD_STOP:=true}" "${HTTPD_SERVICE_NAME:=httpd}"
: "${ENABLE_PHPFPM_STOP:=true}" "${PHPFPM_SERVICE_NAME:=php-fpm}" : "${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}" : "${MAIL_DIR:=/var/vmail}" "${POSTFIX_DIR:=/etc/postfix}" "${DOVECOT_DIR:=/etc/dovecot}"
: "${RESTORE_STRICT_DELETE:=false}" : "${RESTORE_STRICT_DELETE:=false}"
# Preserve old space-separated lists; arrays additionally support spaces in paths. # 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]}" read -r -a MAIL_HOME_EXCLUDES <<< "${MAIL_HOME_EXCLUDES[0]}"
fi fi
for v in ENABLE_UPLOAD ENABLE_REMOTE_RETENTION ENABLE_MAIL_REPORT ENABLE_WORDPRESS ENABLE_NEXTCLOUD \ 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 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" [[ ${!v} == true || ${!v} == false ]] || die "$v must be true or false"
done done
@@ -67,6 +67,10 @@ load_config() {
[[ $ARCHIVE_PREFIX =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]] || die "Unsafe ARCHIVE_PREFIX" [[ $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" [[ $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" [[ $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_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 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_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_GITEA == true ]]; then sources+=("$GITEA_DATA_DIR" "$GITEA_ETC_DIR" "${GITEA_REPOS_DIR:-}" "${GITEA_CUSTOM_DIR:-}"); fi
if [[ $ENABLE_MAIL == true ]]; then 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 if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then sources+=("${MAIL_HOME_DIRS[@]}"); fi
fi fi
# Validate the WHOLE plan before mkdir/chmod touches any configured directory. # Validate the WHOLE plan before mkdir/chmod touches any configured directory.
@@ -149,7 +154,8 @@ build_plan() {
add_db gitea GITEA add_db gitea GITEA
fi fi
if [[ $ENABLE_MAIL == true ]]; then 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 if [[ $ENABLE_MAIL_HOME_DIRS == true ]]; then
local i=0 local i=0
for p in "${MAIL_HOME_DIRS[@]}"; do add_source "mail-home-$i" "$p"; i=$((i+1)); done for p in "${MAIL_HOME_DIRS[@]}"; do add_source "mail-home-$i" "$p"; i=$((i+1)); done
+70
View File
@@ -440,6 +440,76 @@ QUIESCE_SERVICES=(nc-cron)
self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active') self.assertEqual(json.loads(self.state.read_text())['services']['app-backup.timer'],'active')
self.assertNotIn('systemctl stop',self.events.read_text()) 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): def test_package_failed_backup_keeps_timer_stopped_and_rollback_available(self):
canonical=self.configure_upgrade() canonical=self.configure_upgrade()
original=canonical.read_bytes() original=canonical.read_bytes()
+1 -1
View File
@@ -46,7 +46,7 @@ def build(output, version):
set -Eeuo pipefail set -Eeuo pipefail
umask 077 umask 077
if [[ ${{1:-}} == --help ]]; then 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 exit 0
fi fi
[[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }} [[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }}
+52 -1
View File
@@ -15,6 +15,8 @@ UP_INSTALL_DEPS=false
UP_GITEA='' UP_GITEA=''
UP_RCLONE='' UP_RCLONE=''
UP_CRON='' UP_CRON=''
UP_MAIL_DIR=''
UP_MAIL_HOMES=false
UP_STATE='' UP_STATE=''
UP_WORKER=false UP_WORKER=false
while (( $# )); do while (( $# )); do
@@ -26,11 +28,14 @@ while (( $# )); do
--no-gitea) UP_GITEA=false; shift ;; --no-gitea) UP_GITEA=false; shift ;;
--rclone-config) UP_RCLONE=${2:?Missing rclone configuration}; shift 2 ;; --rclone-config) UP_RCLONE=${2:?Missing rclone configuration}; shift 2 ;;
--cron-services) UP_CRON=${2:?Missing comma-separated systemd units}; 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 ;; --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" ;; *) die "Unknown argument: $1" ;;
esac esac
done done
[[ -z $UP_MAIL_DIR || $UP_MAIL_HOMES == false ]] || die 'Choose --mail-dir OR --mail-homes-only'
[[ $EUID == 0 ]] || die 'Run with sudo bash' [[ $EUID == 0 ]] || die 'Run with sudo bash'
need systemctl; need flock need systemctl; need flock
# This lock excludes two coordinators, independently from ordinary backup jobs. # 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"; } append_setting() { printf '%s=%q\n' "$1" "$2" >> "$UP_STATE/candidate.conf"; }
discover() { python3 "$LIB_DIR/update-discover.py" "$@"; } 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 if [[ $UP_WORKER == false ]]; then
systemctl is-active --quiet app-backup-upgrade.service && die 'Upgrade service is already running' systemctl is-active --quiet app-backup-upgrade.service && die 'Upgrade service is already running'
# Only install missing, small prerequisites when requested. Never upgrade PHP, # 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_UPLOAD true
append_setting ENABLE_NEXTCLOUD_MAINTENANCE true append_setting ENABLE_NEXTCLOUD_MAINTENANCE true
append_setting MAIL_BACKUP_MODE stop append_setting MAIL_BACKUP_MODE stop
prepare_mail
if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then
for pair in "ENABLE_HTTPD_STOP:$HTTPD_SERVICE_NAME" "ENABLE_PHPFPM_STOP:$PHPFPM_SERVICE_NAME"; do for pair in "ENABLE_HTTPD_STOP:$HTTPD_SERVICE_NAME" "ENABLE_PHPFPM_STOP:$PHPFPM_SERVICE_NAME"; do
name=${pair%%:*}; unit=${pair#*:} name=${pair%%:*}; unit=${pair#*:}