feat: file and folder properties - mode, owner and group, applied to a folder's contents too (0.0.8)

This commit is contained in:
redtea committed 2026-10-14 19:31:25 +03:00
1 parent 8eb2bbd35d
commit 65359f5497
6 files changed
+337 -2

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
package agent
import (
"bufio"
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
"github.com/x-grk/longpolling-remote-access/internal/proto"
)
// fsStat reports a file's properties, following a symlink to what it points
// at, and the user and group names to choose an owner from.
func fsStat(path string) (proto.FSStatResult, error) {
res := proto.FSStatResult{Path: path}
if li, err := os.Lstat(path); err == nil && li.Mode()&os.ModeSymlink != 0 {
res.Link, _ = os.Readlink(path)
}
info, err := os.Stat(path)
if err != nil {
return res, err
}
res.IsDir, res.Size, res.MTime = info.IsDir(), info.Size(), info.ModTime().Unix()
if st, ok := info.Sys().(*syscall.Stat_t); ok {
res.Mode = uint32(st.Mode) & 07777
res.UID, res.GID = int(st.Uid), int(st.Gid)
} else {
res.Mode = uint32(info.Mode().Perm())
}
res.Owner, res.Group = strconv.Itoa(res.UID), strconv.Itoa(res.GID)
if u, err := user.LookupId(res.Owner); err == nil {
res.Owner = u.Username
}
if g, err := user.LookupGroupId(res.Group); err == nil {
res.Group = g.Name
}
res.Users, res.Groups = namesIn("/etc/passwd"), namesIn("/etc/group")
return res, nil
}
// namesIn lists the first field of a passwd- or group-style file.
func namesIn(file string) []string {
f, err := os.Open(file)
if err != nil {
return nil
}
defer f.Close()
var out []string
sc := bufio.NewScanner(f)
for sc.Scan() && len(out) < 1000 {
line := sc.Text()
if line == "" || line[0] == '#' {
continue
}
if name, _, ok := strings.Cut(line, ":"); ok && name != "" {
out = append(out, name)
}
}
sort.Strings(out)
return out
}
// lookupID resolves a user or group given by name or number; -1 for none.
func lookupID(name string, group bool) (int, error) {
if name == "" {
return -1, nil
}
if n, err := strconv.Atoi(name); err == nil && n >= 0 {
return n, nil
}
if group {
g, err := user.LookupGroup(name)
if err != nil {
return -1, fmt.Errorf("группа %s не найдена", name)
}
return strconv.Atoi(g.Gid)
}
u, err := user.Lookup(name)
if err != nil {
return -1, fmt.Errorf("пользователь %s не найден", name)
}
return strconv.Atoi(u.Uid)
}
// fsChmod sets mode, owner and group on a path, and with Recursive on all
// that is inside a folder. Symlinks inside are not followed.
func fsChmod(a proto.FSChmodArgs) (any, error) {
if a.Path == "" {
return nil, errors.New("не указан путь")
}
if a.Recursive && filepath.Clean(a.Path) == "/" {
return nil, errors.New("отказ: рекурсивно для корня нельзя")
}
if a.Mode != nil && *a.Mode > 07777 {
return nil, errors.New("неверные права")
}
uid, err := lookupID(a.Owner, false)
if err != nil {
return nil, err
}
gid, err := lookupID(a.Group, true)
if err != nil {
return nil, err
}
changed, failed := 0, 0
var errs []string
// The path itself always gets the change; Only picks among what is in it.
apply := func(p string, isDir, isLink, self bool) {
if !self && ((a.Only == "files" && isDir) || (a.Only == "dirs" && !isDir)) {
return
}
var err error
if uid >= 0 || gid >= 0 {
err = os.Lchown(p, uid, gid)
}
// A symlink has no mode of its own: chmod would change its target.
if err == nil && a.Mode != nil && !isLink {
err = syscall.Chmod(p, *a.Mode)
}
if err != nil {
failed++
if len(errs) < 5 {
errs = append(errs, err.Error())
}
return
}
changed++
}
info, err := os.Stat(a.Path)
if err != nil {
return nil, err
}
if !a.Recursive || !info.IsDir() {
apply(a.Path, info.IsDir(), false, true)
} else {
err = filepath.WalkDir(a.Path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
failed++
if len(errs) < 5 {
errs = append(errs, err.Error())
}
return nil
}
apply(p, d.IsDir(), d.Type()&fs.ModeSymlink != 0, p == a.Path)
return nil
})
if err != nil {
return nil, err
}
}
res := map[string]any{"changed": changed, "failed": failed, "errors": errs}
if failed > 0 && changed == 0 {
return res, errors.New(errs[0])
}
return res, nil
}