Files
odboy 400812d114 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 同步更新
2026-11-02 22:42:18 +08:00

273 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 != ".."
}