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>
This commit is contained in:
commit
1d44d067e9
53 files changed
+7278
No files matched your search
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user