package builtin import ( "context" "fmt" "github.com/vladimir/goherence/internal/module" "github.com/vladimir/goherence/internal/ssh" ) // FileModule — аналог ansible.builtin.file: управляет состоянием пути // (директория/файл/отсутствие) и правами доступа. Идемпотентен через `stat`. type FileModule struct{} func (m *FileModule) Name() string { return "file" } func (m *FileModule) Run(ctx context.Context, in module.Input, conn *ssh.Conn) (module.Result, error) { path, ok := in.Args["path"].(string) if !ok { return module.Result{}, fmt.Errorf("file: аргумент path обязателен") } state, _ := in.Args["state"].(string) if state == "" { state = "file" } mode, _ := in.Args["mode"].(string) current, err := m.stat(conn, path) if err != nil { return module.Result{}, err } changed := false var action string switch state { case "directory": if current != "directory" { action = fmt.Sprintf("mkdir -p %s", shArg(path)) changed = true } case "absent": if current != "absent" { action = fmt.Sprintf("rm -rf %s", shArg(path)) changed = true } case "touch": if current == "absent" { action = fmt.Sprintf("touch %s", shArg(path)) changed = true } default: return module.Result{}, fmt.Errorf("file: неподдерживаемый state %q (первая версия: directory/absent/touch)", state) } if mode != "" && current != "absent" { if action != "" { action += " && " } action += fmt.Sprintf("chmod %s %s", mode, shArg(path)) changed = true // не проверяем текущий режим отдельно в первой версии — упрощение } if !changed { return module.Result{SchemaVersion: module.SchemaVersion, Changed: false, Msg: "уже в нужном состоянии"}, nil } if in.CheckMode { return module.Result{SchemaVersion: module.SchemaVersion, Changed: true, Msg: "check mode: " + action}, nil } res, err := conn.Run(action) if err != nil { return module.Result{}, err } if res.ExitCode != 0 { return module.Result{SchemaVersion: module.SchemaVersion, Failed: true, Msg: fmt.Sprintf("команда завершилась с кодом %d: %s", res.ExitCode, res.Stderr)}, nil } return module.Result{SchemaVersion: module.SchemaVersion, Changed: true, Msg: action}, nil } func (m *FileModule) stat(conn *ssh.Conn, path string) (string, error) { res, err := conn.Run(fmt.Sprintf( `test -d %s && echo directory || (test -e %s && echo file || echo absent)`, shArg(path), shArg(path))) if err != nil { return "", err } switch { case containsTrim(res.Stdout, "directory"): return "directory", nil case containsTrim(res.Stdout, "absent"): return "absent", nil default: return "file", nil } } func containsTrim(s, sub string) bool { return len(s) >= len(sub) && (s == sub || s == sub+"\n") }