Skip to content

Commit 5ed1111

Browse files
committed
Add --debug flag and per-command timeouts for restic
1 parent 1a2c5a6 commit 5ed1111

File tree

3 files changed

+47
-16
lines changed

3 files changed

+47
-16
lines changed

internal/backup/backup.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func NewOrchestrator(cfg *config.Config, log *logger.Logger) *Orchestrator {
2929
}
3030
}
3131

32+
func (o *Orchestrator) SetDebug(debug bool) {
33+
o.runner.Debug = debug
34+
}
35+
3236
// Run executes the full backup pipeline:
3337
// preflight -> unlock -> init -> backup (with retry) -> check -> forget -> notify
3438
func (o *Orchestrator) Run(ctx context.Context) error {

internal/restic/restic.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ const (
2121
)
2222

2323
type Runner struct {
24-
cfg *config.Config
25-
log *logger.Logger
24+
cfg *config.Config
25+
log *logger.Logger
26+
Debug bool
2627
}
2728

2829
type Result struct {
@@ -36,10 +37,10 @@ func NewRunner(cfg *config.Config, log *logger.Logger) *Runner {
3637
return &Runner{cfg: cfg, log: log}
3738
}
3839

39-
// redactedEnv returns the restic env vars with secrets masked, formatted as
40-
// a copy-pasteable command prefix.
41-
func (r *Runner) redactedEnv() string {
42-
redact := map[string]bool{
40+
// formatEnv returns the restic env vars formatted as a copy-pasteable command prefix.
41+
// If redact is true, secrets are masked with ***.
42+
func (r *Runner) formatEnv(redact bool) string {
43+
secretKeys := map[string]bool{
4344
"RESTIC_PASSWORD": true,
4445
"AWS_ACCESS_KEY_ID": true,
4546
"AWS_SECRET_ACCESS_KEY": true,
@@ -58,7 +59,7 @@ func (r *Runner) redactedEnv() string {
5859
default:
5960
continue
6061
}
61-
if redact[key] {
62+
if redact && secretKeys[key] {
6263
parts = append(parts, fmt.Sprintf("%s=***", key))
6364
} else {
6465
parts = append(parts, pair)
@@ -67,18 +68,35 @@ func (r *Runner) redactedEnv() string {
6768
return strings.Join(parts, " ")
6869
}
6970

71+
// commandTimeout returns a reasonable timeout based on the restic subcommand.
72+
// Short commands (unlock, version, snapshots, stats, init) get 2 minutes.
73+
// Long commands (backup, check, forget) get 4 hours.
74+
func commandTimeout(args []string) time.Duration {
75+
if len(args) > 0 {
76+
switch args[0] {
77+
case "backup", "check", "forget":
78+
return 4 * time.Hour
79+
}
80+
}
81+
return 2 * time.Minute
82+
}
83+
7084
// run executes a restic command with the configured environment.
7185
func (r *Runner) run(ctx context.Context, args ...string) (*Result, error) {
7286
start := time.Now()
7387

88+
timeout := commandTimeout(args)
89+
ctx, cancel := context.WithTimeout(ctx, timeout)
90+
defer cancel()
91+
7492
cmd := exec.CommandContext(ctx, r.cfg.ResticBinary, args...)
7593
cmd.Env = r.cfg.ResticEnv()
7694

7795
var stdout, stderr bytes.Buffer
7896
cmd.Stdout = &stdout
7997
cmd.Stderr = &stderr
8098

81-
fullCmd := fmt.Sprintf("%s %s %s", r.redactedEnv(), r.cfg.ResticBinary, strings.Join(args, " "))
99+
fullCmd := fmt.Sprintf("%s %s %s", r.formatEnv(!r.Debug), r.cfg.ResticBinary, strings.Join(args, " "))
82100
r.log.Info("running restic", map[string]any{
83101
"cmd": fullCmd,
84102
})

main.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func main() {
6464
// All other commands need a config file
6565
fs := flag.NewFlagSet(cmd, flag.ExitOnError)
6666
configPath := fs.String("config", config.DefaultConfigPath(), "path to config file")
67+
debug := fs.Bool("debug", false, "print full commands with credentials (for manual testing)")
6768
fs.Parse(os.Args[2:])
6869

6970
cfg, err := config.Load(*configPath)
@@ -79,11 +80,11 @@ func main() {
7980

8081
switch cmd {
8182
case "backup":
82-
runBackup(ctx, cfg, log)
83+
runBackup(ctx, cfg, log, *debug)
8384
case "check":
84-
runCheck(ctx, cfg, log)
85+
runCheck(ctx, cfg, log, *debug)
8586
case "status":
86-
runStatus(ctx, cfg, log)
87+
runStatus(ctx, cfg, log, *debug)
8788
case "install":
8889
runInstall(cfg, log, *configPath)
8990
case "uninstall":
@@ -95,7 +96,13 @@ func main() {
9596
}
9697
}
9798

98-
func runBackup(ctx context.Context, cfg *config.Config, log *logger.Logger) {
99+
func newRunner(cfg *config.Config, log *logger.Logger, debug bool) *restic.Runner {
100+
r := restic.NewRunner(cfg, log)
101+
r.Debug = debug
102+
return r
103+
}
104+
105+
func runBackup(ctx context.Context, cfg *config.Config, log *logger.Logger, debug bool) {
99106
// Acquire process lock — auto-clears stale locks from dead processes
100107
lock, err := lockfile.New(0)
101108
if err != nil {
@@ -109,14 +116,15 @@ func runBackup(ctx context.Context, cfg *config.Config, log *logger.Logger) {
109116
defer lock.Release()
110117

111118
orch := backup.NewOrchestrator(cfg, log)
119+
orch.SetDebug(debug)
112120
if err := orch.Run(ctx); err != nil {
113121
log.Error("backup failed", map[string]any{"error": err.Error()})
114122
os.Exit(1)
115123
}
116124
}
117125

118-
func runCheck(ctx context.Context, cfg *config.Config, log *logger.Logger) {
119-
runner := restic.NewRunner(cfg, log)
126+
func runCheck(ctx context.Context, cfg *config.Config, log *logger.Logger, debug bool) {
127+
runner := newRunner(cfg, log, debug)
120128

121129
log.Info("running full integrity check with data read")
122130
result, err := runner.Check(ctx, 100) // full read
@@ -138,8 +146,8 @@ func runCheck(ctx context.Context, cfg *config.Config, log *logger.Logger) {
138146
log.Info("integrity check passed — all data verified")
139147
}
140148

141-
func runStatus(ctx context.Context, cfg *config.Config, log *logger.Logger) {
142-
runner := restic.NewRunner(cfg, log)
149+
func runStatus(ctx context.Context, cfg *config.Config, log *logger.Logger, debug bool) {
150+
runner := newRunner(cfg, log, debug)
143151

144152
fmt.Println("=== Repository Snapshots ===")
145153
result, err := runner.Snapshots(ctx)
@@ -202,6 +210,7 @@ Commands:
202210
203211
Flags:
204212
--config Path to config JSON file (default: restic-sentry.json next to binary)
213+
--debug Print full restic commands with credentials visible (for manual testing)
205214
206215
Examples:
207216
restic-sentry install-restic # download restic

0 commit comments

Comments
 (0)