Phase 9: full backup/restore + domain export/import (spec 7.5, 11.6)

Full server backup (spec 7.5.A): internal/backup produces a tar.gz of all of
/data — a consistent SQLite snapshot via VACUUM INTO, DKIM keys, sasldb2 and a
version manifest; TLS certs (tls/) and the Postfix queue are excluded. Two equal
paths: the panel button (POST /backup, no-store) and the selfpost-backup CLI via
docker exec (spec 11.6). CheckRestore runs before store.Open: a manifest version
mismatch refuses to boot with the image tag to use; a match consumes the
manifest so it only guards the first post-restore boot. Restore is not a
separate branch — Postfix/OpenDKIM regenerate from the restored SQLite as on any
start.

Domain export/import (spec 7.5.B): DomainExport carries the DKIM private key and
each application's working password. SASL secrets are read from sasldb2 via
db_dump (the userPassword property is plaintext) and, on import, re-keyed under
the local realm with saslpasswd2 — so credentials keep working on an instance
with a different hostname, with no DKIM DNS change. Import validates and rolls
back atomically on any failure. db-util (db_dump) is now an explicit image dep.

Verified on the server (selfpost:p9): gofmt/vet/test green; container e2e for
cross-realm domain export/import (SMTP AUTH 235 under the new realm), CLI and
panel backups, same-version restore, and version-mismatch refusal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 22:33:07 +03:00
parent 223f3cdc42
commit ae42450ec1
22 changed files with 1409 additions and 22 deletions
+11
View File
@@ -31,6 +31,12 @@ type Config struct {
// monitoring view (spec 7.2.13). It is the same path the log-tailer role
// follows in cmd/panel.
MailLogPath string
// DataDir and DBPath locate the persistent state a full backup archives
// (spec 7.5.A); Version is stamped into the backup manifest. They mirror the
// panel's own configuration.
DataDir string
DBPath string
Version string
}
// Server is the panel HTTP application.
@@ -102,17 +108,22 @@ func (s *Server) Handler() http.Handler {
authed := http.NewServeMux()
authed.HandleFunc("GET /{$}", s.handleDashboard)
authed.HandleFunc("POST /domains", s.handleAddDomain)
authed.HandleFunc("POST /domains/import", s.handleImportDomain)
authed.HandleFunc("GET /domains/{id}", s.handleDomainDetail)
authed.HandleFunc("GET /domains/{id}/delete", s.handleDeleteConfirm)
authed.HandleFunc("POST /domains/{id}/delete", s.handleDeleteDomain)
authed.HandleFunc("POST /domains/{id}/applications", s.handleAddApplication)
authed.HandleFunc("POST /domains/{id}/ratelimit", s.handleDomainRateLimit)
authed.HandleFunc("POST /domains/{id}/export", s.handleExportDomain)
authed.HandleFunc("POST /applications/{aid}/mode", s.handleUpdateAppMode)
authed.HandleFunc("POST /applications/{aid}/password", s.handleRegenPassword)
authed.HandleFunc("POST /applications/{aid}/ratelimit", s.handleAppRateLimit)
authed.HandleFunc("POST /applications/{aid}/delete", s.handleDeleteApplication)
authed.HandleFunc("POST /reload", s.handleReload)
// Full-server backup download (spec 7.5.A).
authed.HandleFunc("POST /backup", s.handleBackup)
// Monitoring screens (spec 7.2.11-13): each page and its HTMX polling
// fragment (spec 7.1 — the /rows and /body endpoints return HTML, not JSON).
authed.HandleFunc("GET /sendlog", s.handleSendLog)