164 lines
4.1 KiB
Go
164 lines
4.1 KiB
Go
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
|
|
}
|