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.
65 lines
2.6 KiB
Go
65 lines
2.6 KiB
Go
package web
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
|
)
|
|
|
|
// FileBrowser Quantum deep links (R-75).
|
|
//
|
|
// The template is taken VERBATIM from the shipped frontend router (SPIKE P2, read out of the
|
|
// bundle), not guessed:
|
|
//
|
|
// function Ms(n,t,…){ … let a=q2(t), s=`/files/${encodeURIComponent(n)}${a}` … }
|
|
// q2 = … .map(r=>encodeURIComponent(r)).join("/") // per-SEGMENT encoding
|
|
// function H2(n){ t=i.split("/")[2]; … } // source is the 3rd path segment
|
|
//
|
|
// So a link is constructible server-side from (sourceName, relPath) alone — no internal id, no
|
|
// index, no client-side state.
|
|
//
|
|
// A cold link (no FileBrowser session) is NOT lost: the router guard redirects to
|
|
// /login?redirect=<fullPath> and the login handler navigates back to it. The customer may still have
|
|
// to sign in, which is why the UI copy must not promise one click.
|
|
|
|
// fbSourceRoot is the FileBrowser origin for a customer domain. FileBrowser is published at
|
|
// files.<domain> by the base-infra traefik labels.
|
|
func fbSourceRoot(domain string) string {
|
|
return "https://files." + domain
|
|
}
|
|
|
|
// fileBrowserLink builds a deep link into a named FileBrowser source at relPath.
|
|
//
|
|
// ENCODING TRAP, measured in SPIKE P2 — use url.PathEscape, NEVER url.QueryEscape:
|
|
//
|
|
// "Média & könyvtár" PathEscape=M%C3%A9dia%20&%20k%C3%B6nyvt%C3%A1r QueryEscape=M%C3%A9dia+%26+k%C3%B6nyvt%C3%A1r
|
|
// "a+b" PathEscape=a+b QueryEscape=a%2Bb
|
|
//
|
|
// QueryEscape encodes a space as "+", which inside a PATH segment means a literal plus and breaks
|
|
// the link. PathEscape leaves "&" unescaped, which is correct here: "&" is a legal path sub-delim,
|
|
// and html/template escapes it to "&" in the href attribute, which the browser decodes back to
|
|
// "&". The two escapings compose — so do NOT pre-escape for HTML here.
|
|
//
|
|
// Source names cannot contain "/" (they come from filepath.Base or the ASCII import constant), which
|
|
// is what keeps the router's split("/")[2] round-trip intact.
|
|
func fileBrowserLink(domain, sourceName, relPath string) string {
|
|
var b strings.Builder
|
|
b.WriteString(fbSourceRoot(domain))
|
|
b.WriteString("/files/")
|
|
b.WriteString(url.PathEscape(sourceName))
|
|
for _, seg := range strings.Split(strings.Trim(relPath, "/"), "/") {
|
|
if seg == "" {
|
|
continue
|
|
}
|
|
b.WriteString("/")
|
|
b.WriteString(url.PathEscape(seg))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// importFolderLink is the deep link to a drop-zone app's folder inside the canonical import source.
|
|
func importFolderLink(domain, appDir string) string {
|
|
return fileBrowserLink(domain, infra.FileBrowserImportLabel, appDir)
|
|
}
|