# restic.nix - Restic backend implementation { config, lib, pkgs, ... }: with lib; let cfg = config.system.backups; resticCfg = cfg.restic; # Get only restic backups that are enabled resticBackups = filterAttrs (_: backup: backup.backend == "restic" && backup.enable) cfg.backups; # Create restic service configurations createResticServices = mapAttrs ( name: backup: let # Merge global defaults with backup-specific options serviceConfig = recursiveUpdate resticCfg.defaultBackendOptions backup.backendOptions // { inherit (backup) paths; # Use backup-specific timer or fall back to global default timerConfig = if backup.timerConfig != null then backup.timerConfig else resticCfg.timerConfig; }; in serviceConfig ) resticBackups; in { options.system.backups.restic = { enable = mkEnableOption "restic backup backend"; timerConfig = mkOption { type = types.attrs; default = { OnCalendar = "*-*-* 05:00:00"; Persistent = true; }; description = "Default systemd timer configuration for restic backups"; }; defaultBackendOptions = mkOption { type = types.attrs; default = {}; example = { repository = "/backup/restic"; passwordFile = "/etc/nixos/secrets/restic-password"; initialize = true; pruneOpts = [ "--keep-daily 7" "--keep-weekly 5" "--keep-monthly 12" "--keep-yearly 75" ]; }; description = "Default backend options applied to all restic backup jobs"; }; # Advanced options runMaintenance = mkOption { type = types.bool; default = true; description = "Whether to run repository maintenance after backups"; }; maintenanceTimer = mkOption { type = types.attrs; default = { OnCalendar = "*-*-* 06:00:00"; Persistent = true; }; description = "Timer configuration for maintenance tasks"; }; pruneOpts = mkOption { type = types.listOf types.str; default = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 6" "--keep-yearly 3" ]; description = "Default pruning options for maintenance"; }; }; config = mkIf resticCfg.enable { # Register restic backend system.backups.backends.restic = { backupConfig, backupName, ... }: { # Define the proper options schema for restic backendOptions options = { repository = mkOption { type = types.str; description = "Restic repository path or URL"; }; passwordFile = mkOption { type = types.str; description = "Path to file containing the repository password"; }; initialize = mkOption { type = types.bool; default = true; description = "Whether to initialize the repository if it doesn't exist"; }; exclude = mkOption { type = types.listOf types.str; default = []; description = "Patterns to exclude from backup"; }; extraBackupArgs = mkOption { type = types.listOf types.str; default = []; description = "Additional arguments passed to restic backup command"; }; user = mkOption { type = types.str; default = "root"; description = "User to run the backup as"; }; pruneOpts = mkOption { type = types.listOf types.str; default = resticCfg.pruneOpts; description = "Pruning options for this backup"; }; }; # Default config merged with global defaults config = { extraBackupArgs = [ "--tag ${backupName}" "--verbose" ] ++ (resticCfg.defaultBackendOptions.extraBackupArgs or []); }; }; # Create actual restic backup services services.restic.backups = createResticServices; # Add restic package environment.systemPackages = [pkgs.restic]; # Systemd service customizations for restic backups systemd.services = (mapAttrs' ( name: backup: nameValuePair "restic-backups-${name}" { # Custom pre/post scripts preStart = mkBefore backup.preBackupScript; postStop = mkAfter backup.postBackupScript; # Enhanced service configuration serviceConfig = { # Restart configuration Restart = "on-failure"; RestartSec = "5m"; RestartMaxDelaySec = "30m"; RestartSteps = 3; # Rate limiting StartLimitBurst = 4; StartLimitIntervalSec = "2h"; }; # Failure handling could be extended here for notifications # onFailure = optional backup.notifications.failure.enable "restic-backup-${name}-failure-notify.service"; } ) resticBackups) // optionalAttrs resticCfg.runMaintenance { # Repository maintenance service restic-maintenance = { description = "Restic repository maintenance"; after = map (name: "restic-backups-${name}.service") (attrNames resticBackups); environment = resticCfg.defaultBackendOptions // { RESTIC_CACHE_DIR = "/var/cache/restic-maintenance"; }; serviceConfig = { Type = "oneshot"; ExecStart = [ "${pkgs.restic}/bin/restic forget --prune ${concatStringsSep " " resticCfg.pruneOpts}" "${pkgs.restic}/bin/restic check --read-data-subset=500M" ]; User = "root"; CacheDirectory = "restic-maintenance"; CacheDirectoryMode = "0700"; }; }; }; # Maintenance timer systemd.timers = mkIf resticCfg.runMaintenance { restic-maintenance = { description = "Timer for restic repository maintenance"; wantedBy = ["timers.target"]; timerConfig = resticCfg.maintenanceTimer; }; }; # Helpful shell aliases programs.zsh.shellAliases = { restic-snapshots = "restic snapshots --compact --group-by tags"; restic-repo-size = "restic stats --mode raw-data"; } // (mapAttrs' ( name: _: nameValuePair "backup-${name}" "systemctl start restic-backups-${name}" ) resticBackups); }; }