Add --quiet flag, --json output, and config permission check

- Add global --quiet/-q flag to suppress non-error output
- Add --json flag to verify, snapshot rm, and prune commands
- Add config file permission check (warns if world/group readable)
- Update TODO.md to remove completed items
This commit is contained in:
2026-01-16 09:20:29 -08:00
parent 417b25a5f5
commit bdaaadf990
15 changed files with 251 additions and 95 deletions

View File

@@ -36,6 +36,7 @@ func NewInfoCommand() *cobra.Command {
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{

View File

@@ -19,10 +19,10 @@ func NewPruneCommand() *cobra.Command {
Short: "Remove unreferenced blobs",
Long: `Removes blobs that are not referenced by any snapshot.
This command scans all snapshots and their manifests to build a list of
This command scans all snapshots and their manifests to build a list of
referenced blobs, then removes any blobs in storage that are not in this list.
Use this command after deleting snapshots with 'vaultik purge' to reclaim
Use this command after deleting snapshots with 'vaultik purge' to reclaim
storage space.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -39,6 +39,7 @@ storage space.`,
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet || opts.JSON,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -50,7 +51,9 @@ storage space.`,
// Run the prune operation
if err := v.PruneBlobs(opts); err != nil {
if err != context.Canceled {
log.Error("Prune operation failed", "error", err)
if !opts.JSON {
log.Error("Prune operation failed", "error", err)
}
os.Exit(1)
}
}
@@ -75,6 +78,7 @@ storage space.`,
}
cmd.Flags().BoolVar(&opts.Force, "force", false, "Skip confirmation prompt")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output pruning stats as JSON")
return cmd
}

View File

@@ -56,6 +56,7 @@ specifying a path using --config or by setting VAULTIK_CONFIG to a path.`,
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{

View File

@@ -76,6 +76,7 @@ Examples:
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{
fx.Provide(fx.Annotate(

View File

@@ -13,6 +13,7 @@ type RootFlags struct {
ConfigPath string
Verbose bool
Debug bool
Quiet bool
}
var rootFlags RootFlags
@@ -34,6 +35,7 @@ on the source system.`,
cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "", "Path to config file (default: $VAULTIK_CONFIG or /etc/vaultik/config.yml)")
cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false, "Enable verbose output")
cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false, "Enable debug output")
cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false, "Suppress non-error output")
// Add subcommands
cmd.AddCommand(

View File

@@ -62,6 +62,7 @@ specifying a path using --config or by setting VAULTIK_CONFIG to a path.`,
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Cron: opts.Cron,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -127,6 +128,7 @@ func newSnapshotListCommand() *cobra.Command {
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -194,6 +196,7 @@ func newSnapshotPurgeCommand() *cobra.Command {
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -233,7 +236,7 @@ func newSnapshotPurgeCommand() *cobra.Command {
// newSnapshotVerifyCommand creates the 'snapshot verify' subcommand
func newSnapshotVerifyCommand() *cobra.Command {
var deep bool
opts := &vaultik.VerifyOptions{}
cmd := &cobra.Command{
Use: "verify <snapshot-id>",
@@ -255,6 +258,7 @@ func newSnapshotVerifyCommand() *cobra.Command {
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet || opts.JSON,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -262,9 +266,11 @@ func newSnapshotVerifyCommand() *cobra.Command {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go func() {
if err := v.VerifySnapshot(snapshotID, deep); err != nil {
if err := v.VerifySnapshotWithOptions(snapshotID, opts); err != nil {
if err != context.Canceled {
log.Error("Verification failed", "error", err)
if !opts.JSON {
log.Error("Verification failed", "error", err)
}
os.Exit(1)
}
}
@@ -285,7 +291,8 @@ func newSnapshotVerifyCommand() *cobra.Command {
},
}
cmd.Flags().BoolVar(&deep, "deep", false, "Download and verify blob hashes")
cmd.Flags().BoolVar(&opts.Deep, "deep", false, "Download and verify blob hashes")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output verification results as JSON")
return cmd
}
@@ -318,6 +325,7 @@ are still in use, then deletes any blobs that would become orphaned.`,
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet || opts.JSON,
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -327,7 +335,9 @@ are still in use, then deletes any blobs that would become orphaned.`,
go func() {
if _, err := v.RemoveSnapshot(snapshotID, opts); err != nil {
if err != context.Canceled {
log.Error("Failed to remove snapshot", "error", err)
if !opts.JSON {
log.Error("Failed to remove snapshot", "error", err)
}
os.Exit(1)
}
}
@@ -350,6 +360,7 @@ are still in use, then deletes any blobs that would become orphaned.`,
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip confirmation prompt")
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Show what would be deleted without deleting")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output deletion stats as JSON")
return cmd
}
@@ -377,6 +388,7 @@ accumulate from incomplete backups or deleted snapshots.`,
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{},
Invokes: []fx.Option{

View File

@@ -127,6 +127,7 @@ func runWithApp(ctx context.Context, fn func(*StoreApp) error) error {
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet,
},
Modules: []fx.Option{
fx.Provide(func(storer storage.Storer, shutdowner fx.Shutdowner) *StoreApp {

View File

@@ -49,6 +49,7 @@ The command will fail immediately on any verification error and exit with non-ze
LogOptions: log.LogOptions{
Verbose: rootFlags.Verbose,
Debug: rootFlags.Debug,
Quiet: rootFlags.Quiet || opts.JSON, // Suppress log output in JSON mode
},
Modules: []fx.Option{},
Invokes: []fx.Option{
@@ -61,12 +62,14 @@ The command will fail immediately on any verification error and exit with non-ze
if opts.Deep {
err = v.RunDeepVerify(snapshotID, opts)
} else {
err = v.VerifySnapshot(snapshotID, false)
err = v.VerifySnapshotWithOptions(snapshotID, opts)
}
if err != nil {
if err != context.Canceled {
log.Error("Verification failed", "error", err)
if !opts.JSON {
log.Error("Verification failed", "error", err)
}
os.Exit(1)
}
}
@@ -89,6 +92,7 @@ The command will fail immediately on any verification error and exit with non-ze
}
cmd.Flags().BoolVar(&opts.Deep, "deep", false, "Perform deep verification by downloading and verifying all blob contents")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output verification results as JSON")
return cmd
}