Files
mrleo1nidandClaude Opus 5.5 1d44d067e9 backup-master: сервер бэкапов, агент и восстановление VPS
Один бинарник 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>
2026-10-06 21:52:40 +03:00

88 lines
2.7 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"gitea.hsrv.site/mrleo1nid/backup-master/internal/archive"
)
// Каталоги, скрипты из которых ставятся пакетами и в бэкапе не нужны.
var systemBinDirs = []string{"/usr/bin", "/usr/sbin", "/bin", "/sbin", "/usr/lib", "/usr/share", "/lib", "/etc/cron.daily",
"/etc/cron.hourly", "/etc/cron.weekly", "/etc/cron.monthly", "/usr/libexec"}
// checkCron ищет в crontab упоминания скриптов, не покрытых ни одной категорией.
func (c *collector) checkCron() {
var files []string
for _, pattern := range []string{"/var/spool/cron/crontabs/*", "/etc/cron.d/*", "/etc/crontab"} {
m, _ := filepath.Glob(pattern)
files = append(files, m...)
}
var covered []string
for _, mc := range c.plan.Manifest.Categories {
for _, p := range mc.Paths {
covered = append(covered, filepath.ToSlash(p))
}
}
reported := map[string]bool{}
for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
continue
}
for _, p := range cronPaths(string(data)) {
if reported[p] || archive.Covers(covered, p) || archive.Covers(systemBinDirs, p) {
continue
}
fi, err := os.Stat(p)
if err != nil || !fi.Mode().IsRegular() {
continue
}
reported[p] = true
c.warn("cron (%s) запускает %s, но он не попадает в бэкап — добавьте путь в категорию", f, p)
}
}
}
// cronPaths извлекает абсолютные пути из команд crontab, кроме целей перенаправления вывода.
func cronPaths(text string) []string {
var out []string
for _, line := range strings.Split(text, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) > 0 && strings.Contains(fields[0], "=") && !strings.HasPrefix(fields[0], "/") {
// присваивание переменной (SHELL=/bin/bash) в отдельной строке
if len(fields) == 1 {
continue
}
}
redirect := false
for _, tok := range fields {
if strings.HasSuffix(tok, ">") || strings.HasSuffix(tok, "<") {
redirect = true
continue
}
if redirect {
redirect = false
continue
}
// "2>/dev/null", ">>file" — перенаправление слитно с целью
if i := strings.IndexAny(tok, "<>"); i >= 0 {
continue
}
if _, v, ok := strings.Cut(tok, "="); ok {
tok = v
}
tok = strings.Trim(tok, `"';&|()`)
if strings.HasPrefix(tok, "/") && !strings.ContainsAny(tok, "*$") {
out = append(out, tok)
}
}
}
return out
}