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
914 lines
25 KiB
Go
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}})
|
|
}
|