From a6f2535ac41ef8f1c568288a2bd5c5ea2c2acb98 Mon Sep 17 00:00:00 2001 From: KFears Date: Mon, 19 Jan 2026 18:53:01 +0400 Subject: [PATCH 01/12] feat: very painful wip version Signed-off-by: KFears --- hosts/blackberry/default.nix | 1 - hosts/blueberry/default.nix | 1 - nixosModules/common-services/default.nix | 2 +- .../common-services/restic/default.nix | 7 + .../{restic.nix => restic/legacy.nix} | 0 .../common-services/restic/module.nix | 313 ++++++++++++++++++ .../common-services/restic/values.nix | 94 ++++++ suites/common-services.nix | 2 +- 8 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 nixosModules/common-services/restic/default.nix rename nixosModules/common-services/{restic.nix => restic/legacy.nix} (100%) create mode 100644 nixosModules/common-services/restic/module.nix create mode 100644 nixosModules/common-services/restic/values.nix diff --git a/hosts/blackberry/default.nix b/hosts/blackberry/default.nix index e2d8cd6f..a79019ae 100644 --- a/hosts/blackberry/default.nix +++ b/hosts/blackberry/default.nix @@ -33,7 +33,6 @@ backlight = false; # PC GPUs don't do that battery = false; # PC, doesn't have a battery }; - restic.usb-backups = true; impermanence.presets = { enable = true; diff --git a/hosts/blueberry/default.nix b/hosts/blueberry/default.nix index e6a6786a..4ddfe5e7 100644 --- a/hosts/blueberry/default.nix +++ b/hosts/blueberry/default.nix @@ -34,7 +34,6 @@ boot.kernelPackages = pkgs.linuxPackages; nixchad = { boot.bootloader = "systemd-boot"; - restic.usb-backups = true; smartctl-exporter.devices = [ "/dev/nvme0n1" ]; waybar.battery = true; impermanence = { diff --git a/nixosModules/common-services/default.nix b/nixosModules/common-services/default.nix index f90e179d..edd262c3 100644 --- a/nixosModules/common-services/default.nix +++ b/nixosModules/common-services/default.nix @@ -3,6 +3,6 @@ ./alloy ./smartctl-exporter.nix ./syncthing.nix - ./restic.nix + ./restic ]; } diff --git a/nixosModules/common-services/restic/default.nix b/nixosModules/common-services/restic/default.nix new file mode 100644 index 00000000..66044491 --- /dev/null +++ b/nixosModules/common-services/restic/default.nix @@ -0,0 +1,7 @@ +{ + imports = [ + ./module.nix + # ./legacy.nix + ./values.nix + ]; +} diff --git a/nixosModules/common-services/restic.nix b/nixosModules/common-services/restic/legacy.nix similarity index 100% rename from nixosModules/common-services/restic.nix rename to nixosModules/common-services/restic/legacy.nix diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix new file mode 100644 index 00000000..2fbfd694 --- /dev/null +++ b/nixosModules/common-services/restic/module.nix @@ -0,0 +1,313 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: +let + # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" + inherit (utils.systemdUtils.unitOptions) unitOption; +in +{ + options.nixchad.resticModule = { + enable = lib.mkEnableOption "restic" // { + default = true; + }; + + sources = lib.mkOption { + description = '' + Sources from which backups will be created. + ''; + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + unitOptions = lib.mkOption { + description = "Additional systemd unit options"; + type = unitOption; + default = { }; + }; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Which paths to backup, in addition to ones specified via + `dynamicFilesFrom`. If null or an empty array and + `dynamicFilesFrom` is also null, no backup command will be run. + This can be used to create a prune-only job. + ''; + example = [ + "/var/lib/postgresql" + "/home/user/backup" + ]; + }; + + command = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Command to pass to --stdin-from-command. If null or an empty array, and `paths`/`dynamicFilesFrom` + are also null, no backup command will be run. + ''; + example = [ + "sudo" + "-u" + "postgres" + "pg_dumpall" + ]; + }; + + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Patterns to exclude when backing up. See + https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for + details on syntax. + ''; + example = [ + "/var/cache" + "/home/*/.cache" + ".git" + ]; + }; + + dynamicFilesFrom = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + A script that produces a list of files to back up. The + results of this command are given to the '--files-from' + option. The result is merged with paths specified via `paths`. + ''; + example = "find /home/matt/git -type d -name .git"; + }; + }; + } + ) + ); + }; + + destinations = lib.mkOption { + description = '' + Destinations where backups will be stored. + ''; + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + init = lib.mkOption { + description = "Whether to init the destination repository prior to doing any backups"; + type = lib.types.bool; + default = true; + }; + + unitOptions = lib.mkOption { + description = "Additional systemd unit options"; + type = unitOption; + default = { }; + }; + + extraOptions = lib.mkOption { + description = "Additional options added to restic invokation via '-o' flag"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + + settings = lib.mkOption { + description = '' + Restic settings. They are provided as environment variables. They should be provided in upper snake + case (e.g. {env}`RESTIC_PASSWORD_FILE`). See + for supported options. + + This option can also take rclone settings, also as environment variables. They should be provided in + upper snake case (e.g. {env}`RCLONE_SKIP_LINKS`). See for supported + options. Restic will automatically supply the remote type and name for you. To provide secrets to the + backend, it's recommended to create rclone config file yourself, and use {env}`RCLONE_CONFIG` option + to point to it. It is also recommended to use a separate config file if you care about + case-sensitivity for your remote name. + ''; + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + str + (listOf str) + ]); + + options = { + RESTIC_REPOSITORY = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + repository to backup to. + ''; + example = "sftp:backup@192.168.1.100:/backups/my-backup"; + }; + RESTIC_REPOSITORY_FILE = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = '' + Path to the file containing the repository location to backup to. + ''; + }; + RESTIC_PASSWORD_FILE = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Read the repository password from a file. + ''; + example = "/etc/nixos/restic-password"; + }; + + RCLONE_CONFIG = lib.mkOption { + type = + with lib.types; + nullOr (oneOf [ + str + path + ]); + default = null; + description = '' + Location of the rclone configuration file. + ''; + }; + }; + }; + example = lib.literalExpression '' + RESTIC_REPOSITORY = "s3:s3.us-east-1.amazonaws.com/bucket_name/restic"; + RESTIC_PASSWORD_FILE = "/secrets/password-file"; + AWS_ACCESS_KEY_ID = "XXXX"; + AWS_SECRET_ACCESS_KEY = "YYYY"; + RCLONE_BWLIMIT = "10M"; + RCLONE_HARD_DELETE = "true"; + # RCLONE_S3_PROVIDER = "AWS"; + # RCLONE_CONFIG_MYS3_ACCESS_KEY_ID = "XXXX"; + # RCLONE_CONFIG = "/my/config/file"; + ''; + }; + }; + } + ) + ); + }; + + mappings = lib.mkOption { + description = '' + Mappings between sources and destinations. + ''; + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + sources = lib.mkOption { + description = "Backup sources"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + destinations = lib.mkOption { + description = "Backup destinations"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + } + ) + ); + }; + }; + + config = + let + extraOptions = + name: + lib.concatMapStrings (arg: " -o ${arg}") ( + lib.getAttrFromPath [ name "extraOptions" ] config.nixchad.resticModule.destinations + ); + inhibitCmd = + name: + lib.concatStringsSep " " [ + "${pkgs.systemd}/bin/systemd-inhibit" + "--mode='block'" + "--who='restic'" + "--what='idle:sleep:shutdown:handle-lid-switch'" + "--why=${lib.escapeShellArg "Scheduled backup ${name}"} " + ]; + resticCmd = name: "${inhibitCmd name}${lib.getExe pkgs.restic}${extraOptions name}"; + + destinationTemplater = name: destination: { + environment = destination.settings; + path = [ config.programs.ssh.package ]; + restartIfChanged = false; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${resticCmd name} cat config > /dev/null || ${resticCmd name} init"; + User = "root"; + Group = "root"; + RuntimeDirectory = "restic-destination-${name}"; + CacheDirectory = "restic-destination-${name}"; + CacheDirectoryMode = "0700"; + PrivateTmp = true; + }; + }; + + # mapping is { sources = "foo"; destinations = "bar"; } + backupTemplater = mapping: { + environment = lib.getAttrFromPath [ + mapping.destinations + "settings" + ] config.nixchad.resticModule.destinations; + path = [ config.programs.ssh.package ]; + restartIfChanged = false; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${resticCmd} backup"; + User = "root"; + Group = "root"; + RuntimeDirectory = "restic-destination-${mapping.destinations}"; + CacheDirectory = "restic-destination-${mapping.destinations}"; + CacheDirectoryMode = "0700"; + PrivateTmp = true; + }; + }; + + destinationServices = + config.nixchad.resticModule.destinations + |> lib.mapAttrs' ( + name: value: { + name = "restic-destination-${name}"; + value = destinationTemplater name value; + } + ); + + transformMapping = + mapping: + mapping + |> lib.cartesianProduct + |> lib.map (attrset: { + name = "restic-backup-${attrset.sources}-to-${attrset.destinations}"; + value = backupTemplater attrset; + }) + |> lib.listToAttrs; + + backupServices = + config.nixchad.resticModule.mappings + |> lib.mapAttrs (name: mapping: transformMapping mapping) + |> lib.foldlAttrs ( + acc: _name: value: + acc // value + ) { }; + in + { + systemd.services = destinationServices // backupServices; + }; +} diff --git a/nixosModules/common-services/restic/values.nix b/nixosModules/common-services/restic/values.nix new file mode 100644 index 00000000..06f551a5 --- /dev/null +++ b/nixosModules/common-services/restic/values.nix @@ -0,0 +1,94 @@ +{ + config, + lib, + pkgs, + username, + ... +}: +{ + nixchad.resticModule = { + enable = true; + + sources = { + secrets = { + paths = [ + "/secrets" + ]; + }; + stuff = { + paths = [ + "/home/${username}/Sync" + ]; + }; + photos = { + paths = [ + "/home/${username}/Pictures/Photos" + "/home/${username}/Pictures/Photos-phone" + ]; + }; + postgres = { + paths = [ + "/tmp/postgres" + ]; + unitOptions = { + preStart = '' + echo 'creating temporary directory' + mkdir -p /tmp/postgres + echo 'dumping PostgreSQL database' + ${pkgs.shadow.su}/bin/su postgres -c ${config.services.postgresql.package}/bin/pg_dumpall > /tmp/postgres/pgdump.sql + ''; + postStop = '' + rm -rfv /tmp/postgres + ''; + }; + }; + vaultwarden = { + paths = [ + "/tmp/vaultwarden" + ]; + unitOptions = { + preStart = '' + echo 'creating temporary directory' + mkdir -p /tmp/vaultwarden + echo 'copying Vaultwarden data' + cp --reflink=auto -r /var/lib/bitwarden_rs /tmp/vaultwarden + ''; + postStop = '' + rm -rfv /tmp/vaultwarden + ''; + }; + }; + paperless = { + paths = [ + config.services.paperless.dataDir + ]; + }; + }; + + destinations = { + linus = { + settings = { + RESTIC_REPOSITORY = "sftp:kfears@sol.sphalerite.tech:/backup"; + RESTIC_PASSWORD_FILE = "/secrets/restic-backup-linus"; + }; + extraOptions = [ + "sftp.command='ssh kfears@sol.sphalerite.tech -i /home/${username}/.ssh/id_ed25519 -o StrictHostKeyChecking=no -s sftp'" + ]; + }; + }; + + mappings = { + linus = { + sources = [ + "secrets" + "stuff" + "photos" + "postgres" + "vaultwarden" + "paperless" + ]; + destinations = [ "linus" ]; + }; + }; + }; +} diff --git a/suites/common-services.nix b/suites/common-services.nix index 3816dabf..a02829cb 100644 --- a/suites/common-services.nix +++ b/suites/common-services.nix @@ -3,6 +3,6 @@ nixchad = { smartctl-exporter.enable = lib.mkDefault true; alloy.enable = true; - restic.enable = true; + resticModule.enable = true; }; } From cc0f5b0bf5f0aee52269aa21895194f155252c2a Mon Sep 17 00:00:00 2001 From: KFears Date: Tue, 20 Jan 2026 00:50:05 +0400 Subject: [PATCH 02/12] fix: eval succeeds now, try and get actual backups Signed-off-by: KFears --- .../common-services/restic/module.nix | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 2fbfd694..a4418b47 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -85,6 +85,18 @@ in ''; example = "find /home/matt/git -type d -name .git"; }; + + extraBackupArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Extra arguments passed to restic backup. + ''; + example = [ + "--cleanup-cache" + "--exclude-file=/etc/nixos/restic-ignore" + ]; + }; }; } ) @@ -227,7 +239,7 @@ in extraOptions = name: lib.concatMapStrings (arg: " -o ${arg}") ( - lib.getAttrFromPath [ name "extraOptions" ] config.nixchad.resticModule.destinations + lib.attrByPath [ name "extraOptions" ] [ ] config.nixchad.resticModule.destinations ); inhibitCmd = name: @@ -258,6 +270,10 @@ in }; }; + args = source: lib.concatStringsSep " " (source.extraBackupArgs); + argsExtractSource = + mapping: args (lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.sources); + # mapping is { sources = "foo"; destinations = "bar"; } backupTemplater = mapping: { environment = lib.getAttrFromPath [ @@ -266,11 +282,17 @@ in ] config.nixchad.resticModule.destinations; path = [ config.programs.ssh.package ]; restartIfChanged = false; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; + wants = [ + "network-online.target" + "restic-destination-${mapping.destinations}.service" + ]; + after = [ + "network-online.target" + "restic-destination-${mapping.destinations}.service" + ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${resticCmd} backup"; + ExecStart = "${resticCmd mapping.sources} backup ${argsExtractSource mapping.sources}"; User = "root"; Group = "root"; RuntimeDirectory = "restic-destination-${mapping.destinations}"; @@ -292,22 +314,27 @@ in transformMapping = mapping: mapping + # { sources = [x y]; destinations = [z w]; } -> [{ sources = x; destinations = z; } { sources = y; destinations = z; } ..] |> lib.cartesianProduct + # [{ sources = x; destinations = z; } ..] -> [{ name = "restic-backup-source-to-destination"; value = { sources = x; destinations = z; }; } ..] |> lib.map (attrset: { name = "restic-backup-${attrset.sources}-to-${attrset.destinations}"; value = backupTemplater attrset; }) + # Turns that thing above into { "restic-backup-source-to-destination" = { sources = x; .. }; ..} |> lib.listToAttrs; backupServices = config.nixchad.resticModule.mappings + # { mapping-name = { sources = [x y]; ..}; ..} -> { mapping-name = { "restic-backup-source-to-destination" = { sources = x; ..}; ..}; ..} |> lib.mapAttrs (name: mapping: transformMapping mapping) + # Flatten attrs: { mapping-name = { "restic-backup-source-to-destination" = {..}; }; ..} -> { "restic-backup-source-to-destination" = {..}; ..} |> lib.foldlAttrs ( acc: _name: value: acc // value ) { }; in { - systemd.services = destinationServices // backupServices; + systemd.services = lib.traceVal (destinationServices // backupServices); }; } From c3e8dc749a355057ed46fb303b6fc9a0d5213c59 Mon Sep 17 00:00:00 2001 From: KFears Date: Wed, 21 Jan 2026 06:06:31 +0400 Subject: [PATCH 03/12] feat: add rough impl for backups, very wip Signed-off-by: KFears --- .../common-services/restic/module.nix | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index a4418b47..7f45dd7e 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -30,6 +30,18 @@ in default = { }; }; + backupPre = lib.mkOption { + description = "Commands to run before the backup"; + type = lib.types.nullOr lib.types.str; + default = ""; + }; + + backupPost = lib.mkOption { + description = "Commands to run after the backup"; + type = lib.types.nullOr lib.types.str; + default = ""; + }; + paths = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; @@ -267,12 +279,37 @@ in CacheDirectory = "restic-destination-${name}"; CacheDirectoryMode = "0700"; PrivateTmp = true; + # We don't want this to re-run needlessly + RemainAfterExit = true; }; }; - args = source: lib.concatStringsSep " " (source.extraBackupArgs); + args = + source: destination: + lib.concatStringsSep " " ( + source.extraBackupArgs + ++ lib.optionals fileBackup ( + (excludeFlags source) ++ [ "--files-from=${filesFromTmpFile source destination}" ] + ) + ++ lib.optionals commandBackup ([ "--stdin-from-command=true --" ]) source.command + ); argsExtractSource = - mapping: args (lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.sources); + mapping: + args (lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.sources) ( + lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.destinations + ); + + excludeFlags = + backup: + lib.optional ( + backup.exclude != [ ] + ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}"; + fileBackup = backup: (backup.dynamicFilesFrom != null) || (backup.paths != [ ]); + commandBackup = backup: backup.command != [ ]; + doBackup = fileBackup || commandBackup; + + # FIXME: function expects strings, but is provided with attrsets + filesFromTmpFile = sources: destinations: "/run/restic-backup-${sources}-to-${destinations}"; # mapping is { sources = "foo"; destinations = "bar"; } backupTemplater = mapping: { @@ -295,10 +332,32 @@ in ExecStart = "${resticCmd mapping.sources} backup ${argsExtractSource mapping.sources}"; User = "root"; Group = "root"; - RuntimeDirectory = "restic-destination-${mapping.destinations}"; + RuntimeDirectory = "restic-backup-${mapping.sources}-to-${mapping.destinations}"; CacheDirectory = "restic-destination-${mapping.destinations}"; CacheDirectoryMode = "0700"; PrivateTmp = true; + # FIXME: some things expect attrsets and some strings + ExecStartPre = '' + ${lib.optionalString (mapping.sources.backupPre != null) '' + ${pkgs.writeScript "backupPre" mapping.sources.backupPre} + ''} + ${lib.optionalString (mapping.sources.paths != [ ]) '' + cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.sources.paths)} >> ${filesFromTmpFile mapping.sources mapping.destinations} + ''} + ${lib.optionalString (mapping.sources.dynamicFilesFrom != null) '' + ${pkgs.writeScript "dynamicFilesFromScript" mapping.sources.dynamicFilesFrom} >> ${filesFromTmpFile mapping.sources mapping.destinations} + ''} + ''; + # FIXME: some things expect attrsets and some strings + ExecStopPost = '' + ${lib.optionalString (mapping.sources.backupPost != null) '' + ${pkgs.writeScript "backupPost" mapping.sources.backupPost} + ''} + ${lib.optionalString fileBackup '' + rm -f ${filesFromTmpFile} + ''} + ''; + Slice = "restic-destination-${mapping.destinations}.slice"; }; }; From 5d6c680cc485b4655f4a5afb4b8e629f5b069eef Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 17:10:50 +0400 Subject: [PATCH 04/12] feat: port assertion patch from Olivia <3 Signed-off-by: KFears --- nixosModules/common-services/restic/module.nix | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 7f45dd7e..baa8c38a 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -393,7 +393,16 @@ in acc // value ) { }; in - { + lib.mkIf config.nixchad.resticModule.enable { + assertions = ( + lib.mapAttrsToList (name: source: { + assertion = lib.xor (source.paths != [ ] || source.dynamicFilesFrom != null) ( + source.command != [ ] + ); + message = "config.nixchad.resticModule.sources.${name} must specify exactly one of 'paths'/'dynamicFilesFrom' or 'command', but not both"; + }) config.nixchad.resticModule.sources + ); + systemd.services = lib.traceVal (destinationServices // backupServices); }; } From 6582d7d21f0ff63c120b6ec8c34bd5f5a8f2462a Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 17:10:50 +0400 Subject: [PATCH 05/12] feat: port singular patch from Olivia <3 Signed-off-by: KFears --- .../common-services/restic/module.nix | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index baa8c38a..549d49fe 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -309,55 +309,55 @@ in doBackup = fileBackup || commandBackup; # FIXME: function expects strings, but is provided with attrsets - filesFromTmpFile = sources: destinations: "/run/restic-backup-${sources}-to-${destinations}"; + filesFromTmpFile = source: destination: "/run/restic-backup-${source}-to-${destination}"; - # mapping is { sources = "foo"; destinations = "bar"; } + # mapping is { source = "foo"; destination = "bar"; } backupTemplater = mapping: { environment = lib.getAttrFromPath [ - mapping.destinations + mapping.destination "settings" ] config.nixchad.resticModule.destinations; path = [ config.programs.ssh.package ]; restartIfChanged = false; wants = [ "network-online.target" - "restic-destination-${mapping.destinations}.service" + "restic-destination-${mapping.destination}.service" ]; after = [ "network-online.target" - "restic-destination-${mapping.destinations}.service" + "restic-destination-${mapping.destination}.service" ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${resticCmd mapping.sources} backup ${argsExtractSource mapping.sources}"; + ExecStart = "${resticCmd mapping.source} backup ${argsExtractSource mapping.source}"; User = "root"; Group = "root"; - RuntimeDirectory = "restic-backup-${mapping.sources}-to-${mapping.destinations}"; - CacheDirectory = "restic-destination-${mapping.destinations}"; + RuntimeDirectory = "restic-backup-${mapping.source}-to-${mapping.destination}"; + CacheDirectory = "restic-destination-${mapping.destination}"; CacheDirectoryMode = "0700"; PrivateTmp = true; # FIXME: some things expect attrsets and some strings ExecStartPre = '' - ${lib.optionalString (mapping.sources.backupPre != null) '' - ${pkgs.writeScript "backupPre" mapping.sources.backupPre} + ${lib.optionalString (mapping.source.backupPre != null) '' + ${pkgs.writeScript "backupPre" mapping.source.backupPre} ''} - ${lib.optionalString (mapping.sources.paths != [ ]) '' - cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.sources.paths)} >> ${filesFromTmpFile mapping.sources mapping.destinations} + ${lib.optionalString (mapping.source.paths != [ ]) '' + cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.source.paths)} >> ${filesFromTmpFile mapping.source mapping.destination} ''} - ${lib.optionalString (mapping.sources.dynamicFilesFrom != null) '' - ${pkgs.writeScript "dynamicFilesFromScript" mapping.sources.dynamicFilesFrom} >> ${filesFromTmpFile mapping.sources mapping.destinations} + ${lib.optionalString (mapping.source.dynamicFilesFrom != null) '' + ${pkgs.writeScript "dynamicFilesFromScript" mapping.source.dynamicFilesFrom} >> ${filesFromTmpFile mapping.source mapping.destination} ''} ''; # FIXME: some things expect attrsets and some strings ExecStopPost = '' - ${lib.optionalString (mapping.sources.backupPost != null) '' - ${pkgs.writeScript "backupPost" mapping.sources.backupPost} + ${lib.optionalString (mapping.source.backupPost != null) '' + ${pkgs.writeScript "backupPost" mapping.source.backupPost} ''} ${lib.optionalString fileBackup '' rm -f ${filesFromTmpFile} ''} ''; - Slice = "restic-destination-${mapping.destinations}.slice"; + Slice = "restic-destination-${mapping.destination}.slice"; }; }; @@ -375,17 +375,23 @@ in mapping # { sources = [x y]; destinations = [z w]; } -> [{ sources = x; destinations = z; } { sources = y; destinations = z; } ..] |> lib.cartesianProduct - # [{ sources = x; destinations = z; } ..] -> [{ name = "restic-backup-source-to-destination"; value = { sources = x; destinations = z; }; } ..] + # Use singular attribute names from this point on + # [{ sources = x; destinations = z; } ..] -> [{ source = x; destination = z; } ..] |> lib.map (attrset: { - name = "restic-backup-${attrset.sources}-to-${attrset.destinations}"; + source = attrset.sources; + destination = attrset.destinations; + }) + # [{ source = x; destination = z; } ..] -> [{ name = "restic-backup-source-to-destination"; value = { source = x; destination = z; }; } ..] + |> lib.map (attrset: { + name = "restic-backup-${attrset.source}-to-${attrset.destination}"; value = backupTemplater attrset; }) - # Turns that thing above into { "restic-backup-source-to-destination" = { sources = x; .. }; ..} + # Turns that thing above into { "restic-backup-source-to-destination" = { source = x; .. }; ..} |> lib.listToAttrs; backupServices = config.nixchad.resticModule.mappings - # { mapping-name = { sources = [x y]; ..}; ..} -> { mapping-name = { "restic-backup-source-to-destination" = { sources = x; ..}; ..}; ..} + # { mapping-name = { sources = [x y]; ..}; ..} -> { mapping-name = { "restic-backup-source-to-destination" = { source = x; ..}; ..}; ..} |> lib.mapAttrs (name: mapping: transformMapping mapping) # Flatten attrs: { mapping-name = { "restic-backup-source-to-destination" = {..}; }; ..} -> { "restic-backup-source-to-destination" = {..}; ..} |> lib.foldlAttrs ( From 1a40c20a7daf43c863731b32772e3b63270ad41a Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 17:31:50 +0400 Subject: [PATCH 06/12] feat: port name patch by Olivia <3 Signed-off-by: KFears --- .../common-services/restic/module.nix | 81 +++++++++---------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 549d49fe..e5b33273 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -248,21 +248,17 @@ in config = let - extraOptions = - name: - lib.concatMapStrings (arg: " -o ${arg}") ( - lib.attrByPath [ name "extraOptions" ] [ ] config.nixchad.resticModule.destinations - ); + extraOptions = mapping: lib.concatMapStrings (arg: " -o ${arg}") mapping.destinations.extraOptions; inhibitCmd = - name: + mapping: lib.concatStringsSep " " [ "${pkgs.systemd}/bin/systemd-inhibit" "--mode='block'" "--who='restic'" "--what='idle:sleep:shutdown:handle-lid-switch'" - "--why=${lib.escapeShellArg "Scheduled backup ${name}"} " + "--why=${lib.escapeShellArg "Scheduled backup ${mapping.source.name}"} " ]; - resticCmd = name: "${inhibitCmd name}${lib.getExe pkgs.restic}${extraOptions name}"; + resticCmd = mapping: "${inhibitCmd mapping}${lib.getExe pkgs.restic}${extraOptions mapping}"; destinationTemplater = name: destination: { environment = destination.settings; @@ -285,79 +281,68 @@ in }; args = - source: destination: + mapping: lib.concatStringsSep " " ( - source.extraBackupArgs + mapping.source.extraBackupArgs ++ lib.optionals fileBackup ( - (excludeFlags source) ++ [ "--files-from=${filesFromTmpFile source destination}" ] + (excludeFlags mapping.source) ++ [ "--files-from=${filesFromTmpFile mapping}" ] ) - ++ lib.optionals commandBackup ([ "--stdin-from-command=true --" ]) source.command - ); - argsExtractSource = - mapping: - args (lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.sources) ( - lib.getAttrFromPath [ mapping ] config.nixchad.resticModule.destinations + ++ lib.optionals commandBackup ([ "--stdin-from-command=true --" ]) mapping.source.command ); excludeFlags = - backup: + source: lib.optional ( - backup.exclude != [ ] - ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" backup.exclude)}"; - fileBackup = backup: (backup.dynamicFilesFrom != null) || (backup.paths != [ ]); - commandBackup = backup: backup.command != [ ]; - doBackup = fileBackup || commandBackup; + source.exclude != [ ] + ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" source.exclude)}"; + fileBackup = source: (source.dynamicFilesFrom != null) || (source.paths != [ ]); + commandBackup = source: source.command != [ ]; - # FIXME: function expects strings, but is provided with attrsets - filesFromTmpFile = source: destination: "/run/restic-backup-${source}-to-${destination}"; + filesFromTmpFile = + mapping: "/run/restic-backup-${mapping.source.name}-to-${mapping.destination.name}"; - # mapping is { source = "foo"; destination = "bar"; } + # mapping is { source = { name = "foo"; ..}; ..} backupTemplater = mapping: { - environment = lib.getAttrFromPath [ - mapping.destination - "settings" - ] config.nixchad.resticModule.destinations; + environment = mapping.destination.settings; path = [ config.programs.ssh.package ]; restartIfChanged = false; wants = [ "network-online.target" - "restic-destination-${mapping.destination}.service" + "restic-destination-${mapping.destination.name}.service" ]; after = [ "network-online.target" - "restic-destination-${mapping.destination}.service" + "restic-destination-${mapping.destination.name}.service" ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${resticCmd mapping.source} backup ${argsExtractSource mapping.source}"; + ExecStart = "${resticCmd mapping} backup ${args mapping}"; User = "root"; Group = "root"; - RuntimeDirectory = "restic-backup-${mapping.source}-to-${mapping.destination}"; - CacheDirectory = "restic-destination-${mapping.destination}"; + RuntimeDirectory = "restic-backup-${mapping.source.name}-to-${mapping.destination.name}"; + CacheDirectory = "restic-destination-${mapping.destination.name}"; CacheDirectoryMode = "0700"; PrivateTmp = true; - # FIXME: some things expect attrsets and some strings ExecStartPre = '' ${lib.optionalString (mapping.source.backupPre != null) '' ${pkgs.writeScript "backupPre" mapping.source.backupPre} ''} ${lib.optionalString (mapping.source.paths != [ ]) '' - cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.source.paths)} >> ${filesFromTmpFile mapping.source mapping.destination} + cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.source.paths)} >> ${filesFromTmpFile mapping} ''} ${lib.optionalString (mapping.source.dynamicFilesFrom != null) '' - ${pkgs.writeScript "dynamicFilesFromScript" mapping.source.dynamicFilesFrom} >> ${filesFromTmpFile mapping.source mapping.destination} + ${pkgs.writeScript "dynamicFilesFromScript" mapping.source.dynamicFilesFrom} >> ${filesFromTmpFile mapping} ''} ''; - # FIXME: some things expect attrsets and some strings ExecStopPost = '' ${lib.optionalString (mapping.source.backupPost != null) '' ${pkgs.writeScript "backupPost" mapping.source.backupPost} ''} - ${lib.optionalString fileBackup '' - rm -f ${filesFromTmpFile} + ${lib.optionalString (fileBackup mapping.source) '' + rm -f ${filesFromTmpFile mapping} ''} ''; - Slice = "restic-destination-${mapping.destination}.slice"; + Slice = "restic-destination-${mapping.destination.name}.slice"; }; }; @@ -372,7 +357,15 @@ in transformMapping = mapping: - mapping + # Resolves source/destination names to the config attrsets + { + # ["x"..] -> [{ name = "x"; unitOptions = ..; .. } ..] + sources = + mapping.sources |> lib.map (name: { inherit name; } // config.nixchad.resticModule.sources.${name}); + destinations = + mapping.destinations + |> lib.map (name: { inherit name; } // config.nixchad.resticModule.destinations.${name}); + } # { sources = [x y]; destinations = [z w]; } -> [{ sources = x; destinations = z; } { sources = y; destinations = z; } ..] |> lib.cartesianProduct # Use singular attribute names from this point on @@ -383,7 +376,7 @@ in }) # [{ source = x; destination = z; } ..] -> [{ name = "restic-backup-source-to-destination"; value = { source = x; destination = z; }; } ..] |> lib.map (attrset: { - name = "restic-backup-${attrset.source}-to-${attrset.destination}"; + name = "restic-backup-${attrset.source.name}-to-${attrset.destination.name}"; value = backupTemplater attrset; }) # Turns that thing above into { "restic-backup-source-to-destination" = { source = x; .. }; ..} From 454043f81dd6be72829fd4e1e7f3d14f0851ca72 Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 20:32:06 +0400 Subject: [PATCH 07/12] feat: port scoping patch from Olivia <3 Signed-off-by: KFears --- .../common-services/restic/module.nix | 189 +++++++++--------- 1 file changed, 98 insertions(+), 91 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index e5b33273..5ae32d6b 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -248,103 +248,110 @@ in config = let - extraOptions = mapping: lib.concatMapStrings (arg: " -o ${arg}") mapping.destinations.extraOptions; - inhibitCmd = - mapping: - lib.concatStringsSep " " [ - "${pkgs.systemd}/bin/systemd-inhibit" - "--mode='block'" - "--who='restic'" - "--what='idle:sleep:shutdown:handle-lid-switch'" - "--why=${lib.escapeShellArg "Scheduled backup ${mapping.source.name}"} " - ]; - resticCmd = mapping: "${inhibitCmd mapping}${lib.getExe pkgs.restic}${extraOptions mapping}"; - - destinationTemplater = name: destination: { - environment = destination.settings; - path = [ config.programs.ssh.package ]; - restartIfChanged = false; - wants = [ "network-online.target" ]; - after = [ "network-online.target" ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${resticCmd name} cat config > /dev/null || ${resticCmd name} init"; - User = "root"; - Group = "root"; - RuntimeDirectory = "restic-destination-${name}"; - CacheDirectory = "restic-destination-${name}"; - CacheDirectoryMode = "0700"; - PrivateTmp = true; - # We don't want this to re-run needlessly - RemainAfterExit = true; + destinationTemplater = + name: destination: + let + extraOptions = lib.concatMapStrings (arg: " -o ${arg}") destination.extraOptions; + inhibitCmd = lib.concatStringsSep " " [ + "${pkgs.systemd}/bin/systemd-inhibit" + "--mode='block'" + "--who='restic'" + "--what='idle:sleep:shutdown:handle-lid-switch'" + "--why=${lib.escapeShellArg "Initializing destination ${name}"} " + ]; + resticCmd = "${inhibitCmd}${lib.getExe pkgs.restic}${extraOptions}"; + in + { + environment = destination.settings; + path = [ config.programs.ssh.package ]; + restartIfChanged = false; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${resticCmd} cat config > /dev/null || ${resticCmd} init"; + User = "root"; + Group = "root"; + RuntimeDirectory = "restic-destination-${name}"; + CacheDirectory = "restic-destination-${name}"; + CacheDirectoryMode = "0700"; + PrivateTmp = true; + # We don't want this to re-run needlessly + RemainAfterExit = true; + }; }; - }; - args = + # mapping is { source = { name = "foo"; ..}; ..} + backupTemplater = mapping: - lib.concatStringsSep " " ( - mapping.source.extraBackupArgs - ++ lib.optionals fileBackup ( - (excludeFlags mapping.source) ++ [ "--files-from=${filesFromTmpFile mapping}" ] - ) - ++ lib.optionals commandBackup ([ "--stdin-from-command=true --" ]) mapping.source.command - ); + let + inherit (mapping) destination source; - excludeFlags = - source: - lib.optional ( - source.exclude != [ ] - ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" source.exclude)}"; - fileBackup = source: (source.dynamicFilesFrom != null) || (source.paths != [ ]); - commandBackup = source: source.command != [ ]; + extraOptions = lib.concatMapStrings (arg: " -o ${arg}") destination.extraOptions; + inhibitCmd = lib.concatStringsSep " " [ + "${pkgs.systemd}/bin/systemd-inhibit" + "--mode='block'" + "--who='restic'" + "--what='idle:sleep:shutdown:handle-lid-switch'" + "--why=${lib.escapeShellArg "Scheduled backup ${source.name}"} " + ]; + resticCmd = "${inhibitCmd}${lib.getExe pkgs.restic}${extraOptions}"; + excludeFlags = lib.optional ( + source.exclude != [ ] + ) "--exclude-file=${pkgs.writeText "exclude-patterns" (lib.concatStringsSep "\n" source.exclude)}"; + fileBackup = (source.dynamicFilesFrom != null) || (source.paths != [ ]); + commandBackup = source.command != [ ]; + filesFromTmpFile = "/run/restic-backup-${source.name}-to-${destination.name}"; - filesFromTmpFile = - mapping: "/run/restic-backup-${mapping.source.name}-to-${mapping.destination.name}"; - - # mapping is { source = { name = "foo"; ..}; ..} - backupTemplater = mapping: { - environment = mapping.destination.settings; - path = [ config.programs.ssh.package ]; - restartIfChanged = false; - wants = [ - "network-online.target" - "restic-destination-${mapping.destination.name}.service" - ]; - after = [ - "network-online.target" - "restic-destination-${mapping.destination.name}.service" - ]; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${resticCmd mapping} backup ${args mapping}"; - User = "root"; - Group = "root"; - RuntimeDirectory = "restic-backup-${mapping.source.name}-to-${mapping.destination.name}"; - CacheDirectory = "restic-destination-${mapping.destination.name}"; - CacheDirectoryMode = "0700"; - PrivateTmp = true; - ExecStartPre = '' - ${lib.optionalString (mapping.source.backupPre != null) '' - ${pkgs.writeScript "backupPre" mapping.source.backupPre} - ''} - ${lib.optionalString (mapping.source.paths != [ ]) '' - cat ${pkgs.writeText "staticPaths" (lib.concatLines mapping.source.paths)} >> ${filesFromTmpFile mapping} - ''} - ${lib.optionalString (mapping.source.dynamicFilesFrom != null) '' - ${pkgs.writeScript "dynamicFilesFromScript" mapping.source.dynamicFilesFrom} >> ${filesFromTmpFile mapping} - ''} - ''; - ExecStopPost = '' - ${lib.optionalString (mapping.source.backupPost != null) '' - ${pkgs.writeScript "backupPost" mapping.source.backupPost} - ''} - ${lib.optionalString (fileBackup mapping.source) '' - rm -f ${filesFromTmpFile mapping} - ''} - ''; - Slice = "restic-destination-${mapping.destination.name}.slice"; + args = lib.concatStringsSep " " ( + source.extraBackupArgs + ++ lib.optionals fileBackup (excludeFlags ++ [ "--files-from=${filesFromTmpFile}" ]) + ++ lib.optionals commandBackup ([ "--stdin-from-command=true -- " ] ++ source.command) + ); + in + { + environment = destination.settings; + path = [ config.programs.ssh.package ]; + restartIfChanged = false; + wants = [ + "network-online.target" + "restic-destination-${destination.name}.service" + ]; + after = [ + "network-online.target" + "restic-destination-${destination.name}.service" + ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${resticCmd} backup ${args}"; + User = "root"; + Group = "root"; + RuntimeDirectory = "restic-backup-${source.name}-to-${destination.name}"; + CacheDirectory = "restic-destination-${destination.name}"; + CacheDirectoryMode = "0700"; + PrivateTmp = true; + ExecStartPre = '' + ${lib.optionalString (source.backupPre != null) '' + ${pkgs.writeScript "backupPre" source.backupPre} + ''} + ${lib.optionalString (source.paths != [ ]) '' + cat ${pkgs.writeText "staticPaths" (lib.concatLines source.paths)} >> ${filesFromTmpFile} + ''} + ${lib.optionalString (source.dynamicFilesFrom != null) '' + ${pkgs.writeScript "dynamicFilesFromScript" source.dynamicFilesFrom} >> ${filesFromTmpFile} + ''} + ''; + ExecStopPost = '' + ${lib.optionalString (source.backupPost != null) '' + ${pkgs.writeScript "backupPost" source.backupPost} + ''} + ${lib.optionalString (fileBackup source) '' + rm -f ${filesFromTmpFile mapping} + ''} + ''; + Slice = "restic-destination-${destination.name}.slice"; + }; }; - }; destinationServices = config.nixchad.resticModule.destinations From fd1672a1cbf6c2bb39ccf45f661cf18c88379dc0 Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 16:31:08 +0400 Subject: [PATCH 08/12] feat: add slice Signed-off-by: KFears --- nixosModules/common-services/restic/module.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 5ae32d6b..de83a74e 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -410,5 +410,9 @@ in ); systemd.services = lib.traceVal (destinationServices // backupServices); + systemd.slices = lib.traceVal ( + config.nixchad.resticModule.destinations + |> lib.mapAttrs (name: destination: { sliceConfig.ConcurrencySoftMax = "1"; }) + ); }; } From 4c3ea7a14a472e910d738228c351e2b6be523e06 Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 21:14:18 +0400 Subject: [PATCH 09/12] fix: backup commands, use writeShellScript Signed-off-by: KFears --- nixosModules/common-services/restic/module.nix | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index de83a74e..6be26b0f 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -269,7 +269,7 @@ in after = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${resticCmd} cat config > /dev/null || ${resticCmd} init"; + ExecStart = pkgs.writeShellScript "restic-destination-${name}" "${resticCmd} cat config > /dev/null || ${resticCmd} init"; User = "root"; Group = "root"; RuntimeDirectory = "restic-destination-${name}"; @@ -330,10 +330,8 @@ in CacheDirectory = "restic-destination-${destination.name}"; CacheDirectoryMode = "0700"; PrivateTmp = true; - ExecStartPre = '' - ${lib.optionalString (source.backupPre != null) '' - ${pkgs.writeScript "backupPre" source.backupPre} - ''} + ExecStartPre = pkgs.writeShellScript "restic-backup-${source.name}-to-${destination.name}-pre" '' + ${lib.optionalString (source.backupPre != null) source.backupPre} ${lib.optionalString (source.paths != [ ]) '' cat ${pkgs.writeText "staticPaths" (lib.concatLines source.paths)} >> ${filesFromTmpFile} ''} @@ -341,12 +339,10 @@ in ${pkgs.writeScript "dynamicFilesFromScript" source.dynamicFilesFrom} >> ${filesFromTmpFile} ''} ''; - ExecStopPost = '' - ${lib.optionalString (source.backupPost != null) '' - ${pkgs.writeScript "backupPost" source.backupPost} - ''} - ${lib.optionalString (fileBackup source) '' - rm -f ${filesFromTmpFile mapping} + ExecStopPost = pkgs.writeShellScript "restic-backup-${source.name}-to-${destination.name}-post" '' + ${lib.optionalString (source.backupPost != null) source.backupPost} + ${lib.optionalString fileBackup '' + rm -f ${filesFromTmpFile} ''} ''; Slice = "restic-destination-${destination.name}.slice"; From 46bf5a12bbcc8047def05eb1d11f026d40ff73f1 Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 21:14:18 +0400 Subject: [PATCH 10/12] fix: remove traces Signed-off-by: KFears --- nixosModules/common-services/restic/module.nix | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 6be26b0f..5edcce97 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -405,10 +405,9 @@ in }) config.nixchad.resticModule.sources ); - systemd.services = lib.traceVal (destinationServices // backupServices); - systemd.slices = lib.traceVal ( + systemd.services = destinationServices // backupServices; + systemd.slices = config.nixchad.resticModule.destinations - |> lib.mapAttrs (name: destination: { sliceConfig.ConcurrencySoftMax = "1"; }) - ); + |> lib.mapAttrs (name: destination: { sliceConfig.ConcurrencySoftMax = "1"; }); }; } From 7ca2d53d25908f24aae6f98cf89fb163c520b0fa Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 21:14:18 +0400 Subject: [PATCH 11/12] feat: modernize values Signed-off-by: KFears --- .../common-services/restic/values.nix | 170 ++++++++++-------- 1 file changed, 91 insertions(+), 79 deletions(-) diff --git a/nixosModules/common-services/restic/values.nix b/nixosModules/common-services/restic/values.nix index 06f551a5..8f644d36 100644 --- a/nixosModules/common-services/restic/values.nix +++ b/nixosModules/common-services/restic/values.nix @@ -5,90 +5,102 @@ username, ... }: +let + photos = config.networking.hostName != "cloudberry"; + postgres = config.services.postgresql.enable; + vaultwarden = config.services.vaultwarden.enable; + paperless = config.services.paperless.enable; +in { - nixchad.resticModule = { - enable = true; + nixchad.resticModule = ( + lib.mkIf config.nixchad.resticModule.enable { + sources = lib.mkMerge [ + { + secrets = { + paths = [ + "/secrets" + ]; + }; + stuff = { + paths = [ + "/home/${username}/Sync" + ]; + }; + } + (lib.mkIf photos { + photos = { + paths = [ + "/home/${username}/Pictures/Photos" + "/home/${username}/Pictures/Photos-phone" + ]; + }; + }) + (lib.mkIf postgres { + postgres = { + paths = [ + "/tmp/postgres" + ]; + backupPre = '' + echo 'creating temporary directory' + mkdir -p /tmp/postgres + echo 'dumping PostgreSQL database' + ${pkgs.shadow.su}/bin/su postgres -c ${config.services.postgresql.package}/bin/pg_dumpall > /tmp/postgres/pgdump.sql + ''; + backupPost = '' + rm -rfv /tmp/postgres + ''; + }; + }) + (lib.mkIf vaultwarden { + vaultwarden = { + paths = [ + "/tmp/vaultwarden" + ]; + backupPre = '' + echo 'creating temporary directory' + mkdir -p /tmp/vaultwarden + echo 'copying Vaultwarden data' + cp --reflink=auto -r /var/lib/bitwarden_rs /tmp/vaultwarden + ''; + backupPost = '' + rm -rfv /tmp/vaultwarden + ''; + }; + }) + (lib.mkIf paperless { + paperless = { + paths = [ + config.services.paperless.dataDir + ]; + }; + }) + ]; - sources = { - secrets = { - paths = [ - "/secrets" - ]; - }; - stuff = { - paths = [ - "/home/${username}/Sync" - ]; - }; - photos = { - paths = [ - "/home/${username}/Pictures/Photos" - "/home/${username}/Pictures/Photos-phone" - ]; - }; - postgres = { - paths = [ - "/tmp/postgres" - ]; - unitOptions = { - preStart = '' - echo 'creating temporary directory' - mkdir -p /tmp/postgres - echo 'dumping PostgreSQL database' - ${pkgs.shadow.su}/bin/su postgres -c ${config.services.postgresql.package}/bin/pg_dumpall > /tmp/postgres/pgdump.sql - ''; - postStop = '' - rm -rfv /tmp/postgres - ''; - }; - }; - vaultwarden = { - paths = [ - "/tmp/vaultwarden" - ]; - unitOptions = { - preStart = '' - echo 'creating temporary directory' - mkdir -p /tmp/vaultwarden - echo 'copying Vaultwarden data' - cp --reflink=auto -r /var/lib/bitwarden_rs /tmp/vaultwarden - ''; - postStop = '' - rm -rfv /tmp/vaultwarden - ''; + destinations = { + linus = { + settings = { + RESTIC_REPOSITORY = "sftp:kfears@sol.sphalerite.tech:/backup"; + RESTIC_PASSWORD_FILE = "/secrets/restic-backup-linus"; + }; + extraOptions = [ + "sftp.command='ssh kfears@sol.sphalerite.tech -i /home/${username}/.ssh/id_ed25519 -o StrictHostKeyChecking=no -s sftp'" + ]; }; }; - paperless = { - paths = [ - config.services.paperless.dataDir - ]; - }; - }; - destinations = { - linus = { - settings = { - RESTIC_REPOSITORY = "sftp:kfears@sol.sphalerite.tech:/backup"; - RESTIC_PASSWORD_FILE = "/secrets/restic-backup-linus"; + mappings = { + linus = { + sources = [ + "secrets" + "stuff" + ] + ++ (lib.optional photos "photos") + ++ (lib.optional postgres "postgres") + ++ (lib.optional vaultwarden "vaultwarden") + ++ (lib.optional paperless "paperless"); + destinations = [ "linus" ]; }; - extraOptions = [ - "sftp.command='ssh kfears@sol.sphalerite.tech -i /home/${username}/.ssh/id_ed25519 -o StrictHostKeyChecking=no -s sftp'" - ]; - }; - }; - - mappings = { - linus = { - sources = [ - "secrets" - "stuff" - "photos" - "postgres" - "vaultwarden" - "paperless" - ]; - destinations = [ "linus" ]; }; - }; - }; + } + ); } From 0a9512ccca2b654b8589449e50a746160a2e096f Mon Sep 17 00:00:00 2001 From: KFears Date: Fri, 23 Jan 2026 22:25:38 +0400 Subject: [PATCH 12/12] fix: lint checks Signed-off-by: KFears --- .../common-services/restic/module.nix | 383 +++++++++--------- .../common-services/restic/values.nix | 170 ++++---- statix.toml | 3 + 3 files changed, 273 insertions(+), 283 deletions(-) create mode 100644 statix.toml diff --git a/nixosModules/common-services/restic/module.nix b/nixosModules/common-services/restic/module.nix index 5edcce97..79b02f89 100644 --- a/nixosModules/common-services/restic/module.nix +++ b/nixosModules/common-services/restic/module.nix @@ -20,98 +20,95 @@ in Sources from which backups will be created. ''; type = lib.types.attrsOf ( - lib.types.submodule ( - { name, ... }: - { - options = { - unitOptions = lib.mkOption { - description = "Additional systemd unit options"; - type = unitOption; - default = { }; - }; + lib.types.submodule { + options = { + unitOptions = lib.mkOption { + description = "Additional systemd unit options"; + type = unitOption; + default = { }; + }; - backupPre = lib.mkOption { - description = "Commands to run before the backup"; - type = lib.types.nullOr lib.types.str; - default = ""; - }; + backupPre = lib.mkOption { + description = "Commands to run before the backup"; + type = lib.types.nullOr lib.types.str; + default = ""; + }; - backupPost = lib.mkOption { - description = "Commands to run after the backup"; - type = lib.types.nullOr lib.types.str; - default = ""; - }; + backupPost = lib.mkOption { + description = "Commands to run after the backup"; + type = lib.types.nullOr lib.types.str; + default = ""; + }; - paths = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Which paths to backup, in addition to ones specified via - `dynamicFilesFrom`. If null or an empty array and - `dynamicFilesFrom` is also null, no backup command will be run. - This can be used to create a prune-only job. - ''; - example = [ - "/var/lib/postgresql" - "/home/user/backup" - ]; - }; + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Which paths to backup, in addition to ones specified via + `dynamicFilesFrom`. If null or an empty array and + `dynamicFilesFrom` is also null, no backup command will be run. + This can be used to create a prune-only job. + ''; + example = [ + "/var/lib/postgresql" + "/home/user/backup" + ]; + }; - command = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Command to pass to --stdin-from-command. If null or an empty array, and `paths`/`dynamicFilesFrom` - are also null, no backup command will be run. - ''; - example = [ - "sudo" - "-u" - "postgres" - "pg_dumpall" - ]; - }; + command = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Command to pass to --stdin-from-command. If null or an empty array, and `paths`/`dynamicFilesFrom` + are also null, no backup command will be run. + ''; + example = [ + "sudo" + "-u" + "postgres" + "pg_dumpall" + ]; + }; - exclude = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Patterns to exclude when backing up. See - https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for - details on syntax. - ''; - example = [ - "/var/cache" - "/home/*/.cache" - ".git" - ]; - }; + exclude = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Patterns to exclude when backing up. See + https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for + details on syntax. + ''; + example = [ + "/var/cache" + "/home/*/.cache" + ".git" + ]; + }; - dynamicFilesFrom = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - A script that produces a list of files to back up. The - results of this command are given to the '--files-from' - option. The result is merged with paths specified via `paths`. - ''; - example = "find /home/matt/git -type d -name .git"; - }; + dynamicFilesFrom = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + A script that produces a list of files to back up. The + results of this command are given to the '--files-from' + option. The result is merged with paths specified via `paths`. + ''; + example = "find /home/matt/git -type d -name .git"; + }; - extraBackupArgs = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = '' - Extra arguments passed to restic backup. - ''; - example = [ - "--cleanup-cache" - "--exclude-file=/etc/nixos/restic-ignore" - ]; - }; + extraBackupArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Extra arguments passed to restic backup. + ''; + example = [ + "--cleanup-cache" + "--exclude-file=/etc/nixos/restic-ignore" + ]; }; - } - ) + }; + } ); }; @@ -120,103 +117,100 @@ in Destinations where backups will be stored. ''; type = lib.types.attrsOf ( - lib.types.submodule ( - { name, ... }: - { - options = { - init = lib.mkOption { - description = "Whether to init the destination repository prior to doing any backups"; - type = lib.types.bool; - default = true; - }; + lib.types.submodule { + options = { + init = lib.mkOption { + description = "Whether to init the destination repository prior to doing any backups"; + type = lib.types.bool; + default = true; + }; - unitOptions = lib.mkOption { - description = "Additional systemd unit options"; - type = unitOption; - default = { }; - }; + unitOptions = lib.mkOption { + description = "Additional systemd unit options"; + type = unitOption; + default = { }; + }; - extraOptions = lib.mkOption { - description = "Additional options added to restic invokation via '-o' flag"; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; + extraOptions = lib.mkOption { + description = "Additional options added to restic invokation via '-o' flag"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; - settings = lib.mkOption { - description = '' - Restic settings. They are provided as environment variables. They should be provided in upper snake - case (e.g. {env}`RESTIC_PASSWORD_FILE`). See - for supported options. + settings = lib.mkOption { + description = '' + Restic settings. They are provided as environment variables. They should be provided in upper snake + case (e.g. {env}`RESTIC_PASSWORD_FILE`). See + for supported options. - This option can also take rclone settings, also as environment variables. They should be provided in - upper snake case (e.g. {env}`RCLONE_SKIP_LINKS`). See for supported - options. Restic will automatically supply the remote type and name for you. To provide secrets to the - backend, it's recommended to create rclone config file yourself, and use {env}`RCLONE_CONFIG` option - to point to it. It is also recommended to use a separate config file if you care about - case-sensitivity for your remote name. - ''; - type = lib.types.submodule { - freeformType = - with lib.types; - attrsOf (oneOf [ - str - (listOf str) - ]); + This option can also take rclone settings, also as environment variables. They should be provided in + upper snake case (e.g. {env}`RCLONE_SKIP_LINKS`). See for supported + options. Restic will automatically supply the remote type and name for you. To provide secrets to the + backend, it's recommended to create rclone config file yourself, and use {env}`RCLONE_CONFIG` option + to point to it. It is also recommended to use a separate config file if you care about + case-sensitivity for your remote name. + ''; + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + str + (listOf str) + ]); - options = { - RESTIC_REPOSITORY = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - repository to backup to. - ''; - example = "sftp:backup@192.168.1.100:/backups/my-backup"; - }; - RESTIC_REPOSITORY_FILE = lib.mkOption { - type = with lib.types; nullOr path; - default = null; - description = '' - Path to the file containing the repository location to backup to. - ''; - }; - RESTIC_PASSWORD_FILE = lib.mkOption { - type = lib.types.str; - default = ""; - description = '' - Read the repository password from a file. - ''; - example = "/etc/nixos/restic-password"; - }; + options = { + RESTIC_REPOSITORY = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + repository to backup to. + ''; + example = "sftp:backup@192.168.1.100:/backups/my-backup"; + }; + RESTIC_REPOSITORY_FILE = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = '' + Path to the file containing the repository location to backup to. + ''; + }; + RESTIC_PASSWORD_FILE = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Read the repository password from a file. + ''; + example = "/etc/nixos/restic-password"; + }; - RCLONE_CONFIG = lib.mkOption { - type = - with lib.types; - nullOr (oneOf [ - str - path - ]); - default = null; - description = '' - Location of the rclone configuration file. - ''; - }; + RCLONE_CONFIG = lib.mkOption { + type = + with lib.types; + nullOr (oneOf [ + str + path + ]); + default = null; + description = '' + Location of the rclone configuration file. + ''; }; }; - example = lib.literalExpression '' - RESTIC_REPOSITORY = "s3:s3.us-east-1.amazonaws.com/bucket_name/restic"; - RESTIC_PASSWORD_FILE = "/secrets/password-file"; - AWS_ACCESS_KEY_ID = "XXXX"; - AWS_SECRET_ACCESS_KEY = "YYYY"; - RCLONE_BWLIMIT = "10M"; - RCLONE_HARD_DELETE = "true"; - # RCLONE_S3_PROVIDER = "AWS"; - # RCLONE_CONFIG_MYS3_ACCESS_KEY_ID = "XXXX"; - # RCLONE_CONFIG = "/my/config/file"; - ''; }; + example = lib.literalExpression '' + RESTIC_REPOSITORY = "s3:s3.us-east-1.amazonaws.com/bucket_name/restic"; + RESTIC_PASSWORD_FILE = "/secrets/password-file"; + AWS_ACCESS_KEY_ID = "XXXX"; + AWS_SECRET_ACCESS_KEY = "YYYY"; + RCLONE_BWLIMIT = "10M"; + RCLONE_HARD_DELETE = "true"; + # RCLONE_S3_PROVIDER = "AWS"; + # RCLONE_CONFIG_MYS3_ACCESS_KEY_ID = "XXXX"; + # RCLONE_CONFIG = "/my/config/file"; + ''; }; - } - ) + }; + } ); }; @@ -225,23 +219,20 @@ in Mappings between sources and destinations. ''; type = lib.types.attrsOf ( - lib.types.submodule ( - { name, ... }: - { - options = { - sources = lib.mkOption { - description = "Backup sources"; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; - destinations = lib.mkOption { - description = "Backup destinations"; - type = lib.types.listOf lib.types.str; - default = [ ]; - }; + lib.types.submodule { + options = { + sources = lib.mkOption { + description = "Backup sources"; + type = lib.types.listOf lib.types.str; + default = [ ]; }; - } - ) + destinations = lib.mkOption { + description = "Backup destinations"; + type = lib.types.listOf lib.types.str; + default = [ ]; + }; + }; + } ); }; }; @@ -388,7 +379,7 @@ in backupServices = config.nixchad.resticModule.mappings # { mapping-name = { sources = [x y]; ..}; ..} -> { mapping-name = { "restic-backup-source-to-destination" = { source = x; ..}; ..}; ..} - |> lib.mapAttrs (name: mapping: transformMapping mapping) + |> lib.mapAttrs (_name: mapping: transformMapping mapping) # Flatten attrs: { mapping-name = { "restic-backup-source-to-destination" = {..}; }; ..} -> { "restic-backup-source-to-destination" = {..}; ..} |> lib.foldlAttrs ( acc: _name: value: @@ -396,18 +387,16 @@ in ) { }; in lib.mkIf config.nixchad.resticModule.enable { - assertions = ( - lib.mapAttrsToList (name: source: { - assertion = lib.xor (source.paths != [ ] || source.dynamicFilesFrom != null) ( - source.command != [ ] - ); - message = "config.nixchad.resticModule.sources.${name} must specify exactly one of 'paths'/'dynamicFilesFrom' or 'command', but not both"; - }) config.nixchad.resticModule.sources - ); + assertions = lib.mapAttrsToList (name: source: { + assertion = lib.xor (source.paths != [ ] || source.dynamicFilesFrom != null) ( + source.command != [ ] + ); + message = "config.nixchad.resticModule.sources.${name} must specify exactly one of 'paths'/'dynamicFilesFrom' or 'command', but not both"; + }) config.nixchad.resticModule.sources; systemd.services = destinationServices // backupServices; systemd.slices = config.nixchad.resticModule.destinations - |> lib.mapAttrs (name: destination: { sliceConfig.ConcurrencySoftMax = "1"; }); + |> lib.mapAttrs (_: _: { sliceConfig.ConcurrencySoftMax = "1"; }); }; } diff --git a/nixosModules/common-services/restic/values.nix b/nixosModules/common-services/restic/values.nix index 8f644d36..5a08037e 100644 --- a/nixosModules/common-services/restic/values.nix +++ b/nixosModules/common-services/restic/values.nix @@ -12,95 +12,93 @@ let paperless = config.services.paperless.enable; in { - nixchad.resticModule = ( - lib.mkIf config.nixchad.resticModule.enable { - sources = lib.mkMerge [ - { - secrets = { - paths = [ - "/secrets" - ]; - }; - stuff = { - paths = [ - "/home/${username}/Sync" - ]; - }; - } - (lib.mkIf photos { - photos = { - paths = [ - "/home/${username}/Pictures/Photos" - "/home/${username}/Pictures/Photos-phone" - ]; - }; - }) - (lib.mkIf postgres { - postgres = { - paths = [ - "/tmp/postgres" - ]; - backupPre = '' - echo 'creating temporary directory' - mkdir -p /tmp/postgres - echo 'dumping PostgreSQL database' - ${pkgs.shadow.su}/bin/su postgres -c ${config.services.postgresql.package}/bin/pg_dumpall > /tmp/postgres/pgdump.sql - ''; - backupPost = '' - rm -rfv /tmp/postgres - ''; - }; - }) - (lib.mkIf vaultwarden { - vaultwarden = { - paths = [ - "/tmp/vaultwarden" - ]; - backupPre = '' - echo 'creating temporary directory' - mkdir -p /tmp/vaultwarden - echo 'copying Vaultwarden data' - cp --reflink=auto -r /var/lib/bitwarden_rs /tmp/vaultwarden - ''; - backupPost = '' - rm -rfv /tmp/vaultwarden - ''; - }; - }) - (lib.mkIf paperless { - paperless = { - paths = [ - config.services.paperless.dataDir - ]; - }; - }) - ]; - - destinations = { - linus = { - settings = { - RESTIC_REPOSITORY = "sftp:kfears@sol.sphalerite.tech:/backup"; - RESTIC_PASSWORD_FILE = "/secrets/restic-backup-linus"; - }; - extraOptions = [ - "sftp.command='ssh kfears@sol.sphalerite.tech -i /home/${username}/.ssh/id_ed25519 -o StrictHostKeyChecking=no -s sftp'" + nixchad.resticModule = lib.mkIf config.nixchad.resticModule.enable { + sources = lib.mkMerge [ + { + secrets = { + paths = [ + "/secrets" ]; }; - }; + stuff = { + paths = [ + "/home/${username}/Sync" + ]; + }; + } + (lib.mkIf photos { + photos = { + paths = [ + "/home/${username}/Pictures/Photos" + "/home/${username}/Pictures/Photos-phone" + ]; + }; + }) + (lib.mkIf postgres { + postgres = { + paths = [ + "/tmp/postgres" + ]; + backupPre = '' + echo 'creating temporary directory' + mkdir -p /tmp/postgres + echo 'dumping PostgreSQL database' + ${pkgs.shadow.su}/bin/su postgres -c ${config.services.postgresql.package}/bin/pg_dumpall > /tmp/postgres/pgdump.sql + ''; + backupPost = '' + rm -rfv /tmp/postgres + ''; + }; + }) + (lib.mkIf vaultwarden { + vaultwarden = { + paths = [ + "/tmp/vaultwarden" + ]; + backupPre = '' + echo 'creating temporary directory' + mkdir -p /tmp/vaultwarden + echo 'copying Vaultwarden data' + cp --reflink=auto -r /var/lib/bitwarden_rs /tmp/vaultwarden + ''; + backupPost = '' + rm -rfv /tmp/vaultwarden + ''; + }; + }) + (lib.mkIf paperless { + paperless = { + paths = [ + config.services.paperless.dataDir + ]; + }; + }) + ]; - mappings = { - linus = { - sources = [ - "secrets" - "stuff" - ] - ++ (lib.optional photos "photos") - ++ (lib.optional postgres "postgres") - ++ (lib.optional vaultwarden "vaultwarden") - ++ (lib.optional paperless "paperless"); - destinations = [ "linus" ]; + destinations = { + linus = { + settings = { + RESTIC_REPOSITORY = "sftp:kfears@sol.sphalerite.tech:/backup"; + RESTIC_PASSWORD_FILE = "/secrets/restic-backup-linus"; }; + extraOptions = [ + "sftp.command='ssh kfears@sol.sphalerite.tech -i /home/${username}/.ssh/id_ed25519 -o StrictHostKeyChecking=no -s sftp'" + ]; + }; + }; + + mappings = { + linus = { + sources = [ + "secrets" + "stuff" + ] + ++ (lib.optional photos "photos") + ++ (lib.optional postgres "postgres") + ++ (lib.optional vaultwarden "vaultwarden") + ++ (lib.optional paperless "paperless"); + destinations = [ "linus" ]; }; - } - ); + }; + }; } diff --git a/statix.toml b/statix.toml new file mode 100644 index 00000000..99b705f8 --- /dev/null +++ b/statix.toml @@ -0,0 +1,3 @@ +disabled = [ + "eta_reduction" +]