v0.277.0: kept data loads from the off-site copy too (R-691 (2))
gates / gates (push) Successful in 25s

Use my kept data / Load consider the off-site snapshot when it is newer than every local copy or
the only one; the unit is downloaded alone, judged (drive, data, recorded data version) and only
then restored. The page names the copy and its date.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-28 09:57:11 +02:00
parent 0c1023e503
commit 6479933e8e
14 changed files with 696 additions and 77 deletions
+67 -39
View File
@@ -1,6 +1,7 @@
package web
import (
"context"
"net/http"
"net/url"
"os"
@@ -11,6 +12,7 @@ import (
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
@@ -63,32 +65,44 @@ func KeptViewName(it stacks.KeptItem) string {
return base + filepath.Base(it.Path)
}
// keptBackupFor names the copy a Load would use for it, and whether one exists.
func (s *Server) keptBackupFor(lang string, it stacks.KeptItem) (string, string, bool) {
if s.backupMgr == nil {
return s.msgLang(lang, "kept.backup.none"), "", false
// keptBackupFor names the copy a Load would use for it, and whether one exists. off is the off-site copy
// per app (backup.KeptOffsiteCopies, asked once per page): it is used when it is newer than every local
// copy, or the only one (R-691 (2)).
func (s *Server) keptBackupFor(lang string, it stacks.KeptItem, off map[string]backup.KeptCopy) (string, backup.KeptCopy, bool) {
none := s.msgLang(lang, "kept.backup.none")
if s.backupMgr == nil || it.App == "" && it.Kind != stacks.KeptKindDated {
return none, backup.KeptCopy{}, false
}
var local backup.KeptCopy
var lok bool
if it.Kind == stacks.KeptKindDated {
if it.UnitDir == "" {
return s.msgLang(lang, "kept.backup.none"), "", false
// A dated folder's own unit is the copy taken with those files.
if it.UnitDir != "" {
if c, ok := s.backupMgr.KeptCopyAt(it.UnitDir, it.Drive, 1); ok {
return s.msgLang(lang, "kept.backup.with", c.Time.In(getTimezone()).Format("2006-01-02 15:04")), c, true
}
}
if c, ok := s.backupMgr.KeptCopyAt(it.UnitDir, it.Drive, 1); ok {
return s.msgLang(lang, "kept.backup.with", c.Time.In(getTimezone()).Format("2006-01-02 15:04")), c.UnitDir, true
}
return s.msgLang(lang, "kept.backup.none"), "", false
} else {
local, lok = s.backupMgr.KeptDBCopy(it.App, it.Drive)
}
if it.App == "" {
return s.msgLang(lang, "kept.backup.none"), "", false
}
c, ok := s.backupMgr.KeptDBCopy(it.App, it.Drive)
oc, ook := off[it.App]
c, ok := backup.KeptNewer(local, lok, oc, ook && it.App != "")
if !ok {
return s.msgLang(lang, "kept.backup.none"), "", false
return none, backup.KeptCopy{}, false
}
key := "kept.backup.own"
if c.Tier == 2 {
key = "kept.backup.second"
return s.msgLang(lang, backup.KeptCopyKey(c.Tier), c.Time.In(getTimezone()).Format("2006-01-02 15:04")), c, true
}
// keptOffsite asks the off-site repository ONCE for every listed app.
func (s *Server) keptOffsite(ctx context.Context, items []stacks.KeptItem) map[string]backup.KeptCopy {
if s.backupMgr == nil {
return nil
}
return s.msgLang(lang, key, c.Time.In(getTimezone()).Format("2006-01-02 15:04")), c.UnitDir, true
var apps []string
for _, it := range items {
apps = append(apps, it.App)
}
return s.backupMgr.KeptOffsiteCopies(ctx, apps)
}
func (s *Server) keptPageHandler(w http.ResponseWriter, r *http.Request) {
@@ -97,12 +111,14 @@ func (s *Server) keptPageHandler(w http.ResponseWriter, r *http.Request) {
data["TitleKey"] = "page.title.kept_data"
var rows []keptRow
if s.stackMgr != nil {
for _, it := range s.stackMgr.ListKept(s.keptDrives()) {
backup, _, can := s.keptBackupFor(lang, it)
items := s.stackMgr.ListKept(s.keptDrives())
off := s.keptOffsite(r.Context(), items)
for _, it := range items {
backupName, _, can := s.keptBackupFor(lang, it, off)
row := keptRow{
DisplayName: it.DisplayName, App: it.App, Path: it.Path, Drive: it.Drive,
Date: it.Date.In(getTimezone()).Format("2006-01-02 15:04"),
Size: appbackup.HumanizeBytes(it.SizeBytes), Backup: backup, CanLoad: can && it.App != "",
Size: appbackup.HumanizeBytes(it.SizeBytes), Backup: backupName, CanLoad: can && it.App != "",
LookURL: fileBrowserLink(s.cfg.Customer.Domain, s.msgLang(s.boxLang(), "kept.fb_source"), KeptViewName(it)),
}
row.DeleteText = s.msgLang(lang, "kept.delete.confirm", it.DisplayName, row.Size)
@@ -165,31 +181,43 @@ func (s *Server) keptLoadHandler(w http.ResponseWriter, r *http.Request) {
s.keptRedirect(w, r, "flash_error", msg)
return
}
_, unit, can := s.keptBackupFor(lang, it)
_, c, can := s.keptBackupFor(lang, it, s.keptOffsite(r.Context(), []stacks.KeptItem{it}))
if !can {
s.keptRedirect(w, r, "flash_error", s.msgLang(lang, "kept.choice.use_off"))
return
}
if it.Kind == stacks.KeptKindDated {
if _, err := s.stackMgr.RestoreKeptFiles(it); err != nil {
app, disp := it.App, it.DisplayName
okMsg := func(error) string { return s.msgLang(lang, "kept.load.done", disp) }
failMsg := func(err error) string { return s.msgLang(lang, "kept.load.failed", disp, err) }
after := func(ok bool) {
if ok {
if ran, err := s.stackMgr.RunAfterLoad(app, 10*time.Minute); ran && err != nil {
s.logger.Printf("[WARN] [web] kept load %s: after_load failed: %v", app, err)
}
s.stackMgr.FinishKeptLoad(it, s.backupMgr.PrimaryUnitHome(app, it.Drive))
}
s.SyncFileBrowserMounts()
}
// A dated folder's files move back BEFORE the load — for the off-site copy only after its unit is
// downloaded and judged, so a failed download or a refusal leaves the kept folder as it was.
moveBack := func() error {
if it.Kind != stacks.KeptKindDated {
return nil
}
_, err := s.stackMgr.RestoreKeptFiles(it)
return err
}
if c.Tier == backup.KeptTierOffsite {
s.logger.Printf("[INFO] [web] kept LOAD %s: %s from the off-site snapshot %s (from %s)", it.App, it.Path, c.SnapshotID, r.RemoteAddr)
s.backupMgr.LoadKeptOffsite(app, it.Drive, c, moveBack, okMsg, failMsg, after)
} else {
if err := moveBack(); err != nil {
s.keptRedirect(w, r, "flash_error", s.errText(r, err))
return
}
s.logger.Printf("[INFO] [web] kept LOAD %s: %s from %s (from %s)", it.App, it.Path, c.UnitDir, r.RemoteAddr)
s.backupMgr.LoadKeptApp(app, c.UnitDir, okMsg, failMsg, after)
}
s.logger.Printf("[INFO] [web] kept LOAD %s: %s from %s (from %s)", it.App, it.Path, unit, r.RemoteAddr)
app, disp := it.App, it.DisplayName
s.backupMgr.LoadKeptApp(app, unit,
func(error) string { return s.msgLang(lang, "kept.load.done", disp) },
func(err error) string { return s.msgLang(lang, "kept.load.failed", disp, err) },
func(ok bool) {
if ok {
if ran, err := s.stackMgr.RunAfterLoad(app, 10*time.Minute); ran && err != nil {
s.logger.Printf("[WARN] [web] kept load %s: after_load failed: %v", app, err)
}
s.stackMgr.FinishKeptLoad(it, s.backupMgr.PrimaryUnitHome(app, it.Drive))
}
s.SyncFileBrowserMounts()
})
http.Redirect(w, r, "/backups/restore?"+flashQuery("flash", "flash.restore.started"), http.StatusFound)
}