grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / backup / duplicity.nix
blob46625ec5460e484bc6650770fb1aeb97d64198ea
1 { config, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.services.duplicity;
7   stateDirectory = "/var/lib/duplicity";
9   localTarget =
10     if hasPrefix "file://" cfg.targetUrl
11     then removePrefix "file://" cfg.targetUrl else null;
15   options.services.duplicity = {
16     enable = mkEnableOption "backups with duplicity";
18     root = mkOption {
19       type = types.path;
20       default = "/";
21       description = ''
22         Root directory to backup.
23       '';
24     };
26     include = mkOption {
27       type = types.listOf types.str;
28       default = [ ];
29       example = [ "/home" ];
30       description = ''
31         List of paths to include into the backups. See the FILE SELECTION
32         section in {manpage}`duplicity(1)` for details on the syntax.
33       '';
34     };
36     exclude = mkOption {
37       type = types.listOf types.str;
38       default = [ ];
39       description = ''
40         List of paths to exclude from backups. See the FILE SELECTION section in
41         {manpage}`duplicity(1)` for details on the syntax.
42       '';
43     };
45     includeFileList = mkOption {
46       type = types.nullOr types.path;
47       default = null;
48       example = /path/to/fileList.txt;
49       description = ''
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.
53       '';
54     };
56     excludeFileList = mkOption {
57       type = types.nullOr types.path;
58       default = null;
59       example = /path/to/fileList.txt;
60       description = ''
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.
64       '';
65     };
67     targetUrl = mkOption {
68       type = types.str;
69       example = "s3://host:port/prefix";
70       description = ''
71         Target url to backup to. See the URL FORMAT section in
72         {manpage}`duplicity(1)` for supported urls.
73       '';
74     };
76     secretFile = mkOption {
77       type = types.nullOr types.path;
78       default = null;
79       description = ''
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:
83         ```
84         PASSPHRASE=«...»
85         AWS_ACCESS_KEY_ID=«...»
86         AWS_SECRET_ACCESS_KEY=«...»
87         ```
88       '';
89     };
91     frequency = mkOption {
92       type = types.nullOr types.str;
93       default = "daily";
94       description = ''
95         Run duplicity with the given frequency (see
96         {manpage}`systemd.time(7)` for the format).
97         If null, do not run automatically.
98       '';
99     };
101     extraFlags = mkOption {
102       type = types.listOf types.str;
103       default = [ ];
104       example = [ "--backend-retry-delay" "100" ];
105       description = ''
106         Extra command-line flags passed to duplicity. See
107         {manpage}`duplicity(1)`.
108       '';
109     };
111     fullIfOlderThan = mkOption {
112       type = types.str;
113       default = "never";
114       example = "1M";
115       description = ''
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.
122       '';
123     };
125     cleanup = {
126       maxAge = mkOption {
127         type = types.nullOr types.str;
128         default = null;
129         example = "6M";
130         description = ''
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.
133         '';
134       };
135       maxFull = mkOption {
136         type = types.nullOr types.int;
137         default = null;
138         example = 2;
139         description = ''
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).
143         '';
144       };
145       maxIncr = mkOption {
146         type = types.nullOr types.int;
147         default = null;
148         example = 1;
149         description = ''
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).
153         '';
154       };
155     };
156   };
158   config = mkIf cfg.enable {
159     systemd = {
160       services.duplicity = {
161         description = "backup files with duplicity";
163         environment.HOME = stateDirectory;
165         script =
166           let
167             target = escapeShellArg cfg.targetUrl;
168             extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
169             dup = "${pkgs.duplicity}/bin/duplicity";
170           in
171           ''
172             set -x
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 ])
184               )} ${extra}
185           '';
186         serviceConfig = {
187           PrivateTmp = true;
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;
195         };
196       } // optionalAttrs (cfg.frequency != null) {
197         startAt = cfg.frequency;
198       };
200       tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
201     };
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 != [ ];
207       message = ''
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.")
211       '';
212     };
213   };