2021-04-25 12:00:00 +00:00
|
|
|
{
|
|
|
|
config,
|
|
|
|
lib,
|
|
|
|
pkgs,
|
|
|
|
...
|
|
|
|
}:
|
2019-01-05 13:55:01 +01:00
|
|
|
let
|
|
|
|
cfg = config.services.duplicity;
|
|
|
|
|
|
|
|
stateDirectory = "/var/lib/duplicity";
|
|
|
|
|
2021-04-25 12:00:00 +00:00
|
|
|
localTarget =
|
2024-08-30 00:46:40 +02:00
|
|
|
if lib.hasPrefix "file://" cfg.targetUrl then lib.removePrefix "file://" cfg.targetUrl else null;
|
2019-01-05 13:55:01 +01:00
|
|
|
|
2021-04-25 12:00:00 +00:00
|
|
|
in
|
|
|
|
{
|
2019-01-05 13:55:01 +01:00
|
|
|
options.services.duplicity = {
|
2024-08-30 00:46:40 +02:00
|
|
|
enable = lib.mkEnableOption "backups with duplicity";
|
2019-01-05 13:55:01 +01:00
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
root = lib.mkOption {
|
|
|
|
type = lib.types.path;
|
2019-01-05 13:55:01 +01:00
|
|
|
default = "/";
|
|
|
|
description = ''
|
|
|
|
Root directory to backup.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
include = lib.mkOption {
|
|
|
|
type = lib.types.listOf lib.types.str;
|
2021-04-25 12:00:00 +00:00
|
|
|
default = [ ];
|
2019-01-05 13:55:01 +01:00
|
|
|
example = [ "/home" ];
|
|
|
|
description = ''
|
|
|
|
List of paths to include into the backups. See the FILE SELECTION
|
|
|
|
section in {manpage}`duplicity(1)` for details on the syntax.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
exclude = lib.mkOption {
|
|
|
|
type = lib.types.listOf lib.types.str;
|
2021-04-25 12:00:00 +00:00
|
|
|
default = [ ];
|
2019-01-05 13:55:01 +01:00
|
|
|
description = ''
|
|
|
|
List of paths to exclude from backups. See the FILE SELECTION section in
|
|
|
|
{manpage}`duplicity(1)` for details on the syntax.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
includeFileList = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.path;
|
2024-07-08 17:02:42 +02:00
|
|
|
default = null;
|
|
|
|
example = /path/to/fileList.txt;
|
|
|
|
description = ''
|
|
|
|
File containing newline-separated list of paths to include into the
|
|
|
|
backups. See the FILE SELECTION section in {manpage}`duplicity(1)` for
|
|
|
|
details on the syntax.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
excludeFileList = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.path;
|
2024-07-08 17:02:42 +02:00
|
|
|
default = null;
|
|
|
|
example = /path/to/fileList.txt;
|
|
|
|
description = ''
|
|
|
|
File containing newline-separated list of paths to exclude into the
|
|
|
|
backups. See the FILE SELECTION section in {manpage}`duplicity(1)` for
|
|
|
|
details on the syntax.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
targetUrl = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2019-01-05 13:55:01 +01:00
|
|
|
example = "s3://host:port/prefix";
|
|
|
|
description = ''
|
|
|
|
Target url to backup to. See the URL FORMAT section in
|
|
|
|
{manpage}`duplicity(1)` for supported urls.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
secretFile = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.path;
|
2019-01-05 13:55:01 +01:00
|
|
|
default = null;
|
|
|
|
description = ''
|
|
|
|
Path of a file containing secrets (gpg passphrase, access key...) in
|
|
|
|
the format of EnvironmentFile as described by
|
|
|
|
{manpage}`systemd.exec(5)`. For example:
|
2022-08-30 14:08:50 +02:00
|
|
|
```
|
2022-08-03 01:57:59 +02:00
|
|
|
PASSPHRASE=«...»
|
|
|
|
AWS_ACCESS_KEY_ID=«...»
|
|
|
|
AWS_SECRET_ACCESS_KEY=«...»
|
2022-08-30 14:08:50 +02:00
|
|
|
```
|
2019-01-05 13:55:01 +01:00
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
frequency = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
2019-01-05 13:55:01 +01:00
|
|
|
default = "daily";
|
|
|
|
description = ''
|
|
|
|
Run duplicity with the given frequency (see
|
|
|
|
{manpage}`systemd.time(7)` for the format).
|
|
|
|
If null, do not run automatically.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
extraFlags = lib.mkOption {
|
|
|
|
type = lib.types.listOf lib.types.str;
|
2021-04-25 12:00:00 +00:00
|
|
|
default = [ ];
|
2021-05-13 12:00:00 +00:00
|
|
|
example = [
|
|
|
|
"--backend-retry-delay"
|
|
|
|
"100"
|
|
|
|
];
|
2019-01-05 13:55:01 +01:00
|
|
|
description = ''
|
|
|
|
Extra command-line flags passed to duplicity. See
|
|
|
|
{manpage}`duplicity(1)`.
|
|
|
|
'';
|
|
|
|
};
|
2021-04-25 12:00:00 +00:00
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
fullIfOlderThan = lib.mkOption {
|
|
|
|
type = lib.types.str;
|
2021-05-13 12:00:00 +00:00
|
|
|
default = "never";
|
|
|
|
example = "1M";
|
|
|
|
description = ''
|
|
|
|
If `"never"` (the default) always do incremental
|
|
|
|
backups (the first backup will be a full backup, of course). If
|
|
|
|
`"always"` always do full backups. Otherwise, this
|
|
|
|
must be a string representing a duration. Full backups will be made
|
|
|
|
when the latest full backup is older than this duration. If this is not
|
|
|
|
the case, an incremental backup is performed.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2021-04-25 12:00:00 +00:00
|
|
|
cleanup = {
|
2024-08-30 00:46:40 +02:00
|
|
|
maxAge = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.str;
|
2021-04-25 12:00:00 +00:00
|
|
|
default = null;
|
|
|
|
example = "6M";
|
|
|
|
description = ''
|
|
|
|
If non-null, delete all backup sets older than the given time. Old backup sets
|
|
|
|
will not be deleted if backup sets newer than time depend on them.
|
|
|
|
'';
|
|
|
|
};
|
2024-08-30 00:46:40 +02:00
|
|
|
maxFull = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.int;
|
2021-04-25 12:00:00 +00:00
|
|
|
default = null;
|
|
|
|
example = 2;
|
|
|
|
description = ''
|
|
|
|
If non-null, delete all backups sets that are older than the count:th last full
|
|
|
|
backup (in other words, keep the last count full backups and
|
|
|
|
associated incremental sets).
|
|
|
|
'';
|
|
|
|
};
|
2024-08-30 00:46:40 +02:00
|
|
|
maxIncr = lib.mkOption {
|
|
|
|
type = lib.types.nullOr lib.types.int;
|
2021-05-13 12:00:00 +00:00
|
|
|
default = null;
|
|
|
|
example = 1;
|
|
|
|
description = ''
|
|
|
|
If non-null, delete incremental sets of all backups sets that are
|
|
|
|
older than the count:th last full backup (in other words, keep only
|
|
|
|
old full backups and not their increments).
|
|
|
|
'';
|
|
|
|
};
|
2021-04-25 12:00:00 +00:00
|
|
|
};
|
2019-01-05 13:55:01 +01:00
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
config = lib.mkIf cfg.enable {
|
2019-01-05 13:55:01 +01:00
|
|
|
systemd = {
|
|
|
|
services.duplicity =
|
|
|
|
{
|
|
|
|
description = "backup files with duplicity";
|
2024-12-10 20:26:33 +01:00
|
|
|
|
2019-01-05 13:55:01 +01:00
|
|
|
environment.HOME = stateDirectory;
|
2024-12-10 20:26:33 +01:00
|
|
|
|
2021-04-25 12:00:00 +00:00
|
|
|
script =
|
|
|
|
let
|
2024-08-30 00:46:40 +02:00
|
|
|
target = lib.escapeShellArg cfg.targetUrl;
|
|
|
|
extra = lib.escapeShellArgs (
|
|
|
|
[
|
|
|
|
"--archive-dir"
|
|
|
|
stateDirectory
|
|
|
|
]
|
|
|
|
++ cfg.extraFlags
|
|
|
|
);
|
2021-04-25 12:00:00 +00:00
|
|
|
dup = "${pkgs.duplicity}/bin/duplicity";
|
|
|
|
in
|
|
|
|
''
|
|
|
|
set -x
|
|
|
|
${dup} cleanup ${target} --force ${extra}
|
|
|
|
${lib.optionalString (
|
|
|
|
cfg.cleanup.maxAge != null
|
|
|
|
) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
|
|
|
|
${lib.optionalString (
|
|
|
|
cfg.cleanup.maxFull != null
|
|
|
|
) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
|
2021-06-27 09:39:23 +02:00
|
|
|
${lib.optionalString (
|
|
|
|
cfg.cleanup.maxIncr != null
|
|
|
|
) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
|
2021-05-13 12:00:00 +00:00
|
|
|
exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${
|
|
|
|
lib.escapeShellArgs (
|
2021-04-25 12:00:00 +00:00
|
|
|
[
|
|
|
|
cfg.root
|
|
|
|
cfg.targetUrl
|
|
|
|
]
|
2024-07-08 17:02:42 +02:00
|
|
|
++ lib.optionals (cfg.includeFileList != null) [
|
|
|
|
"--include-filelist"
|
|
|
|
cfg.includeFileList
|
|
|
|
]
|
|
|
|
++ lib.optionals (cfg.excludeFileList != null) [
|
|
|
|
"--exclude-filelist"
|
|
|
|
cfg.excludeFileList
|
|
|
|
]
|
2024-08-30 00:46:40 +02:00
|
|
|
++ lib.concatMap (p: [
|
|
|
|
"--include"
|
|
|
|
p
|
|
|
|
]) cfg.include
|
|
|
|
++ lib.concatMap (p: [
|
|
|
|
"--exclude"
|
|
|
|
p
|
|
|
|
]) cfg.exclude
|
2021-05-13 12:00:00 +00:00
|
|
|
++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [
|
|
|
|
"--full-if-older-than"
|
|
|
|
cfg.fullIfOlderThan
|
|
|
|
])
|
2021-04-25 12:00:00 +00:00
|
|
|
)
|
|
|
|
} ${extra}
|
2019-01-05 13:55:01 +01:00
|
|
|
'';
|
2021-04-25 12:00:00 +00:00
|
|
|
serviceConfig =
|
|
|
|
{
|
2019-01-05 13:55:01 +01:00
|
|
|
PrivateTmp = true;
|
|
|
|
ProtectSystem = "strict";
|
|
|
|
ProtectHome = "read-only";
|
|
|
|
StateDirectory = baseNameOf stateDirectory;
|
2024-08-30 00:46:40 +02:00
|
|
|
}
|
|
|
|
// lib.optionalAttrs (localTarget != null) {
|
2019-01-05 13:55:01 +01:00
|
|
|
ReadWritePaths = localTarget;
|
2024-08-30 00:46:40 +02:00
|
|
|
}
|
|
|
|
// lib.optionalAttrs (cfg.secretFile != null) {
|
2019-01-05 13:55:01 +01:00
|
|
|
EnvironmentFile = cfg.secretFile;
|
2024-12-10 20:26:33 +01:00
|
|
|
};
|
|
|
|
}
|
2024-08-30 00:46:40 +02:00
|
|
|
// lib.optionalAttrs (cfg.frequency != null) {
|
2019-01-05 13:55:01 +01:00
|
|
|
startAt = cfg.frequency;
|
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
tmpfiles.rules = lib.optional (localTarget != null) "d ${localTarget} 0700 root root -";
|
2019-01-05 13:55:01 +01:00
|
|
|
};
|
|
|
|
|
2024-08-30 00:46:40 +02:00
|
|
|
assertions = lib.singleton {
|
2019-01-05 13:55:01 +01:00
|
|
|
# Duplicity will fail if the last file selection option is an include. It
|
|
|
|
# is not always possible to detect but this simple case can be caught.
|
2021-04-25 12:00:00 +00:00
|
|
|
assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
|
2019-01-05 13:55:01 +01:00
|
|
|
message = ''
|
|
|
|
Duplicity will fail if you only specify included paths ("Because the
|
|
|
|
default is to include all files, the expression is redundant. Exiting
|
|
|
|
because this probably isn't what you meant.")
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|