Database: use backup and restore apis (#29511)
This commit is contained in:
parent
758e89ff1a
commit
b76af6101d
6 changed files with 122 additions and 78 deletions
|
|
@ -1,9 +1,9 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
|
@ -340,95 +340,130 @@ func getBackup(authObject auth.Auth) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(db.FilePath)
|
||||
filename := "evcc-backup-" + time.Now().Format("2006-01-02--15-04") + ".db"
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "evcc-backup-*.db")
|
||||
if err != nil {
|
||||
http.Error(w, "Could not open DB file: "+err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, "Creating backup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpName := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
if err := db.Backup(r.Context(), tmpName); err != nil {
|
||||
http.Error(w, "Backup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(tmpName)
|
||||
if err != nil {
|
||||
http.Error(w, "Opening backup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
filename := "evcc-backup-" + time.Now().Format("2006-01-02--15-04") + ".db"
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
||||
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
http.Error(w, "Error streaming DB file: "+err.Error(), http.StatusInternalServerError)
|
||||
http.Error(w, "Streaming backup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createLocalDatabaseBackup creates a local backup in case of catastrophic error in reset or restore
|
||||
func createLocalDatabaseBackup() error {
|
||||
backupPath := db.FilePath + ".bak"
|
||||
|
||||
src, err := os.Open(db.FilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database file: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create backup file: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
// clean up partial backup on error
|
||||
os.Remove(backupPath)
|
||||
return fmt.Errorf("failed to copy database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func createLocalDatabaseBackup(ctx context.Context) error {
|
||||
return db.Backup(ctx, db.FilePath()+".bak")
|
||||
}
|
||||
|
||||
func restoreDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(32 << 20) // 32MB max memory
|
||||
// cap upload size to bound disk usage
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 256<<20)
|
||||
|
||||
mr, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !adminPasswordValid(authObject, r.FormValue("password")) {
|
||||
var (
|
||||
password string
|
||||
tmpName string
|
||||
)
|
||||
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Upload failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch part.FormName() {
|
||||
case "password":
|
||||
b, err := io.ReadAll(io.LimitReader(part, 1<<10))
|
||||
part.Close()
|
||||
if err != nil {
|
||||
http.Error(w, "Upload failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
password = string(b)
|
||||
|
||||
case "file":
|
||||
tmpFile, err := os.CreateTemp("", "evcc-restore-*.db")
|
||||
if err != nil {
|
||||
part.Close()
|
||||
http.Error(w, "Failed to create temp file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tmpName = tmpFile.Name()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
_, copyErr := io.Copy(tmpFile, part)
|
||||
closeErr := tmpFile.Close()
|
||||
part.Close()
|
||||
if copyErr != nil || closeErr != nil {
|
||||
http.Error(w, "Upload failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
part.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if !adminPasswordValid(authObject, password) {
|
||||
http.Error(w, "Invalid password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get uploaded file: "+err.Error(), http.StatusBadRequest)
|
||||
if tmpName == "" {
|
||||
http.Error(w, "Missing file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
settings.Persist()
|
||||
|
||||
// close db connection to avoid corruption
|
||||
if err := db.Close(); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// create local backup before overwriting
|
||||
if err := createLocalDatabaseBackup(); err != nil {
|
||||
http.Error(w, "Failed to create local backup: "+err.Error(), http.StatusInternalServerError)
|
||||
if err := createLocalDatabaseBackup(r.Context()); err != nil {
|
||||
http.Error(w, "Backup failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// overwrite DB file
|
||||
f, err := os.Create(db.FilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "Could not open DB file for writing: "+err.Error(), http.StatusInternalServerError)
|
||||
if err := db.Restore(r.Context(), tmpName); err != nil {
|
||||
http.Error(w, "Restore failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, file); err != nil {
|
||||
http.Error(w, "Failed to write DB file: "+err.Error(), http.StatusInternalServerError)
|
||||
// close DB so the WAL is checkpointed into the main file before shutdown
|
||||
// hooks (e.g. settings.Persist) can overwrite the restored content
|
||||
if err := db.Close(); err != nil {
|
||||
http.Error(w, "DB close failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -457,7 +492,7 @@ func resetDatabase(authObject auth.Auth, shutdown func()) http.HandlerFunc {
|
|||
|
||||
settings.Persist()
|
||||
|
||||
if err := createLocalDatabaseBackup(); err != nil {
|
||||
if err := createLocalDatabaseBackup(r.Context()); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue