Write the apps snapshot in config init only on macOS (closes #408)
check / check (push) Successful in 21m39s
check / check (push) Successful in 21m39s
`config init` wrote an `apps` snapshot of `/Applications` on every system. That directory exists only on macOS, so on Linux the first `snapshot create` stopped on it unless `--skip-errors` was given. `defaultConfigText` takes the operating system name and returns the file with the `apps` snapshot and its comment only for `darwin`; `config init` passes `runtime.GOOS`. The macOS file is unchanged byte for byte. Model: opus-5-5
This commit is contained in:
1 parent
13a35a986a
commit
60943d8f5e
4 files changed
+75
-13
No files matched your search
@@ -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
|
||||||
# with sensible excludes)
|
# on macOS only, with sensible excludes)
|
||||||
vaultik snapshot create
|
vaultik snapshot create
|
||||||
|
|
||||||
# see what you have
|
# see what you have
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ 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
|
||||||
|
|||||||
+27
-4
@@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -30,7 +31,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 defaultConfigTemplate,
|
// configYAMLIndent matches the 2-space indentation of the default config,
|
||||||
// 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
|
||||||
@@ -50,7 +51,22 @@ var (
|
|||||||
"so the file was not changed")
|
"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
|
# Documentation: https://sneak.berlin/go/vaultik
|
||||||
|
|
||||||
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
||||||
@@ -168,7 +184,10 @@ 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
|
||||||
@@ -189,7 +208,10 @@ snapshots:
|
|||||||
- "/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:
|
||||||
@@ -311,7 +333,8 @@ 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(defaultConfigTemplate), configFileMode)
|
err = os.WriteFile(path, []byte(defaultConfigText(runtime.GOOS)),
|
||||||
|
configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing config file: %w", err)
|
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,
|
// 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
|
// 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) {
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import (
|
|||||||
"sneak.berlin/go/vaultik/internal/ui"
|
"sneak.berlin/go/vaultik/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
// TestDefaultConfigParsesOnMacOS ensures the config `config init` writes on
|
||||||
// that unmarshals into the Config struct with the expected snapshots.
|
// macOS is valid YAML that unmarshals into the Config struct with the
|
||||||
func TestDefaultConfigTemplateParses(t *testing.T) {
|
// expected snapshots.
|
||||||
|
func TestDefaultConfigParsesOnMacOS(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
|
|
||||||
err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg)
|
err := yaml.Unmarshal([]byte(defaultConfigText(macOS)), &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)
|
||||||
}
|
}
|
||||||
@@ -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.
|
// testRecipient is a valid age public key.
|
||||||
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
||||||
|
|
||||||
@@ -71,7 +105,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(defaultConfigTemplate), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -220,7 +254,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.
|
||||||
{defaultConfigTemplate, "snapshots.apps.paths", "/srv",
|
{defaultConfigText(macOS), "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",
|
||||||
@@ -380,7 +414,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(defaultConfigTemplate), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user