From d59ce5ff3a84b60e2a5e3606df4e66412775baf9 Mon Sep 17 00:00:00 2001 From: notplants <@notplants> Date: Mon, 2 Nov 2026 18:19:56 +0000 Subject: [PATCH] fix: restore Mailu SQLite database in place --- abra.sh | 1 + compose.yml | 9 ++++++++- restore_sqlite.py | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 restore_sqlite.py diff --git a/abra.sh b/abra.sh index ebf8add..c095712 100644 --- a/abra.sh +++ b/abra.sh @@ -2,3 +2,4 @@ export CERTDUMPER_POST_VERSION=v1 export POSTFIX_OVERRIDE_VERSION=v17 export SENDER_LOGIN_VERSIONS=v2 export BACKUP_SQLITE_VERSION=v1 +export RESTORE_SQLITE_VERSION=v1 diff --git a/compose.yml b/compose.yml index 61e443c..3ccbd2f 100644 --- a/compose.yml +++ b/compose.yml @@ -96,7 +96,7 @@ services: - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - - "coop-cloud.${STACK_NAME}.version=4.0.0+2024.06.67" + - "coop-cloud.${STACK_NAME}.version=4.0.1+2024.06.67" - "backupbot.backup=${ENABLE_BACKUPS:-true}" - "backupbot.backup.volumes.traefik_letsencrypt=false" - "backupbot.backup.volumes.mailu.path=backup" @@ -122,9 +122,13 @@ services: - source: backup_sqlite target: /usr/local/bin/backup_sqlite.py mode: 0444 + - source: restore_sqlite + target: /usr/local/bin/restore_sqlite.py + mode: 0444 deploy: labels: backupbot.backup.pre-hook: "python3 /usr/local/bin/backup_sqlite.py" + backupbot.restore.post-hook: "python3 /usr/local/bin/restore_sqlite.py" imap: image: ghcr.io/mailu/dovecot:2024.06.67 @@ -234,6 +238,9 @@ configs: backup_sqlite: name: ${STACK_NAME}_backup_sqlite_${BACKUP_SQLITE_VERSION} file: backup_sqlite.py + restore_sqlite: + name: ${STACK_NAME}_restore_sqlite_${RESTORE_SQLITE_VERSION} + file: restore_sqlite.py secrets: diff --git a/restore_sqlite.py b/restore_sqlite.py new file mode 100644 index 0000000..51791cd --- /dev/null +++ b/restore_sqlite.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Restore Mailu's SQLite snapshot without replacing the live database inode.""" + +import sqlite3 +from pathlib import Path + +backup_path = Path("/data/backup/main.db") +live_path = Path("/data/main.db") + +# The source and destination must already exist. A missing snapshot or live +# database is a restore failure, not a reason to create an empty database. +with sqlite3.connect( + f"{backup_path.as_uri()}?mode=ro", uri=True, timeout=60 +) as source: + if source.execute("PRAGMA integrity_check").fetchall() != [("ok",)]: + raise RuntimeError(f"Invalid SQLite snapshot: {backup_path}") + with sqlite3.connect( + f"{live_path.as_uri()}?mode=rw", uri=True, timeout=60 + ) as destination: + source.backup(destination, pages=256, sleep=0.1) + +print(f"SQLite restore complete: {live_path}", flush=True)