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:
@@ -848,7 +848,13 @@ func walkMerge(lg *log.Logger, srcNS, dstNS string, skip map[string]bool, assert
|
||||
if assertOnly {
|
||||
return nil
|
||||
}
|
||||
return os.MkdirAll(dst, 0o755)
|
||||
if err := os.MkdirAll(dst, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
// #8 (v0.66.0): preserve the SOURCE dir's full mode (incl. setgid) + group, so the userdata
|
||||
// ownership convention (2775 setgid, gid 1000) survives a whole-drive migration. MkdirAll's
|
||||
// mode is umask-masked + drops setgid, so re-stamp explicitly from the source.
|
||||
return preserveDirOwnership(dst, d)
|
||||
}
|
||||
|
||||
// Symlink: recreate-if-absent (copy mode); ignored in assert mode.
|
||||
@@ -994,10 +1000,15 @@ func copyFile(src, dst string) (int64, error) {
|
||||
os.Remove(tmp)
|
||||
return 0, err
|
||||
}
|
||||
if err := os.Chmod(tmp, fi.Mode().Perm()); err != nil {
|
||||
// #8 (v0.66.0): preserve the SOURCE file's FULL mode (incl. setgid/setuid/sticky — not .Perm(),
|
||||
// which masks them off) + group, so the userdata convention survives a whole-drive migration.
|
||||
if err := os.Chmod(tmp, fi.Mode()); err != nil {
|
||||
os.Remove(tmp)
|
||||
return 0, err
|
||||
}
|
||||
if gid, ok := appbackup.StatGID(fi); ok {
|
||||
_ = os.Chown(tmp, -1, gid) // best-effort; needs root for an arbitrary group (the controller is)
|
||||
}
|
||||
if err := os.Rename(tmp, dst); err != nil {
|
||||
os.Remove(tmp)
|
||||
return 0, err
|
||||
@@ -1005,6 +1016,22 @@ func copyFile(src, dst string) (int64, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// preserveDirOwnership re-stamps a freshly-created target dir with the SOURCE dir's full mode (incl.
|
||||
// setgid) and group — part of the #8 fix so the userdata convention survives a migration.
|
||||
func preserveDirOwnership(dst string, d fs.DirEntry) error {
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(dst, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
if gid, ok := appbackup.StatGID(info); ok {
|
||||
_ = os.Chown(dst, -1, gid) // best-effort; root sets an arbitrary group (the controller is root)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileSum returns the hex sha256 of a file (streaming).
|
||||
func fileSum(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
|
||||
Reference in New Issue
Block a user