Add 'vaultik init' command and quickstart section in README

New init command writes a default config file with commented
explanations for every setting. Uses XDG config directory via
github.com/adrg/xdg for platform-appropriate paths:
  macOS: ~/Library/Application Support/vaultik/config.yml
  Linux: ~/.config/vaultik/config.yml
  root:  /etc/vaultik/config.yml

Config resolution now searches the XDG path before /etc/vaultik/.
Refuses to overwrite an existing file. Created with 0600 permissions.

README quickstart rewritten as a single copy-pasteable shell block
walking through install, keygen, init, edit, first backup, verify,
and cron setup.
This commit is contained in:
2026-06-10 11:01:29 -07:00
parent 2e2b02a056
commit e6ee488d9d
4 changed files with 197 additions and 67 deletions

View File

@@ -46,66 +46,34 @@ go install git.eeqj.de/sneak/vaultik@latest
## quick start ## quick start
1. **generate keypair** ```sh
# 1. Install
go install git.eeqj.de/sneak/vaultik@latest
```sh # 2. Generate an age keypair (store the private key somewhere safe, offline)
age-keygen -o agekey.txt age-keygen -o key.txt
grep 'public key:' agekey.txt # the public key is printed to stdout and also in key.txt
```
2. **write config** (see `config.example.yml` for all options) # 3. Create a default config file
vaultik init
# Writes to the platform config directory with commented defaults:
# macOS: ~/Library/Application Support/vaultik/config.yml
# Linux: ~/.config/vaultik/config.yml
# root: /etc/vaultik/config.yml
```yaml # 4. Edit the config: set age_recipients, snapshots, and storage_url
snapshots: # (init prints the path it wrote to)
system:
paths:
- /etc
- /var/lib
exclude:
- '*.cache'
home:
paths:
- /home/user/documents
- /home/user/photos
exclude: # 5. Run your first backup
- '*.log' vaultik snapshot create
- '*.tmp'
- '.git'
- 'node_modules'
age_recipients: # 6. Verify it worked
- age1YOUR_PUBLIC_KEY_HERE vaultik snapshot list
vaultik snapshot verify <snapshot-id>
# Storage backend (pick one): # 7. Set up a daily cron job (keeps last 4 weeks of snapshots)
storage_url: "s3://mybucket/backups?endpoint=s3.example.com&region=us-east-1" # 0 3 * * * vaultik snapshot create --cron --prune --keep-newer-than 4w
# storage_url: "file:///mnt/backups" ```
# storage_url: "rclone://myremote/path/to/backups"
# For s3:// URLs, credentials are still required:
s3:
access_key_id: ...
secret_access_key: ...
```
3. **run**
```sh
# Back up all configured snapshots
vaultik --config /etc/vaultik.yml snapshot create
# Back up specific snapshots by name
vaultik --config /etc/vaultik.yml snapshot create home system
# Silent mode for cron
vaultik --config /etc/vaultik.yml snapshot create --cron
# Back up and clean up old snapshots + orphan blobs in one shot
vaultik --config /etc/vaultik.yml snapshot create --prune
# Daily cron: back up, keep last 4 weeks of snapshots
vaultik --config /etc/vaultik.yml snapshot create --cron --prune --keep-newer-than 4w
```
--- ---
@@ -114,6 +82,7 @@ go install git.eeqj.de/sneak/vaultik@latest
### commands ### commands
```sh ```sh
vaultik [--config <path>] init
vaultik [--config <path>] snapshot create [snapshot-names...] [--cron] [--prune] [--keep-newer-than <duration>] [--skip-errors] vaultik [--config <path>] snapshot create [snapshot-names...] [--cron] [--prune] [--keep-newer-than <duration>] [--skip-errors]
vaultik [--config <path>] snapshot list [--json] vaultik [--config <path>] snapshot list [--json]
vaultik [--config <path>] snapshot verify <snapshot-id> [--deep] [--json] vaultik [--config <path>] snapshot verify <snapshot-id> [--deep] [--json]
@@ -132,7 +101,7 @@ vaultik version
### global flags ### global flags
* `--config <path>`: Path to config file (default: `$VAULTIK_CONFIG` or `/etc/vaultik/config.yml`) * `--config <path>`: Path to config file (default: `$VAULTIK_CONFIG`, then platform config dir, then `/etc/vaultik/config.yml`)
* `--verbose`, `-v`: Enable verbose output * `--verbose`, `-v`: Enable verbose output
* `--debug`: Enable debug output * `--debug`: Enable debug output
* `--quiet`, `-q`: Suppress non-error output * `--quiet`, `-q`: Suppress non-error output
@@ -145,6 +114,12 @@ vaultik version
### command details ### command details
**init**: Write a default config file with commented explanations for every
setting. Writes to the path from `--config`, `$VAULTIK_CONFIG`, or the
platform config directory (`~/Library/Application Support/vaultik/` on macOS,
`~/.config/vaultik/` on Linux, `/etc/vaultik/` as root). Refuses to overwrite an
existing file. Created with mode `0600` since it will contain credentials.
**snapshot create**: Perform incremental backup of configured snapshots. **snapshot create**: Perform incremental backup of configured snapshots.
* Optional snapshot names argument to create specific snapshots (default: all) * Optional snapshot names argument to create specific snapshots (default: all)
* `--cron`: Silent unless error (for crontab) * `--cron`: Silent unless error (for crontab)

View File

@@ -18,7 +18,7 @@ func TestCLIEntry(t *testing.T) {
} }
// Verify all subcommands are registered // Verify all subcommands are registered
expectedCommands := []string{"snapshot", "store", "restore", "prune", "info", "version", "remote", "database"} expectedCommands := []string{"init", "snapshot", "store", "restore", "prune", "info", "version", "remote", "database"}
for _, expected := range expectedCommands { for _, expected := range expectedCommands {
found := false found := false
for _, cmd := range cmd.Commands() { for _, cmd := range cmd.Commands() {

136
internal/cli/init.go Normal file
View File

@@ -0,0 +1,136 @@
package cli
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
const defaultConfigTemplate = `# vaultik configuration
# Documentation: https://git.eeqj.de/sneak/vaultik
# ─── REQUIRED ────────────────────────────────────────────────────────────────
# Age recipient public keys for encryption.
# Backups are encrypted to ALL listed recipients. Any one of the corresponding
# private keys can decrypt. Generate a keypair with:
# age-keygen -o key.txt && grep 'public key' key.txt
age_recipients:
- age1REPLACE_WITH_YOUR_PUBLIC_KEY
# Named snapshots. Each snapshot backs up one or more paths and can have its
# own exclude patterns in addition to the global excludes below.
snapshots:
home:
paths:
- ~/Documents
- ~/Pictures
# exclude:
# - "*.cache"
# Storage backend (pick ONE of the three forms below).
#
# S3-compatible:
# storage_url: "s3://mybucket/backups?endpoint=s3.example.com&region=us-east-1"
# (also set s3.access_key_id and s3.secret_access_key below)
#
# Local filesystem:
# storage_url: "file:///mnt/backups/vaultik"
#
# Rclone (requires rclone configured separately):
# storage_url: "rclone://myremote/path/to/backups"
storage_url: ""
# ─── S3 CREDENTIALS (required for s3:// storage_url) ────────────────────────
# s3:
# access_key_id: YOUR_ACCESS_KEY
# secret_access_key: YOUR_SECRET_KEY
# # region: us-east-1 # Default: us-east-1
# # use_ssl: true # Default: true
# # part_size: 5MB # Multipart upload part size. Default: 5MB
# ─── OPTIONAL ────────────────────────────────────────────────────────────────
# Global exclude patterns applied to ALL snapshots.
# Snapshot-specific excludes are additive.
# exclude:
# - "*.log"
# - "*.tmp"
# - ".git"
# - "node_modules"
# Average chunk size for content-defined chunking (FastCDC).
# Smaller = better deduplication but more metadata overhead.
# Accepts: 1MB, 10M, 64KB, etc.
# Default: 10MB
# chunk_size: 10MB
# Maximum blob size before splitting into a new blob.
# Accepts: 1GB, 10G, 500MB, etc.
# Default: 10GB
# blob_size_limit: 10GB
# Zstd compression level (1-19). Higher = better ratio but slower.
# Default: 3
# compression_level: 3
# Hostname used in snapshot IDs. Default: system hostname.
# hostname: myserver
# Path to the local SQLite index database.
# Default: ~/.local/share/berlin.sneak.app.vaultik/index.sqlite
# index_path: /path/to/index.sqlite
`
// NewInitCommand creates the init command that writes a default config file.
func NewInitCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Write a default config file",
Long: `Creates a default configuration file with commented explanations
for every setting. If a config file already exists at the target path,
the command refuses to overwrite it.
The config is written to the path from --config, $VAULTIK_CONFIG, or
the platform default config directory (e.g. ~/Library/Application Support/
on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
path := configPathForInit()
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("config file already exists: %s", path)
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("creating config directory %s: %w", dir, err)
}
if err := os.WriteFile(path, []byte(defaultConfigTemplate), 0o600); err != nil {
return fmt.Errorf("writing config file: %w", err)
}
fmt.Printf("Config written to %s\n", path)
fmt.Println("Edit it to set your age_recipients, snapshots, and storage_url.")
return nil
},
}
return cmd
}
// configPathForInit returns the config path to write, checking --config flag,
// VAULTIK_CONFIG env, and the platform default.
func configPathForInit() string {
if rootFlags.ConfigPath != "" {
return rootFlags.ConfigPath
}
if envPath := os.Getenv("VAULTIK_CONFIG"); envPath != "" {
return envPath
}
return DefaultConfigPath()
}

