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