Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/backup"
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/i18n"
|
||||
)
|
||||
|
||||
func (a *Admin) handleSettingsExport(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="volumen-backup.tar.gz"`)
|
||||
if r.Method == http.MethodHead {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
// The archive streams straight to the client: the app routes it past
|
||||
// the buffering wrappers, so no copy of it waits in memory. An error
|
||||
// before the first byte still answers as a plain 500; after it, the
|
||||
// download ends truncated and the gzip footer makes that visible.
|
||||
sent := false
|
||||
if err := backup.Write(writeTracker{w, &sent}, a.deps.Backup); err != nil {
|
||||
slog.Error("admin: backup export failed", "error", err)
|
||||
if !sent {
|
||||
w.Header().Del("Content-Type")
|
||||
w.Header().Del("Content-Disposition")
|
||||
http.Error(w, "The backup could not be written: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeTracker records whether anything reached the client, so a failure
|
||||
// can still choose between a clean error page and a logged truncation.
|
||||
type writeTracker struct {
|
||||
w http.ResponseWriter
|
||||
sent *bool
|
||||
}
|
||||
|
||||
func (t writeTracker) Write(p []byte) (int, error) {
|
||||
*t.sent = true
|
||||
return t.w.Write(p)
|
||||
}
|
||||
|
||||
func (a *Admin) handleSettingsImport(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.requireCSRF(w, r) {
|
||||
return
|
||||
}
|
||||
file, _, err := r.FormFile("backup")
|
||||
if err != nil {
|
||||
a.renderSettings(w, r, a.tr(r, "No backup file selected."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
written, err := backup.Restore(file, a.deps.Backup)
|
||||
if written > 0 {
|
||||
// A partial restore changed files on disk; the caches must drop
|
||||
// even when a later entry failed, or the admin keeps serving the
|
||||
// pre-import state until an unrelated write invalidates them.
|
||||
a.deps.Store.InvalidateCache()
|
||||
a.deps.Users.Invalidate()
|
||||
a.deps.Templates.Invalidate()
|
||||
a.deps.Tokens.Invalidate()
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("admin: backup import failed", "error", err)
|
||||
a.renderSettings(w, r, a.trf(r, "Could not restore backup: %s", err.Error()), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if written == 0 {
|
||||
a.renderSettings(w, r, a.tr(r, "The archive holds no files this deployment recognises."), "", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
a.record(r, "backup.imported", fmt.Sprintf("%d files", written), nil)
|
||||
a.renderSettings(w, r, "", i18n.Admin.N(a.langFor(r), "backup.files", written), http.StatusOK)
|
||||
}
|
||||
|
||||
// --- updates ----------------------------------------------------------------
|
||||
Reference in New Issue
Block a user