6aecac1cc8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
180 lines
6.8 KiB
Go
180 lines
6.8 KiB
Go
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))))
|
||
})
|
||
}
|