P2R-02 slice 2 follow-up: refuse to delete a host's last source group
Belt-and-braces: the UI now disables the Delete button when a group is the only one on the host (with a tooltip explaining why), and the server-side handler returns 409 if a curl/form-replay tries anyway. Every host needs at least one source group to be backup-able, so the 'last group on a fresh host' case is a meaningful accident to guard against.
This commit is contained in:
@@ -310,6 +310,20 @@ func (s *Server) handleUISourceGroupDelete(w stdhttp.ResponseWriter, r *stdhttp.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refuse to delete the host's last source group — every host
|
||||||
|
// needs at least one to be backup-able. UI disables the button
|
||||||
|
// in this case; this guards against form-replay / curl.
|
||||||
|
groups, err := s.deps.Store.ListSourceGroupsByHost(r.Context(), host.ID)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("ui sources: count groups", "err", err)
|
||||||
|
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(groups) <= 1 {
|
||||||
|
stdhttp.Error(w, "this is the host's only source group — create another one first", stdhttp.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.deps.Store.DeleteSourceGroup(r.Context(), host.ID, gid); err != nil {
|
if err := s.deps.Store.DeleteSourceGroup(r.Context(), host.ID, gid); err != nil {
|
||||||
if errors.Is(err, store.ErrNotFound) {
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
stdhttp.NotFound(w, r)
|
stdhttp.NotFound(w, r)
|
||||||
|
|||||||
@@ -66,6 +66,9 @@
|
|||||||
{{if gt $row.UsedBy 0}}
|
{{if gt $row.UsedBy 0}}
|
||||||
<button class="btn btn-danger" disabled
|
<button class="btn btn-danger" disabled
|
||||||
title="remove this group from {{$row.UsedBy}} schedule{{if ne $row.UsedBy 1}}s{{end}} first">Delete</button>
|
title="remove this group from {{$row.UsedBy}} schedule{{if ne $row.UsedBy 1}}s{{end}} first">Delete</button>
|
||||||
|
{{else if eq (len $page.Groups) 1}}
|
||||||
|
<button class="btn btn-danger" disabled
|
||||||
|
title="this is the host's only source group — create another one first">Delete</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form method="post" action="/hosts/{{$host.ID}}/sources/{{$g.ID}}/delete" style="display: inline;"
|
<form method="post" action="/hosts/{{$host.ID}}/sources/{{$g.ID}}/delete" style="display: inline;"
|
||||||
onsubmit="return confirm('Delete source group "{{$g.Name}}"? Existing snapshots are not affected.');">
|
onsubmit="return confirm('Delete source group "{{$g.Name}}"? Existing snapshots are not affected.');">
|
||||||
|
|||||||
Reference in New Issue
Block a user