v0.172.0 — R-75: canonical import root, catalog-derived skeleton, import surfaces
${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.
This commit is contained in:
@@ -513,49 +513,32 @@ func buildPathInfo(path string) HDDPath {
|
||||
|
||||
// ParseComposeUserdataMounts reads a docker-compose.yml and extracts the host bind-source paths that
|
||||
// reference ${USERDATA_PATH} (resolved to userdataPath) — the dirs the deploy belt must pre-create
|
||||
// with the userdata convention. Same scanner shape as ParseComposeHDDMounts.
|
||||
// with the userdata convention.
|
||||
//
|
||||
// R-75: this is now a thin RESOLVER over ParseComposeClassifiableBinds, which is the ONE authoritative
|
||||
// compose-bind scanner. The two used to be byte-for-byte duplicate scanners (SPIKE §3) differing only
|
||||
// in what they threw away, so a fix to one silently skipped the other; the classifier won because it
|
||||
// is the richer of the two (it keeps the root and the :ro flag, both of which this function discards
|
||||
// but the classification and derivation paths need).
|
||||
//
|
||||
// ONE deliberate behaviour drop, recorded rather than hidden: the old textual
|
||||
// strings.ReplaceAll("${USERDATA_PATH}", …) + containment check also accepted a bind written as a
|
||||
// LITERAL absolute path that happened to fall under userdataPath. The classifier matches the ${VAR}
|
||||
// reference only. No catalog template has ever used the literal form (verified across all 53 in
|
||||
// SPIKE §2 — every host token is a ${VAR}, a named volume, or the docker socket), and such a compose
|
||||
// would be pinned to one machine's drive layout, so the capability was dead.
|
||||
func ParseComposeUserdataMounts(composePath, userdataPath string) []string {
|
||||
if userdataPath == "" {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(composePath)
|
||||
if err != nil {
|
||||
return nil
|
||||
var out []string
|
||||
for _, b := range ParseComposeClassifiableBinds(composePath) {
|
||||
if b.Root != appbackup.RootUserdata {
|
||||
continue
|
||||
}
|
||||
out = append(out, filepath.Join(userdataPath, filepath.FromSlash(b.RelPath)))
|
||||
}
|
||||
var mounts []string
|
||||
seen := make(map[string]bool)
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
||||
inVolumes := false
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "volumes:") {
|
||||
inVolumes = true
|
||||
continue
|
||||
}
|
||||
if inVolumes && !strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "#") && line != "" {
|
||||
inVolumes = false
|
||||
}
|
||||
if !inVolumes || !strings.HasPrefix(line, "- ") {
|
||||
continue
|
||||
}
|
||||
mountStr := strings.Trim(strings.TrimPrefix(line, "- "), "\"'")
|
||||
parts := strings.SplitN(mountStr, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
hostPath := strings.ReplaceAll(parts[0], "${USERDATA_PATH}", userdataPath)
|
||||
cleanPath := filepath.Clean(hostPath)
|
||||
cleanUD := filepath.Clean(userdataPath)
|
||||
// must be userdataPath itself or a subpath (clean before check — traversal-safe)
|
||||
if cleanPath != cleanUD && !strings.HasPrefix(cleanPath, cleanUD+string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
if !seen[cleanPath] {
|
||||
seen[cleanPath] = true
|
||||
mounts = append(mounts, cleanPath)
|
||||
}
|
||||
}
|
||||
return mounts
|
||||
return out
|
||||
}
|
||||
|
||||
// ExportDataMounts returns the host directories a .fab export must capture for an app: the
|
||||
|
||||
Reference in New Issue
Block a user