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