grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / backup / borgbackup.nix
blob36f2274d5eaac9ce771d05b6bde91e08da769ea1
1 { config, lib, pkgs, ... }:
2 let
4   isLocalPath = x:
5     builtins.substring 0 1 x == "/"      # absolute path
6     || builtins.substring 0 1 x == "."   # relative path
7     || builtins.match "[.*:.*]" == null; # not machine:path
9   mkExcludeFile = cfg:
10     # Write each exclude pattern to a new line
11     pkgs.writeText "excludefile" (lib.concatMapStrings (s: s + "\n") cfg.exclude);
13   mkPatternsFile = cfg:
14     # Write each pattern to a new line
15     pkgs.writeText "patternsfile" (lib.concatMapStrings (s: s + "\n") cfg.patterns);
17   mkKeepArgs = cfg:
18     # If cfg.prune.keep e.g. has a yearly attribute,
19     # its content is passed on as --keep-yearly
20     lib.concatStringsSep " "
21       (lib.mapAttrsToList (x: y: "--keep-${x}=${toString y}") cfg.prune.keep);
23   mkBackupScript = name: cfg: pkgs.writeShellScript "${name}-script" (''
24     set -e
25     on_exit()
26     {
27       exitStatus=$?
28       ${cfg.postHook}
29       exit $exitStatus
30     }
31     trap on_exit EXIT
33     borgWrapper () {
34       local result
35       borg "$@" && result=$? || result=$?
36       if [[ -z "${toString cfg.failOnWarnings}" ]] && [[ "$result" == 1 ]]; then
37         echo "ignoring warning return value 1"
38         return 0
39       else
40         return "$result"
41       fi
42     }
44     archiveName="${lib.optionalString (cfg.archiveBaseName != null) (cfg.archiveBaseName + "-")}$(date ${cfg.dateFormat})"
45     archiveSuffix="${lib.optionalString cfg.appendFailedSuffix ".failed"}"
46     ${cfg.preHook}
47   '' + lib.optionalString cfg.doInit ''
48     # Run borg init if the repo doesn't exist yet
49     if ! borgWrapper list $extraArgs > /dev/null; then
50       borgWrapper init $extraArgs \
51         --encryption ${cfg.encryption.mode} \
52         $extraInitArgs
53       ${cfg.postInit}
54     fi
55   '' + ''
56     (
57       set -o pipefail
58       ${lib.optionalString (cfg.dumpCommand != null) ''${lib.escapeShellArg cfg.dumpCommand} | \''}
59       borgWrapper create $extraArgs \
60         --compression ${cfg.compression} \
61         --exclude-from ${mkExcludeFile cfg} \
62         --patterns-from ${mkPatternsFile cfg} \
63         $extraCreateArgs \
64         "::$archiveName$archiveSuffix" \
65         ${if cfg.paths == null then "-" else lib.escapeShellArgs cfg.paths}
66     )
67   '' + lib.optionalString cfg.appendFailedSuffix ''
68     borgWrapper rename $extraArgs \
69       "::$archiveName$archiveSuffix" "$archiveName"
70   '' + ''
71     ${cfg.postCreate}
72   '' + lib.optionalString (cfg.prune.keep != { }) ''
73     borgWrapper prune $extraArgs \
74       ${mkKeepArgs cfg} \
75       ${lib.optionalString (cfg.prune.prefix != null) "--glob-archives ${lib.escapeShellArg "${cfg.prune.prefix}*"}"} \
76       $extraPruneArgs
77     borgWrapper compact $extraArgs $extraCompactArgs
78     ${cfg.postPrune}
79   '');
81   mkPassEnv = cfg: with cfg.encryption;
82     if passCommand != null then
83       { BORG_PASSCOMMAND = passCommand; }
84     else if passphrase != null then
85       { BORG_PASSPHRASE = passphrase; }
86     else { };
88   mkBackupService = name: cfg:
89     let
90       userHome = config.users.users.${cfg.user}.home;
91       backupJobName = "borgbackup-job-${name}";
92       backupScript = mkBackupScript backupJobName cfg;
93     in lib.nameValuePair backupJobName {
94       description = "BorgBackup job ${name}";
95       path =  [
96         config.services.borgbackup.package pkgs.openssh
97       ];
98       script = "exec " + lib.optionalString cfg.inhibitsSleep ''\
99         ${pkgs.systemd}/bin/systemd-inhibit \
100             --who="borgbackup" \
101             --what="sleep" \
102             --why="Scheduled backup" \
103         '' + backupScript;
104       unitConfig = lib.optionalAttrs (isLocalPath cfg.repo) {
105         RequiresMountsFor = [ cfg.repo ];
106       };
107       serviceConfig = {
108         User = cfg.user;
109         Group = cfg.group;
110         # Only run when no other process is using CPU or disk
111         CPUSchedulingPolicy = "idle";
112         IOSchedulingClass = "idle";
113         ProtectSystem = "strict";
114         ReadWritePaths =
115           [ "${userHome}/.config/borg" "${userHome}/.cache/borg" ]
116           ++ cfg.readWritePaths
117           # Borg needs write access to repo if it is not remote
118           ++ lib.optional (isLocalPath cfg.repo) cfg.repo;
119         PrivateTmp = cfg.privateTmp;
120       };
121       environment = {
122         BORG_REPO = cfg.repo;
123         inherit (cfg) extraArgs extraInitArgs extraCreateArgs extraPruneArgs extraCompactArgs;
124       } // (mkPassEnv cfg) // cfg.environment;
125     };
127   mkBackupTimers = name: cfg:
128     lib.nameValuePair "borgbackup-job-${name}" {
129       description = "BorgBackup job ${name} timer";
130       wantedBy = [ "timers.target" ];
131       timerConfig = {
132         Persistent = cfg.persistentTimer;
133         OnCalendar = cfg.startAt;
134       };
135       # if remote-backup wait for network
136       after = lib.optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
137       wants = lib.optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
138     };
140   # utility function around makeWrapper
141   mkWrapperDrv = {
142       original, name, set ? {}
143     }:
144     pkgs.runCommand "${name}-wrapper" {
145       nativeBuildInputs = [ pkgs.makeWrapper ];
146     } (with lib; ''
147       makeWrapper "${original}" "$out/bin/${name}" \
148         ${lib.concatStringsSep " \\\n " (lib.mapAttrsToList (name: value: ''--set ${name} "${value}"'') set)}
149     '');
151   mkBorgWrapper = name: cfg: mkWrapperDrv {
152     original = lib.getExe config.services.borgbackup.package;
153     name = "borg-job-${name}";
154     set = { BORG_REPO = cfg.repo; } // (mkPassEnv cfg) // cfg.environment;
155   };
157   # Paths listed in ReadWritePaths must exist before service is started
158   mkTmpfiles = name: cfg:
159     let
160       settings = { inherit (cfg) user group; };
161     in lib.nameValuePair "borgbackup-job-${name}" ({
162       # Create parent dirs separately, to ensure correct ownership.
163       "${config.users.users."${cfg.user}".home}/.config".d = settings;
164       "${config.users.users."${cfg.user}".home}/.cache".d = settings;
165       "${config.users.users."${cfg.user}".home}/.config/borg".d = settings;
166       "${config.users.users."${cfg.user}".home}/.cache/borg".d = settings;
167     } // lib.optionalAttrs (isLocalPath cfg.repo && !cfg.removableDevice) {
168       "${cfg.repo}".d = settings;
169     });
171   mkPassAssertion = name: cfg: {
172     assertion = with cfg.encryption;
173       mode != "none" -> passCommand != null || passphrase != null;
174     message =
175       "passCommand or passphrase has to be specified because"
176       + '' borgbackup.jobs.${name}.encryption != "none"'';
177   };
179   mkRepoService = name: cfg:
180     lib.nameValuePair "borgbackup-repo-${name}" {
181       description = "Create BorgBackup repository ${name} directory";
182       script = ''
183         mkdir -p ${lib.escapeShellArg cfg.path}
184         chown ${cfg.user}:${cfg.group} ${lib.escapeShellArg cfg.path}
185       '';
186       serviceConfig = {
187         # The service's only task is to ensure that the specified path exists
188         Type = "oneshot";
189       };
190       wantedBy = [ "multi-user.target" ];
191     };
193   mkAuthorizedKey = cfg: appendOnly: key:
194     let
195       # Because of the following line, clients do not need to specify an absolute repo path
196       cdCommand = "cd ${lib.escapeShellArg cfg.path}";
197       restrictedArg = "--restrict-to-${if cfg.allowSubRepos then "path" else "repository"} .";
198       appendOnlyArg = lib.optionalString appendOnly "--append-only";
199       quotaArg = lib.optionalString (cfg.quota != null) "--storage-quota ${cfg.quota}";
200       serveCommand = "borg serve ${restrictedArg} ${appendOnlyArg} ${quotaArg}";
201     in
202       ''command="${cdCommand} && ${serveCommand}",restrict ${key}'';
204   mkUsersConfig = name: cfg: {
205     users.${cfg.user} = {
206       openssh.authorizedKeys.keys =
207         (map (mkAuthorizedKey cfg false) cfg.authorizedKeys
208         ++ map (mkAuthorizedKey cfg true) cfg.authorizedKeysAppendOnly);
209       useDefaultShell = true;
210       group = cfg.group;
211       isSystemUser = true;
212     };
213     groups.${cfg.group} = { };
214   };
216   mkKeysAssertion = name: cfg: {
217     assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeysAppendOnly != [ ];
218     message =
219       "borgbackup.repos.${name} does not make sense"
220       + " without at least one public key";
221   };
223   mkSourceAssertions = name: cfg: {
224     assertion = lib.count isNull [ cfg.dumpCommand cfg.paths ] == 1;
225     message = ''
226       Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand
227       must be set.
228     '';
229   };
231   mkRemovableDeviceAssertions = name: cfg: {
232     assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice;
233     message = ''
234       borgbackup.repos.${name}: repo isn't a local path, thus it can't be a removable device!
235     '';
236   };
238 in {
239   meta.maintainers = with lib.maintainers; [ dotlambda ];
240   meta.doc = ./borgbackup.md;
242   ###### interface
244   options.services.borgbackup.package = lib.mkPackageOption pkgs "borgbackup" { };
246   options.services.borgbackup.jobs = lib.mkOption {
247     description = ''
248       Deduplicating backups using BorgBackup.
249       Adding a job will cause a borg-job-NAME wrapper to be added
250       to your system path, so that you can perform maintenance easily.
251       See also the chapter about BorgBackup in the NixOS manual.
252     '';
253     default = { };
254     example = lib.literalExpression ''
255       { # for a local backup
256         rootBackup = {
257           paths = "/";
258           exclude = [ "/nix" ];
259           repo = "/path/to/local/repo";
260           encryption = {
261             mode = "repokey";
262             passphrase = "secret";
263           };
264           compression = "auto,lzma";
265           startAt = "weekly";
266         };
267       }
268       { # Root backing each day up to a remote backup server. We assume that you have
269         #   * created a password less key: ssh-keygen -N "" -t ed25519 -f /path/to/ssh_key
270         #     best practices are: use -t ed25519, /path/to = /run/keys
271         #   * the passphrase is in the file /run/keys/borgbackup_passphrase
272         #   * you have initialized the repository manually
273         paths = [ "/etc" "/home" ];
274         exclude = [ "/nix" "'**/.cache'" ];
275         doInit = false;
276         repo =  "user3@arep.repo.borgbase.com:repo";
277         encryption = {
278           mode = "repokey-blake2";
279           passCommand = "cat /path/to/passphrase";
280         };
281         environment = { BORG_RSH = "ssh -i /path/to/ssh_key"; };
282         compression = "auto,lzma";
283         startAt = "daily";
284     };
285     '';
286     type = lib.types.attrsOf (lib.types.submodule (let globalConfig = config; in
287       { name, config, ... }: {
288         options = {
290           paths = lib.mkOption {
291             type = with lib.types; nullOr (coercedTo str lib.singleton (listOf str));
292             default = null;
293             description = ''
294               Path(s) to back up.
295               Mutually exclusive with {option}`dumpCommand`.
296             '';
297             example = "/home/user";
298           };
300           dumpCommand = lib.mkOption {
301             type = with lib.types; nullOr path;
302             default = null;
303             description = ''
304               Backup the stdout of this program instead of filesystem paths.
305               Mutually exclusive with {option}`paths`.
306             '';
307             example = "/path/to/createZFSsend.sh";
308           };
310           repo = lib.mkOption {
311             type = lib.types.str;
312             description = "Remote or local repository to back up to.";
313             example = "user@machine:/path/to/repo";
314           };
316           removableDevice = lib.mkOption {
317             type = lib.types.bool;
318             default = false;
319             description = "Whether the repo (which must be local) is a removable device.";
320           };
322           archiveBaseName = lib.mkOption {
323             type = lib.types.nullOr (lib.types.strMatching "[^/{}]+");
324             default = "${globalConfig.networking.hostName}-${name}";
325             defaultText = lib.literalExpression ''"''${config.networking.hostName}-<name>"'';
326             description = ''
327               How to name the created archives. A timestamp, whose format is
328               determined by {option}`dateFormat`, will be appended. The full
329               name can be modified at runtime (`$archiveName`).
330               Placeholders like `{hostname}` must not be used.
331               Use `null` for no base name.
332             '';
333           };
335           dateFormat = lib.mkOption {
336             type = lib.types.str;
337             description = ''
338               Arguments passed to {command}`date`
339               to create a timestamp suffix for the archive name.
340             '';
341             default = "+%Y-%m-%dT%H:%M:%S";
342             example = "-u +%s";
343           };
345           startAt = lib.mkOption {
346             type = with lib.types; either str (listOf str);
347             default = "daily";
348             description = ''
349               When or how often the backup should run.
350               Must be in the format described in
351               {manpage}`systemd.time(7)`.
352               If you do not want the backup to start
353               automatically, use `[ ]`.
354               It will generate a systemd service borgbackup-job-NAME.
355               You may trigger it manually via systemctl restart borgbackup-job-NAME.
356             '';
357           };
359           persistentTimer = lib.mkOption {
360             default = false;
361             type = lib.types.bool;
362             example = true;
363             description = ''
364               Set the `Persistent` option for the
365               {manpage}`systemd.timer(5)`
366               which triggers the backup immediately if the last trigger
367               was missed (e.g. if the system was powered down).
368             '';
369           };
371           inhibitsSleep = lib.mkOption {
372             default = false;
373             type = lib.types.bool;
374             example = true;
375             description = ''
376               Prevents the system from sleeping while backing up.
377             '';
378           };
380           user = lib.mkOption {
381             type = lib.types.str;
382             description = ''
383               The user {command}`borg` is run as.
384               User or group need read permission
385               for the specified {option}`paths`.
386             '';
387             default = "root";
388           };
390           group = lib.mkOption {
391             type = lib.types.str;
392             description = ''
393               The group borg is run as. User or group needs read permission
394               for the specified {option}`paths`.
395             '';
396             default = "root";
397           };
399           encryption.mode = lib.mkOption {
400             type = lib.types.enum [
401               "repokey" "keyfile"
402               "repokey-blake2" "keyfile-blake2"
403               "authenticated" "authenticated-blake2"
404               "none"
405             ];
406             description = ''
407               Encryption mode to use. Setting a mode
408               other than `"none"` requires
409               you to specify a {option}`passCommand`
410               or a {option}`passphrase`.
411             '';
412             example = "repokey-blake2";
413           };
415           encryption.passCommand = lib.mkOption {
416             type = with lib.types; nullOr str;
417             description = ''
418               A command which prints the passphrase to stdout.
419               Mutually exclusive with {option}`passphrase`.
420             '';
421             default = null;
422             example = "cat /path/to/passphrase_file";
423           };
425           encryption.passphrase = lib.mkOption {
426             type = with lib.types; nullOr str;
427             description = ''
428               The passphrase the backups are encrypted with.
429               Mutually exclusive with {option}`passCommand`.
430               If you do not want the passphrase to be stored in the
431               world-readable Nix store, use {option}`passCommand`.
432             '';
433             default = null;
434           };
436           compression = lib.mkOption {
437             # "auto" is optional,
438             # compression mode must be given,
439             # compression level is optional
440             type = lib.types.strMatching "none|(auto,)?(lz4|zstd|zlib|lzma)(,[[:digit:]]{1,2})?";
441             description = ''
442               Compression method to use. Refer to
443               {command}`borg help compression`
444               for all available options.
445             '';
446             default = "lz4";
447             example = "auto,lzma";
448           };
450           exclude = lib.mkOption {
451             type = with lib.types; listOf str;
452             description = ''
453               Exclude paths matching any of the given patterns. See
454               {command}`borg help patterns` for pattern syntax.
455             '';
456             default = [ ];
457             example = [
458               "/home/*/.cache"
459               "/nix"
460             ];
461           };
463           patterns = lib.mkOption {
464             type = with lib.types; listOf str;
465             description = ''
466               Include/exclude paths matching the given patterns. The first
467               matching patterns is used, so if an include pattern (prefix `+`)
468               matches before an exclude pattern (prefix `-`), the file is
469               backed up. See [{command}`borg help patterns`](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns) for pattern syntax.
470             '';
471             default = [ ];
472             example = [
473               "+ /home/susan"
474               "- /home/*"
475             ];
476           };
478           readWritePaths = lib.mkOption {
479             type = with lib.types; listOf path;
480             description = ''
481               By default, borg cannot write anywhere on the system but
482               `$HOME/.config/borg` and `$HOME/.cache/borg`.
483               If, for example, your preHook script needs to dump files
484               somewhere, put those directories here.
485             '';
486             default = [ ];
487             example = [
488               "/var/backup/mysqldump"
489             ];
490           };
492           privateTmp = lib.mkOption {
493             type = lib.types.bool;
494             description = ''
495               Set the `PrivateTmp` option for
496               the systemd-service. Set to false if you need sockets
497               or other files from global /tmp.
498             '';
499             default = true;
500           };
502           failOnWarnings = lib.mkOption {
503             type = lib.types.bool;
504             description = ''
505               Fail the whole backup job if any borg command returns a warning
506               (exit code 1), for example because a file changed during backup.
507             '';
508             default = true;
509           };
511           doInit = lib.mkOption {
512             type = lib.types.bool;
513             description = ''
514               Run {command}`borg init` if the
515               specified {option}`repo` does not exist.
516               You should set this to `false`
517               if the repository is located on an external drive
518               that might not always be mounted.
519             '';
520             default = true;
521           };
523           appendFailedSuffix = lib.mkOption {
524             type = lib.types.bool;
525             description = ''
526               Append a `.failed` suffix
527               to the archive name, which is only removed if
528               {command}`borg create` has a zero exit status.
529             '';
530             default = true;
531           };
533           prune.keep = lib.mkOption {
534             # Specifying e.g. `prune.keep.yearly = -1`
535             # means there is no limit of yearly archives to keep
536             # The regex is for use with e.g. --keep-within 1y
537             type = with lib.types; attrsOf (either int (strMatching "[[:digit:]]+[Hdwmy]"));
538             description = ''
539               Prune a repository by deleting all archives not matching any of the
540               specified retention options. See {command}`borg help prune`
541               for the available options.
542             '';
543             default = { };
544             example = lib.literalExpression ''
545               {
546                 within = "1d"; # Keep all archives from the last day
547                 daily = 7;
548                 weekly = 4;
549                 monthly = -1;  # Keep at least one archive for each month
550               }
551             '';
552           };
554           prune.prefix = lib.mkOption {
555             type = lib.types.nullOr (lib.types.str);
556             description = ''
557               Only consider archive names starting with this prefix for pruning.
558               By default, only archives created by this job are considered.
559               Use `""` or `null` to consider all archives.
560             '';
561             default = config.archiveBaseName;
562             defaultText = lib.literalExpression "archiveBaseName";
563           };
565           environment = lib.mkOption {
566             type = with lib.types; attrsOf str;
567             description = ''
568               Environment variables passed to the backup script.
569               You can for example specify which SSH key to use.
570             '';
571             default = { };
572             example = { BORG_RSH = "ssh -i /path/to/key"; };
573           };
575           preHook = lib.mkOption {
576             type = lib.types.lines;
577             description = ''
578               Shell commands to run before the backup.
579               This can for example be used to mount file systems.
580             '';
581             default = "";
582             example = ''
583               # To add excluded paths at runtime
584               extraCreateArgs="$extraCreateArgs --exclude /some/path"
585             '';
586           };
588           postInit = lib.mkOption {
589             type = lib.types.lines;
590             description = ''
591               Shell commands to run after {command}`borg init`.
592             '';
593             default = "";
594           };
596           postCreate = lib.mkOption {
597             type = lib.types.lines;
598             description = ''
599               Shell commands to run after {command}`borg create`. The name
600               of the created archive is stored in `$archiveName`.
601             '';
602             default = "";
603           };
605           postPrune = lib.mkOption {
606             type = lib.types.lines;
607             description = ''
608               Shell commands to run after {command}`borg prune`.
609             '';
610             default = "";
611           };
613           postHook = lib.mkOption {
614             type = lib.types.lines;
615             description = ''
616               Shell commands to run just before exit. They are executed
617               even if a previous command exits with a non-zero exit code.
618               The latter is available as `$exitStatus`.
619             '';
620             default = "";
621           };
623           extraArgs = lib.mkOption {
624             type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str;
625             description = ''
626               Additional arguments for all {command}`borg` calls the
627               service has. Handle with care.
628             '';
629             default = [ ];
630             example = [ "--remote-path=/path/to/borg" ];
631           };
633           extraInitArgs = lib.mkOption {
634             type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str;
635             description = ''
636               Additional arguments for {command}`borg init`.
637               Can also be set at runtime using `$extraInitArgs`.
638             '';
639             default = [ ];
640             example = [ "--append-only" ];
641           };
643           extraCreateArgs = lib.mkOption {
644             type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str;
645             description = ''
646               Additional arguments for {command}`borg create`.
647               Can also be set at runtime using `$extraCreateArgs`.
648             '';
649             default = [ ];
650             example = [
651               "--stats"
652               "--checkpoint-interval 600"
653             ];
654           };
656           extraPruneArgs = lib.mkOption {
657             type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str;
658             description = ''
659               Additional arguments for {command}`borg prune`.
660               Can also be set at runtime using `$extraPruneArgs`.
661             '';
662             default = [ ];
663             example = [ "--save-space" ];
664           };
666           extraCompactArgs = lib.mkOption {
667             type = with lib.types; coercedTo (listOf str) lib.escapeShellArgs str;
668             description = ''
669               Additional arguments for {command}`borg compact`.
670               Can also be set at runtime using `$extraCompactArgs`.
671             '';
672             default = [ ];
673             example = [ "--cleanup-commits" ];
674           };
675         };
676       }
677     ));
678   };
680   options.services.borgbackup.repos = lib.mkOption {
681     description = ''
682       Serve BorgBackup repositories to given public SSH keys,
683       restricting their access to the repository only.
684       See also the chapter about BorgBackup in the NixOS manual.
685       Also, clients do not need to specify the absolute path when accessing the repository,
686       i.e. `user@machine:.` is enough. (Note colon and dot.)
687     '';
688     default = { };
689     type = lib.types.attrsOf (lib.types.submodule (
690       { ... }: {
691         options = {
692           path = lib.mkOption {
693             type = lib.types.path;
694             description = ''
695               Where to store the backups. Note that the directory
696               is created automatically, with correct permissions.
697             '';
698             default = "/var/lib/borgbackup";
699           };
701           user = lib.mkOption {
702             type = lib.types.str;
703             description = ''
704               The user {command}`borg serve` is run as.
705               User or group needs write permission
706               for the specified {option}`path`.
707             '';
708             default = "borg";
709           };
711           group = lib.mkOption {
712             type = lib.types.str;
713             description = ''
714               The group {command}`borg serve` is run as.
715               User or group needs write permission
716               for the specified {option}`path`.
717             '';
718             default = "borg";
719           };
721           authorizedKeys = lib.mkOption {
722             type = with lib.types; listOf str;
723             description = ''
724               Public SSH keys that are given full write access to this repository.
725               You should use a different SSH key for each repository you write to, because
726               the specified keys are restricted to running {command}`borg serve`
727               and can only access this single repository.
728             '';
729             default = [ ];
730           };
732           authorizedKeysAppendOnly = lib.mkOption {
733             type = with lib.types; listOf str;
734             description = ''
735               Public SSH keys that can only be used to append new data (archives) to the repository.
736               Note that archives can still be marked as deleted and are subsequently removed from disk
737               upon accessing the repo with full write access, e.g. when pruning.
738             '';
739             default = [ ];
740           };
742           allowSubRepos = lib.mkOption {
743             type = lib.types.bool;
744             description = ''
745               Allow clients to create repositories in subdirectories of the
746               specified {option}`path`. These can be accessed using
747               `user@machine:path/to/subrepo`. Note that a
748               {option}`quota` applies to repositories independently.
749               Therefore, if this is enabled, clients can create multiple
750               repositories and upload an arbitrary amount of data.
751             '';
752             default = false;
753           };
755           quota = lib.mkOption {
756             # See the definition of parse_file_size() in src/borg/helpers/parseformat.py
757             type = with lib.types; nullOr (strMatching "[[:digit:].]+[KMGTP]?");
758             description = ''
759               Storage quota for the repository. This quota is ensured for all
760               sub-repositories if {option}`allowSubRepos` is enabled
761               but not for the overall storage space used.
762             '';
763             default = null;
764             example = "100G";
765           };
767         };
768       }
769     ));
770   };
772   ###### implementation
774   config = lib.mkIf (with config.services.borgbackup; jobs != { } || repos != { })
775     (with config.services.borgbackup; {
776       assertions =
777         lib.mapAttrsToList mkPassAssertion jobs
778         ++ lib.mapAttrsToList mkKeysAssertion repos
779         ++ lib.mapAttrsToList mkSourceAssertions jobs
780         ++ lib.mapAttrsToList mkRemovableDeviceAssertions jobs;
782       systemd.tmpfiles.settings = lib.mapAttrs' mkTmpfiles jobs;
784       systemd.services =
785         # A job named "foo" is mapped to systemd.services.borgbackup-job-foo
786         lib.mapAttrs' mkBackupService jobs
787         # A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo
788         // lib.mapAttrs' mkRepoService repos;
790       # A job named "foo" is mapped to systemd.timers.borgbackup-job-foo
791       # only generate the timer if interval (startAt) is set
792       systemd.timers = lib.mapAttrs' mkBackupTimers (lib.filterAttrs (_: cfg: cfg.startAt != []) jobs);
794       users = lib.mkMerge (lib.mapAttrsToList mkUsersConfig repos);
796       environment.systemPackages =
797         [ config.services.borgbackup.package ] ++ (lib.mapAttrsToList mkBorgWrapper jobs);
798     });