Skip to content

Commit

Permalink
nixos/paperless: add backup feature
Browse files Browse the repository at this point in the history
Paperless includes a document exporter that can be used for
backups.

This extends the module to provide a way to enable and configure
a timer, the backup parameters and allow providing a post-processing
script (e.g. to ship the backup somewhere else, clean up, ...).

It works out of the box when just enabling it but can be customized.

Includes suitable tests.
  • Loading branch information
ctheune committed Jan 4, 2025
1 parent 14f4013 commit ed36619
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 4 deletions.
3 changes: 3 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@

- `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.

- The paperless module now has an option for regular automatic export of
documents data using the integrated document exporter.

- Caddy can now be built with plugins by using `caddy.withPlugins`, a `passthru` function that accepts an attribute set as a parameter. The `plugins` argument represents a list of Caddy plugins, with each Caddy plugin being a versioned module. The `hash` argument represents the `vendorHash` of the resulting Caddy source code with the plugins added.

Example:
Expand Down
102 changes: 98 additions & 4 deletions nixos/modules/services/misc/paperless.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ config, pkgs, lib, ... }:
{ config, options, pkgs, lib, ... }:
let
cfg = config.services.paperless;

Expand Down Expand Up @@ -82,7 +82,7 @@ let
};
in
{
meta.maintainers = with lib.maintainers; [ leona SuperSandro2000 erikarvstedt ];
meta.maintainers = with lib.maintainers; [ leona SuperSandro2000 erikarvstedt atemu theuni ];

imports = [
(lib.mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
Expand Down Expand Up @@ -252,9 +252,60 @@ in
'';
};
};

exporter = {

enable = lib.mkEnableOption "regular automatic document exports";

directory = lib.mkOption {
type = lib.types.str;
default = cfg.dataDir + "/exports";
defaultText = "\${dataDir}/exports";
description = "Directory to store exports.";
};

onCalendar = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "01:30:00";
description = ''
When to run the exporter.
Fills in the OnCalendar section of a timer and uses a systemd.time(7)
format.
`null` disables the timer; allowing you to trigger the
`paperless-exporter` service through other means.
'';
};

options = lib.mkOption {
type = with lib.types; attrsOf anything;
default = {
"no-progress-bar" = true;
"no-color" = true;
"compare-checksums" = true;
"delete" = true;
};
description = "Options to pass to the document exporter";
};

preScript = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Script to run right before the export";
};

postScript = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Script to run after finishing the export";
};

};

};

config = lib.mkIf cfg.enable {
config = lib.mkIf cfg.enable (lib.mkMerge [ {
services.redis.servers.paperless.enable = lib.mkIf enableRedis true;

services.postgresql = lib.mkIf cfg.database.createLocally {
Expand Down Expand Up @@ -439,5 +490,48 @@ in
gid = config.ids.gids.paperless;
};
};
};
}

(lib.mkIf cfg.exporter.enable {
systemd.tmpfiles.rules = [
"d '${cfg.exporter.directory}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
];

systemd.services.paperless-exporter = {
startAt = lib.defaultTo [] cfg.exporter.onCalendar;

serviceConfig = {
User = cfg.user;
WorkingDirectory = cfg.dataDir;
};

unitConfig = let
services = [
"paperless-consumer.service"
"paperless-scheduler.service"
"paperless-task-queue.service"
"paperless-web.service" ];
in {
# Shut down the paperless services while the exporter runs
Conflicts = services;
After = services;
# Bring them back up afterwards, regardless of pass/fail
OnFailure = services;
OnSuccess = services;
};
enableStrictShellChecks = true;
script = ''
echo "Running pre script ..."
${cfg.exporter.preScript}
echo "Exporting documents ..."
./paperless-manage document_exporter ${cfg.exporter.directory} ${lib.cli.toGNUCommandLineShell {} cfg.exporter.options}
echo "Running post script ..."
${cfg.exporter.postScript}
'';
};
})
]);

}
40 changes: 40 additions & 0 deletions nixos/tests/paperless.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ import ./make-test-python.nix ({ lib, ... }: {
services.paperless = {
enable = true;
passwordFile = builtins.toFile "password" "admin";

exporter = {
enable = true;

options = {
"no-color" = false; # override a default option
"no-thumbnail" = true; # add a new option
};

preScript = ''
echo "Hello World"
'';

postScript = ''
echo "Goodbye World"
'';
};
};
};
postgres = { config, pkgs, ... }: {
Expand All @@ -25,6 +42,11 @@ import ./make-test-python.nix ({ lib, ... }: {
def test_paperless(node):
node.wait_for_unit("paperless-consumer.service")
# Double check that our attrset option override works as expected
cmdline = node.succeed("grep 'paperless-manage' $(systemctl cat paperless-exporter | grep ExecStart | cut -f 2 -d=)")
print(f"Exporter command line {cmdline!r}")
assert cmdline.strip() == "./paperless-manage document_exporter /var/lib/paperless/exports --compare-checksums --delete --no-progress-bar --no-thumbnail", "Unexpected exporter command line"
with subtest("Add a document via the file system"):
node.succeed(
"convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
Expand Down Expand Up @@ -73,6 +95,24 @@ import ./make-test-python.nix ({ lib, ... }: {
metadata = json.loads(node.succeed("curl -u admin:admin -fs localhost:28981/api/documents/3/metadata/"))
assert "original_checksum" in metadata
# Check exporter config looks good
with subtest("Exporter config is good"):
node.succeed("systemctl start --wait paperless-exporter")
node.wait_for_unit("paperless-web.service")
node.wait_for_unit("paperless-consumer.service")
node.wait_for_unit("paperless-scheduler.service")
node.wait_for_unit("paperless-task-queue.service")
output = node.succeed("journalctl -u paperless-exporter.service")
print(output)
assert "Hello World" in output, "Missing pre script output"
assert "Goodbye World" in output, "Missing post script output"
node.succeed("ls -lah /var/lib/paperless/exports/manifest.json")
timers = node.succeed("systemctl list-timers paperless-exporter")
print(timers)
assert "paperless-exporter.timer paperless-exporter.service" in timers, "missing timer"
assert "1 timers listed." in timers, "incorrect number of timers"
test_paperless(simple)
simple.send_monitor_command("quit")
simple.wait_for_shutdown()
Expand Down

0 comments on commit ed36619

Please sign in to comment.