diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index e78673514e3b..730867450389 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -339,6 +339,7 @@
rss2email = 312;
cockroachdb = 313;
zoneminder = 314;
+ paperless = 315;
# When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
@@ -638,6 +639,7 @@
rss2email = 312;
cockroachdb = 313;
zoneminder = 314;
+ paperless = 315;
# When adding a gid, make sure it doesn't match an existing
# uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 56c44a43c6e3..1a649385451b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -436,6 +436,7 @@
./services/misc/octoprint.nix
./services/misc/osrm.nix
./services/misc/packagekit.nix
+ ./services/misc/paperless.nix
./services/misc/parsoid.nix
./services/misc/phd.nix
./services/misc/plex.nix
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
new file mode 100644
index 000000000000..4e6cd80e2425
--- /dev/null
+++ b/nixos/modules/services/misc/paperless.nix
@@ -0,0 +1,185 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+ cfg = config.services.paperless;
+
+ defaultUser = "paperless";
+
+ manage = cfg.package.withConfig {
+ config = {
+ PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+ PAPERLESS_INLINE_DOC = "true";
+ PAPERLESS_DISABLE_LOGIN = "true";
+ } // cfg.extraConfig;
+ inherit (cfg) dataDir ocrLanguages;
+ paperlessPkg = cfg.package;
+ };
+in
+{
+ options.services.paperless = {
+ enable = mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Enable Paperless.
+
+ When started, the Paperless database is automatically created if it doesn't
+ exist and updated if the Paperless package has changed.
+ Both tasks are achieved by running a Django migration.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/paperless";
+ description = "Directory to store the Paperless data.";
+ };
+
+ consumptionDir = mkOption {
+ type = types.str;
+ default = "${cfg.dataDir}/consume";
+ defaultText = "\${dataDir}/consume";
+ description = "Directory from which new documents are imported.";
+ };
+
+ consumptionDirIsPublic = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether all users can write to the consumption dir.";
+ };
+
+ ocrLanguages = mkOption {
+ type = with types; nullOr (listOf string);
+ default = null;
+ description = ''
+ Languages available for OCR via Tesseract, specified as
+ ISO 639-2/T language codes.
+ If unset, defaults to all available languages.
+ '';
+ example = [ "eng" "spa" "jpn" ];
+ };
+
+ address = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = "Server listening address.";
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 28981;
+ description = "Server port to listen on.";
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = {};
+ description = ''
+ Extra paperless config options.
+
+ The config values are evaluated as double-quoted Bash string literals.
+
+ See paperless-src/paperless.conf.example for available options.
+
+ To enable user authentication, set PAPERLESS_DISABLE_LOGIN = "false"
+ and run the shell command $dataDir/paperless-manage createsuperuser.
+
+ To define secret options without storing them in /nix/store, use the following pattern:
+ PAPERLESS_PASSPHRASE = "$(< /etc/my_passphrase_file)"
+ '';
+ example = literalExample ''
+ {
+ PAPERLESS_OCR_LANGUAGE = "deu";
+ }
+ '';
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = defaultUser;
+ description = "User under which Paperless runs.";
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.paperless;
+ defaultText = "pkgs.paperless";
+ description = "The Paperless package to use.";
+ };
+
+ manage = mkOption {
+ type = types.package;
+ readOnly = true;
+ default = manage;
+ description = ''
+ A script to manage the Paperless instance.
+ It wraps Django's manage.py and is also available at
+ $dataDir/manage-paperless
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+
+ systemd.tmpfiles.rules = [
+ "d '${cfg.dataDir}' - ${cfg.user} ${cfg.user} - -"
+ ] ++ (optional cfg.consumptionDirIsPublic
+ "d '${cfg.consumptionDir}' 777 ${cfg.user} ${cfg.user} - -"
+ # If the consumption dir is not created here, it's automatically created by
+ # 'manage' with the default permissions.
+ );
+
+ systemd.services.paperless-consumer = {
+ description = "Paperless document consumer";
+ serviceConfig = {
+ User = cfg.user;
+ ExecStart = "${manage} document_consumer";
+ Restart = "always";
+ };
+ after = [ "systemd-tmpfiles-setup.service" ];
+ wantedBy = [ "multi-user.target" ];
+ preStart = ''
+ if [[ $(readlink ${cfg.dataDir}/paperless-manage) != ${manage} ]]; then
+ ln -sf ${manage} ${cfg.dataDir}/paperless-manage
+ fi
+
+ ${manage.setupEnv}
+ # Auto-migrate on first run or if the package has changed
+ versionFile="$PAPERLESS_DBDIR/src-version"
+ if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+ python $paperlessSrc/manage.py migrate
+ echo ${cfg.package} > "$versionFile"
+ fi
+ '';
+ };
+
+ systemd.services.paperless-server = {
+ description = "Paperless document server";
+ serviceConfig = {
+ User = cfg.user;
+ ExecStart = "${manage} runserver --noreload ${cfg.address}:${toString cfg.port}";
+ Restart = "always";
+ };
+ # Bind to `paperless-consumer` so that the server never runs
+ # during migrations
+ bindsTo = [ "paperless-consumer.service" ];
+ after = [ "paperless-consumer.service" ];
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ users = optionalAttrs (cfg.user == defaultUser) {
+ users = [{
+ name = defaultUser;
+ group = defaultUser;
+ uid = config.ids.uids.paperless;
+ home = cfg.dataDir;
+ }];
+
+ groups = [{
+ name = defaultUser;
+ gid = config.ids.gids.paperless;
+ }];
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 5639b2668c30..efb0b1c3db8d 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -188,6 +188,7 @@ in
pam-oath-login = handleTest ./pam-oath-login.nix {};
pam-u2f = handleTest ./pam-u2f.nix {};
pantheon = handleTest ./pantheon.nix {};
+ paperless = handleTest ./paperless.nix {};
peerflix = handleTest ./peerflix.nix {};
pgjwt = handleTest ./pgjwt.nix {};
pgmanage = handleTest ./pgmanage.nix {};
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
new file mode 100644
index 000000000000..860ad0a6218f
--- /dev/null
+++ b/nixos/tests/paperless.nix
@@ -0,0 +1,29 @@
+import ./make-test.nix ({ lib, ... } : {
+ name = "paperless";
+ meta = with lib.maintainers; {
+ maintainers = [ earvstedt ];
+ };
+
+ machine = { pkgs, ... }: {
+ environment.systemPackages = with pkgs; [ imagemagick jq ];
+ services.paperless = {
+ enable = true;
+ ocrLanguages = [ "eng" ];
+ };
+ };
+
+ testScript = ''
+ $machine->waitForUnit("paperless-consumer.service");
+ # Create test doc
+ $machine->succeed('convert -size 400x40 xc:white -font "DejaVu-Sans" -pointsize 20 -fill black \
+ -annotate +5+20 "hello world 16-10-2005" /var/lib/paperless/consume/doc.png');
+
+ $machine->waitForUnit("paperless-server.service");
+ # Wait until server accepts connections
+ $machine->waitUntilSucceeds("curl -s localhost:28981");
+ # Wait until document is consumed
+ $machine->waitUntilSucceeds('(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))');
+ $machine->succeed("curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'")
+ =~ /2005-10-16/ or die;
+ '';
+})