exec.go

  1package restic
  2
  3import (
  4	"fmt"
  5	"os"
  6	"os/exec"
  7	"strings"
  8	"syscall"
  9
 10	"git.secluded.site/keld/internal/config"
 11)
 12
 13// DefaultExecutable is the restic binary name used when KELD_EXECUTABLE is
 14// unset.
 15const DefaultExecutable = "restic"
 16
 17// Run replaces the current process with restic, configured according to cfg.
 18func Run(cfg *config.ResolvedConfig) error {
 19	exe := executable()
 20
 21	path, err := exec.LookPath(exe)
 22	if err != nil {
 23		return fmt.Errorf("finding %s: %w", exe, err)
 24	}
 25
 26	if cfg.Workdir != "" {
 27		if err := os.Chdir(cfg.Workdir); err != nil {
 28			return fmt.Errorf("chdir %s: %w", cfg.Workdir, err)
 29		}
 30	}
 31
 32	argv := buildArgv(exe, cfg)
 33	env := buildEnv(cfg.Environ)
 34
 35	return syscall.Exec(path, argv, env)
 36}
 37
 38// DryRun formats a human-readable summary of what Run would execute.
 39func DryRun(cfg *config.ResolvedConfig) string {
 40	var b strings.Builder
 41
 42	if len(cfg.SectionsRead) > 0 {
 43		fmt.Fprintf(&b, "config sections: %s\n", strings.Join(cfg.SectionsRead, " → "))
 44	}
 45
 46	if cfg.Workdir != "" {
 47		fmt.Fprintf(&b, "workdir: %s\n", cfg.Workdir)
 48	}
 49
 50	if len(cfg.Environ) > 0 {
 51		fmt.Fprintln(&b, "environ:")
 52		for k, v := range cfg.Environ {
 53			fmt.Fprintf(&b, "  %s=%s\n", k, v)
 54		}
 55	}
 56
 57	argv := buildArgv(executable(), cfg)
 58	fmt.Fprintf(&b, "command: %s\n", quotedJoin(argv))
 59
 60	return b.String()
 61}
 62
 63// executable returns the restic binary name, respecting KELD_EXECUTABLE.
 64func executable() string {
 65	if e := os.Getenv("KELD_EXECUTABLE"); e != "" {
 66		return config.ExpandPath(e)
 67	}
 68	return DefaultExecutable
 69}
 70
 71// buildArgv assembles the full argument vector for the restic process.
 72func buildArgv(exe string, cfg *config.ResolvedConfig) []string {
 73	argv := []string{exe}
 74
 75	if cfg.Command != "" {
 76		argv = append(argv, cfg.Command)
 77	}
 78
 79	for _, f := range cfg.Flags {
 80		argv = append(argv, f.Name)
 81		if f.Value != "" {
 82			argv = append(argv, f.Value)
 83		}
 84	}
 85
 86	argv = append(argv, cfg.Arguments...)
 87	return argv
 88}
 89
 90// buildEnv merges extra environment variables into the current process env.
 91func buildEnv(extra map[string]string) []string {
 92	env := os.Environ()
 93	for k, v := range extra {
 94		env = append(env, k+"="+v)
 95	}
 96	return env
 97}
 98
 99// quotedJoin formats an argv slice by wrapping every element in double
100// quotes. This makes the dry-run output unambiguous — each argument
101// boundary is visible even when values contain spaces.
102func quotedJoin(argv []string) string {
103	var b strings.Builder
104	for i, arg := range argv {
105		if i > 0 {
106			b.WriteByte(' ')
107		}
108		b.WriteByte('"')
109		b.WriteString(arg)
110		b.WriteByte('"')
111	}
112	return b.String()
113}