Полнота бэкапа: SSH, весь UFW, свои юниты, конфиги пакетов, системные параметры
CI / test (push) Successful in 1m27s
Release / release (push) Successful in 1m26s

Агент:
- ssh: sshd_config(.d), ключи хоста, authorized_keys root и пользователей
- system: весь /etc/ufw и /etc/default/ufw, /etc/sysctl.conf, /etc/hosts,
  свои файлы в /etc/cron.d (пакетные отсекаются через dpkg -S)
- certs: /etc/letsencrypt, /root/.acme.sh; xui: сертификаты по путям из базы
- docker: daemon.json и логины в registry; telegraf: /etc/default/telegraf
- caddy: бинарник бэкапится, если Caddy собран с плагинами
- units: свои systemd-юниты с состоянием enabled/active и drop-in *.service.d
- etc: изменённые конфиги любых пакетов (dpkg conffiles) и свои файлы в /etc,
  кроме привязанного к железу и облаку
- в манифесте: часовой пояс, swap, вручную поставленные пакеты; версия
  apt-пакета в строке категории
- предупреждения: программы юнитов вне бэкапа, каталоги /opt и /usr/local/bin
  без категории, контейнеры без compose

Restore:
- часовой пояс, swap-файл, доустановка вручную поставленных пакетов
- свои юниты включаются и запускаются как на старом сервере

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-17 15:19:34 +03:00
1 parent cbd3cde01d
commit 1947b26913
14 files changed
+747 -45

No files matched your search

+57
View File
@@ -5,9 +5,12 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"gitea.hsrv.site/mrleo1nid/backup-master/internal/archive"
"gitea.hsrv.site/mrleo1nid/backup-master/internal/manifest"
"gitea.hsrv.site/mrleo1nid/backup-master/internal/sysx"
_ "modernc.org/sqlite"
)
@@ -32,9 +35,63 @@ func (c *collector) snapshotXUI(mc *manifest.Category) error {
}
mc.Dumps = append(mc.Dumps, manifest.Dump{Name: "x-ui.db", Kind: "sqlite", Target: xuiDB})
c.add(manifest.MainPart, dumpItem(mc.Name, out, "x-ui.db"))
// сертификаты панели, подписки и инбаундов могут лежать где угодно — берём пути из базы
for _, p := range xuiCertPaths(out) {
if !sysx.Exists(p) {
c.warn("xui: в настройках указан %s, но файла нет", p)
continue
}
if archive.Covers(mc.Paths, filepath.ToSlash(p)) {
continue
}
mc.Paths = append(mc.Paths, p)
c.add(manifest.MainPart, filesItem(mc.Name, p, nil))
}
return nil
}
var certFileRe = regexp.MustCompile(`"(?:certificateFile|keyFile)"\s*:\s*"([^"]+)"`)
// xuiCertPaths читает из снимка базы 3x-ui пути к сертификатам и ключам.
func xuiCertPaths(dbPath string) []string {
db, err := sql.Open("sqlite", "file:"+dbPath+"?mode=ro")
if err != nil {
return nil
}
defer db.Close()
seen := map[string]bool{}
var out []string
addPath := func(p string) {
p = strings.TrimSpace(p)
if strings.HasPrefix(p, "/") && !seen[p] {
seen[p] = true
out = append(out, p)
}
}
if rows, err := db.Query(`SELECT value FROM settings WHERE key IN ('webCertFile','webKeyFile','subCertFile','subKeyFile')`); err == nil {
for rows.Next() {
var v string
if rows.Scan(&v) == nil {
addPath(v)
}
}
rows.Close()
}
if rows, err := db.Query(`SELECT stream_settings FROM inbounds`); err == nil {
for rows.Next() {
var v string
if rows.Scan(&v) == nil {
for _, m := range certFileRe.FindAllStringSubmatch(v, -1) {
addPath(m[1])
}
}
}
rows.Close()
}
return out
}
func vacuumInto(src, dst string) error {
_ = os.Remove(dst)
db, err := sql.Open("sqlite", "file:"+src+"?_pragma=busy_timeout(10000)")