Один бинарник 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>
72 lines
1.7 KiB
Go
72 lines
1.7 KiB
Go
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
|
||
}
|