1 Commits
Author SHA1 Message Date
sneak 0d6df8c2a2 Label the commit date "commit date" in version and banner (closes #409)
check / check (push) Failing after 23m58s
The value is the commit's date, which the Makefile and the Dockerfile
stamp in, not the time of the build. `vaultik version` labelled it
"build date" and the startup banner "built on". The other version
labels gain one space so every value still starts in one column.

The tests give the commit and the commit date different values, since
the test binary is not stamped and holds "unknown" in both.

Model: opus-5-5
2026-11-10 20:45:02 +00:00
4 changed files with 13 additions and 75 deletions

No files matched your search

+2 -2
View File
@@ -32,8 +32,8 @@ mkdir /Volumes/usbstick/mybackup
# (System Settings → Privacy & Security → Full Disk Access), otherwise # (System Settings → Privacy & Security → Full Disk Access), otherwise
# the backup will abort with a permission error on protected directories # the backup will abort with a permission error on protected directories
# run your first backup (the default config backs up ~, and /Applications # run your first backup (the default config backs up ~ and /Applications
# on macOS only, with sensible excludes) # with sensible excludes)
vaultik snapshot create vaultik snapshot create
# see what you have # see what you have
-5
View File
@@ -24,11 +24,6 @@ owner's; a `v*` tag runs the release build in
# Completed Steps # Completed Steps
- 2026-11-10: Made `config init` write the `apps` snapshot, which backs
up `/Applications`, only on macOS
([issue #408](https://git.eeqj.de/sneak/vaultik/issues/408)). On
Linux the first `snapshot create` stopped on the missing directory.
- 2026-10-15: Stopped `snapshot create --prune` pruning after a backup - 2026-10-15: Stopped `snapshot create --prune` pruning after a backup
in which a volume mounted below a configured path in an earlier in which a volume mounted below a configured path in an earlier
snapshot of the same name was not mounted snapshot of the same name was not mounted
+4 -27
View File
@@ -8,7 +8,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -31,7 +30,7 @@ const configSetArgs = 2
// parent config dirs (e.g. ~/.config) are conventionally traversable. // parent config dirs (e.g. ~/.config) are conventionally traversable.
const configDirMode = 0o755 const configDirMode = 0o755
// configYAMLIndent matches the 2-space indentation of the default config, // configYAMLIndent matches the 2-space indentation of defaultConfigTemplate,
// so `config set` writes the file back with the same indentation rather than // so `config set` writes the file back with the same indentation rather than
// yaml.Marshal's 4-space default. // yaml.Marshal's 4-space default.
const configYAMLIndent = 2 const configYAMLIndent = 2
@@ -51,22 +50,7 @@ var (
"so the file was not changed") "so the file was not changed")
) )
// macOS is the value of runtime.GOOS on macOS. const defaultConfigTemplate = `# vaultik configuration
const macOS = "darwin"
// defaultConfigText returns the file `config init` writes on the operating
// system goos, named as runtime.GOOS names it. Only macOS gets the apps
// snapshot, since /Applications exists nowhere else.
func defaultConfigText(goos string) string {
if goos == macOS {
return defaultConfigHead + appsSnapshotConfig + defaultConfigTail
}
return defaultConfigHead + defaultConfigTail
}
// defaultConfigHead is the default config up to the end of the home snapshot.
const defaultConfigHead = `# vaultik configuration
# Documentation: https://sneak.berlin/go/vaultik # Documentation: https://sneak.berlin/go/vaultik
# ─── REQUIRED ──────────────────────────────────────────────────────────────── # ─── REQUIRED ────────────────────────────────────────────────────────────────
@@ -184,10 +168,7 @@ snapshots:
- "/Movies/ProxyMedia" - "/Movies/ProxyMedia"
- "/Music/iTunes/Album Artwork" - "/Music/iTunes/Album Artwork"
- "/Pictures/iPod Photo Cache" - "/Pictures/iPod Photo Cache"
`
// appsSnapshotConfig is the apps snapshot of the default config.
const appsSnapshotConfig = `
# Third-party applications. OS-provided apps live in /System/Applications # Third-party applications. OS-provided apps live in /System/Applications
# on modern macOS and are never in /Applications, but Apple-installed # on modern macOS and are never in /Applications, but Apple-installed
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since # App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
@@ -208,10 +189,7 @@ const appsSnapshotConfig = `
- "/Steam.app" - "/Steam.app"
- "/VirtualBox.app" - "/VirtualBox.app"
- "/Utilities/Adobe Installers" - "/Utilities/Adobe Installers"
`
// defaultConfigTail is the default config after the snapshots.
const defaultConfigTail = `
# Storage backend (pick ONE of the three forms below). # Storage backend (pick ONE of the three forms below).
# #
# S3-compatible: # S3-compatible:
@@ -333,8 +311,7 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
return fmt.Errorf("creating config directory %s: %w", dir, err) return fmt.Errorf("creating config directory %s: %w", dir, err)
} }
err = os.WriteFile(path, []byte(defaultConfigText(runtime.GOOS)), err = os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
configFileMode)
if err != nil { if err != nil {
return fmt.Errorf("writing config file: %w", err) return fmt.Errorf("writing config file: %w", err)
} }
@@ -583,7 +560,7 @@ func yamlPathOnly(root *yaml.Node, keys []string) (*yaml.Node, error) {
} }
// marshalConfigYAML renders a config document tree with 2-space indentation, // marshalConfigYAML renders a config document tree with 2-space indentation,
// matching the default config. yaml.Marshal defaults to 4 spaces, which // matching defaultConfigTemplate. yaml.Marshal defaults to 4 spaces, which
// would reindent the whole file on the first `config set` despite the promise // would reindent the whole file on the first `config set` despite the promise
// to preserve formatting. // to preserve formatting.
func marshalConfigYAML(root *yaml.Node) ([]byte, error) { func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
+7 -41
View File
@@ -13,15 +13,14 @@ import (
"sneak.berlin/go/vaultik/internal/ui" "sneak.berlin/go/vaultik/internal/ui"
) )
// TestDefaultConfigParsesOnMacOS ensures the config `config init` writes on // TestDefaultConfigTemplateParses ensures the init template is valid YAML
// macOS is valid YAML that unmarshals into the Config struct with the // that unmarshals into the Config struct with the expected snapshots.
// expected snapshots. func TestDefaultConfigTemplateParses(t *testing.T) {
func TestDefaultConfigParsesOnMacOS(t *testing.T) {
t.Parallel() t.Parallel()
var cfg config.Config var cfg config.Config
err := yaml.Unmarshal([]byte(defaultConfigText("darwin")), &cfg) err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg)
if err != nil { if err != nil {
t.Fatalf("default config template is not valid YAML: %v", err) t.Fatalf("default config template is not valid YAML: %v", err)
} }
@@ -59,39 +58,6 @@ func TestDefaultConfigParsesOnMacOS(t *testing.T) {
} }
} }
// TestDefaultConfigOnLinux checks that the config `config init` writes on
// Linux loads with its home snapshot and without the apps snapshot, whose
// /Applications exists only on macOS.
func TestDefaultConfigOnLinux(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "config.yml")
err := os.WriteFile(path, []byte(defaultConfigText("linux")), configFileMode)
if err != nil {
t.Fatalf("write config: %v", err)
}
err = writeConfigSet(ui.NewWithColor(&bytes.Buffer{}, false),
path, "storage_url", "file:///mnt/backups")
if err != nil {
t.Fatalf("config set storage_url: %v", err)
}
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("config.Load: %v", err)
}
if _, ok := cfg.Snapshots["home"]; !ok {
t.Error("expected 'home' snapshot in the Linux default config")
}
if _, ok := cfg.Snapshots["apps"]; ok {
t.Error("the Linux default config should have no 'apps' snapshot")
}
}
// testRecipient is a valid age public key. // testRecipient is a valid age public key.
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj" const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
@@ -105,7 +71,7 @@ func TestConfigSetRecipientOnFreshConfig(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yml") path := filepath.Join(t.TempDir(), "config.yml")
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode) err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
if err != nil { if err != nil {
t.Fatalf("write config: %v", err) t.Fatalf("write config: %v", err)
} }
@@ -254,7 +220,7 @@ func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
{levelReference, "blob_size_limit", "1MB", {levelReference, "blob_size_limit", "1MB",
"blob_size_limit must be at least"}, "blob_size_limit must be at least"},
// The type check on a config that does not load yet. // The type check on a config that does not load yet.
{defaultConfigText(macOS), "snapshots.apps.paths", "/srv", {defaultConfigTemplate, "snapshots.apps.paths", "/srv",
"into []string"}, "into []string"},
// The type check on a config that cannot be read here. // The type check on a config that cannot be read here.
{configWithUnsetReference, "snapshots.home.paths", "/x", {configWithUnsetReference, "snapshots.home.paths", "/x",
@@ -414,7 +380,7 @@ func loadAfterConfigSet(t *testing.T, key, value string) *config.Config {
path := filepath.Join(t.TempDir(), "config.yml") path := filepath.Join(t.TempDir(), "config.yml")
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode) err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
if err != nil { if err != nil {
t.Fatalf("write config: %v", err) t.Fatalf("write config: %v", err)
} }