View File

@@ -3,7 +3,9 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -32,13 +34,14 @@ on the source system.`,
} }
// Add global flags // Add global flags
cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "", "Path to config file (default: $VAULTIK_CONFIG or /etc/vaultik/config.yml)") cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "", "Path to config file (default: $VAULTIK_CONFIG or platform config dir)")
cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false, "Enable verbose output") cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false, "Enable verbose output")
cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false, "Enable debug output") cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false, "Enable debug output")
cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false, "Suppress non-error output") cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false, "Suppress non-error output")
// Add subcommands // Add subcommands
cmd.AddCommand( cmd.AddCommand(
NewInitCommand(),
NewRestoreCommand(), NewRestoreCommand(),
NewPruneCommand(), NewPruneCommand(),
NewStoreCommand(), NewStoreCommand(),
@@ -59,25 +62,41 @@ func GetRootFlags() RootFlags {
} }
// ResolveConfigPath resolves the config file path from flags, environment, or default. // ResolveConfigPath resolves the config file path from flags, environment, or default.
// It checks in order: 1) --config flag, 2) VAULTIK_CONFIG environment variable, // Search order: --config flag, VAULTIK_CONFIG env, XDG config dir, /etc/vaultik/config.yml.
// 3) default location /etc/vaultik/config.yml. Returns an error if no valid
// config file can be found through any of these methods.
func ResolveConfigPath() (string, error) { func ResolveConfigPath() (string, error) {
// First check global flag
if rootFlags.ConfigPath != "" { if rootFlags.ConfigPath != "" {
return rootFlags.ConfigPath, nil return rootFlags.ConfigPath, nil
} }
// Then check environment variable
if envPath := os.Getenv("VAULTIK_CONFIG"); envPath != "" { if envPath := os.Getenv("VAULTIK_CONFIG"); envPath != "" {
return envPath, nil return envPath, nil
} }
// Finally check default location for _, path := range defaultConfigPaths() {
defaultPath := "/etc/vaultik/config.yml" if _, err := os.Stat(path); err == nil {
if _, err := os.Stat(defaultPath); err == nil { return path, nil
return defaultPath, nil }
} }
return "", fmt.Errorf("no config file found; specify one with --config, set VAULTIK_CONFIG, or create %s", defaultPath) return "", fmt.Errorf("no config file found; run 'vaultik init' to create one, or specify with --config")
}
// defaultConfigPaths returns the ordered list of config paths to search.
// On macOS: ~/Library/Application Support/vaultik/config.yml
// On Linux: ~/.config/vaultik/config.yml
// Fallback: /etc/vaultik/config.yml
func defaultConfigPaths() []string {
return []string{
filepath.Join(xdg.ConfigHome, "vaultik", "config.yml"),
"/etc/vaultik/config.yml",
}
}
// DefaultConfigPath returns the platform-appropriate default config path.
// Used by the init command and in help text.
func DefaultConfigPath() string {
if os.Getuid() == 0 {
return "/etc/vaultik/config.yml"
}
return filepath.Join(xdg.ConfigHome, "vaultik", "config.yml")
} }