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 borgWrapper () {
37 local result
38 borg "$@" && result=$? || result=$?
39 if [[ -z "${toString cfg.failOnWarnings}" ]] && [[ "$result" == 1 ]]; then
40 echo "ignoring warning return value 1"
41 return 0
42 else
43 return "$result"
44 fi
45 }
46
47 archiveName="${optionalString (cfg.archiveBaseName != null) (cfg.archiveBaseName + "-")}$(date ${cfg.dateFormat})"
48 archiveSuffix="${optionalString cfg.appendFailedSuffix ".failed"}"
49 ${cfg.preHook}
50 '' + optionalString cfg.doInit ''
51 # Run borg init if the repo doesn't exist yet
52 if ! borgWrapper list $extraArgs > /dev/null; then
53 borgWrapper init $extraArgs \
54 --encryption ${cfg.encryption.mode} \
55 $extraInitArgs
56 ${cfg.postInit}
57 fi
58 '' + ''
59 (
60 set -o pipefail
61 ${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''}
62 borgWrapper create $extraArgs \
63 --compression ${cfg.compression} \
64 --exclude-from ${mkExcludeFile cfg} \
65 --patterns-from ${mkPatternsFile cfg} \
66 $extraCreateArgs \
67 "::$archiveName$archiveSuffix" \
68 ${if cfg.paths == null then "-" else escapeShellArgs cfg.paths}
69 )
70 '' + optionalString cfg.appendFailedSuffix ''
71 borgWrapper rename $extraArgs \
72 "::$archiveName$archiveSuffix" "$archiveName"
73 '' + ''
74 ${cfg.postCreate}
75 '' + optionalString (cfg.prune.keep != { }) ''
76 borgWrapper prune $extraArgs \
77 ${mkKeepArgs cfg} \
78 ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \
79 $extraPruneArgs
80 borgWrapper compact $extraArgs $extraCompactArgs
81 ${cfg.postPrune}
82 '');
83
84 mkPassEnv = cfg: with cfg.encryption;
85 if passCommand != null then
86 { BORG_PASSCOMMAND = passCommand; }
87 else if passphrase != null then
88 { BORG_PASSPHRASE = passphrase; }
89 else { };
90
91 mkBackupService = name: cfg:
92 let
93 userHome = config.users.users.${cfg.user}.home;
94 backupJobName = "borgbackup-job-${name}";
95 backupScript = mkBackupScript backupJobName cfg;
96 in nameValuePair backupJobName {
97 description = "BorgBackup job ${name}";
98 path = [
99 config.services.borgbackup.package pkgs.openssh
100 ];
101 script = "exec " + optionalString cfg.inhibitsSleep ''\
102 ${pkgs.systemd}/bin/systemd-inhibit \
103 --who="borgbackup" \
104 --what="sleep" \
105 --why="Scheduled backup" \
106 '' + backupScript;
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 ++ 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 };
126
127 mkBackupTimers = name: cfg:
128 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 = optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
137 wants = optional (cfg.persistentTimer && !isLocalPath cfg.repo) "network-online.target";
138 };
139
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 ${concatStringsSep " \\\n " (mapAttrsToList (name: value: ''--set ${name} "${value}"'') set)}
149 '');
150
151 mkBorgWrapper = name: cfg: mkWrapperDrv {
152 original = getExe config.services.borgbackup.package;
153 name = "borg-job-${name}";
154 set = { BORG_REPO = cfg.repo; } // (mkPassEnv cfg) // cfg.environment;
155 };
156
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 } // optionalAttrs (isLocalPath cfg.repo && !cfg.removableDevice) {
168 "${cfg.repo}".d = settings;
169 });
170
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 };
178
179 mkRepoService = name: cfg:
180 nameValuePair "borgbackup-repo-${name}" {
181 description = "Create BorgBackup repository ${name} directory";
182 script = ''
183 mkdir -p ${escapeShellArg cfg.path}
184 chown ${cfg.user}:${cfg.group} ${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 };
192
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 ${escapeShellArg cfg.path}";
197 restrictedArg = "--restrict-to-${if cfg.allowSubRepos then "path" else "repository"} .";
198 appendOnlyArg = optionalString appendOnly "--append-only";
199 quotaArg = optionalString (cfg.quota != null) "--storage-quota ${cfg.quota}";
200 serveCommand = "borg serve ${restrictedArg} ${appendOnlyArg} ${quotaArg}";
201 in
202 ''command="${cdCommand} && ${serveCommand}",restrict ${key}'';
203
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 };
215
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 };
222
223 mkSourceAssertions = name: cfg: {
224 assertion = 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 };
230
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 };
237
238in {
239 meta.maintainers = with maintainers; [ dotlambda ];
240 meta.doc = ./borgbackup.md;
241
242 ###### interface
243
244 options.services.borgbackup.package = mkPackageOption pkgs "borgbackup" { };
245
246 options.services.borgbackup.jobs = 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 = 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 = types.attrsOf (types.submodule (let globalConfig = config; in
287 { name, config, ... }: {
288 options = {
289
290 paths = mkOption {
291 type = with 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 };
299
300 dumpCommand = mkOption {
301 type = with 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 };
309
310 repo = mkOption {
311 type = types.str;
312 description = "Remote or local repository to back up to.";
313 example = "user@machine:/path/to/repo";
314 };
315
316 removableDevice = mkOption {
317 type = types.bool;
318 default = false;
319 description = "Whether the repo (which must be local) is a removable device.";
320 };
321
322 archiveBaseName = mkOption {
323 type = types.nullOr (types.strMatching "[^/{}]+");
324 default = "${globalConfig.networking.hostName}-${name}";
325 defaultText = 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 };
334
335 dateFormat = mkOption {
336 type = 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 };
344
345 startAt = mkOption {
346 type = with 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 };
358
359 persistentTimer = mkOption {
360 default = false;
361 type = types.bool;
362 example = true;
363 description = ''
364 Set the `persistentTimer` 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 };
370
371 inhibitsSleep = mkOption {
372 default = false;
373 type = types.bool;
374 example = true;
375 description = ''
376 Prevents the system from sleeping while backing up.
377 '';
378 };
379
380 user = mkOption {
381 type = 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 };
389
390 group = mkOption {
391 type = 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 };
398
399 encryption.mode = mkOption {
400 type = 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 };
414
415 encryption.passCommand = mkOption {
416 type = with 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 };
424
425 encryption.passphrase = mkOption {
426 type = with 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 };
435
436 compression = mkOption {
437 # "auto" is optional,
438 # compression mode must be given,
439 # compression level is optional
440 type = 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 };
449
450 exclude = mkOption {
451 type = with 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 };
462
463 patterns = mkOption {
464 type = with 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 };
477
478 readWritePaths = mkOption {
479 type = with 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 };
491
492 privateTmp = mkOption {
493 type = 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 };
501
502 failOnWarnings = mkOption {
503 type = 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 };
510
511 doInit = mkOption {
512 type = 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 };
522
523 appendFailedSuffix = mkOption {
524 type = 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 };
532
533 prune.keep = 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 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 = 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 };
553
554 prune.prefix = mkOption {
555 type = types.nullOr (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 = literalExpression "archiveBaseName";
563 };
564
565 environment = mkOption {
566 type = with 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 };
574
575 preHook = mkOption {
576 type = 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 };
587
588 postInit = mkOption {
589 type = types.lines;
590 description = ''
591 Shell commands to run after {command}`borg init`.
592 '';
593 default = "";
594 };
595
596 postCreate = mkOption {
597 type = 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 };
604
605 postPrune = mkOption {
606 type = types.lines;
607 description = ''
608 Shell commands to run after {command}`borg prune`.
609 '';
610 default = "";
611 };
612
613 postHook = mkOption {
614 type = 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 };
622
623 extraArgs = mkOption {
624 type = with types; coercedTo (listOf str) 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 };
632
633 extraInitArgs = mkOption {
634 type = with types; coercedTo (listOf str) 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 };
642
643 extraCreateArgs = mkOption {
644 type = with types; coercedTo (listOf str) 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 };
655
656 extraPruneArgs = mkOption {
657 type = with types; coercedTo (listOf str) 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 };
665
666 extraCompactArgs = mkOption {
667 type = with types; coercedTo (listOf str) 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 };
679
680 options.services.borgbackup.repos = 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 = types.attrsOf (types.submodule (
690 { ... }: {
691 options = {
692 path = mkOption {
693 type = 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 };
700
701 user = mkOption {
702 type = 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 };
710
711 group = mkOption {
712 type = 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 };
720
721 authorizedKeys = mkOption {
722 type = with 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 };
731
732 authorizedKeysAppendOnly = mkOption {
733 type = with 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 };
741
742 allowSubRepos = mkOption {
743 type = 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 };
754
755 quota = mkOption {
756 # See the definition of parse_file_size() in src/borg/helpers/parseformat.py
757 type = with 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 };
766
767 };
768 }
769 ));
770 };
771
772 ###### implementation
773
774 config = mkIf (with config.services.borgbackup; jobs != { } || repos != { })
775 (with config.services.borgbackup; {
776 assertions =
777 mapAttrsToList mkPassAssertion jobs
778 ++ mapAttrsToList mkKeysAssertion repos
779 ++ mapAttrsToList mkSourceAssertions jobs
780 ++ mapAttrsToList mkRemovableDeviceAssertions jobs;
781
782 systemd.tmpfiles.settings = mapAttrs' mkTmpfiles jobs;
783
784 systemd.services =
785 # A job named "foo" is mapped to systemd.services.borgbackup-job-foo
786 mapAttrs' mkBackupService jobs
787 # A repo named "foo" is mapped to systemd.services.borgbackup-repo-foo
788 // mapAttrs' mkRepoService repos;
789
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 = mapAttrs' mkBackupTimers (filterAttrs (_: cfg: cfg.startAt != []) jobs);
793
794 users = mkMerge (mapAttrsToList mkUsersConfig repos);
795
796 environment.systemPackages =
797 [ config.services.borgbackup.package ] ++ (mapAttrsToList mkBorgWrapper jobs);
798 });
799}