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.
82 lines
3.3 KiB
Go
82 lines
3.3 KiB
Go
package stacks
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
|
)
|
|
|
|
// Catalog-derived userdata skeleton (R-75).
|
|
//
|
|
// Before this, the customer-facing tree was a hardcoded Go list, so a new catalog app with a folder
|
|
// needed a controller release. The set is now DERIVED from the catalog the controller has already
|
|
// synced to disk — using the same authoritative parser the classifier uses — merged with the
|
|
// carry-list (appbackup.UserdataSkeletonCarry) so it can only ever ADD.
|
|
//
|
|
// Scope note: this derives ${USERDATA_PATH} binds only. ${IMPORT_PATH} binds are NOT part of a
|
|
// drive's skeleton — the canonical drop-zone lives once, on the system drive, and is ensured by
|
|
// EnsureImportRoot instead.
|
|
|
|
// DeriveUserdataDirs returns the ${USERDATA_PATH}-relative dirs implied by every template in
|
|
// stacksDir. ALL catalog apps count, not just deployed ones (Fork-2 ruling): the skeleton is
|
|
// storage-path-scoped and idempotent, and the all-apps set is barely larger than the historical
|
|
// hardcoded one (SPIKE P0(a): 14 vs 14, differing by one entry each way). Deployed-only filtering
|
|
// belongs in the UI, where an empty folder would actually confuse someone.
|
|
//
|
|
// Unreadable dirs / missing composes are skipped silently — a partially-synced catalog must degrade
|
|
// to "fewer derived dirs", never to an error that blocks the skeleton (the carry-list is the floor).
|
|
// The result is sorted; BuildUserdataSkeleton sorts again after merging, so both layers are pinned.
|
|
func DeriveUserdataDirs(stacksDir string) []string {
|
|
entries, err := os.ReadDir(stacksDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
set := map[string]bool{}
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
composePath := filepath.Join(stacksDir, e.Name(), "docker-compose.yml")
|
|
for _, b := range ParseComposeClassifiableBinds(composePath) {
|
|
if b.Root != appbackup.RootUserdata || b.RelPath == "" {
|
|
continue
|
|
}
|
|
set[b.RelPath] = true
|
|
}
|
|
}
|
|
out := make([]string, 0, len(set))
|
|
for d := range set {
|
|
out = append(out, d)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// UserdataSkeletonDirs is the merged, sorted set to create on a storage path: catalog-derived plus
|
|
// the carry-list. This is THE function every skeleton caller should use.
|
|
func (m *Manager) UserdataSkeletonDirs() []string {
|
|
return appbackup.BuildUserdataSkeleton(DeriveUserdataDirs(m.cfg.Paths.StacksDir))
|
|
}
|
|
|
|
// EnsureUserdataSkeleton applies the merged skeleton to a storage path's namespace root. Signature
|
|
// kept as func(string) error so it drops straight into fbPathDeps.ensureSkeleton.
|
|
func (m *Manager) EnsureUserdataSkeleton(nsRoot string) error {
|
|
return appbackup.EnsureUserdataSkeleton(nsRoot, m.UserdataSkeletonDirs())
|
|
}
|
|
|
|
// EnsureImportRoot creates the CANONICAL drop-zone root on the system drive with the userdata
|
|
// convention, so it exists (and is browsable + shareable) even before any drop-zone app is deployed.
|
|
// Idempotent; a no-op when the import root is unresolvable.
|
|
//
|
|
// It deliberately does NOT pre-create per-app subfolders: those are the deploy belt's job, so an app
|
|
// the customer never installed does not litter the drop-zone with an empty folder.
|
|
func (m *Manager) EnsureImportRoot() error {
|
|
root := m.GetImportRoot()
|
|
if root == "" {
|
|
return nil
|
|
}
|
|
return appbackup.EnsureUserdataDir(root)
|
|
}
|