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,112 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gitea.hsrv.site/mrleo1nid/backup-master/internal/manifest"
|
||||
)
|
||||
|
||||
// SelectGFS возвращает индексы бэкапов, которые нужно сохранить.
|
||||
// times отсортированы от новых к старым. Последний бэкап сохраняется всегда,
|
||||
// дальше — самый новый в каждом из daily последних дней, weekly недель (ISO), monthly месяцев.
|
||||
func SelectGFS(times []time.Time, daily, weekly, monthly int) map[int]bool {
|
||||
keep := map[int]bool{}
|
||||
if len(times) == 0 {
|
||||
return keep
|
||||
}
|
||||
keep[0] = true
|
||||
bucket := func(n int, key func(time.Time) string) {
|
||||
seen := map[string]bool{}
|
||||
for i, t := range times {
|
||||
if len(seen) >= n {
|
||||
return
|
||||
}
|
||||
k := key(t)
|
||||
if !seen[k] {
|
||||
seen[k] = true
|
||||
keep[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
bucket(daily, func(t time.Time) string { return t.Format("2006-01-02") })
|
||||
bucket(weekly, func(t time.Time) string {
|
||||
y, w := t.ISOWeek()
|
||||
return fmt.Sprintf("%d-W%02d", y, w)
|
||||
})
|
||||
bucket(monthly, func(t time.Time) string { return t.Format("2006-01") })
|
||||
return keep
|
||||
}
|
||||
|
||||
// applyRetention удаляет лишние бэкапы клиента и старые heavy-части.
|
||||
func (s *Server) applyRetention(c *Client) error {
|
||||
backups, err := s.st.ListBackups(c.ID, StatusComplete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
times := make([]time.Time, len(backups))
|
||||
for i, b := range backups {
|
||||
times[i] = time.Unix(b.CreatedAt, 0)
|
||||
}
|
||||
keep := SelectGFS(times, c.KeepDaily, c.KeepWeekly, c.KeepMonthly)
|
||||
|
||||
// heavy-части: по каждому имени — только heavy_keep последних.
|
||||
// Бэкап с сохраняемой heavy-частью не удаляется целиком, даже если GFS его не выбрал.
|
||||
type excess struct {
|
||||
b *Backup
|
||||
name string
|
||||
}
|
||||
var drop []excess
|
||||
heavySeen := map[string]int{}
|
||||
for i, b := range backups {
|
||||
for _, p := range b.Parts {
|
||||
if p.Name == manifest.MainPart || p.Status != PartComplete {
|
||||
continue
|
||||
}
|
||||
heavySeen[p.Name]++
|
||||
if heavySeen[p.Name] <= max(c.HeavyKeep, 0) {
|
||||
keep[i] = true
|
||||
} else {
|
||||
drop = append(drop, excess{b, p.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, d := range drop {
|
||||
if err := s.deletePart(d.b, d.name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range backups {
|
||||
if keep[i] {
|
||||
continue
|
||||
}
|
||||
if err := s.deleteBackup(b); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ретеншн: %s удалён бэкап #%d от %s", c.Domain, b.ID, time.Unix(b.CreatedAt, 0).Format(time.DateTime))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) deletePart(b *Backup, name string) error {
|
||||
if err := os.Remove(s.partPath(b.Dir, name)); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := s.st.SetPartStatus(b.ID, name, PartDeleted); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.st.RecalcTotal(b.ID)
|
||||
}
|
||||
|
||||
func (s *Server) deleteBackup(b *Backup) error {
|
||||
if b.Dir != "" {
|
||||
if err := os.RemoveAll(filepath.Join(s.backupsDir(), b.Dir)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.st.DeleteBackup(b.ID)
|
||||
}
|
||||
Reference in new issue
Block a user