Write the apps snapshot in config init only on macOS #413
No files matched your search
@@ -32,8 +32,8 @@ mkdir /Volumes/usbstick/mybackup
|
||||
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
||||
# the backup will abort with a permission error on protected directories
|
||||
|
||||
# run your first backup (the default config backs up ~ and /Applications
|
||||
# with sensible excludes)
|
||||
# run your first backup (the default config backs up ~, and /Applications
|
||||
# on macOS only, with sensible excludes)
|
||||
vaultik snapshot create
|
||||
|
||||
# see what you have
|
||||
|
||||
@@ -24,6 +24,11 @@ owner's; a `v*` tag runs the release build in
|
||||
|
||||
# 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
|
||||
in which a volume mounted below a configured path in an earlier
|
||||
snapshot of the same name was not mounted
|
||||
|
||||
+27
-4
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -30,7 +31,7 @@ const configSetArgs = 2
|
||||
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
||||
const configDirMode = 0o755
|
||||
|
||||
// configYAMLIndent matches the 2-space indentation of defaultConfigTemplate,
|
||||
// configYAMLIndent matches the 2-space indentation of the default config,
|
||||
// so `config set` writes the file back with the same indentation rather than
|
||||
// yaml.Marshal's 4-space default.
|
||||
const configYAMLIndent = 2
|
||||
@@ -50,7 +51,22 @@ var (
|
||||
"so the file was not changed")
|
||||
)
|
||||
|
||||
const defaultConfigTemplate = `# vaultik configuration
|
||||
// macOS is the value of runtime.GOOS on macOS.
|
||||
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
|
||||
|
||||
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
||||
@@ -168,7 +184,10 @@ snapshots:
|
||||
- "/Movies/ProxyMedia"
|
||||
- "/Music/iTunes/Album Artwork"
|
||||
- "/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
|
||||
# on modern macOS and are never in /Applications, but Apple-installed
|
||||
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
||||
@@ -189,7 +208,10 @@ snapshots:
|
||||
- "/Steam.app"
|
||||
- "/VirtualBox.app"
|
||||
- "/Utilities/Adobe Installers"
|
||||
`
|
||||
|
||||
// defaultConfigTail is the default config after the snapshots.
|
||||
const defaultConfigTail = `
|
||||
# Storage backend (pick ONE of the three forms below).
|
||||
#
|
||||
# S3-compatible:
|
||||
@@ -311,7 +333,8 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
|
||||
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||
err = os.WriteFile(path, []byte(defaultConfigText(runtime.GOOS)),
|
||||
configFileMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing config file: %w", err)
|
||||
}
|
||||
@@ -560,7 +583,7 @@ func yamlPathOnly(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
||||
}
|
||||
|
||||
// marshalConfigYAML renders a config document tree with 2-space indentation,
|
||||
// matching defaultConfigTemplate. yaml.Marshal defaults to 4 spaces, which
|
||||
// matching the default config. yaml.Marshal defaults to 4 spaces, which
|
||||
// would reindent the whole file on the first `config set` despite the promise
|
||||
// to preserve formatting.
|
||||
func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
|
||||
|
||||
@@ -13,14 +13,15 @@ import (
|
||||
"sneak.berlin/go/vaultik/internal/ui"
|
||||
)
|
||||
|
||||
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
||||
// that unmarshals into the Config struct with the expected snapshots.
|
||||
func TestDefaultConfigTemplateParses(t *testing.T) {
|
||||
// TestDefaultConfigParsesOnMacOS ensures the config `config init` writes on
|
||||
// macOS is valid YAML that unmarshals into the Config struct with the
|
||||
// expected snapshots.
|
||||
func TestDefaultConfigParsesOnMacOS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var cfg config.Config
|
||||
|
||||
err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg)
|
||||
err := yaml.Unmarshal([]byte(defaultConfigText("darwin")), &cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("default config template is not valid YAML: %v", err)
|
||||
}
|
||||
@@ -58,6 +59,39 @@ func TestDefaultConfigTemplateParses(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.
|
||||
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
||||
|
||||
@@ -71,7 +105,7 @@ func TestConfigSetRecipientOnFreshConfig(t *testing.T) {
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
|
||||
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||
if err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
@@ -220,7 +254,7 @@ func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
|
||||
{levelReference, "blob_size_limit", "1MB",
|
||||
"blob_size_limit must be at least"},
|
||||
// The type check on a config that does not load yet.
|
||||
{defaultConfigTemplate, "snapshots.apps.paths", "/srv",
|
||||
{defaultConfigText(macOS), "snapshots.apps.paths", "/srv",
|
||||
"into []string"},
|
||||
// The type check on a config that cannot be read here.
|
||||
{configWithUnsetReference, "snapshots.home.paths", "/x",
|
||||
@@ -380,7 +414,7 @@ func loadAfterConfigSet(t *testing.T, key, value string) *config.Config {
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
|
||||
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||
if err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user