All user-facing output now goes through a single ui.Writer with a
uniform style:
》 (white) for begin / info / notice
》 (green) for complete / success
Warning: for warnings (orange)
ERROR: for errors (red)
》 (indented) for progress heartbeats
Color is enabled when stdout is a TTY and NO_COLOR is unset.
Standards:
- Complete-sentence messages with fully qualified terms ("backup
destination store", "local index database", "snapshot source
files enumeration").
- Every Complete has a matching Begin.
- Natural verb tense conveys state ("Uploading" -> "Uploaded"). The
words "begin"/"complete" never appear in message bodies; the marker
color carries that information.
- ETA means clock time, not duration. Progress lines say "estimated
remaining time (<dur>), finish at <time>" with both labeled.
Adds globals.CommitDate (populated by Makefile/Dockerfile/goreleaser
via ldflags from `git show -s --format=%cI HEAD`) and a startup banner
printed once per invocation.
Strips fx call-chain noise from startup errors so users see the actual
underlying error (e.g. "creating base path: mkdir /Volumes/BACKUPS:
permission denied" instead of three layers of "could not build
arguments for function ...").
README documents the output style and the ui package conventions.
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
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/crypto"
|
|
"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
|
|
cancel context.CancelFunc
|
|
|
|
// IO
|
|
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. Defaults to a
|
|
// writer wrapping Stdout; the cli layer replaces it with a discarding
|
|
// writer in --cron mode.
|
|
UI *ui.Writer
|
|
}
|
|
|
|
// VaultikParams contains all parameters for New that can be provided by fx
|
|
type VaultikParams 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 VaultikParams) *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()
|
|
}
|
|
|
|
// CanDecrypt returns true if this Vaultik instance has decryption capabilities
|
|
func (v *Vaultik) CanDecrypt() bool {
|
|
return v.Config.AgeSecretKey != ""
|
|
}
|
|
|
|
// GetEncryptor creates a new Encryptor instance based on the configured age recipients
|
|
// Returns an error if no recipients are configured
|
|
func (v *Vaultik) GetEncryptor() (*crypto.Encryptor, error) {
|
|
if len(v.Config.AgeRecipients) == 0 {
|
|
return nil, fmt.Errorf("no age recipients configured")
|
|
}
|
|
return crypto.NewEncryptor(v.Config.AgeRecipients)
|
|
}
|
|
|
|
// GetDecryptor creates a new Decryptor instance based on the configured age secret key
|
|
// Returns an error if no secret key is configured
|
|
func (v *Vaultik) GetDecryptor() (*crypto.Decryptor, error) {
|
|
if v.Config.AgeSecretKey == "" {
|
|
return nil, fmt.Errorf("no age secret key configured")
|
|
}
|
|
return crypto.NewDecryptor(v.Config.AgeSecretKey)
|
|
}
|
|
|
|
// GetFilesystem returns the filesystem instance used by Vaultik
|
|
func (v *Vaultik) GetFilesystem() afero.Fs {
|
|
return v.Fs
|
|
}
|
|
|
|
// printfStdout writes formatted output to stdout.
|
|
func (v *Vaultik) printfStdout(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...)
|
|
}
|
|
|
|
// printfStderr writes formatted output to stderr.
|
|
func (v *Vaultik) printfStderr(format string, args ...any) {
|
|
_, _ = fmt.Fprintf(v.Stderr, format, 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,
|
|
}
|
|
}
|