{ config, lib, pkgs, ...}: with lib; let cfg = config.services.duplicity; stateDirectory = "/var/lib/duplicity"; localTarget = if hasPrefix "file://" cfg.targetUrl then removePrefix "file://" cfg.targetUrl else null; in { options.services.duplicity = { enable = mkEnableOption "backups with duplicity"; root = mkOption { type = types.path; default = "/"; description = '' Root directory to backup. ''; }; include = mkOption { type = types.listOf types.str; default = []; example = [ "/home" ]; description = '' List of paths to include into the backups. See the FILE SELECTION section in duplicity 1 for details on the syntax. ''; }; exclude = mkOption { type = types.listOf types.str; default = []; description = '' List of paths to exclude from backups. See the FILE SELECTION section in duplicity 1 for details on the syntax. ''; }; targetUrl = mkOption { type = types.str; example = "s3://host:port/prefix"; description = '' Target url to backup to. See the URL FORMAT section in duplicity 1 for supported urls. ''; }; secretFile = mkOption { type = types.nullOr types.path; default = null; description = '' Path of a file containing secrets (gpg passphrase, access key...) in the format of EnvironmentFile as described by systemd.exec 5. For example: PASSPHRASE=... AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... ''; }; frequency = mkOption { type = types.nullOr types.str; default = "daily"; description = '' Run duplicity with the given frequency (see systemd.time 7 for the format). If null, do not run automatically. ''; }; extraFlags = mkOption { type = types.listOf types.str; default = []; example = [ "--full-if-older-than" "1M" ]; description = '' Extra command-line flags passed to duplicity. See duplicity 1. ''; }; }; config = mkIf cfg.enable { systemd = { services.duplicity = { description = "backup files with duplicity"; environment.HOME = stateDirectory; serviceConfig = { ExecStart = '' ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs ( [ cfg.root cfg.targetUrl "--archive-dir" stateDirectory ] ++ concatMap (p: [ "--include" p ]) cfg.include ++ concatMap (p: [ "--exclude" p ]) cfg.exclude ++ cfg.extraFlags)} ''; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; StateDirectory = baseNameOf stateDirectory; } // optionalAttrs (localTarget != null) { ReadWritePaths = localTarget; } // optionalAttrs (cfg.secretFile != null) { EnvironmentFile = cfg.secretFile; }; } // optionalAttrs (cfg.frequency != null) { startAt = cfg.frequency; }; tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -"; }; assertions = singleton { # Duplicity will fail if the last file selection option is an include. It # is not always possible to detect but this simple case can be caught. assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != []; message = '' Duplicity will fail if you only specify included paths ("Because the default is to include all files, the expression is redundant. Exiting because this probably isn't what you meant.") ''; }; }; }