1 { config, lib, pkgs, ... }:
5 cfg = config.services.duplicity;
7 stateDirectory = "/var/lib/duplicity";
10 if hasPrefix "file://" cfg.targetUrl
11 then removePrefix "file://" cfg.targetUrl else null;
15 options.services.duplicity = {
16 enable = mkEnableOption "backups with duplicity";
22 Root directory to backup.
27 type = types.listOf types.str;
29 example = [ "/home" ];
31 List of paths to include into the backups. See the FILE SELECTION
32 section in {manpage}`duplicity(1)` for details on the syntax.
37 type = types.listOf types.str;
40 List of paths to exclude from backups. See the FILE SELECTION section in
41 {manpage}`duplicity(1)` for details on the syntax.
45 includeFileList = mkOption {
46 type = types.nullOr types.path;
48 example = /path/to/fileList.txt;
50 File containing newline-separated list of paths to include into the
51 backups. See the FILE SELECTION section in {manpage}`duplicity(1)` for
52 details on the syntax.
56 excludeFileList = mkOption {
57 type = types.nullOr types.path;
59 example = /path/to/fileList.txt;
61 File containing newline-separated list of paths to exclude into the
62 backups. See the FILE SELECTION section in {manpage}`duplicity(1)` for
63 details on the syntax.
67 targetUrl = mkOption {
69 example = "s3://host:port/prefix";
71 Target url to backup to. See the URL FORMAT section in
72 {manpage}`duplicity(1)` for supported urls.
76 secretFile = mkOption {
77 type = types.nullOr types.path;
80 Path of a file containing secrets (gpg passphrase, access key...) in
81 the format of EnvironmentFile as described by
82 {manpage}`systemd.exec(5)`. For example:
85 AWS_ACCESS_KEY_ID=«...»
86 AWS_SECRET_ACCESS_KEY=«...»
91 frequency = mkOption {
92 type = types.nullOr types.str;
95 Run duplicity with the given frequency (see
96 {manpage}`systemd.time(7)` for the format).
97 If null, do not run automatically.
101 extraFlags = mkOption {
102 type = types.listOf types.str;
104 example = [ "--backend-retry-delay" "100" ];
106 Extra command-line flags passed to duplicity. See
107 {manpage}`duplicity(1)`.
111 fullIfOlderThan = mkOption {
116 If `"never"` (the default) always do incremental
117 backups (the first backup will be a full backup, of course). If
118 `"always"` always do full backups. Otherwise, this
119 must be a string representing a duration. Full backups will be made
120 when the latest full backup is older than this duration. If this is not
121 the case, an incremental backup is performed.
127 type = types.nullOr types.str;
131 If non-null, delete all backup sets older than the given time. Old backup sets
132 will not be deleted if backup sets newer than time depend on them.
136 type = types.nullOr types.int;
140 If non-null, delete all backups sets that are older than the count:th last full
141 backup (in other words, keep the last count full backups and
142 associated incremental sets).
146 type = types.nullOr types.int;
150 If non-null, delete incremental sets of all backups sets that are
151 older than the count:th last full backup (in other words, keep only
152 old full backups and not their increments).
158 config = mkIf cfg.enable {
160 services.duplicity = {
161 description = "backup files with duplicity";
163 environment.HOME = stateDirectory;
167 target = escapeShellArg cfg.targetUrl;
168 extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
169 dup = "${pkgs.duplicity}/bin/duplicity";
173 ${dup} cleanup ${target} --force ${extra}
174 ${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
175 ${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
176 ${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
177 exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
178 [ cfg.root cfg.targetUrl ]
179 ++ lib.optionals (cfg.includeFileList != null) [ "--include-filelist" cfg.includeFileList ]
180 ++ lib.optionals (cfg.excludeFileList != null) [ "--exclude-filelist" cfg.excludeFileList ]
181 ++ concatMap (p: [ "--include" p ]) cfg.include
182 ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
183 ++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
188 ProtectSystem = "strict";
189 ProtectHome = "read-only";
190 StateDirectory = baseNameOf stateDirectory;
191 } // optionalAttrs (localTarget != null) {
192 ReadWritePaths = localTarget;
193 } // optionalAttrs (cfg.secretFile != null) {
194 EnvironmentFile = cfg.secretFile;
196 } // optionalAttrs (cfg.frequency != null) {
197 startAt = cfg.frequency;
200 tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
203 assertions = singleton {
204 # Duplicity will fail if the last file selection option is an include. It
205 # is not always possible to detect but this simple case can be caught.
206 assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
208 Duplicity will fail if you only specify included paths ("Because the
209 default is to include all files, the expression is redundant. Exiting
210 because this probably isn't what you meant.")