v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s

The unit's data files are stamped with the versions that wrote them; the capture keeps the
definition the data belongs to; a restore never starts data under another version's
definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's
definition); every tier's time is its data's; the conversion-copy release needs a dump on
the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept
view joins the folder's owning group, language switch resyncs (R-691); a restore-generated
login is not shown as the password (R-694). Red-proofs in
felhom.eu/documentation/audits/version-travel-2026-09-26/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-26 10:35:22 +02:00
parent fb2bcdd5d6
commit b6810f14ff
47 changed files with 3771 additions and 135 deletions
+13 -3
View File
@@ -145,7 +145,17 @@ func RenderCloudflared(d CloudflaredData) (map[string]FileSpec, error) {
// RenderFileBrowserCompose returns FileBrowser's docker-compose.yml for the given domain and storage
// volume-mount lines. Ported verbatim from internal/web/handlers.go (the single source of truth now
// lives here so the pinned image can't diverge between bring-up and the web storage-sync path).
func RenderFileBrowserCompose(domain string, storageMounts []string) string {
func RenderFileBrowserCompose(domain string, storageMounts []string, groupAdd ...int) string {
// R-691 (v0.275.0): supplementary groups — the owning groups of kept folders the view must READ (a
// 0770 folder another user owns, e.g. nextcloud's www-data). Their binds are `:ro`. Absent → the
// compose is byte-for-byte what it was.
groupSection := ""
if len(groupAdd) > 0 {
groupSection = "\n # Kept data's owning groups (auto-generated): the read-only view reads a folder another user owns.\n group_add:"
for _, g := range groupAdd {
groupSection += fmt.Sprintf("\n - \"%d\"", g)
}
}
storageSection := ""
if len(storageMounts) > 0 {
storageSection = "\n # Storage paths (auto-generated by felhom-controller)\n" +
@@ -165,7 +175,7 @@ services:
# setgid), letting the content apps (group 1000) write into them. The gtstef/filebrowser image is a
# single Go binary (entrypoint ./filebrowser) and does NOT honor a UMASK env (verified: -e UMASK=002
# leaves PID1 at 0022), so we wrap the entrypoint to set the process umask before exec.
entrypoint: ["sh", "-c", "umask 002; exec /home/filebrowser/filebrowser"]
entrypoint: ["sh", "-c", "umask 002; exec /home/filebrowser/filebrowser"]%s
environment:
- TZ=Europe/Budapest
- FILEBROWSER_CONFIG=/home/filebrowser/config.yaml
@@ -198,7 +208,7 @@ volumes:
networks:
traefik-public:
external: true
`, domain, FileBrowserImage, storageSection, domain)
`, domain, FileBrowserImage, groupSection, storageSection, domain)
}
// RenderControllerRoute returns a traefik file-provider dynamic config routing the controller's own