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}