.fab exclusion scoping: classes in the manual export (Task 4, v0.136.0)

SQ6 over-capture FIXED for classified apps: the userdata root tar is exclude-scoped (keeps only
ancestors/descendants of a SELECTED bind relpath — R1-C, the tier2Reconcile keep-rule); no selected
userdata bind → no root tar (radarr state-only). New appbackup.ComputeFabBuckets (shared
resolveGuardCollapse pipeline; class buckets; guards over ALL classes; no cross-bucket containment).
appexport/fabplan.go: computeFabPlan + tarDirectoryExcluding + fabEstimateSplit. ExportRequest gains
DeselectOptional/OptInExcluded (both start handlers — two-call-site); mandatory is a server-side
floor. Manifest v1 + import UNTOUCHED; legacy apps byte-identical to v0.130.0. Estimate additive
class split; export UI: locked-mandatory/optional-checkboxes/excluded-opt-in + two-number warning.
All 6 §10 red-proofs verified. 6D Accept #1 now runs against this shape.
This commit is contained in:
2026-07-15 11:28:40 +02:00
parent 5f216138e5
commit cf9ce01917
19 changed files with 1392 additions and 177 deletions
@@ -78,9 +78,11 @@ func (s *Server) apiExportDownloadStart(w http.ResponseWriter, r *http.Request)
return
}
var req struct {
StackName string `json:"stack_name"`
Password string `json:"password"`
StopApp bool `json:"stop_app"`
StackName string `json:"stack_name"`
Password string `json:"password"`
StopApp bool `json:"stop_app"`
DeselectOptional []string `json:"deselect_optional"` // Task 4: `.fab` class selection (two-call-site)
OptInExcluded []string `json:"opt_in_excluded"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "Invalid request body", http.StatusBadRequest)
@@ -98,10 +100,12 @@ func (s *Server) apiExportDownloadStart(w http.ResponseWriter, r *http.Request)
}
// A concurrent export/import gets the exporter's own busy answer (single-flight).
if err := s.appExporter.StartExport(appexport.ExportRequest{
StackName: req.StackName,
DestDrive: s.fabDownloadRoot(),
Password: req.Password,
StopApp: req.StopApp,
StackName: req.StackName,
DestDrive: s.fabDownloadRoot(),
Password: req.Password,
StopApp: req.StopApp,
DeselectOptional: req.DeselectOptional,
OptInExcluded: req.OptInExcluded,
}); err != nil {
s.logger.Printf("[ERROR] [web] download-export start failed for %s: %v", req.StackName, err)
jsonError(w, err.Error(), http.StatusConflict)