feat: 新增 file/svc/archive/sys/log/fw 分组并扩充 net、proc
- file:findbig 按大小、findrecent 按修改时间、findname 按名称查找文件 - svc:systemctl / launchctl / sc 统一接口,status、enable、disable、logs - archive:按扩展名自动识别格式并解压,支持 tar.gz/tgz/tar.bz2/tar.xz/tar/zip/gz - sysinfo:OS、CPU、内存、磁盘一览 - log:journalctl / log show / wevtutil 搜索关键字与查看最近日志 - fw:ufw / firewalld / iptables / ALF / netsh 统一接口,status、allow、deny - net 新增 ping、trace(mtr 优先)、dns(dig 优先)、curl HTTP 诊断 - proc 新增 topmem、topcpu、killname - 危险操作(svc enable/disable、proc killname、fw allow/deny)需 --yes 确认 - 注册表、executor 派发表、cli 分组说明与 README 同步更新
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// runArchiveExtract implements `kopsc archive extract <file>`: auto-detect
|
||||
// archive format and extract to current directory (or --dir).
|
||||
func runArchiveExtract(ctx context.Context, req BuiltinRequest) error {
|
||||
opts, err := parseArchiveExtractArgs(req.Args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src := opts.file
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return fmt.Errorf("文件 %q 不存在: %w", src, err)
|
||||
}
|
||||
|
||||
dest := opts.dest
|
||||
if dest == "" {
|
||||
dest = "."
|
||||
}
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return fmt.Errorf("创建目标目录 %q 失败: %w", dest, err)
|
||||
}
|
||||
|
||||
lower := strings.ToLower(src)
|
||||
switch {
|
||||
case strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz"):
|
||||
return extractTarGz(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar.bz2") || strings.HasSuffix(lower, ".tbz2"):
|
||||
return extractTarViaPipe(ctx, "bzip2", "-dc", src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar.xz") || strings.HasSuffix(lower, ".txz"):
|
||||
return extractTarViaPipe(ctx, "xz", "-dc", src, dest, req)
|
||||
case strings.HasSuffix(lower, ".tar"):
|
||||
return extractTarFile(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".zip") || strings.HasSuffix(lower, ".jar") || strings.HasSuffix(lower, ".war"):
|
||||
return extractZip(src, dest, req)
|
||||
case strings.HasSuffix(lower, ".gz"):
|
||||
return extractGz(src, dest, req)
|
||||
default:
|
||||
return fmt.Errorf("无法识别的压缩格式: %q(支持 .tar.gz .tgz .tar.bz2 .tar.xz .tar .zip .gz)", src)
|
||||
}
|
||||
}
|
||||
|
||||
type archiveExtractOptions struct {
|
||||
file string
|
||||
dest string
|
||||
}
|
||||
|
||||
func parseArchiveExtractArgs(args []string) (archiveExtractOptions, error) {
|
||||
var opts archiveExtractOptions
|
||||
var positional []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--":
|
||||
positional = append(positional, args[i+1:]...)
|
||||
i = len(args)
|
||||
case arg == "--dir" || arg == "-d" || arg == "-C":
|
||||
if i+1 >= len(args) {
|
||||
return opts, fmt.Errorf("%s 缺少目标目录", arg)
|
||||
}
|
||||
i++
|
||||
opts.dest = args[i]
|
||||
case strings.HasPrefix(arg, "--dir="):
|
||||
opts.dest = strings.TrimPrefix(arg, "--dir=")
|
||||
case strings.HasPrefix(arg, "-") && arg != "-":
|
||||
return opts, fmt.Errorf("未知选项 %q", arg)
|
||||
default:
|
||||
positional = append(positional, arg)
|
||||
}
|
||||
}
|
||||
if len(positional) == 0 {
|
||||
return opts, fmt.Errorf("缺少压缩文件路径;用法: kopsc archive extract <文件>")
|
||||
}
|
||||
opts.file = positional[0]
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func extractTarGz(src, dest string, req BuiltinRequest) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 gzip 失败: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
fmt.Fprintf(req.Stdout, "解压 tar.gz: %s -> %s\n", src, dest)
|
||||
return writeTar(tar.NewReader(gz), dest, req)
|
||||
}
|
||||
|
||||
func extractTarFile(src, dest string, req BuiltinRequest) error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fmt.Fprintf(req.Stdout, "解压 tar: %s -> %s\n", src, dest)
|
||||
return writeTar(tar.NewReader(f), dest, req)
|
||||
}
|
||||
|
||||
func extractTarViaPipe(ctx context.Context, decompressor, flag, src, dest string, req BuiltinRequest) error {
|
||||
if _, err := exec.LookPath(decompressor); err != nil {
|
||||
return fmt.Errorf("解压 %s 需要 %s,请先安装", filepath.Ext(src), decompressor)
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "解压 %s: %s -> %s\n", decompressor, src, dest)
|
||||
cmd := exec.CommandContext(ctx, decompressor, flag, src)
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stderr = req.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("启动 %s 失败: %w", decompressor, err)
|
||||
}
|
||||
err = writeTar(tar.NewReader(stdoutPipe), dest, req)
|
||||
if waitErr := cmd.Wait(); waitErr != nil && err == nil {
|
||||
err = waitErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func writeTar(tr *tar.Reader, dest string, req BuiltinRequest) error {
|
||||
count := 0
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 tar 失败: %w", err)
|
||||
}
|
||||
target := filepath.Join(dest, header.Name)
|
||||
if !isWithinDir(target, dest) {
|
||||
return fmt.Errorf("tar 条目 %q 试图逃逸出目标目录", header.Name)
|
||||
}
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
count++
|
||||
case tar.TypeSymlink:
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
os.Remove(target)
|
||||
if err := os.Symlink(header.Linkname, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压 %d 个文件\n", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZip(src, dest string, req BuiltinRequest) error {
|
||||
fmt.Fprintf(req.Stdout, "解压 zip: %s -> %s\n", src, dest)
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 zip 失败: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
count := 0
|
||||
for _, f := range r.File {
|
||||
target := filepath.Join(dest, f.Name)
|
||||
if !isWithinDir(target, dest) {
|
||||
return fmt.Errorf("zip 条目 %q 试图逃逸出目标目录", f.Name)
|
||||
}
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if dir := filepath.Dir(target); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(out, rc)
|
||||
rc.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count++
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压 %d 个文件\n", count)
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractGz(src, dest string, req BuiltinRequest) error {
|
||||
fmt.Fprintf(req.Stdout, "解压 gz: %s -> %s\n", src, dest)
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
gz, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 gzip 失败: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
outName := strings.TrimSuffix(filepath.Base(src), ".gz")
|
||||
if outName == "" {
|
||||
outName = "output"
|
||||
}
|
||||
target := filepath.Join(dest, outName)
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, gz); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(req.Stdout, "已解压: %s\n", target)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWithinDir checks that target is inside dest (prevents zip-slip attacks).
|
||||
func isWithinDir(target, dest string) bool {
|
||||
rel, err := filepath.Rel(dest, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".."
|
||||
}
|
||||
Reference in New Issue
Block a user