Files
mesh-catalog/modules/mesh-files/cmd/mesh-files/files.go
T
jschoubben 5d98bcc0db
mesh/delivery-group group feat/900-node-files checking: composed check out of date: novox/mesh-controller@210f7cf5e64b was judged against trunk 611e2275cf7c, t…
mesh/merge-gate out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 3db85a0dde83, the trunk is now 8b53a3663244; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
mesh-files: the holder of node-files, reads and writes with trash, owned paths and an act log, so file work on a node needs no shell (issue 900, ADR 0394)
2026-10-16 14:50:03 +02:00

914 lines
25 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
// OwnedWord is where the controller names the paths a module owns on this node (novox/hq ADR 0394 §5): a JSON
// object from each absolute path to "module (tool)".
const OwnedWord = "MESH_OWNED_PATHS"
// ownedFrom reads the controller's object. Absent is not an error: the controller part may roll later than this
// module, and until it does, nothing is owned.
func ownedFrom(raw string) (map[string]string, error) {
owned := map[string]string{}
if strings.TrimSpace(raw) == "" {
return owned, nil
}
if err := json.Unmarshal([]byte(raw), &owned); err != nil {
return nil, fmt.Errorf("%s is not an object from a path to its owner: %v", OwnedWord, err)
}
for p := range owned {
if !strings.HasPrefix(p, "/") {
return nil, fmt.Errorf("%s names %q, which is not an absolute path", OwnedWord, p)
}
}
return owned, nil
}
// Ident is an owner and a group, by number.
type Ident struct{ UID, GID int }
func ownerOnDisk(p string) (Ident, error) {
info, err := os.Lstat(p)
if err != nil {
return Ident{}, err
}
st, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return Ident{}, fmt.Errorf("%s: this system does not say who owns a file", p)
}
return Ident{int(st.Uid), int(st.Gid)}, nil
}
// Files reads and manages one node's files (novox/hq ADR 0394). It acts as the account it runs as, and repeats
// an act through Root where that account's rights do not reach.
type Files struct {
Reader
// State is the module's state directory: the trash and the act log.
State string
// Owned maps a path a module owns on this node to "module (tool)".
Owned map[string]string
// Node is this node's name, for the notice of an override.
Node string
// Tell tells the operator a plain notice (operator-channel.tell).
Tell func(headline string, lines []string, subject string) error
// OwnerOf says who owns a file; Lchown changes it; NewID makes a trash id. Tests replace each.
OwnerOf func(path string) (Ident, error)
Lchown func(path string, uid, gid int) error
NewID func() string
mu sync.Mutex
}
// Act is who asks a write, and why.
type Act struct {
// Caller is the bus user the runtime proved (never an argument).
Caller string
// Reason is why the file is changed; required.
Reason string
// Override is a reason to change a path a module owns anyway.
Override string
}
// ActAnswer is what a write verb answers.
type ActAnswer struct {
Done string `json:"done"`
Paths []string `json:"paths"`
Trashed string `json:"trashed,omitempty"`
Overrides []string `json:"overridden,omitempty"`
Notice string `json:"notice,omitempty"`
}
var remote = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*:/`)
func notRemote(p string) error {
if remote.MatchString(strings.TrimSpace(p)) {
return fmt.Errorf("%s is on another node: copying between nodes comes in step 2 (issue 903)", p)
}
return nil
}
func (f *Files) ownerOf(p string) (Ident, error) {
if f.OwnerOf != nil {
return f.OwnerOf(p)
}
return ownerOnDisk(p)
}
func (f *Files) asRoot(bin string, args ...string) error {
if f.Root == nil {
return errors.New("this account may not do that, and sudo is not available to this holder")
}
_, err := f.Root.Run(bin, args...)
return err
}
func denied(err error) bool { return errors.Is(err, fs.ErrPermission) }
// lchown changes an owner (-1: leave as it is), through root where the account may not.
func (f *Files) lchown(path string, uid, gid int) error {
do := f.Lchown
if do == nil {
do = os.Lchown
}
err := do(path, uid, gid)
if err == nil || !denied(err) {
return err
}
spec := ""
if uid >= 0 {
spec = strconv.Itoa(uid)
}
if gid >= 0 {
spec += ":" + strconv.Itoa(gid)
}
if spec == "" {
return nil
}
return f.asRoot("/usr/bin/chown", spec, "--", path)
}
// chownTo gives a file an owner it has not got yet.
func (f *Files) chownTo(path string, id Ident) error {
if now, err := ownerOnDisk(path); err == nil && now == id {
return nil
}
return f.lchown(path, id.UID, id.GID)
}
func fileMode(perm uint32) fs.FileMode {
m := fs.FileMode(perm & 0o777)
if perm&0o4000 != 0 {
m |= fs.ModeSetuid
}
if perm&0o2000 != 0 {
m |= fs.ModeSetgid
}
if perm&0o1000 != 0 {
m |= fs.ModeSticky
}
return m
}
func permOf(m fs.FileMode) uint32 {
p := uint32(m.Perm())
if m&fs.ModeSetuid != 0 {
p |= 0o4000
}
if m&fs.ModeSetgid != 0 {
p |= 0o2000
}
if m&fs.ModeSticky != 0 {
p |= 0o1000
}
return p
}
// setMode sets a file's permission bits, through root where the account may not.
func (f *Files) setMode(path string, perm uint32) error {
err := os.Chmod(path, fileMode(perm))
if err == nil || !denied(err) {
return err
}
return f.asRoot("/usr/bin/chmod", fmt.Sprintf("%04o", perm), "--", path)
}
// ---- the owned paths (§5) and the act log (§6) ----
func within2(path, place string) bool { return within(filepath.Clean(path), filepath.Clean(place)) }
// guard refuses a change of what a module owns, unless the act gives an override. wide: the act also takes
// away or changes everything beneath the path, so a path that holds an owned one is owned too. It answers the
// owned paths the override went through.
func (f *Files) guard(a Act, wide bool, paths ...string) ([]hit, error) {
var hits []hit
for _, p := range paths {
for owned, owner := range f.Owned {
if within2(p, owned) || (wide && within2(owned, p)) {
if a.Override == "" {
return nil, fmt.Errorf("%s is owned by %s: change it with that, or give override with a reason "+
"(novox/hq ADR 0394 §5)", owned, owner)
}
hits = append(hits, hit{owned, owner})
}
}
}
return hits, nil
}
// hit is an owned path an override went through, and who owns it.
type hit struct{ Path, Owner string }
func (a Act) check() error {
if strings.TrimSpace(a.Reason) == "" {
return errors.New("reason is required: say why the file is changed, for the audit trail (novox/hq ADR 0394 §6)")
}
return nil
}
type actLine struct {
Time string `json:"time"`
Caller string `json:"caller"`
Verb string `json:"verb"`
Paths []string `json:"paths"`
Reason string `json:"reason"`
Override string `json:"override,omitempty"`
Owned []string `json:"owned,omitempty"`
Trashed string `json:"trashed,omitempty"`
}
// finish records a write act that was done: one line in the act log, and, for an override that was needed, a
// notice to the operator. A notice that cannot be sent is said in the answer, never swallowed.
func (f *Files) finish(a Act, verb string, hits []hit, ans ActAnswer) (ActAnswer, error) {
l := actLine{Time: f.Now().UTC().Format(time.RFC3339), Caller: a.Caller, Verb: verb, Paths: ans.Paths,
Reason: a.Reason, Trashed: ans.Trashed}
if len(hits) > 0 {
l.Override = a.Override
for _, h := range hits {
l.Owned = append(l.Owned, h.Path+" ("+h.Owner+")")
}
ans.Overrides = l.Owned
}
body, _ := json.Marshal(l)
if err := os.MkdirAll(f.State, 0o700); err != nil {
return ans, fmt.Errorf("%s was done, but the act log cannot be written: %v", verb, err)
}
log, err := os.OpenFile(filepath.Join(f.State, "acts.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
if err == nil {
_, err = log.Write(append(body, '\n'))
log.Close()
}
if err != nil {
return ans, fmt.Errorf("%s was done, but the act log cannot be written: %v", verb, err)
}
if len(hits) > 0 && f.Tell != nil {
// No path in the words: the operator channel holds its words to the content rule, which refuses one.
// The path is in the act log.
err := f.Tell("A module's file was changed by override",
[]string{fmt.Sprintf("%s owns it, and %s changed it anyway.", strings.Join(owners(hits), " and "), a.Caller),
"The reason given: " + a.Override},
"mesh-files on "+f.Node)
if err != nil {
ans.Notice = "the operator was not told: " + err.Error()
fmt.Fprintf(os.Stderr, "[mesh-files] %s\n", ans.Notice)
}
}
return ans, nil
}
func owners(hits []hit) []string {
seen := map[string]bool{}
var out []string
for _, h := range hits {
if !seen[h.Owner] {
seen[h.Owner] = true
out = append(out, h.Owner)
}
}
return out
}
// ---- the write verbs ----
func (f *Files) begin(a Act) error {
if err := a.check(); err != nil {
return err
}
f.sweep()
return nil
}
func parseMode(s string) (uint32, error) {
n, err := strconv.ParseUint(strings.TrimSpace(s), 8, 32)
if err != nil || n > 0o7777 {
return 0, fmt.Errorf("%q is not a mode such as 0644", s)
}
return uint32(n), nil
}
// Write writes one text file whole. The old file, if any, goes to the trash first; the new one keeps its owner
// and mode, or, when new, takes its folder's owner and group (ADR 0394 §3).
func (f *Files) Write(a Act, raw, content, mode string) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
path, err := absolute(raw)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, false, path)
if err != nil {
return ActAnswer{}, err
}
var perm uint32 = 0o644
asked := false
if strings.TrimSpace(mode) != "" {
if perm, err = parseMode(mode); err != nil {
return ActAnswer{}, err
}
asked = true
}
if _, err := os.Stat(filepath.Dir(path)); err != nil {
return ActAnswer{}, fmt.Errorf("the folder of %s cannot be reached: %s", path, why(err))
}
want, trashed := Ident{}, ""
if old, err := os.Lstat(path); err == nil {
if !old.Mode().IsRegular() {
return ActAnswer{}, fmt.Errorf("%s is a %s, not a file: write replaces a file whole", path, typeOf(old.Mode()))
}
if want, err = f.ownerOf(path); err != nil {
return ActAnswer{}, err
}
if !asked {
perm = permOf(old.Mode())
}
if trashed, err = f.toTrash(a, path); err != nil {
return ActAnswer{}, err
}
} else if want, err = f.ownerOf(filepath.Dir(path)); err != nil {
return ActAnswer{}, err
}
if err := f.create(path, content, perm); err != nil {
if trashed != "" {
if _, rerr := f.restore(trashed); rerr != nil {
err = fmt.Errorf("%v (and the old file could not be put back from the trash, id %s: %v)", err, trashed, rerr)
}
}
return ActAnswer{}, err
}
if err := f.chownTo(path, want); err != nil {
return ActAnswer{}, fmt.Errorf("%s was written, but its owner could not be set: %v", path, err)
}
if perm&0o6000 != 0 { // the chown cleared setuid and setgid
if err := f.setMode(path, perm); err != nil {
return ActAnswer{}, err
}
}
return f.finish(a, "write", hits, ActAnswer{Done: "wrote " + path, Paths: []string{path}, Trashed: trashed})
}
// create makes a new file with a content and a mode, as the account, or through root where the folder is not its.
func (f *Files) create(path, content string, perm uint32) error {
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err == nil {
_, err = io.WriteString(out, content)
if cerr := out.Close(); err == nil {
err = cerr
}
if err != nil {
os.Remove(path)
return err
}
return f.setMode(path, perm)
}
if !denied(err) {
return err
}
tmp := filepath.Join(f.State, "tmp-"+f.NewID())
if err := os.WriteFile(tmp, []byte(content), 0o600); err != nil {
return err
}
defer os.Remove(tmp)
if err := f.asRoot("/usr/bin/cp", "--", tmp, path); err != nil {
return err
}
return f.setMode(path, perm)
}
// copyTree copies a file, a link or a directory with what it holds, keeping modes, owners and times (cp -a).
func (f *Files) copyTree(from, to string) error {
info, err := os.Lstat(from)
if err != nil {
return err
}
id, err := f.ownerOf(from)
if err != nil {
return err
}
switch {
case info.Mode()&fs.ModeSymlink != 0:
target, err := os.Readlink(from)
if err != nil {
return err
}
if err := os.Symlink(target, to); err != nil {
return err
}
return f.chownTo(to, id)
case info.IsDir():
if err := os.Mkdir(to, 0o700); err != nil {
return err
}
entries, err := os.ReadDir(from)
if err != nil {
return err
}
for _, e := range entries {
if err := f.copyTree(filepath.Join(from, e.Name()), filepath.Join(to, e.Name())); err != nil {
return err
}
}
case info.Mode().IsRegular():
in, err := os.Open(from)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
_, err = io.Copy(out, in)
if cerr := out.Close(); err == nil {
err = cerr
}
if err != nil {
return err
}
default:
return fmt.Errorf("%s is a %s, which is not copied", from, typeOf(info.Mode()))
}
// The owner first: a chown clears setuid and setgid, which the mode then sets again.
if err := f.chownTo(to, id); err != nil {
return err
}
if err := f.setMode(to, permOf(info.Mode())); err != nil {
return err
}
return os.Chtimes(to, info.ModTime(), info.ModTime())
}
// relocate moves a path: a rename, a copy and removal across file systems, and, where the account may not, mv
// through root. It keeps owner and mode.
func (f *Files) relocate(from, to string) error {
err := os.Rename(from, to)
switch {
case err == nil:
return nil
case errors.Is(err, syscall.EXDEV):
if err := f.copyTree(from, to); err != nil {
os.RemoveAll(to)
return err
}
return f.removeAll(from)
case denied(err):
return f.asRoot("/usr/bin/mv", "--", from, to)
}
return err
}
func (f *Files) removeAll(p string) error {
err := os.RemoveAll(p)
if err != nil && denied(err) {
return f.asRoot("/usr/bin/rm", "-rf", "--", p)
}
return err
}
// target checks the new path of a copy or a move: on this node, in a folder that is there, and not the source,
// inside it or holding it. An existing target is overwritten by the caller.
func target(rawFrom, from, raw string) (string, error) {
if err := notRemote(raw); err != nil {
return "", err
}
to, err := absolute(raw)
if err != nil {
return "", err
}
if within(to, from) || within(from, to) {
return "", fmt.Errorf("%s and %s are the same path, or one is inside the other", from, to)
}
if _, err := os.Stat(filepath.Dir(to)); err != nil {
return "", fmt.Errorf("the folder of %s cannot be reached: %s", to, why(err))
}
return to, nil
}
// overwritten puts an existing target in the trash and says whose it was, so the new one can take its owner and
// mode; it is nothing for a target that is not there.
type overwritten struct {
trashed string
id Ident
perm uint32
was bool
}
func (f *Files) makeRoom(a Act, to string) (overwritten, error) {
info, err := os.Lstat(to)
if err != nil {
return overwritten{}, nil
}
id, err := f.ownerOf(to)
if err != nil {
return overwritten{}, err
}
tid, err := f.toTrash(a, to)
if err != nil {
return overwritten{}, err
}
return overwritten{trashed: tid, id: id, perm: permOf(info.Mode()), was: true}, nil
}
// settle gives the new target the overwritten one's owner and mode, or, if the act failed, puts the old one back.
func (f *Files) settle(o overwritten, to string, actErr error) error {
if !o.was {
return actErr
}
if actErr != nil {
if _, rerr := f.restore(o.trashed); rerr != nil {
return fmt.Errorf("%v (and the old target could not be put back from the trash, id %s: %v)", actErr, o.trashed, rerr)
}
return actErr
}
if err := f.chownTo(to, o.id); err != nil {
return fmt.Errorf("%s is there, but its owner could not be set: %v", to, err)
}
if err := f.setMode(to, o.perm); err != nil {
return fmt.Errorf("%s is there, but its mode could not be set: %v", to, err)
}
return nil
}
// Copy copies a file or a directory to a new path on this node, keeping owner and mode.
func (f *Files) Copy(a Act, rawFrom, rawTo string) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
if err := notRemote(rawFrom); err != nil {
return ActAnswer{}, err
}
from, err := absolute(rawFrom)
if err != nil {
return ActAnswer{}, err
}
to, err := target(rawFrom, from, rawTo)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, true, to) // an existing target is replaced whole, with what it holds
if err != nil {
return ActAnswer{}, err
}
if _, err := os.Lstat(from); err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be copied: %s", from, why(err))
}
room, err := f.makeRoom(a, to)
if err != nil {
return ActAnswer{}, err
}
err = f.copyTree(from, to)
if err != nil {
os.RemoveAll(to)
if denied(err) {
err = f.asRoot("/usr/bin/cp", "-a", "--", from, to)
}
if err != nil {
err = fmt.Errorf("%s cannot be copied: %v", from, err)
}
}
if err := f.settle(room, to, err); err != nil {
return ActAnswer{}, err
}
return f.finish(a, "copy", hits, ActAnswer{Done: "copied " + from + " to " + to, Paths: []string{from, to}, Trashed: room.trashed})
}
// Move moves or renames a file or a directory on this node, keeping owner and mode.
func (f *Files) Move(a Act, rawFrom, rawTo string) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
if err := notRemote(rawFrom); err != nil {
return ActAnswer{}, err
}
from, err := absolute(rawFrom)
if err != nil {
return ActAnswer{}, err
}
to, err := target(rawFrom, from, rawTo)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, true, from)
if err != nil {
return ActAnswer{}, err
}
more, err := f.guard(a, true, to)
if err != nil {
return ActAnswer{}, err
}
hits = append(hits, more...)
if _, err := os.Lstat(from); err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be moved: %s", from, why(err))
}
room, err := f.makeRoom(a, to)
if err != nil {
return ActAnswer{}, err
}
err = f.relocate(from, to)
if err != nil {
err = fmt.Errorf("%s cannot be moved: %v", from, err)
}
if err := f.settle(room, to, err); err != nil {
return ActAnswer{}, err
}
return f.finish(a, "move", hits, ActAnswer{Done: "moved " + from + " to " + to, Paths: []string{from, to}, Trashed: room.trashed})
}
// Delete moves a file or a directory into the node's trash.
func (f *Files) Delete(a Act, raw string, recursive bool) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
path, err := absolute(raw)
if err != nil {
return ActAnswer{}, err
}
if path == "/" {
return ActAnswer{}, errors.New("/ is not deleted")
}
hits, err := f.guard(a, true, path)
if err != nil {
return ActAnswer{}, err
}
info, err := os.Lstat(path)
if err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be deleted: %s", path, why(err))
}
if info.IsDir() && !recursive {
return ActAnswer{}, fmt.Errorf("%s is a directory: give recursive \"true\" to delete it with what it holds", path)
}
id, err := f.toTrash(a, path)
if err != nil {
return ActAnswer{}, err
}
return f.finish(a, "delete", hits, ActAnswer{Done: "deleted " + path + " into the trash", Paths: []string{path}, Trashed: id})
}
// Mkdir makes a directory, and with parents the missing ones above it; each takes its folder's owner.
func (f *Files) Mkdir(a Act, raw string, parents bool) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
path, err := absolute(raw)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, false, path)
if err != nil {
return ActAnswer{}, err
}
if _, err := os.Lstat(path); err == nil {
return ActAnswer{}, fmt.Errorf("%s is there already", path)
}
var missing []string // from the first missing folder down
for p := path; ; p = filepath.Dir(p) {
if _, err := os.Lstat(p); err == nil {
break
}
missing = append([]string{p}, missing...)
if p == "/" {
break
}
}
if len(missing) > 1 && !parents {
return ActAnswer{}, fmt.Errorf("the folder of %s is not there: give parents \"true\" to make it too", path)
}
var made []string
for i, p := range missing {
err := os.Mkdir(p, 0o755)
if err == nil {
made = append(made, p)
continue
}
if !denied(err) {
return ActAnswer{}, err
}
// Root makes this folder and, with parents, every one below it in the same call.
args := []string{"--", p}
if parents {
args = []string{"-p", "--", path}
}
if err := f.asRoot("/usr/bin/mkdir", args...); err != nil {
return ActAnswer{}, err
}
made = append(made, missing[i:]...)
break
}
for _, p := range made {
if err := f.setMode(p, 0o755); err != nil {
return ActAnswer{}, err
}
if err := f.dirOwner(p); err != nil {
return ActAnswer{}, err
}
}
return f.finish(a, "mkdir", hits, ActAnswer{Done: "made " + path, Paths: []string{path}})
}
// dirOwner gives a new folder the owner and group of the folder it is in.
func (f *Files) dirOwner(p string) error {
id, err := f.ownerOf(filepath.Dir(p))
if err != nil {
return err
}
return f.chownTo(p, id)
}
var symbolic = regexp.MustCompile(`^([ugoa]*)([-+=])([rwxXst]*)$`)
// newMode applies a mode as chmod says it: octal, or comma-separated clauses such as u+x,go-w.
func newMode(cur uint32, isDir bool, spec string) (uint32, error) {
spec = strings.TrimSpace(spec)
if m, err := parseMode(spec); err == nil {
return m, nil
}
for _, clause := range strings.Split(spec, ",") {
m := symbolic.FindStringSubmatch(clause)
if m == nil {
return 0, fmt.Errorf("%q is not a mode such as 0644 or u+x", spec)
}
who := m[1]
if who == "" || strings.Contains(who, "a") {
who = "ugo"
}
var bits, mask uint32
for _, c := range m[3] {
switch c {
case 'r':
bits |= 0o444
case 'w':
bits |= 0o222
case 'x':
bits |= 0o111
case 'X':
if isDir || cur&0o111 != 0 {
bits |= 0o111
}
case 's':
bits |= 0o6000
case 't':
bits |= 0o1000
}
}
for _, w := range who {
switch w {
case 'u':
mask |= 0o4700
case 'g':
mask |= 0o2070
case 'o':
mask |= 0o1007
}
}
bits &= mask
switch m[2] {
case "+":
cur |= bits
case "-":
cur &^= bits
case "=":
cur = cur&^(mask&0o777) | bits&0o777
}
}
return cur, nil
}
// walk lists a path and, when recursive, everything under it (links are not followed), children before parents.
func walk(path string, recursive bool) ([]string, error) {
if !recursive {
return []string{path}, nil
}
var all []string
err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
all = append(all, p)
return nil
})
for i, j := 0, len(all)-1; i < j; i, j = i+1, j-1 {
all[i], all[j] = all[j], all[i]
}
return all, err
}
// Chmod changes the mode of a path, and with recursive of everything under it.
func (f *Files) Chmod(a Act, raw, mode string, recursive bool) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
path, err := absolute(raw)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, recursive, path)
if err != nil {
return ActAnswer{}, err
}
if _, err := newMode(0, false, mode); err != nil {
return ActAnswer{}, err
}
all, err := walk(path, recursive)
if err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be changed: %v", path, err)
}
for _, p := range all {
info, err := os.Lstat(p)
if err != nil {
return ActAnswer{}, err
}
if info.Mode()&fs.ModeSymlink != 0 {
continue
}
m, _ := newMode(permOf(info.Mode()), info.IsDir(), mode)
if err := f.setMode(p, m); err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be changed: %v", p, err)
}
}
return f.finish(a, "chmod", hits, ActAnswer{Done: "set the mode of " + path + " to " + mode, Paths: []string{path}})
}
// number resolves an account or group, by name or number, through this node's account files.
func (f *Files) number(kind, v string) (int, error) {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
return n, nil
}
names := f.names()
if kind == "owner" {
for _, u := range names.users {
if u.Name == v {
return u.UID, nil
}
}
} else {
for _, g := range names.groups {
if g.Name == v {
return g.GID, nil
}
}
}
return 0, fmt.Errorf("%s %q is not an account or group of this node", kind, v)
}
// Chown changes the owner, and with it the group, of a path, and with recursive of everything under it.
func (f *Files) Chown(a Act, raw, owner, group string, recursive bool) (ActAnswer, error) {
f.mu.Lock()
defer f.mu.Unlock()
if err := f.begin(a); err != nil {
return ActAnswer{}, err
}
path, err := absolute(raw)
if err != nil {
return ActAnswer{}, err
}
hits, err := f.guard(a, recursive, path)
if err != nil {
return ActAnswer{}, err
}
uid, err := f.number("owner", owner)
if err != nil {
return ActAnswer{}, err
}
gid := -1
if strings.TrimSpace(group) != "" {
if gid, err = f.number("group", group); err != nil {
return ActAnswer{}, err
}
}
all, err := walk(path, recursive)
if err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be changed: %v", path, err)
}
for _, p := range all {
if err := f.lchown(p, uid, gid); err != nil {
return ActAnswer{}, fmt.Errorf("%s cannot be changed: %v", p, err)
}
}
return f.finish(a, "chown", hits, ActAnswer{Done: "gave " + path + " to " + owner, Paths: []string{path}})
}