# A home-manager port of NixOS' `services.borgbackup` module. I tried to make # it as close to it as possible plus some other options such as adding pattern # files (since it is my preference of indicating which files are included). { config, lib, pkgs, ... }: let cfg = config.services.borgbackup; borgJobsModule = { name, lib, config, ... }: { options = { exportWrapperScript = lib.mkEnableOption "export wrapper script as part of the environment" // { default = true; }; extraArgs = lib.mkOption { type = with lib.types; listOf str; description = '' Extra arguments to be passed to all Borg procedures in the resulting script. ::: {.caution} Be careful with this option as it can affect all commands. See the `extraArgs` equivalent of those specific operations first because adding values here. ::: ''; default = [ ]; example = [ "--remote-path=/path/to/borg/repo" ]; }; extraCreateArgs = lib.mkOption { type = with lib.types; listOf str; description = '' Additional arguments for `borg create`. ''; default = [ ]; example = [ "--stats" "--checkpoint-interval" "600" ]; }; extraInitArgs = lib.mkOption { type = with lib.types; listOf str; description = '' Extra arguments to be passed to `borg init`, when applicable. ''; default = [ ]; example = [ "--make-parent-dirs" "--append-only" ]; }; patternFiles = lib.mkOption { type = with lib.types; listOf path; description = '' List of paths containing patterns for the Borg job. ''; default = [ ]; example = lib.literalExpression '' [ ./config/borg/patterns/home ./config/borg/patterns/server ./config/borg/patterns/games ] ''; }; doInit = lib.mkEnableOption "initialization of the BorgBackup repo"; startAt = lib.mkOption { type = lib.types.nonEmptyStr; description = '' Indicates how much the backup procedure occurs. ''; default = "daily"; example = "04:30"; }; environment = lib.mkOption { type = with lib.types; attrsOf str; description = '' Extra environment variables to be set along to the backup service. You could indicate SSH-related settings here for example. ''; default = { }; example = lib.literalExpression '' { BORG_RSH = "ssh -i ''${config.home.homeDirectory}/.ssh/borg-key.pub"; } ''; }; encryption.passCommand = lib.mkOption { type = with lib.types; nullOr str; description = '' Command used to retrieve the password of the repository. ::: {.note} Mutually exclusive with {option}`encryption.passphrase`. ::: ''; default = null; example = lib.literalExpression '' cat ''${config.home.homeDirectory}/borg-secret ''; }; encryption.passphrase = lib.mkOption { type = with lib.types; nullOr str; description = '' Passphrase used to lock the repository. ::: {.note} This will also store the password as plain-text file in the Nix store directory. If you don't want that, use {option}`encryption.passCommand` instead. Mutually exclusive with {option}`encryption.passCommand`. ::: ''; default = null; }; }; }; mkPassEnv = v: # Prefer the pass command option since it is the safer option. if v.encryption.passCommand != null then { BORG_PASSCOMMAND = v.encryption.passCommand; } else if v.encryption.passphrase != null then { BORG_PASSPHRASE = v.encryption.passphrase; } else { }; makeJobName = name: "borg-job-${name}"; mkBorgWrapperScripts = n: v: let executableName = makeJobName n; setEnv = { BORG_REPO = v.repo; } // (mkPassEnv v) // v.environment; mkWrapperFlag = n: v: ''--set ${lib.escapeShellArg n} "${v}"''; in pkgs.runCommand "${n}-wrapper" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' makeWrapper "${ lib.getExe' cfg.package "borg" } "$out/bin/${executableName}" \ ${ lib.concatStringsSep " \\\n" (lib.mapAttrsToList mkWrapperFlag setEnv) } ''; mkBorgServiceUnit = n: v: lib.nameValuePair (makeJobName n) { Unit = { Description = "Periodic BorgBackup job '${n}'"; }; Service = { CPUSchedulingPolicy = "idle"; IOSchedulingClass = "idle"; Environment = lib.attrsToList (n: v: "${n}=${v}") ({ inherit (v) extraArgs extraInitArgs extraCreateArgs; } // v.environment // (mkPassEnv v)) ++ [ "BORG_REPO=${v.repo}" ]; ExecStart = let borgScript = pkgs.writeShellApplication { name = "borg-job-${n}-script"; runtimeInputs = [ cfg.package ]; text = '' on_exit() { exitStatus=$? ${cfg.postHook} exit $exitStatus } trap on_exit EXIT borgWrapper () { local result borg "$@" && result=$? || result=$? if [[ -z "${ toString cfg.failOnWarnings }" ]] && [[ "$result" == 1 ]]; then echo "ignoring warning return value 1" return 0 else return "$result" fi } archiveName="${ lib.optionalString (cfg.archiveBaseName != null) (cfg.archiveBaseName + "-") }$(date ${cfg.dateFormat})" archiveSuffix="${ lib.optionalString cfg.appendFailedSuffix ".failed" }" ${cfg.preHook} '' + lib.optionalString cfg.doInit '' # Run borg init if the repo doesn't exist yet if ! borgWrapper list $extraArgs > /dev/null; then borgWrapper init $extraArgs \ --encryption ${cfg.encryption.mode} \ $extraInitArgs ${cfg.postInit} fi ''; }; in lib.getExe borgScript; }; }; mkBorgTimerUnit = n: v: lib.nameValuePair (makeJobName n) { Unit.Description = "Periodic BorgBackup job '${n}'"; Timer = { Persistent = true; RandomizedDelaySec = "1min"; OnCalendar = v.startAt; }; Install.WantedBy = [ "timers.target" ]; }; in { options.services.borgbackup = { enable = lib.mkEnableOption "periodic backups with BorgBackup"; package = lib.mkPackageOption pkgs "borgbackup" { }; jobs = lib.mkOption { type = with lib.types; attrsOf (submodule borgJobsModule); description = '' A set of Borg backup jobs to be done within the home environment. Each job can have a wrapper script `borg-job-{name}` as part of your home environment to make maintenance easier. ''; default = { }; example = lib.literalExpression '' { personal = { doInit = true; encryption = { mode = "repokey"; passCommand = "cat ''${config.xdg.configHome}/backup/secret"; }; patternFiles = [ ./config/borg/patterns/data ./config/borg/patterns/games ]; startAt = "05:30; }; } ''; }; }; config = lib.mkIf cfg.enable { home.packages = let jobs' = lib.filterAttrs (n: v: v.exportWrapperScript) cfg.jobs; in lib.mapAttrsToList mkBorgWrapperScripts jobs'; systemd.user.services = lib.mapAttrs' mkBorgServiceUnit cfg.jobs; systemd.user.timers = lib.mapAttrs' mkBorgTimerUnit cfg.jobs; }; }