2958946517
${IMPORT_PATH} = <system namespace root>/userdata/import — ONE drop-zone per box,
on the system drive, injected at BOTH compose-env builders with NO per-drive
fallback (unresolvable leaves it unset so compose fails loudly rather than
quietly building a second, dead drop-zone).
Third BindRoot (RootImport) + Import list in BackupSpec, extended through
ValidateBackupSpec/ClassifyBinds. Load-bearing: a stale `userdata: import/<app>`
entry against the moved bind would be a WHOLE-BLOCK reject, taking the app's
mandatory hdd classification with it.
Exhaustive-root audit: resolveAbs/structuralGuard/ComputeCaptureSet/
ComputeFabBuckets now take importRoot explicitly (an import bind resolved
against hddPath would name a directory on the wrong drive); unresolvable is
refused loudly into Skipped. GetImportRoot added to both provider interfaces.
Catalog-derived skeleton: UserdataSkeleton() -> UserdataSkeletonCarry() +
BuildUserdataSkeleton(), SORTED. The carry-list makes zero-removals true by
construction (`documents` is in no catalog app but on both boxes) and is the
fresh-box floor. The sort is not tidiness: the naive map-order derivation
measured 20 distinct outputs from 20 identical runs, which with fbNeedsRecreate
is a fleet-wide FileBrowser restart loop.
One authoritative compose parser: ParseComposeUserdataMounts now delegates to
ParseComposeClassifiableBinds. Import root excluded from per-app migration.
Surfaces: FileBrowser /srv/beolvasas source; app-page "Hova tegyem a fajlokat?"
with PathEscape deep links (never QueryEscape) and class-driven copy;
data_paths: annotation with the Fork-3 asymmetry; system-owned beolvasas SMB
share refused server-side at handler AND store, button omitted in template.
Caught on the way: the sharing template's row struct was function-local, so
adding {{if .System}} would have 500'd every share row. ShareRow is now
package-level and the render test uses the handler's own type.
Tests 915 -> 949, all green. MinAgent unchanged.
77 lines
3.8 KiB
Go
77 lines
3.8 KiB
Go
package backup
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
|
)
|
|
|
|
// Offsite capture-set resolution (Task 3a, architecture doc §2/§6). Turns an app's Task-3-core
|
|
// TierOffsite capture set (recovery unit + MANDATORY userdata only) into the extra absolute paths
|
|
// appended to the app's restic snapshot, plus the Hungarian customer warnings for LOUD capture gaps.
|
|
//
|
|
// SP-3.4 is law here: restic 0.14.0 does NOT error on a missing source path — it skips with a warning,
|
|
// exits 0, and silently writes a partial snapshot. So a skipped/missing MANDATORY path is detected in
|
|
// THIS function (the structural-guard Skipped list + an os.Stat filter) and surfaced in BOTH the
|
|
// English log and the Hungarian LastWarning. A restic exit code proves nothing about a missing path.
|
|
|
|
// offboxBlocked records an app whose enlarged (userdata-carrying) push was refused by the pre-push
|
|
// quota gate. The unit-only push still proceeds (never a protection regression). estBytes is the
|
|
// mandatory-set size estimate that would have been added.
|
|
type offboxBlocked struct {
|
|
stack string
|
|
estBytes int64
|
|
}
|
|
|
|
// offboxCaptureSet computes an app's OFFSITE mandatory capture paths to add to its recovery-unit
|
|
// snapshot, plus any Hungarian warnings for capture gaps. It never returns optional/excluded paths
|
|
// (the TierOffsite filter drops them — §2). Returns (nil, nil) for the legacy / no-provider / no-block
|
|
// world: offsite stays UNIT-ONLY, byte-identical to pre-v0.134.0 (the SQ5 cost-regression guard).
|
|
func (m *Manager) offboxCaptureSet(stack string) (extra []string, warns []string) {
|
|
if m.stackProvider == nil {
|
|
return nil, nil // no provider wired → legacy world → unit only
|
|
}
|
|
binds, has := m.stackProvider.GetStackClassifiedBinds(stack)
|
|
if !has {
|
|
return nil, nil // no backup block → legacy → unit only
|
|
}
|
|
// Resolve against the app's LIVE HDD_PATH (raw — NOT GetAppDrivePath, whose systemDataPath fallback
|
|
// would resolve userdata onto the wrong drive). Empty ⇒ undeployed / no HDD (decision §2.4):
|
|
// mandatory-path resolution needs the live HDD_PATH, so push unit-only + a loud WARN.
|
|
hdd := strings.TrimSpace(m.stackProvider.GetStackHDDPath(stack))
|
|
if hdd == "" {
|
|
m.logger.Printf("[WARN] [offbox] %s: not deployed — offsite push is unit-only (mandatory userdata not resolvable)", stack)
|
|
return nil, []string{fmt.Sprintf("Figyelmeztetés: a(z) %s nincs telepítve — csak a mentési egység került a távoli mentésbe.", stack)}
|
|
}
|
|
nsRoot := m.namespaceRoot(hdd)
|
|
cs := appbackup.ComputeCaptureSet(binds, has, appbackup.TierOffsite, nsRoot, m.stackProvider.GetImportRoot())
|
|
|
|
var gaps []string
|
|
// Structurally-refused MANDATORY paths (traversal / bare drive-root / reserved backups/ zone) are
|
|
// loud ERROR gaps — the path the customer thinks is protected is not in the snapshot.
|
|
for _, sk := range cs.Skipped {
|
|
if sk.Class == appbackup.ClassMandatory {
|
|
m.logger.Printf("[ERROR] [offbox] %s: mandatory path refused by a structural guard (%s): %s/%s — NOT in the offsite snapshot",
|
|
stack, sk.Reason, sk.Root, sk.RelPath)
|
|
gaps = append(gaps, sk.RelPath)
|
|
}
|
|
}
|
|
// Stat-filter (§2.5): a declared mandatory path absent on disk. restic would skip it SILENTLY
|
|
// (SP-3.4), so drop it from argv AND warn — never a silent "looks backed up but isn't".
|
|
for _, p := range cs.Paths {
|
|
if _, err := os.Stat(p.Abs); err != nil {
|
|
m.logger.Printf("[WARN] [offbox] %s: mandatory data path missing on disk, skipped from offsite: %s", stack, p.Abs)
|
|
gaps = append(gaps, p.RelPath)
|
|
continue
|
|
}
|
|
extra = append(extra, p.Abs)
|
|
}
|
|
if len(gaps) > 0 {
|
|
warns = append(warns, fmt.Sprintf("Figyelmeztetés: a(z) %s alkalmazás egyes adatmappái nem kerültek a távoli mentésbe: %s.",
|
|
stack, strings.Join(gaps, ", ")))
|
|
}
|
|
return extra, warns
|
|
}
|