v0.66.0: userdata layout + shared-storage ownership convention
appbackup/userdata.go: EnsureUserdataDir (MkdirAll + explicit setgid Chmod 2775 +
chown gid 1000), UserdataSkeleton, EnsureUserdataSkeleton; linux chown/StatGID +
non-linux stubs. stackEnv injects USERDATA_PATH=<HDD_PATH>/userdata. Skeleton
pre-created on register + FileBrowser sync; deploy belt (composeExecCustomEnv on
'up') pre-creates every ${USERDATA_PATH} bind source. FileBrowser mounts userdata
(was appdata) — uid 1000 can now write into 2775 setgid. #8: migrate merge walk +
copyFile preserve source setgid+group so the convention survives MigrateAll.
Non-hollow tests incl. Linux setgid assertions + migration-preserve companion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
||||
@@ -1456,18 +1457,19 @@ func (s *Server) syncFileBrowserMounts(resetDBOnChange bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build volume mount lines. SCOPE to the drive's `appdata/` subtree only (Phase 4A): the customer
|
||||
// browses their userdata, but the recovery units + Tier 2 copies under `backups/` are NOT mounted
|
||||
// into FileBrowser at all — so the thing that restores them can't be browsed or (even read-only)
|
||||
// surfaced. mkdir the appdata dir first so the bind source exists with sane ownership.
|
||||
// Build volume mount lines. SCOPE to the drive's `userdata/` subtree (v0.66.0): the customer
|
||||
// browses ONLY userdata — app internals (appdata/) and the recovery units + Tier 2 copies
|
||||
// (backups/) are NOT mounted into FileBrowser. userdata is owned group 1000 mode 2775 (setgid),
|
||||
// and FileBrowser runs as uid 1000 → it can create folders + upload files (the old appdata mount
|
||||
// was guest-root 0755 → permission-denied). Pre-create the full skeleton with the convention.
|
||||
var storageMounts []string
|
||||
for _, sp := range paths {
|
||||
mountName := filepath.Base(sp.Path) // "/mnt/hdd_1" → "hdd_1"
|
||||
appdataSrc := filepath.Join(sp.Path, "appdata")
|
||||
if err := os.MkdirAll(appdataSrc, 0755); err != nil {
|
||||
s.logger.Printf("[WARN] [web] FileBrowser: could not ensure appdata dir %s: %v", appdataSrc, err)
|
||||
if err := appbackup.EnsureUserdataSkeleton(sp.Path); err != nil {
|
||||
s.logger.Printf("[WARN] [web] FileBrowser: could not ensure userdata skeleton on %s: %v", sp.Path, err)
|
||||
}
|
||||
line := fmt.Sprintf(" - %s:/srv/%s", appdataSrc, mountName)
|
||||
userdataSrc := appbackup.UserdataDir(sp.Path)
|
||||
line := fmt.Sprintf(" - %s:/srv/%s", userdataSrc, mountName)
|
||||
storageMounts = append(storageMounts, line)
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,12 @@ func (s *Server) registerStoragePath(where, label string, setDefault bool) error
|
||||
if strings.TrimSpace(label) == "" {
|
||||
label = settings.InferStorageLabel(where)
|
||||
}
|
||||
// v0.66.0: create the full userdata skeleton with the shared-storage convention (2775 setgid,
|
||||
// gid 1000) the moment a drive is registered — system drive AND additional drives. Idempotent;
|
||||
// best-effort (a perms hiccup shouldn't block registration).
|
||||
if err := appbackup.EnsureUserdataSkeleton(where); err != nil {
|
||||
s.logger.Printf("[WARN] [web] userdata skeleton on %s: %v", where, err)
|
||||
}
|
||||
// Change 4: re-enrolling a previously-DECOMMISSIONED drive must un-retire it. AddStoragePath
|
||||
// dedups a re-register into a no-op, so without this the soft marker would persist forever and the
|
||||
// apps' "missing storage" indicator would never clear.
|
||||
|
||||
Reference in New Issue
Block a user