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:
@@ -115,6 +115,19 @@ func (s *Server) sharingResolveStorageRoot(raw string) (string, error) {
|
||||
return "", errNotShareable
|
||||
}
|
||||
|
||||
// ShareRow is one row of the shares table. It is a PACKAGE-LEVEL type, not a function-local struct,
|
||||
// so the render test constructs the exact shape the handler passes: this template reads .System and
|
||||
// .Available, and a field present in one and missing from the other is a render-time 500 that no
|
||||
// handler test would catch (the template-gate class this project has hit four times).
|
||||
type ShareRow struct {
|
||||
Name string
|
||||
Path string
|
||||
ReadOnly bool
|
||||
Offsite bool
|
||||
System bool // controller-owned (R-75): no delete button, and the handler refuses it anyway
|
||||
Available bool
|
||||
}
|
||||
|
||||
// sharingPageData assembles the „Megosztás" page state.
|
||||
func (s *Server) sharingPageData() map[string]interface{} {
|
||||
data := s.settingsBaseData("sharing", "Hálózati megosztás")
|
||||
@@ -134,18 +147,12 @@ func (s *Server) sharingPageData() map[string]interface{} {
|
||||
data["SMBDirectAddress"] = s.sambaLANAddress()
|
||||
}
|
||||
|
||||
type shareRow struct {
|
||||
Name string
|
||||
Path string
|
||||
ReadOnly bool
|
||||
Offsite bool
|
||||
Available bool
|
||||
}
|
||||
var rows []shareRow
|
||||
var rows []ShareRow
|
||||
for _, sh := range s.settings.GetSMBShares() {
|
||||
fi, err := os.Stat(sh.Path)
|
||||
rows = append(rows, shareRow{
|
||||
rows = append(rows, ShareRow{
|
||||
Name: sh.Name, Path: sh.Path, ReadOnly: sh.ReadOnly, Offsite: sh.Offsite,
|
||||
System: sh.System,
|
||||
Available: err == nil && fi.IsDir(),
|
||||
})
|
||||
}
|
||||
@@ -272,6 +279,14 @@ func (s *Server) sharingEnableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
sharingRedirect(w, r, "A hálózati megosztás kikapcsolva. A mappák és a fájlok megmaradtak.")
|
||||
return
|
||||
}
|
||||
// R-75: the canonical drop-zone share exists whenever sharing is ON — and NEVER before. Enabling
|
||||
// sharing is the customer's decision (it puts SMB on the household LAN and demands a household
|
||||
// password); deploying a drop-zone app must not trigger it. "Mandatory" here means "always present
|
||||
// once sharing is on", not "turns sharing on".
|
||||
if err := s.ensureImportShare(); err != nil {
|
||||
s.logger.Printf("[WARN] [sharing] could not ensure the import share: %v", err)
|
||||
}
|
||||
|
||||
// v0.147.0 (4b): the bring-up runs DETACHED and the page polls it. Synchronously it was a form
|
||||
// post that hung for minutes on a first-enable image pull and then flashed „Beállítás mentve."
|
||||
// regardless of whether anything actually came up.
|
||||
@@ -411,6 +426,15 @@ func (s *Server) sharingShareCreateHandler(w http.ResponseWriter, r *http.Reques
|
||||
func (s *Server) sharingShareDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
// SERVER-SIDE refusal for controller-owned shares (R-75), BEFORE any mutation. The template also
|
||||
// omits the button; both are required and they prove different things — a render gate is not
|
||||
// enforcement, and a handler check is not reachability (the v0.70.1 ghost-delete lesson).
|
||||
for _, sh := range s.settings.GetSMBShares() {
|
||||
if strings.EqualFold(sh.Name, name) && sh.System {
|
||||
sharingRedirect(w, r, "Ez a megosztás a rendszer része, nem törölhető.")
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := s.settings.RemoveSMBShare(name); err != nil {
|
||||
sharingRedirect(w, r, err.Error())
|
||||
return
|
||||
@@ -524,3 +548,40 @@ func writeSharingJSON(w http.ResponseWriter, code int, v interface{}) {
|
||||
fmt.Fprintf(w, `{"error":"encode"}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureImportShare creates the controller-owned drop-zone share (R-75) if it is not already there.
|
||||
// Idempotent, and a no-op when the import root is unresolvable.
|
||||
//
|
||||
// It writes to the store DIRECTLY rather than going through sharingResolvePath: that guard validates
|
||||
// paths a CUSTOMER supplied through the picker, and refuses anything outside a registered storage
|
||||
// root. The system drive is deliberately not registered (registering it would make a 50 GB volume
|
||||
// holding the recovery units a customer-visible drive, a deploy target and a wipe candidate), so the
|
||||
// guard would refuse this path — correctly, for customer input. A controller-generated constant is a
|
||||
// different trust class.
|
||||
//
|
||||
// Offsite is FALSE: the drop-zone is class `excluded` data, and shipping an inbox offsite would
|
||||
// contradict the class the backup engines already act on.
|
||||
func (s *Server) ensureImportShare() error {
|
||||
if s.stackMgr == nil {
|
||||
return nil
|
||||
}
|
||||
root := s.stackMgr.GetImportRoot()
|
||||
if root == "" {
|
||||
return nil
|
||||
}
|
||||
for _, sh := range s.settings.GetSMBShares() {
|
||||
if strings.EqualFold(sh.Name, settings.SystemImportShareName) {
|
||||
return nil // already present
|
||||
}
|
||||
}
|
||||
if err := s.stackMgr.EnsureImportRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.settings.AddSMBShare(settings.SMBShare{
|
||||
Name: settings.SystemImportShareName,
|
||||
Path: root,
|
||||
ReadOnly: false,
|
||||
Offsite: false,
|
||||
System: true,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user