Один бинарник backupctl: - server: API для агентов и restore, веб-морда (один админ), SQLite, ретеншн GFS, heavy-части, уведомления ntfy, раздача agent.sh/restore.sh; - backup/plan: сбор по каталогу категорий (files/systemd/xui/compose), горячие дампы Postgres/Mongo/MySQL, снимок x-ui.db, docker-тома, digest образов, проверка crontab, потоковая отправка tar.zst; - restore: провижининг на основе vps_installer с теми же версиями пакетов, recipes 3x-ui/nebula/sing-box нужных версий, восстановление compose с заливкой дампов и закреплением образов по digest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
48 lines
1.2 KiB
Go
48 lines
1.2 KiB
Go
package agent
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.hsrv.site/mrleo1nid/backup-master/internal/manifest"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const xuiDB = "/etc/x-ui/x-ui.db"
|
|
|
|
// snapshotXUI делает консистентную копию базы 3x-ui на горячую (VACUUM INTO),
|
|
// без логина в панель и без остановки сервиса.
|
|
func (c *collector) snapshotXUI(mc *manifest.Category) error {
|
|
if _, err := os.Stat(xuiDB); err != nil {
|
|
c.warn("xui: нет %s", xuiDB)
|
|
return nil
|
|
}
|
|
dir, err := c.spoolDir(mc.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := filepath.Join(dir, "x-ui.db")
|
|
if err := vacuumInto(xuiDB, out); err != nil {
|
|
c.warn("xui: снимок базы не удался: %v", err)
|
|
return nil
|
|
}
|
|
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"))
|
|
return nil
|
|
}
|
|
|
|
func vacuumInto(src, dst string) error {
|
|
_ = os.Remove(dst)
|
|
db, err := sql.Open("sqlite", "file:"+src+"?_pragma=busy_timeout(10000)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
_, err = db.Exec(fmt.Sprintf("VACUUM INTO '%s'", strings.ReplaceAll(dst, "'", "''")))
|
|
return err
|
|
}
|