Files
Bridge-and-Join-s/internal/lkgateway/backup.go
T

180 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// Резервное копирование сервера bj (05.10.2026): базы PostgreSQL bj и igate, данные bj и шлюза НРД — на
// сетевое хранилище (SMB) по расписанию; восстановление — в мастере настройки (переустановка сервера).
// Делает root: таймер bj-backup.timer → /usr/local/sbin/bj-backup; настройка, проверка, список и
// восстановление — /usr/local/sbin/bj-backup-admin через root-демон bj-keymedia. Пароль хранилища bj
// не хранит: он передаётся root-скрипту и лежит в /etc/bj/backup-smb.cred (только root).
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// BackupSettings — куда и как часто копировать (без пароля).
type BackupSettings struct {
Enabled bool `json:"enabled"`
Share string `json:"share,omitempty"` // //10.10.10.11/Backup
Dir string `json:"dir,omitempty"` // BJ_Backup/bj
User string `json:"user,omitempty"`
PasswordSet bool `json:"password_set,omitempty"`
EveryHours int `json:"every_hours,omitempty"`
KeepDays int `json:"keep_days,omitempty"`
SavedAt time.Time `json:"saved_at,omitempty"`
}
func (b BackupSettings) every() int {
switch b.EveryHours {
case 1, 2, 3, 4, 6, 8, 12, 24:
return b.EveryHours
}
return 6
}
func (b BackupSettings) keep() int {
if b.KeepDays >= 1 {
return b.KeepDays
}
return 14
}
func (b BackupSettings) dir() string { return pickStr(strings.TrimSpace(b.Dir), "BJ_Backup/bj") }
// BackupStatus — итог последней копии (пишет root-скрипт).
type BackupStatus struct {
At string `json:"at"`
OK bool `json:"ok"`
Dir string `json:"dir"`
Msg string `json:"msg"`
}
const (
backupStatusFile = "/var/lib/bj/backup-status.json"
restoreStatusFile = "/var/lib/bj/restore-status.json"
)
func backupStatus() *BackupStatus {
raw, err := os.ReadFile(backupStatusFile)
if err != nil {
return nil
}
var s BackupStatus
if json.Unmarshal(raw, &s) != nil {
return nil
}
return &s
}
// backupOp — команда root-скрипту bj-backup-admin (config|test|run|list|verify|restore) с параметрами.
func backupOp(ctx context.Context, op string, params map[string]string) (map[string]any, error) {
extra, _ := json.Marshal(params)
return runKeymediaOp(ctx, "backup-"+op, map[string]string{"extra": string(extra)})
}
// backupParams — хранилище из формы (share/dir/user/password); пустое — из настроек (root берёт свои).
func backupParams(r *http.Request) map[string]string {
p := map[string]string{}
for _, k := range []string{"share", "dir", "user", "password", "name"} {
if v := strings.TrimSpace(r.FormValue(k)); v != "" {
p[k] = v
}
}
return p
}
func jsonOut(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(v)
}
func opResult(res map[string]any, err error) map[string]any {
if err != nil {
return map[string]any{"ok": false, "error": err.Error()}
}
return res
}
func registerBackup(mux *http.ServeMux, rc *RuntimeConfig) {
// сохранить настройки (и передать root-скрипту: конфигурация, пароль, расписание таймера)
mux.HandleFunc("/admin/setup/backup", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
b := rc.Snapshot().Backup
b.Enabled = r.FormValue("enabled") != ""
b.Share = strings.TrimSpace(r.FormValue("share"))
b.Dir = strings.TrimSpace(r.FormValue("dir"))
b.User = strings.TrimSpace(r.FormValue("user"))
b.EveryHours, _ = strconv.Atoi(r.FormValue("every_hours"))
b.KeepDays, _ = strconv.Atoi(r.FormValue("keep_days"))
pass := r.FormValue("password")
if b.Share == "" {
setupFlash(w, r, "Резервное копирование: укажите хранилище, например //10.10.10.11/Backup")
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
res, err := backupOp(ctx, "config", map[string]string{"share": b.Share, "dir": b.dir(), "user": b.User, "password": pass,
"every_hours": strconv.Itoa(b.every()), "keep_days": strconv.Itoa(b.keep()), "enabled": strconv.FormatBool(b.Enabled)})
if err != nil {
setupFlash(w, r, "Резервное копирование: не сохранено — "+err.Error())
return
}
if pass != "" {
b.PasswordSet = true
}
b.SavedAt = time.Now()
_ = rc.UpdateBackup(b)
setupFlash(w, r, "Резервное копирование: "+fmt.Sprint(res["output"]))
})
mux.HandleFunc("/admin/setup/backup/test", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "test", backupParams(r))))
})
mux.HandleFunc("/admin/setup/backup/run", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "run", nil)))
})
mux.HandleFunc("/admin/setup/backup/list", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "list", backupParams(r))))
})
// проверка копии: скачать и сверить контрольные суммы (ничего не восстанавливает)
mux.HandleFunc("/admin/setup/backup/verify", func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "verify", backupParams(r))))
})
mux.HandleFunc("/admin/setup/backup/status", func(w http.ResponseWriter, r *http.Request) {
out := map[string]any{"backup": backupStatus()}
if raw, err := os.ReadFile(restoreStatusFile); err == nil {
var rs map[string]any
if json.Unmarshal(raw, &rs) == nil {
out["restore"] = rs
}
}
jsonOut(w, out)
})
// восстановление: останавливает bj и шлюз НРД, поэтому выполняется в фоне; страница опрашивает status
mux.HandleFunc("/admin/setup/backup/restore", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
jsonOut(w, opResult(backupOp(ctx, "restore", backupParams(r))))
})
}