v0.325.0: share + escrow limiters reserve first; family sessions as fingerprints; samba renames apply; path guard widened; gates in a worktree
gates / gates (push) Successful in 45s

R-1111 reserveAttempt is the one limiter body (share password, escrow re-auth, dashboard sign-in). R-1112
family.json holds fingerprints; old files are converted at load. R-1113 the household's pass reuses its
family session. R-1109 the samba compose carries the smb.conf hash. R-1110 TestNoBareDrivePathToPathHelpers.
R-1114 shared_scripts.py resolves the felhom.eu sibling from a worktree. Each red-proved.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-17 09:36:34 +02:00
parent 7d41c2973c
commit 94f44931dd
28 changed files with 945 additions and 190 deletions
+19 -1
View File
@@ -1,6 +1,8 @@
package infra
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
@@ -96,6 +98,13 @@ func RenderSambaConfig(d SambaData) string {
// the default bridge is deaf to LAN multicast), the pinned image, smb.conf bind-mounted read-only,
// the passdb named volume, and one bind per share (:ro for read-only shares — defense in depth beside
// the smb.conf-level `read only`). Deterministic: share binds are sorted so the output is stable.
//
// The label felhom.smb-conf-sha256 carries the hash of the smb.conf this compose runs with (R-1109). smb.conf is a
// bind-mounted FILE replaced by atomic rename, so a running container keeps the old inode, and `compose up -d`
// recreates only when the COMPOSE changes: a share renamed at the same path changed smb.conf alone and was never
// served. With the hash in the compose, a config change is a compose change, so compose recreates — and an
// unchanged config renders a byte-identical compose, so a pass with nothing new recreates nothing.
// Pinned by TestRenderSambaCompose_ConfigHashLabel (infra) and TestSambaReconcile_SamePathRenameChangesCompose.
func RenderSambaCompose(d SambaData) string {
shares := make([]SambaShareRender, len(d.Shares))
copy(shares, d.Shares)
@@ -121,6 +130,8 @@ services:
container_name: %[1]s
restart: unless-stopped
network_mode: host
labels:
- felhom.smb-conf-sha256=%[9]s
environment:
- FELHOM_SERVER_NAME=%[3]s
- FELHOM_IFACE=%[8]s
@@ -132,5 +143,12 @@ services:
%[7]s
volumes:
%[5]s:
`, SambaContainerName, SambaImage, d.ServerName, d.UID, SambaPassdbVolume, SambaPassdbMount, binds.String(), SambaHostInterface)
`, SambaContainerName, SambaImage, d.ServerName, d.UID, SambaPassdbVolume, SambaPassdbMount, binds.String(), SambaHostInterface,
SambaConfigHash(d))
}
// SambaConfigHash is the sha256 (hex) of the smb.conf RenderSambaConfig produces for d — the compose label value.
func SambaConfigHash(d SambaData) string {
sum := sha256.Sum256([]byte(RenderSambaConfig(d)))
return hex.EncodeToString(sum[:])
}
+28
View File
@@ -108,3 +108,31 @@ func TestRenderSambaCompose(t *testing.T) {
t.Error("writable share must NOT be :ro")
}
}
// R-1109: a share renamed at the SAME path changes smb.conf only. The compose must change with it (the config-hash
// label), or `compose up -d` keeps the running container on the old bind-mounted inode and the new name is never
// served. And an unchanged config must render a byte-identical compose (no recreate on every pass).
func TestRenderSambaCompose_ConfigHashLabel(t *testing.T) {
path := "/mnt/felhom-drives/hdd_1/userdata/shares/iratok"
before := SambaData{ServerName: "FELHOM", UID: 1000, Shares: []SambaShareRender{{Name: "dokumentumok", Path: path}}}
after := SambaData{ServerName: "FELHOM", UID: 1000, Shares: []SambaShareRender{{Name: "iratok", Path: path}}}
if RenderSambaConfig(before) == RenderSambaConfig(after) {
t.Fatal("setup: the rename must change smb.conf")
}
cb, ca := RenderSambaCompose(before), RenderSambaCompose(after)
if cb == ca {
t.Fatalf("a same-path rename left the compose byte-identical — compose up -d will not recreate (R-1109):\n%s", ca)
}
if want := "- felhom.smb-conf-sha256=" + SambaConfigHash(after); !strings.Contains(ca, want) {
t.Errorf("compose missing the label %q\n%s", want, ca)
}
if RenderSambaCompose(after) != ca {
t.Error("the same data must render the same compose (a recreate on every pass otherwise)")
}
// The premise of the bug, pinned: the binds are the same on both sides, so before the label nothing else differed.
bind := " - " + path + ":" + path + "\n"
if !strings.Contains(cb, bind) || !strings.Contains(ca, bind) {
t.Errorf("both composes must bind %q", bind)
}
}