backup: the panel's full backup works, without the queue
Found by the e2e restore scenario of the next commit: the Full backup button answered 200 with ten bytes. The archive includes the Postfix queue, Postfix keeps every queue file and directory readable by its own user alone (not configurable), and the panel runs as its own user - the walk stopped at /data/postfix/queue/active with "permission denied" after the headers were out. So since the queue moved under /data in 1.3.0. Confirmed on the test server: selfpost-backup as root makes the whole archive, the same command as user panel fails the same way. Owner, 2026-10-11: the panel's archive comes without the queue. - backup.Params.Unprivileged: the queue is left out, and so is a file under certs/ that cannot be opened (the private key on the test server is root and Postfix's group only), named through OnWarn. The selfpost-backup command leaves it unset and takes both, as before. - backup.Check reads through everything Create would archive, writing nothing. The panel runs it before the first byte: a failure is a message on the page, not a download cut short. - The box, Help (design commit before this one) and the guide say what the panel's archive leaves out; the guide's comparison table has rows for the queue and the private key, and tells to put the key back before starting a restored instance. go vet ./internal/backup/ ./internal/web/handlers/: ok go test ./internal/backup/ ./cmd/... ./internal/web/...: ok make e2e on the test server, with the restore scenario: ok Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,11 +70,33 @@ func (h *Handlers) HandleBackup(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderBackupPageWith(w, r, http.StatusBadRequest, "", pwErr)
|
||||
return
|
||||
}
|
||||
// The panel is not root: its archive leaves out the Postfix queue and a
|
||||
// certificate file it may not open (backup.Params.Unprivileged).
|
||||
params := backup.Params{
|
||||
DataDir: h.cfg.DataDir,
|
||||
DBPath: h.cfg.DBPath,
|
||||
Version: h.cfg.Version,
|
||||
DeployRoot: h.cfg.DeployRoot,
|
||||
Unprivileged: true,
|
||||
OnWarn: func(msg string) {
|
||||
logf("panel: full backup: %s", msg)
|
||||
},
|
||||
}
|
||||
if err := backup.ValidateDeployRoot(h.cfg.DeployRoot); err != nil {
|
||||
logf("panel: full backup: %v", err)
|
||||
h.renderBackupPageWith(w, r, http.StatusBadRequest, "", deployBackupErr(err))
|
||||
return
|
||||
}
|
||||
// Everything is read through once before the first byte goes out. Past the
|
||||
// headers a failure can only cut the download short - which is what the
|
||||
// button did from 1.3.0 to 2.0.0, with a 200 and ten bytes, because the
|
||||
// queue was not the panel's to read.
|
||||
if err := backup.Check(params); err != nil {
|
||||
logf("panel: full backup: %v", err)
|
||||
h.renderBackupPageWith(w, r, http.StatusInternalServerError, "",
|
||||
"Could not read everything a backup holds. Nothing was downloaded; the reason is in the panel's log.")
|
||||
return
|
||||
}
|
||||
|
||||
stamp := time.Now().UTC().Format("20060102-150405")
|
||||
filename := fmt.Sprintf("selfpost-backup-%s.tar.gz", stamp)
|
||||
@@ -109,15 +131,7 @@ func (h *Handlers) HandleBackup(w http.ResponseWriter, r *http.Request) {
|
||||
sink = env
|
||||
}
|
||||
|
||||
if err := backup.Create(sink, backup.Params{
|
||||
DataDir: h.cfg.DataDir,
|
||||
DBPath: h.cfg.DBPath,
|
||||
Version: h.cfg.Version,
|
||||
DeployRoot: h.cfg.DeployRoot,
|
||||
OnWarn: func(msg string) {
|
||||
logf("panel: full backup: %s", msg)
|
||||
},
|
||||
}); err != nil {
|
||||
if err := backup.Create(sink, params); err != nil {
|
||||
logf("panel: full backup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -188,9 +188,10 @@ var inboundTopics = []HelpTopic{
|
||||
|
||||
var serverTopics = []HelpTopic{
|
||||
{ID: HelpBackup, Title: "Backup and moving a domain", Body: []Text{
|
||||
Rich("Full backup downloads one archive of the whole instance: ", Code("data/"), " (the database, DKIM keys, application credentials and the queue), ",
|
||||
Rich("Full backup downloads one archive of the whole instance: ", Code("data/"), " (the database, DKIM keys and application credentials), ",
|
||||
Code("docker-compose.yml"), ", ", Code(".env"), " and ", Code("certs/"),
|
||||
". It holds secrets, so leave Encrypt with a password ticked: the file is sealed as ", Code(".spbk"),
|
||||
". The panel cannot read the Postfix queue, nor usually the private key in ", Code("certs/"),
|
||||
", and leaves them out; ", Code("selfpost-backup"), " run in the container takes both. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as ", Code(".spbk"),
|
||||
", the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a ",
|
||||
Code(".spbk"), " first with ", Code("selfpost-backup -decrypt"),
|
||||
") and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and ",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="column is-6">
|
||||
{{template "box_open" .Full}}
|
||||
{{template "body_open"}}
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials, the queue), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive, without the Postfix queue and without a certificate file the panel may not read. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<form class="sp-form" method="post" action="{{.FullAction}}">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" value="1"{{if .Encrypt}} checked{{end}} data-encrypt-toggle> Encrypt with a password (<code>.spbk</code>)</label></div>
|
||||
<div data-encrypt-fields>
|
||||
|
||||
Reference in New Issue
Block a user