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:
2026-06-14 21:58:49 +02:00
parent cbaa53f565
commit c48f95fe06
17 changed files with 523 additions and 34 deletions
+32
View File
@@ -1,5 +1,37 @@
## Changelog
### v0.66.0 — userdata layout + shared-storage ownership convention (2026-06-14)
Customer-facing `userdata/` tree (sibling of appdata/backups under each drive's felhom-data namespace)
with a shared-ownership convention so FileBrowser + content apps collaborate without permission
collisions. Spike: `felhom.eu/documentation/audits/SPIKE-userdata-layout-2026-06-14.md`. Pairs with the
app-catalog commit that repoints media mounts to `${USERDATA_PATH}`.
- **Convention helper** (`internal/appbackup/userdata.go`): `EnsureUserdataDir`/`EnsureDirOwned` =
MkdirAll → explicit `Chmod(ModeSetgid|0775)` (MkdirAll's mode is umask-masked AND drops setgid) →
chown group to `SharedContentGID` (1000). `UserdataDir`, `UserdataSkeleton` (media/{movies,tv,music,
audiobooks,books,comics,photos}, downloads, import/{paperless,calibre}, roms, documents),
`EnsureUserdataSkeleton`. Linux chown via `chownGID`/`StatGID` (`userdata_linux.go`); no-op stub
off-Linux (`userdata_other.go`).
- **USERDATA_PATH injection** (`stackEnv`, manager.go): injects `USERDATA_PATH = <HDD_PATH>/userdata`
(HDD_PATH is the namespace root) alongside HDD_PATH, so the catalog's `${USERDATA_PATH}/...` mounts
resolve.
- **Skeleton pre-create**: `registerStoragePath` + `syncFileBrowserMounts` ensure the full skeleton on
every storage path (system + additional drives) with the convention.
- **Deploy belt**: `composeExecCustomEnv` (gated on `up`) pre-creates every `${USERDATA_PATH}/...`
bind source the stack declares (`ParseComposeUserdataMounts` + `ensureUserdataMounts`) so Docker
never auto-creates a userdata dir as guest-root — covers apps not in the skeleton.
- **FileBrowser mount switch** (`syncFileBrowserMounts`): mounts `<drive>/userdata` (was `appdata`) →
`/srv/<name>`. FileBrowser runs as uid 1000 → can now create folders + upload into the 2775 setgid
userdata (fixes the permission-denied); app internals (appdata/) are no longer browsable.
- **#8 migration fix** (`migrate.go`): the non-app merge walk now preserves the SOURCE dir's full mode
(incl. setgid via `preserveDirOwnership`) + group, and `copyFile` preserves the full file mode
(`fi.Mode()`, not `.Perm()`) + group — so the ownership convention survives a whole-drive `MigrateAll`.
- Non-hollow tests: `EnsureDirOwned` produces 02775+setgid+gid (Linux companion proves a plain MkdirAll
has NO setgid); skeleton structure; `ParseComposeUserdataMounts` selectivity; deploy belt creates the
declared dirs; **migration preserves setgid+group** (Linux; mutation-proven against the pre-fix
0755/.Perm() path).
### v0.65.0 — data migration + self-serve decommission (B1+B2) (2026-06-14)
Customer-self-serve storage **migration** (move app data between drives) and **decommission** (retire