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
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:
@@ -3,8 +3,11 @@ package web
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
@@ -190,15 +193,6 @@ func (s *Server) keptLoadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/backups/restore?"+flashQuery("flash", "flash.restore.started"), http.StatusFound)
|
||||
}
|
||||
|
||||
// keptFileBrowserBinds are the read-only binds of the „Megőrzött adatok" source: one per listed item,
|
||||
// `:ro`, under /srv/<source dir> — never a live app's folder (ListKept never lists one).
|
||||
func (s *Server) keptFileBrowserBinds() []string {
|
||||
if s.stackMgr == nil {
|
||||
return nil
|
||||
}
|
||||
return keptBindLines(s.stackMgr.ListKept(s.keptDrives()))
|
||||
}
|
||||
|
||||
// keptBindLines renders the compose bind lines — each READ-ONLY. Pinned by TestKept_FileBrowserBindsAreReadOnly.
|
||||
func keptBindLines(items []stacks.KeptItem) []string {
|
||||
var out []string
|
||||
@@ -207,3 +201,40 @@ func keptBindLines(items []stacks.KeptItem) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// keptReadGroups is the R-691 rule — decided by CC unattended 2026-09-26, operator may reverse (`07` §6.5):
|
||||
// the read-only view reads a kept folder another user owns by joining that folder's OWNING GROUP, never by
|
||||
// changing the household's files or their permissions (nextcloud checks its data folder's mode after a
|
||||
// Load). A group is added only when the folder is group-READABLE and the group is neither root's (0 — it
|
||||
// would reach every root-group file in the view's other mounts) nor the view's own (1000). The kept binds
|
||||
// are `:ro`, so the added group cannot write kept data. Sorted, unique. Pinned by TestR691_KeptReadGroups.
|
||||
func keptReadGroups(items []stacks.KeptItem, owner func(path string) (gid int, mode os.FileMode, ok bool)) []int {
|
||||
seen := map[int]bool{}
|
||||
var out []int
|
||||
for _, it := range items {
|
||||
gid, mode, ok := owner(it.Path)
|
||||
if !ok || gid == 0 || gid == fileBrowserUID || mode&0o040 == 0 || seen[gid] {
|
||||
continue
|
||||
}
|
||||
seen[gid] = true
|
||||
out = append(out, gid)
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// fileBrowserUID is the uid:gid the file-browser image runs as (gtstef/filebrowser: `filebrowser`, 1000).
|
||||
const fileBrowserUID = 1000
|
||||
|
||||
// statOwner is keptReadGroups' production owner reader.
|
||||
func statOwner(path string) (int, os.FileMode, bool) {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, 0, false
|
||||
}
|
||||
return int(st.Gid), fi.Mode().Perm(), true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user