// Package vaultik implements the core backup, restore, verify, prune, // and snapshot-management operations behind the vaultik CLI. package vaultik import ( "bytes" "context" "fmt" "io" "os" "github.com/spf13/afero" "go.uber.org/fx" "sneak.berlin/go/vaultik/internal/config" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/globals" "sneak.berlin/go/vaultik/internal/snapshot" "sneak.berlin/go/vaultik/internal/storage" "sneak.berlin/go/vaultik/internal/ui" ) // Vaultik contains all dependencies needed for vaultik operations type Vaultik struct { Globals *globals.Globals Config *config.Config DB *database.DB Repositories *database.Repositories Storage storage.Storer ScannerFactory snapshot.ScannerFactory SnapshotManager *snapshot.SnapshotManager Shutdowner fx.Shutdowner Fs afero.Fs // Context management ctx context.Context //nolint:containedctx // ctx bound at construction by design cancel context.CancelFunc // IO. Stdout carries the output the user asked for and nothing else, // so that `--json | jq` works. Stderr completes the standard triple // for anything a command needs to write there directly; diagnostics // are not that — they go through internal/log, which writes to the // process's stderr. No production code writes to Stderr today, so // searching for its writers turns up nothing; it is kept as the // injection point a direct stderr write would otherwise have to // invent, and removing it would make the triple asymmetric for no // gain. Stdout io.Writer Stderr io.Writer Stdin io.Reader // UI is the writer for user-facing status, progress, warnings, errors. // See package internal/ui for formatting conventions. It always wraps // Stdout and is never swapped out; under --cron (and --quiet) the cli // layer instead calls UI.SetQuiet(true), which drops Begin, Complete, // Info, Notice, Detail, Progress, and Banner messages. Warning and // Error are still emitted in that mode, so callers must not assume // that --cron makes this writer silent. UI *ui.Writer // restoreCacheObserver, if non-nil, is invoked once with the // restore-side blob disk cache immediately after the cache is // created and again immediately before it is closed. Only // internal-package tests set this; the type is unexported so // callers outside this package can't reach it. restoreCacheObserver func(*blobDiskCache) } // Params contains all parameters for New that can be provided by fx type Params struct { fx.In Globals *globals.Globals Config *config.Config DB *database.DB Repositories *database.Repositories Storage storage.Storer ScannerFactory snapshot.ScannerFactory SnapshotManager *snapshot.SnapshotManager Shutdowner fx.Shutdowner Fs afero.Fs `optional:"true"` } // New creates a new Vaultik instance with proper context management // It automatically includes crypto capabilities if age_secret_key is configured func New(params Params) *Vaultik { ctx, cancel := context.WithCancel(context.Background()) // Use provided filesystem or default to OS filesystem fs := params.Fs if fs == nil { fs = afero.NewOsFs() } // Set filesystem on SnapshotManager params.SnapshotManager.SetFilesystem(fs) return &Vaultik{ Globals: params.Globals, Config: params.Config, DB: params.DB, Repositories: params.Repositories, Storage: params.Storage, ScannerFactory: params.ScannerFactory, SnapshotManager: params.SnapshotManager, Shutdowner: params.Shutdowner, Fs: fs, ctx: ctx, cancel: cancel, Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, UI: ui.New(os.Stdout), } } // Context returns the Vaultik's context func (v *Vaultik) Context() context.Context { return v.ctx } // SetContext sets the Vaultik's context (primarily for testing) func (v *Vaultik) SetContext(ctx context.Context) { v.ctx = ctx } // Cancel cancels the Vaultik's context func (v *Vaultik) Cancel() { v.cancel() } // StartOperation runs fn in its own goroutine and returns a stop // function. fn is the command being run (a restore, verify, prune, and // so on); it observes cancellation through the Vaultik context and // removes its decrypted scratch files (the blob cache and the temporary // snapshot database) from the temp directory as it unwinds. // // Calling stop cancels the Vaultik context and then blocks until fn has // returned — so that unwinding, and the cleanup it does, completes // before the caller proceeds — or until the passed context is done, // whichever comes first. It reports whether fn returned before that // deadline. A signal-driven shutdown must call stop before the process // exits; otherwise the process can exit mid-operation and leave // decrypted data behind. func (v *Vaultik) StartOperation(fn func()) func(context.Context) bool { done := make(chan struct{}) go func() { defer close(done) fn() }() return func(ctx context.Context) bool { v.Cancel() select { case <-done: return true case <-ctx.Done(): return false } } } // CanDecrypt returns true if this Vaultik instance has decryption capabilities func (v *Vaultik) CanDecrypt() bool { return v.Config.AgeSecretKey != "" } // GetFilesystem returns the filesystem instance used by Vaultik // //nolint:ireturn // afero.Fs is the filesystem abstraction by design func (v *Vaultik) GetFilesystem() afero.Fs { return v.Fs } // stdoutf writes formatted output to stdout. func (v *Vaultik) stdoutf(format string, args ...any) { _, _ = fmt.Fprintf(v.Stdout, format, args...) } // printlnStdout writes a line to stdout. func (v *Vaultik) printlnStdout(args ...any) { _, _ = fmt.Fprintln(v.Stdout, args...) } // scanStdin reads a line of input from stdin. func (v *Vaultik) scanStdin(a ...any) (int, error) { return fmt.Fscanln(v.Stdin, a...) } // TestVaultik wraps a Vaultik with captured stdout/stderr for testing type TestVaultik struct { *Vaultik Stdout *bytes.Buffer Stderr *bytes.Buffer Stdin *bytes.Buffer } // NewForTesting creates a minimal Vaultik instance for testing purposes. // Only the Storage field is populated; other fields are nil. // Returns a TestVaultik that captures stdout/stderr in buffers. func NewForTesting(storage storage.Storer) *TestVaultik { ctx, cancel := context.WithCancel(context.Background()) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} stdin := &bytes.Buffer{} return &TestVaultik{ Vaultik: &Vaultik{ Storage: storage, ctx: ctx, cancel: cancel, Stdout: stdout, Stderr: stderr, Stdin: stdin, }, Stdout: stdout, Stderr: stderr, Stdin: stdin, } }