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) }