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:
81
README.md
81
README.md
@@ -46,65 +46,33 @@ go install git.eeqj.de/sneak/vaultik@latest
|
|||||||
|
|
||||||
## quick start
|
## quick start
|
||||||
|
|
||||||
1. **generate keypair**
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
age-keygen -o agekey.txt
|
# 1. Install
|
||||||
grep 'public key:' agekey.txt
|
go install git.eeqj.de/sneak/vaultik@latest
|
||||||
```
|
|
||||||
|
|
||||||
2. **write config** (see `config.example.yml` for all options)
|
# 2. Generate an age keypair (store the private key somewhere safe, offline)
|
||||||
|
age-keygen -o key.txt
|
||||||
|
# the public key is printed to stdout and also in key.txt
|
||||||
|
|
||||||
```yaml
|
# 3. Create a default config file
|
||||||
snapshots:
|
vaultik init
|
||||||
system:
|
# Writes to the platform config directory with commented defaults:
|
||||||
paths:
|
# macOS: ~/Library/Application Support/vaultik/config.yml
|
||||||
- /etc
|
# Linux: ~/.config/vaultik/config.yml
|
||||||
- /var/lib
|
# root: /etc/vaultik/config.yml
|
||||||
exclude:
|
|
||||||
- '*.cache'
|
|
||||||
home:
|
|
||||||
paths:
|
|
||||||
- /home/user/documents
|
|
||||||
- /home/user/photos
|
|
||||||
|
|
||||||
exclude:
|
# 4. Edit the config: set age_recipients, snapshots, and storage_url
|
||||||
- '*.log'
|
# (init prints the path it wrote to)
|
||||||
- '*.tmp'
|
|
||||||
- '.git'
|
|
||||||
- 'node_modules'
|
|
||||||
|
|
||||||
age_recipients:
|
# 5. Run your first backup
|
||||||
- age1YOUR_PUBLIC_KEY_HERE
|
vaultik snapshot create
|
||||||
|
|
||||||
# Storage backend (pick one):
|
# 6. Verify it worked
|
||||||
storage_url: "s3://mybucket/backups?endpoint=s3.example.com®ion=us-east-1"
|
vaultik snapshot list
|
||||||
# storage_url: "file:///mnt/backups"
|
vaultik snapshot verify <snapshot-id>
|
||||||
# storage_url: "rclone://myremote/path/to/backups"
|
|
||||||
|
|
||||||
# For s3:// URLs, credentials are still required:
|
# 7. Set up a daily cron job (keeps last 4 weeks of snapshots)
|
||||||
s3:
|
# 0 3 * * * vaultik snapshot create --cron --prune --keep-newer-than 4w
|
||||||
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)
|
||||||
|
|||||||
@@ -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
136
internal/cli/init.go
Normal 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®ion=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()
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user