Files
lpra/internal/agent/fs.go
T

276 lines
6.7 KiB
Go

package agent
import (
"archive/zip"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"sort"
"strings"
"unicode/utf8"
"github.com/x-grk/longpolling-remote-access/internal/proto"
)
// fsReadCap bounds how many bytes a view/read returns.
const fsReadCap = proto.FSReadCap
// fsDownloadCap bounds a browser download (kept under the 64 MiB result/body
// limit once base64-encoded).
const fsDownloadCap = 40 * 1024 * 1024
// dispatchFS runs one native file-manager operation.
func dispatchFS(env proto.TaskEnvelope) (any, error) {
switch env.Type {
case proto.TaskFSList:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return fsList(a.Path)
case proto.TaskFSRead:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return fsReadCapped(a.Path, fsReadCap)
case proto.TaskFSDownload:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return fsReadCapped(a.Path, fsDownloadCap)
case proto.TaskFSWrite:
var a proto.FSWriteArgs
_ = json.Unmarshal(env.Args, &a)
return fsWrite(a.Path, a.Data)
case proto.TaskFSMkdir:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return map[string]string{"path": a.Path}, os.MkdirAll(a.Path, 0o755)
case proto.TaskFSDelete:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return map[string]string{"path": a.Path}, os.Remove(a.Path)
case proto.TaskFSDelTree:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
if a.Path == "" || a.Path == "/" {
return nil, errors.New("отказ: удаление корня запрещено")
}
return map[string]string{"path": a.Path}, os.RemoveAll(a.Path)
case proto.TaskFSMove:
var a proto.FSMoveArgs
_ = json.Unmarshal(env.Args, &a)
return map[string]string{"src": a.Src, "dst": a.Dst}, os.Rename(a.Src, a.Dst)
case proto.TaskFSUnzip:
var a proto.FSUnzipArgs
_ = json.Unmarshal(env.Args, &a)
return fsUnzip(a.Path, a.Dest)
case proto.TaskFSZip:
var a proto.FSZipArgs
_ = json.Unmarshal(env.Args, &a)
return fsZip(a.Paths, a.Dest)
case proto.TaskFSStat:
var a proto.FSPathArgs
_ = json.Unmarshal(env.Args, &a)
return fsStat(a.Path)
case proto.TaskFSChmod:
var a proto.FSChmodArgs
_ = json.Unmarshal(env.Args, &a)
return fsChmod(a)
case proto.TaskFSCopy:
var a proto.FSCopyArgs
_ = json.Unmarshal(env.Args, &a)
return fsCopy(a)
default:
return nil, fmt.Errorf("неизвестная операция %q", env.Type)
}
}
func fsList(path string) (proto.FSListResult, error) {
if path == "" {
path = "/"
}
// "~" / "~name" is where the file manager starts: the home directory, or
// the root when there is none to open.
home := strings.HasPrefix(path, "~") && !strings.Contains(path, "/")
if home {
path = homeDir(path[1:])
}
abs, _ := filepath.Abs(path)
entries, err := os.ReadDir(abs)
if err != nil && home && abs != "/" {
abs = "/"
entries, err = os.ReadDir(abs)
}
if err != nil {
return proto.FSListResult{}, err
}
out := make([]proto.FSEntry, 0, len(entries))
for _, e := range entries {
info, err := e.Info()
if err != nil {
continue
}
out = append(out, proto.FSEntry{
Name: e.Name(),
IsDir: e.IsDir(),
Size: info.Size(),
Mode: info.Mode().String(),
MTime: info.ModTime().Unix(),
})
}
// Directories first, then alphabetical.
sort.Slice(out, func(i, j int) bool {
if out[i].IsDir != out[j].IsDir {
return out[i].IsDir
}
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
})
return proto.FSListResult{Path: abs, Entries: out}, nil
}
func fsReadCapped(path string, cap int) (proto.FSReadResult, error) {
info, err := os.Stat(path)
if err != nil {
return proto.FSReadResult{}, err
}
if info.IsDir() {
return proto.FSReadResult{}, errors.New("это каталог")
}
f, err := os.Open(path)
if err != nil {
return proto.FSReadResult{}, err
}
defer f.Close()
buf := make([]byte, cap+1)
n, _ := io.ReadFull(f, buf)
truncated := n > cap
if truncated {
n = cap
}
data := buf[:n]
binary := !utf8.Valid(data) || containsNUL(data)
return proto.FSReadResult{
Path: path,
Size: info.Size(),
Content: base64.StdEncoding.EncodeToString(data),
Truncated: truncated,
Binary: binary,
}, nil
}
func containsNUL(b []byte) bool {
for _, c := range b {
if c == 0 {
return true
}
}
return false
}
func fsWrite(path, dataB64 string) (any, error) {
data, err := base64.StdEncoding.DecodeString(dataB64)
if err != nil {
return nil, err
}
if err := os.WriteFile(path, data, 0o644); err != nil {
return nil, err
}
return map[string]any{"path": path, "size": len(data)}, nil
}
func fsUnzip(path, dest string) (any, error) {
if dest == "" {
dest = filepath.Dir(path)
}
bin, err := exec.LookPath("unzip")
if err != nil {
return nil, errors.New("unzip не установлен на клиенте")
}
out, err := exec.Command(bin, "-o", path, "-d", dest).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unzip: %v: %s", err, strings.TrimSpace(string(out)))
}
return map[string]string{"dest": dest}, nil
}
// fsZip creates dest as a zip archive of the given paths (files and dirs).
func fsZip(paths []string, dest string) (any, error) {
if dest == "" {
return nil, errors.New("не указан путь архива")
}
if len(paths) == 0 {
return nil, errors.New("не выбраны файлы")
}
f, err := os.Create(dest)
if err != nil {
return nil, err
}
defer f.Close()
zw := zip.NewWriter(f)
defer zw.Close()
for _, p := range paths {
parent := filepath.Dir(p)
err := filepath.Walk(p, func(fp string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(parent, fp)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if info.IsDir() {
_, err := zw.Create(rel + "/")
return err
}
w, err := zw.Create(rel)
if err != nil {
return err
}
src, err := os.Open(fp)
if err != nil {
return err
}
defer src.Close()
_, err = io.Copy(w, src)
return err
})
if err != nil {
return nil, err
}
}
return map[string]string{"dest": dest}, nil
}
// homeDir returns the home directory of the named user ("" = the user the
// agent runs as), or "/" when it cannot be told.
func homeDir(name string) string {
if name != "" {
if validUserName(name) {
if u, err := user.Lookup(name); err == nil && u.HomeDir != "" {
return u.HomeDir
}
}
return "/"
}
if u, err := user.Current(); err == nil && u.HomeDir != "" {
return u.HomeDir
}
if h := os.Getenv("HOME"); h != "" {
return h
}
// A root service often has neither a passwd entry we can read nor $HOME.
if os.Geteuid() == 0 {
if runtime.GOOS == "darwin" {
return "/var/root"
}
return "/root"
}
return "/"
}