From 485f3296d9876711f2d038d48a07f9b6bc07d38b Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 17 Jun 2026 01:41:09 +0200 Subject: [PATCH] Fix config-not-found errors, dev-build hint, unify output writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ResolveConfigPath now stats explicit paths from --config and $VAULTIK_CONFIG and produces an actionable error naming the bad path and suggesting 'vaultik config init' (with the right path in the --config case). The default-search failure message lists the paths it tried. The scanner no longer hard-codes os.Stdout vs io.Discard based on EnableProgress. ScannerConfig and ScannerParams take an explicit Output io.Writer, and the Vaultik caller passes v.Stdout — which itself is set to io.Discard in --cron mode. One knob controls both scanner-level and Vaultik-level user-facing output. The version command prints a hint when Version == "dev" telling the user this is a development build without embedded version metadata. --- internal/cli/root.go | 19 ++++++++++++++----- internal/cli/version.go | 6 ++++++ internal/snapshot/module.go | 4 ++++ internal/snapshot/scanner.go | 15 ++++++++------- internal/vaultik/snapshot.go | 1 + 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/internal/cli/root.go b/internal/cli/root.go index 3f090b9..59fe347 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/adrg/xdg" "github.com/spf13/cobra" @@ -63,13 +64,21 @@ func GetRootFlags() RootFlags { // ResolveConfigPath resolves the config file path from flags, environment, or default. // Search order: --config flag, VAULTIK_CONFIG env, XDG config dir, /etc/vaultik/config.yml. +// Explicit paths from --config and $VAULTIK_CONFIG are checked for existence +// so the user gets a clear error instead of a downstream YAML parser failure. func ResolveConfigPath() (string, error) { - if rootFlags.ConfigPath != "" { - return rootFlags.ConfigPath, nil + if path := rootFlags.ConfigPath; path != "" { + if _, err := os.Stat(path); err != nil { + return "", fmt.Errorf("config file from --config not found: %s (run 'vaultik config init --config %s' to create it)", path, path) + } + return path, nil } - if envPath := os.Getenv("VAULTIK_CONFIG"); envPath != "" { - return envPath, nil + if path := os.Getenv("VAULTIK_CONFIG"); path != "" { + if _, err := os.Stat(path); err != nil { + return "", fmt.Errorf("config file from $VAULTIK_CONFIG not found: %s (unset VAULTIK_CONFIG, point it at an existing file, or run 'vaultik config init')", path) + } + return path, nil } for _, path := range defaultConfigPaths() { @@ -78,7 +87,7 @@ func ResolveConfigPath() (string, error) { } } - return "", fmt.Errorf("no config file found; run 'vaultik config init' to create one, or specify with --config") + return "", fmt.Errorf("no config file found at %s (run 'vaultik config init' to create the default config, or pass --config )", strings.Join(defaultConfigPaths(), " or ")) } // defaultConfigPaths returns the ordered list of config paths to search. diff --git a/internal/cli/version.go b/internal/cli/version.go index 602cfac..2c632c2 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -20,6 +20,12 @@ func NewVersionCommand() *cobra.Command { fmt.Printf(" commit: %s\n", globals.Commit) fmt.Printf(" go: %s\n", runtime.Version()) fmt.Printf(" os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) + if globals.Version == "dev" { + fmt.Println() + fmt.Println("This is a development build (no version information embedded).") + fmt.Println("Build a release binary with 'make vaultik' or download from") + fmt.Println("https://sneak.berlin/go/vaultik for embedded version metadata.") + } }, } diff --git a/internal/snapshot/module.go b/internal/snapshot/module.go index c542772..44cd6a1 100644 --- a/internal/snapshot/module.go +++ b/internal/snapshot/module.go @@ -1,6 +1,8 @@ package snapshot import ( + "io" + "github.com/spf13/afero" "go.uber.org/fx" "sneak.berlin/go/vaultik/internal/config" @@ -11,6 +13,7 @@ import ( // ScannerParams holds parameters for scanner creation type ScannerParams struct { EnableProgress bool + Output io.Writer // Where one-off scanner messages go; nil disables them Fs afero.Fs Exclude []string // Exclude patterns (combined global + snapshot-specific) SkipErrors bool // Skip file read errors (log loudly but continue) @@ -46,6 +49,7 @@ func provideScannerFactory(cfg *config.Config, repos *database.Repositories, sto CompressionLevel: cfg.CompressionLevel, AgeRecipients: cfg.AgeRecipients, EnableProgress: params.EnableProgress, + Output: params.Output, Exclude: excludes, SkipErrors: params.SkipErrors, }) diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index 320b96a..78d7421 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -92,10 +92,11 @@ type ScannerConfig struct { Storage storage.Storer MaxBlobSize int64 CompressionLevel int - AgeRecipients []string // Optional, empty means no encryption - EnableProgress bool // Enable progress reporting - Exclude []string // Glob patterns for files/directories to exclude - SkipErrors bool // Skip file read errors (log loudly but continue) + AgeRecipients []string // Optional, empty means no encryption + EnableProgress bool // Enable the live progress reporter (ETAs, throughput) + Output io.Writer // Where one-off scanner messages go; nil disables them + Exclude []string // Glob patterns for files/directories to exclude + SkipErrors bool // Skip file read errors (log loudly but continue) } // ScanResult contains the results of a scan operation @@ -142,9 +143,9 @@ func NewScanner(cfg ScannerConfig) *Scanner { // Compile exclude patterns compiledExclude := compileExcludePatterns(cfg.Exclude) - output := io.Writer(io.Discard) - if cfg.EnableProgress { - output = os.Stdout + output := cfg.Output + if output == nil { + output = io.Discard } return &Scanner{ diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 22cba97..3396fdd 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -156,6 +156,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna scanner := v.ScannerFactory(snapshot.ScannerParams{ EnableProgress: !opts.Cron, + Output: v.Stdout, Fs: v.Fs, Exclude: v.Config.GetExcludes(snapName), SkipErrors: opts.SkipErrors,