Refactor CLI to use flags instead of positional arguments

- Change all commands to use flags (--bucket, --prefix, etc.)
- Add --config flag to backup command
- Support VAULTIK_CONFIG environment variable for config path
- Use /etc/vaultik/config.yml as default config location
- Add test/config.yaml for testing
- Update tests to use environment variable for config path
- Add .gitignore for build artifacts and local configs
- Update documentation to reflect new CLI syntax
This commit is contained in:
2025-07-20 09:45:24 +02:00
parent 3e8b98dec6
commit bcbc186286
11 changed files with 207 additions and 59 deletions

View File

@@ -3,6 +3,7 @@ package cli
import (
"context"
"fmt"
"os"
"git.eeqj.de/sneak/vaultik/internal/config"
"git.eeqj.de/sneak/vaultik/internal/globals"
@@ -22,16 +23,32 @@ func NewBackupCommand() *cobra.Command {
opts := &BackupOptions{}
cmd := &cobra.Command{
Use: "backup <config.yaml>",
Use: "backup",
Short: "Perform incremental backup",
Long: `Backup configured directories using incremental deduplication and encryption`,
Args: cobra.ExactArgs(1),
Long: `Backup configured directories using incremental deduplication and encryption.
Config is located at /etc/vaultik/config.yml, but can be overridden by specifying
a path using --config or by setting VAULTIK_CONFIG to a path.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.ConfigPath = args[0]
// If --config not specified, check environment variable
if opts.ConfigPath == "" {
opts.ConfigPath = os.Getenv("VAULTIK_CONFIG")
}
// If still not specified, use default
if opts.ConfigPath == "" {
defaultConfig := "/etc/vaultik/config.yml"
if _, err := os.Stat(defaultConfig); err == nil {
opts.ConfigPath = defaultConfig
} else {
return fmt.Errorf("no config file specified, VAULTIK_CONFIG not set, and %s not found", defaultConfig)
}
}
return runBackup(cmd.Context(), opts)
},
}
cmd.Flags().StringVar(&opts.ConfigPath, "config", "", "Path to config file")
cmd.Flags().BoolVar(&opts.Daemon, "daemon", false, "Run in daemon mode with inotify monitoring")
cmd.Flags().BoolVar(&opts.Cron, "cron", false, "Run in cron mode (silent unless error)")

View File

@@ -31,4 +31,20 @@ func TestCLIEntry(t *testing.T) {
t.Errorf("Expected command '%s' not found", expected)
}
}
// Verify backup command has proper flags
backupCmd, _, err := cmd.Find([]string{"backup"})
if err != nil {
t.Errorf("Failed to find backup command: %v", err)
} else {
if backupCmd.Flag("config") == nil {
t.Error("Backup command missing --config flag")
}
if backupCmd.Flag("daemon") == nil {
t.Error("Backup command missing --daemon flag")
}
if backupCmd.Flag("cron") == nil {
t.Error("Backup command missing --cron flag")
}
}
}

View File

@@ -21,23 +21,40 @@ type FetchOptions struct {
// NewFetchCommand creates the fetch command
func NewFetchCommand() *cobra.Command {
opts := &FetchOptions{}
cmd := &cobra.Command{
Use: "fetch <bucket> <prefix> <snapshot_id> <filepath> <target>",
Use: "fetch",
Short: "Extract single file from backup",
Long: `Download and decrypt a single file from a backup snapshot`,
Args: cobra.ExactArgs(5),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts := &FetchOptions{
Bucket: args[0],
Prefix: args[1],
SnapshotID: args[2],
FilePath: args[3],
Target: args[4],
// Validate required flags
if opts.Bucket == "" {
return fmt.Errorf("--bucket is required")
}
if opts.Prefix == "" {
return fmt.Errorf("--prefix is required")
}
if opts.SnapshotID == "" {
return fmt.Errorf("--snapshot is required")
}
if opts.FilePath == "" {
return fmt.Errorf("--file is required")
}
if opts.Target == "" {
return fmt.Errorf("--target is required")
}
return runFetch(cmd.Context(), opts)
},
}
cmd.Flags().StringVar(&opts.Bucket, "bucket", "", "S3 bucket name")
cmd.Flags().StringVar(&opts.Prefix, "prefix", "", "S3 prefix")
cmd.Flags().StringVar(&opts.SnapshotID, "snapshot", "", "Snapshot ID")
cmd.Flags().StringVar(&opts.FilePath, "file", "", "Path of file to extract from backup")
cmd.Flags().StringVar(&opts.Target, "target", "", "Target path for extracted file")
return cmd
}

View File

@@ -22,17 +22,24 @@ func NewPruneCommand() *cobra.Command {
opts := &PruneOptions{}
cmd := &cobra.Command{
Use: "prune <bucket> <prefix>",
Use: "prune",
Short: "Remove unreferenced blobs",
Long: `Delete blobs that are no longer referenced by any snapshot`,
Args: cobra.ExactArgs(2),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.Bucket = args[0]
opts.Prefix = args[1]
// Validate required flags
if opts.Bucket == "" {
return fmt.Errorf("--bucket is required")
}
if opts.Prefix == "" {
return fmt.Errorf("--prefix is required")
}
return runPrune(cmd.Context(), opts)
},
}
cmd.Flags().StringVar(&opts.Bucket, "bucket", "", "S3 bucket name")
cmd.Flags().StringVar(&opts.Prefix, "prefix", "", "S3 prefix")
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Show what would be deleted without actually deleting")
return cmd

View File

@@ -20,22 +20,36 @@ type RestoreOptions struct {
// NewRestoreCommand creates the restore command
func NewRestoreCommand() *cobra.Command {
opts := &RestoreOptions{}
cmd := &cobra.Command{
Use: "restore <bucket> <prefix> <snapshot_id> <target_dir>",
Use: "restore",
Short: "Restore files from backup",
Long: `Download and decrypt files from a backup snapshot`,
Args: cobra.ExactArgs(4),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts := &RestoreOptions{
Bucket: args[0],
Prefix: args[1],
SnapshotID: args[2],
TargetDir: args[3],
// Validate required flags
if opts.Bucket == "" {
return fmt.Errorf("--bucket is required")
}
if opts.Prefix == "" {
return fmt.Errorf("--prefix is required")
}
if opts.SnapshotID == "" {
return fmt.Errorf("--snapshot is required")
}
if opts.TargetDir == "" {
return fmt.Errorf("--target is required")
}
return runRestore(cmd.Context(), opts)
},
}
cmd.Flags().StringVar(&opts.Bucket, "bucket", "", "S3 bucket name")
cmd.Flags().StringVar(&opts.Prefix, "prefix", "", "S3 prefix")
cmd.Flags().StringVar(&opts.SnapshotID, "snapshot", "", "Snapshot ID to restore")
cmd.Flags().StringVar(&opts.TargetDir, "target", "", "Target directory for restore")
return cmd
}

View File

@@ -23,20 +23,25 @@ func NewVerifyCommand() *cobra.Command {
opts := &VerifyOptions{}
cmd := &cobra.Command{
Use: "verify <bucket> <prefix> [<snapshot_id>]",
Use: "verify",
Short: "Verify backup integrity",
Long: `Check that all referenced blobs exist and verify metadata integrity`,
Args: cobra.RangeArgs(2, 3),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
opts.Bucket = args[0]
opts.Prefix = args[1]
if len(args) > 2 {
opts.SnapshotID = args[2]
// Validate required flags
if opts.Bucket == "" {
return fmt.Errorf("--bucket is required")
}
if opts.Prefix == "" {
return fmt.Errorf("--prefix is required")
}
return runVerify(cmd.Context(), opts)
},
}
cmd.Flags().StringVar(&opts.Bucket, "bucket", "", "S3 bucket name")
cmd.Flags().StringVar(&opts.Prefix, "prefix", "", "S3 prefix")
cmd.Flags().StringVar(&opts.SnapshotID, "snapshot", "", "Snapshot ID to verify (optional, defaults to latest)")
cmd.Flags().BoolVar(&opts.Quick, "quick", false, "Perform quick verification by checking blob existence and S3 content hashes without downloading")
return cmd