feat(web): C2 Parts 2+3 — POST /backup/tier2/restore + "Fájlok visszaállítása" button
- Endpoint next to /backup/restore; handler mirrors backupRestoreHandler (ParseForm → validStackName → backupMgr guard → WARN with RemoteAddr → RestoreTier2Files → flash). Flash strings: "<stack>: N fájl visszaállítva a másodlagos másolatból." / "Nincs hiányzó fájl — minden fájl megvan a helyén." / "Fájl-visszaállítás sikertelen: <err>" (refusals carry the Hungarian reasons from the engine). - backups.html: the button on the healthy Tier-2 layer row only (the Tier2Configured branch already excludes disconnected/inactive; additionally gated on Tier2LastRun), inline POST form with CSRF + confirm dialog naming the additive-only semantics and the last-copy timestamp. Template gates (id + emoji) green. - Handler guard test (C6): traversal/empty → exact Hungarian flash, no work started (nil backupMgr would panic if reached). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -896,6 +896,44 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/backups?flash="+msg, http.StatusFound)
|
||||
}
|
||||
|
||||
// backupTier2RestoreHandler (C2, closes F2) restores an app's MISSING user files in place from its
|
||||
// recorded Tier-2 copy — additive-only: existing live files are never overwritten and nothing is
|
||||
// ever deleted (see backup.RestoreTier2Files). Same handler shape as backupRestoreHandler.
|
||||
func (s *Server) backupTier2RestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
stackName := r.FormValue("stack_name")
|
||||
|
||||
if stackName == "" {
|
||||
http.Redirect(w, r, "/backups?flash_error=Hi%C3%A1nyz%C3%B3+param%C3%A9terek", http.StatusFound)
|
||||
return
|
||||
}
|
||||
// Same F2-defense as the unit restore: a stack name is a single segment, never a path.
|
||||
if !validStackName(stackName) {
|
||||
s.logger.Printf("[WARN] [web] Tier-2 file restore rejected: invalid stack_name %q from %s", stackName, r.RemoteAddr)
|
||||
http.Redirect(w, r, "/backups?flash_error=%C3%89rv%C3%A9nytelen+alkalmaz%C3%A1sn%C3%A9v", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if s.backupMgr == nil {
|
||||
http.Redirect(w, r, "/backups?flash_error=Ment%C3%A9s+nincs+be%C3%A1ll%C3%ADtva", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[WARN] [web] Tier-2 file restore requested: stack=%s from %s", stackName, r.RemoteAddr)
|
||||
|
||||
n, err := s.backupMgr.RestoreTier2Files(stackName)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Tier-2 file restore failed: %v", err)
|
||||
http.Redirect(w, r, "/backups?flash_error="+url.QueryEscape("Fájl-visszaállítás sikertelen: "+err.Error()), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
msg := "Nincs hiányzó fájl — minden fájl megvan a helyén."
|
||||
if n > 0 {
|
||||
msg = fmt.Sprintf("%s: %d fájl visszaállítva a másodlagos másolatból.", stackName, n)
|
||||
}
|
||||
http.Redirect(w, r, "/backups?flash="+url.QueryEscape(msg), http.StatusFound)
|
||||
}
|
||||
|
||||
// settingsBaseData is the shared identity block used by every settings-family subpage
|
||||
// (D1 split: /settings, /settings/notifications, /settings/security, /storage).
|
||||
func (s *Server) settingsBaseData(page, title string) map[string]interface{} {
|
||||
|
||||
Reference in New Issue
Block a user