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:
mrleo1nidandClaude Opus 5.5 committed 2026-10-06 21:52:40 +03:00
commit 1d44d067e9
53 files changed
+7278

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
package archive
import (
"path"
"strings"
)
// Matcher проверяет абсолютные пути (со слешами) по списку исключений.
//
// - шаблон без glob-символов — префикс: "/opt/x/pgdata" исключает и сам каталог, и всё внутри;
// - шаблон с "/" и glob-символами — сопоставляется с полным путём ("/etc/x-ui/x-ui.db*");
// - шаблон без "/" — с именем файла ("*.swp").
type Matcher struct {
patterns []string
}
func NewMatcher(patterns ...[]string) *Matcher {
m := &Matcher{}
for _, ps := range patterns {
for _, p := range ps {
p = strings.TrimSpace(p)
if p == "" {
continue
}
if p != "/" {
p = strings.TrimSuffix(p, "/")
}
m.patterns = append(m.patterns, p)
}
}
return m
}
func (m *Matcher) Add(p ...string) {
m.patterns = append(m.patterns, NewMatcher(p).patterns...)
}
func (m *Matcher) Match(abs string) bool {
if m == nil {
return false
}
for _, p := range m.patterns {
if !strings.ContainsAny(p, "*?[") {
if abs == p || strings.HasPrefix(abs, p+"/") {
return true
}
continue
}
if strings.Contains(p, "/") {
if ok, _ := path.Match(p, abs); ok {
return true
}
continue
}
if ok, _ := path.Match(p, path.Base(abs)); ok {
return true
}
}
return false
}
// Covers сообщает, попадает ли abs под один из путей (точное совпадение или вложенность).
func Covers(paths []string, abs string) bool {
for _, p := range paths {
p = strings.TrimSuffix(p, "/")
if abs == p || strings.HasPrefix(abs, p+"/") {
return true
}
}
return false
}