.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:
@@ -27,6 +27,27 @@ type ExportEstimate struct {
|
||||
// failed). When true, DataSizeBytes is a partial/understated sum and FitsOnDest is FORCED false
|
||||
// — a failed read must NEVER render as "fits". The UI shows "ismeretlen méret".
|
||||
SizeUnknown bool `json:"size_unknown"`
|
||||
|
||||
// Task 4 class split (classified apps only; empty for legacy — existing fields above are
|
||||
// unchanged, so old JSON consumers keep working). BaseBytes = config + DB + volumes + mandatory
|
||||
// (always in the bundle). OptionalItems are pre-selected, ExcludedItems are opt-in — each carries
|
||||
// its own size so the UI recomputes the total client-side per checkbox toggle (no extra du calls).
|
||||
HasClassification bool `json:"has_classification"`
|
||||
BaseBytes int64 `json:"base_bytes"`
|
||||
BaseHuman string `json:"base_human"`
|
||||
MandatoryItems []FabItem `json:"mandatory_items,omitempty"`
|
||||
OptionalItems []FabItem `json:"optional_items,omitempty"`
|
||||
ExcludedItems []FabItem `json:"excluded_items,omitempty"`
|
||||
}
|
||||
|
||||
// FabItem is one class-scoped path in the `.fab` selection UI: Key is the DeselectOptional/OptInExcluded
|
||||
// value ("root/rel"), RelPath is the display path, Bytes/Human its du size.
|
||||
type FabItem struct {
|
||||
Key string `json:"key"`
|
||||
Root string `json:"root"`
|
||||
RelPath string `json:"rel_path"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
Human string `json:"human"`
|
||||
}
|
||||
|
||||
// EstimateExport calculates size estimates for an app export.
|
||||
@@ -57,6 +78,7 @@ func (e *Exporter) EstimateExport(stackName, destDrive string) (*ExportEstimate,
|
||||
}
|
||||
volumes := e.provider.GetDockerVolumes(stackName)
|
||||
e.debugf("EstimateExport: Docker volumes: %v", volumes)
|
||||
var volumeBytes int64
|
||||
for _, vol := range volumes {
|
||||
volSize, err := volumeSizer(vol)
|
||||
if err != nil {
|
||||
@@ -68,6 +90,7 @@ func (e *Exporter) EstimateExport(stackName, destDrive string) (*ExportEstimate,
|
||||
}
|
||||
e.debugf("EstimateExport: volume %s = %s", vol, humanizeBytes(volSize))
|
||||
est.DataSizeBytes += volSize
|
||||
volumeBytes += volSize
|
||||
}
|
||||
if est.SizeUnknown {
|
||||
est.DataSizeHuman = "ismeretlen méret"
|
||||
@@ -78,6 +101,10 @@ func (e *Exporter) EstimateExport(stackName, destDrive string) (*ExportEstimate,
|
||||
est.TotalSizeBytes = est.ConfigSizeBytes + est.DataSizeBytes
|
||||
est.TotalSizeHuman = humanizeBytes(est.TotalSizeBytes)
|
||||
|
||||
// Task 4: the class split (classified apps only). Independent du over each bucket path — additive,
|
||||
// never touches the fields/fits gate above.
|
||||
e.fabEstimateSplit(stackName, est, volumeBytes)
|
||||
|
||||
// Rough time estimate: ~500 MB/min for HDDs, minimum 1 minute
|
||||
minutes := int(est.TotalSizeBytes / (500 * 1024 * 1024))
|
||||
if minutes < 1 {
|
||||
|
||||
Reference in New Issue
Block a user