package api import ( "fmt" "net/http" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/appbackup" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // keptChoiceData is what the install page needs to ask the household (`09` §3 decision 36). The // sentences come rendered in the request's language; the page shows them as they are. type keptChoiceData struct { Code string `json:"code"` // "kept_data_choice" Title string `json:"title"` Body string `json:"body"` UseLabel string `json:"use_label"` UseDesc string `json:"use_desc"` // "" when use is off UseOff string `json:"use_off"` // "" when use is offered UseOffered bool `json:"use_offered"` // a database copy exists for these files FreshLabel string `json:"fresh_label"` FreshDesc string `json:"fresh_desc"` NotBacked string `json:"not_backed_up"` } // keptDataAtInstall answers the install when the app's private drive folder already holds data: // // - no choice → 409 kept_data_choice with the sentences (nothing moved, nothing installed); // - "use" → a LOAD from the newest usable copy (backup.KeptDBCopy), the removed-app restore with // the unit named, then the app's after_load; 202. Refused 409 with use_off when no copy exists; // - "fresh" → not handled here: DeployStack moves the old data aside and installs. // // Returns true when it wrote the response. func (r *Router) keptDataAtInstall(w http.ResponseWriter, req *http.Request, name, hdd, choice string) bool { old := r.stackMgr.OldAppData(name, hdd) if len(old) == 0 || choice == stacks.KeptChoiceFresh { return false } display := name if st, ok := r.stackMgr.GetStack(name); ok && st.Meta.DisplayName != "" { display = st.Meta.DisplayName } var size int64 var newest time.Time for _, p := range old { size += stacks.DirSizeBytes(p) if t := stacks.DirModTime(p); t.After(newest) { newest = t } } var cp struct { ok bool time time.Time dir string } if r.backupMgr != nil { if c, ok := r.backupMgr.KeptDBCopy(name, hdd); ok { cp.ok, cp.time, cp.dir = true, c.Time, c.UnitDir } } lang := r.langFor(req) data := keptChoiceData{ Code: "kept_data_choice", Title: r.msgLang(lang, "kept.choice.title"), Body: r.msgLang(lang, "kept.choice.body", display, appbackup.HumanizeBytes(size), localDay(newest)), UseLabel: r.msgLang(lang, "kept.choice.use.label"), UseOffered: cp.ok, FreshLabel: r.msgLang(lang, "kept.choice.fresh.label"), FreshDesc: r.msgLang(lang, "kept.choice.fresh.desc"), NotBacked: r.msgLang(lang, "kept.not_backed_up"), } if cp.ok { data.UseDesc = r.msgLang(lang, "kept.choice.use.desc", localDay(cp.time)) } else { data.UseOff = r.msgLang(lang, "kept.choice.use_off") } switch choice { case "": r.logger.Printf("[INFO] [api] Deploy %s: the drive already holds its old data %v (%d bytes) — asking the household (use offered: %v)", name, old, size, cp.ok) writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: data.Title, Data: data}) return true case stacks.KeptChoiceUse: if !cp.ok { r.logger.Printf("[WARN] [api] Deploy %s: use my kept data REFUSED — no database copy for %s", name, hdd) writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: data.UseOff, Data: data}) return true } if st := r.backupMgr.RestoreStatus(); st.Running || r.backupMgr.IsRunning() { writeJSON(w, http.StatusConflict, apiResponse{OK: false, Error: r.msgLang(lang, "api.kept.busy")}) return true } r.logger.Printf("[INFO] [api] Deploy %s: USE MY KEPT DATA — loading from %s (%s) under the kept files %v", name, cp.dir, cp.time.UTC().Format(time.RFC3339), old) r.startKeptLoad(name, cp.dir, lang, nil) writeJSON(w, http.StatusAccepted, apiResponse{OK: true, Message: r.msgLang(lang, "kept.load.started", display)}) return true default: writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: fmt.Sprintf("kept_data %q is not a choice", choice)}) return true } } // startKeptLoad runs the load and, when it succeeded, the app's after_load. then(ok) runs last. func (r *Router) startKeptLoad(name, unitDir, lang string, then func(ok bool)) { display := name if st, ok := r.stackMgr.GetStack(name); ok && st.Meta.DisplayName != "" { display = st.Meta.DisplayName } r.backupMgr.LoadKeptApp(name, unitDir, func(error) string { return r.msgLang(lang, "kept.load.done", display) }, func(err error) string { return r.msgLang(lang, "kept.load.failed", display, err) }, func(ok bool) { if ok { if ran, err := r.stackMgr.RunAfterLoad(name, 10*time.Minute); ran && err != nil { r.logger.Printf("[WARN] [api] kept load %s: after_load failed: %v", name, err) } } if then != nil { then(ok) } }) } func localDay(t time.Time) string { if t.IsZero() { return "?" } loc, err := time.LoadLocation("Europe/Budapest") if err != nil { loc = time.UTC } return t.In(loc).Format("2006-01-02") }