From f16e7b582403ed55c8cb250c4b7e7d93450e6c14 Mon Sep 17 00:00:00 2001 From: Liassica Date: Tue, 20 Feb 2024 19:03:20 -0600 Subject: [PATCH] borgmatic: add option for pattern matching Borgmatic has support for Borg's pattern matching. It is mutually exclusive with the existing `sourceDirectories` option, so assertions have been added to make sure that both are not set at the same time (but also that at least one of them is). Additionally, tests have been added to test the following configurations: `patterns` instead of `sourceDirectories`, both at the same time, and neither. --- modules/programs/borgmatic.nix | 46 ++++++++++++++- .../both-sourcedirectories-and-patterns.nix | 26 +++++++++ tests/modules/programs/borgmatic/default.nix | 5 ++ ...neither-sourcedirectories-nor-patterns.nix | 18 ++++++ .../borgmatic/patterns-configuration.nix | 58 +++++++++++++++++++ 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix create mode 100644 tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix create mode 100644 tests/modules/programs/borgmatic/patterns-configuration.nix diff --git a/modules/programs/borgmatic.nix b/modules/programs/borgmatic.nix index 19670775a..ecc6f5cf3 100644 --- a/modules/programs/borgmatic.nix +++ b/modules/programs/borgmatic.nix @@ -76,12 +76,39 @@ let (mkAfter [ (toString hmExcludeFile) ]); options = { location = { - sourceDirectories = mkOption { + sourceDirectories = mkNullableOption { type = types.listOf types.str; - description = "Directories to backup."; + default = null; + description = '' + Directories to backup. + + Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.patterns). + ''; example = literalExpression "[config.home.homeDirectory]"; }; + patterns = mkNullableOption { + type = types.listOf types.str; + default = null; + description = '' + Patterns to include/exclude. + + See the output of `borg help patterns` for the syntax. Pattern paths + are relative to `/` even when a different recursion root is set. + + Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.sourceDirectories). + ''; + example = literalExpression '' + [ + "R /home/user" + "- home/user/.cache" + "- home/user/Downloads" + "+ home/user/Videos/Important Video" + "- home/user/Videos" + ] + ''; + }; + repositories = mkOption { type = types.listOf (types.either types.str repositoryOption); apply = cleanRepositories; @@ -194,6 +221,7 @@ let writeConfig = config: generators.toYAML { } (removeNullValues ({ source_directories = config.location.sourceDirectories; + patterns = config.location.patterns; repositories = config.location.repositories; encryption_passcommand = config.storage.encryptionPasscommand; keep_within = config.retention.keepWithin; @@ -247,7 +275,19 @@ in { assertions = [ (lib.hm.assertions.assertPlatform "programs.borgmatic" pkgs lib.platforms.linux) - ]; + ] ++ (mapAttrsToList (backup: opts: { + assertion = opts.location.sourceDirectories == null + || opts.location.patterns == null; + message = '' + Borgmatic backup configuration "${backup}" cannot specify both 'location.sourceDirectories' and 'location.patterns'. + ''; + }) cfg.backups) ++ (mapAttrsToList (backup: opts: { + assertion = !(opts.location.sourceDirectories == null + && opts.location.patterns == null); + message = '' + Borgmatic backup configuration "${backup}" must specify one of 'location.sourceDirectories' or 'location.patterns'. + ''; + }) cfg.backups); xdg.configFile = with lib.attrsets; mapAttrs' (configName: config: diff --git a/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix b/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix new file mode 100644 index 000000000..106a0c18c --- /dev/null +++ b/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix @@ -0,0 +1,26 @@ +{ config, pkgs, ... }: + +let + + backups = config.programs.borgmatic.backups; + +in { + programs.borgmatic = { + enable = true; + backups = { + main = { + location = { + sourceDirectories = [ "/my-stuff-to-backup" ]; + patterns = [ "R /" "+ my-stuff-to-backup" ]; + repositories = [ "/mnt/disk1" ]; + }; + }; + }; + }; + + test.stubs.borgmatic = { }; + + test.asserts.assertions.expected = ['' + Borgmatic backup configuration "main" cannot specify both 'location.sourceDirectories' and 'location.patterns'. + '']; +} diff --git a/tests/modules/programs/borgmatic/default.nix b/tests/modules/programs/borgmatic/default.nix index 53ea9be89..8cc0f924d 100644 --- a/tests/modules/programs/borgmatic/default.nix +++ b/tests/modules/programs/borgmatic/default.nix @@ -1,5 +1,10 @@ { borgmatic-program-basic-configuration = ./basic-configuration.nix; + borgmatic-program-patterns-configuration = ./patterns-configuration.nix; + borgmatic-program-both-sourcedirectories-and-patterns = + ./both-sourcedirectories-and-patterns.nix; + borgmatic-program-neither-sourcedirectories-nor-patterns = + ./neither-sourcedirectories-nor-patterns.nix; borgmatic-program-include-hm-symlinks = ./include-hm-symlinks.nix; borgmatic-program-exclude-hm-symlinks = ./exclude-hm-symlinks.nix; borgmatic-program-exclude-hm-symlinks-nothing-else = diff --git a/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix b/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix new file mode 100644 index 000000000..596fdf1cd --- /dev/null +++ b/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix @@ -0,0 +1,18 @@ +{ config, pkgs, ... }: + +let + + backups = config.programs.borgmatic.backups; + +in { + programs.borgmatic = { + enable = true; + backups = { main = { location = { repositories = [ "/mnt/disk1" ]; }; }; }; + }; + + test.stubs.borgmatic = { }; + + test.asserts.assertions.expected = ['' + Borgmatic backup configuration "main" must specify one of 'location.sourceDirectories' or 'location.patterns'. + '']; +} diff --git a/tests/modules/programs/borgmatic/patterns-configuration.nix b/tests/modules/programs/borgmatic/patterns-configuration.nix new file mode 100644 index 000000000..098708cad --- /dev/null +++ b/tests/modules/programs/borgmatic/patterns-configuration.nix @@ -0,0 +1,58 @@ +{ config, pkgs, ... }: + +let + + boolToString = bool: if bool then "true" else "false"; + backups = config.programs.borgmatic.backups; + +in { + programs.borgmatic = { + enable = true; + backups = { + main = { + location = { + patterns = [ + "R /home/user" + "+ home/user/stuff-to-backup" + "+ home/user/junk/important-file" + "- home/user/junk" + ]; + repositories = [ "/mnt/backup-drive" ]; + }; + }; + }; + }; + + test.stubs.borgmatic = { }; + + nmt.script = '' + config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml + assertFileExists $config_file + + declare -A expectations + + expectations[patterns[0]]="${ + builtins.elemAt backups.main.location.patterns 0 + }" + expectations[patterns[1]]="${ + builtins.elemAt backups.main.location.patterns 1 + }" + expectations[patterns[2]]="${ + builtins.elemAt backups.main.location.patterns 2 + }" + expectations[patterns[3]]="${ + builtins.elemAt backups.main.location.patterns 3 + }" + + yq=${pkgs.yq-go}/bin/yq + + for filter in "''${!expectations[@]}"; do + expected_value="''${expectations[$filter]}" + actual_value="$($yq ".$filter" $config_file)" + + if [[ "$actual_value" != "$expected_value" ]]; then + fail "Expected '$filter' to be '$expected_value' but was '$actual_value'" + fi + done + ''; +}