Skip to content

Commit

Permalink
paperless-ng: init at 1.4.2
Browse files Browse the repository at this point in the history
  • Loading branch information
Flakebi committed May 15, 2021
1 parent a8d47c1 commit 7cb1a20
Show file tree
Hide file tree
Showing 6 changed files with 458 additions and 0 deletions.
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@
./services/misc/osrm.nix
./services/misc/packagekit.nix
./services/misc/paperless.nix
./services/misc/paperless-ng.nix
./services/misc/parsoid.nix
./services/misc/plex.nix
./services/misc/plikd.nix
Expand Down
254 changes: 254 additions & 0 deletions nixos/modules/services/misc/paperless-ng.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
{ config, pkgs, lib, ... }:

with lib;
let
cfg = config.services.paperless-ng;

defaultUser = "paperless";

env = {
PAPERLESS_DATA_DIR = cfg.dataDir;
PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
} // cfg.extraConfig;

manage = let
setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
in pkgs.writeShellScript "manage" ''
${setupEnv}
exec ${cfg.package}/bin/paperless-ng "$@"
'';

# Secure the services
defaultServiceConfig = {
BindReadOnlyPaths = [
"/nix/store"
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
];
BindPaths = [
cfg.consumptionDir
cfg.dataDir
cfg.mediaDir
];
CapabilityBoundingSet = "";
# ProtectClock= adds DeviceAllow=char-rtc r
DeviceAllow = "";
# User is set explicitely
#DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
# Needs to communicate to redis
#PrivateNetwork = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
# Breaks if the home dir of the user is in /home
#ProtectHome = true;
ProtectHostname = true;
ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
TemporaryFileSystem = "/:ro";
UMask = "0066";
};
in
{
options.services.paperless-ng = {
enable = mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Paperless-ng.
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.
A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
<literal>''${dataDir}/paperless-ng-manage</literal>.
'';
};

dataDir = mkOption {
type = types.str;
default = "/var/lib/paperless";
description = "Directory to store the Paperless data.";
};

mediaDir = mkOption {
type = types.str;
default = "${cfg.dataDir}/media";
defaultText = "\${dataDir}/consume";
description = "Directory to store the Paperless documents.";
};

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.";
};

address = mkOption {
type = types.str;
default = "localhost";
description = "Web interface address.";
};

port = mkOption {
type = types.int;
default = 28981;
description = "Web interface port.";
};

extraConfig = mkOption {
type = types.attrs;
default = {};
description = ''
Extra paperless-ng config options.
See <link xlink:href="https://paperless-ng.readthedocs.io/en/latest/configuration.html">the documentation</link>
for available options.
'';
example = literalExample ''
{
PAPERLESS_OCR_LANGUAGE = "deu+eng";
}
'';
};

user = mkOption {
type = types.str;
default = defaultUser;
description = "User under which Paperless runs.";
};

package = mkOption {
type = types.package;
default = pkgs.paperless-ng;
defaultText = "pkgs.paperless-ng";
description = "The Paperless package to use.";
};
};

config = mkIf cfg.enable {
assertions = [
{
assertion = config.services.paperless.enable ->
(config.services.paperless.dataDir != cfg.dataDir && config.services.paperless.port != cfg.port);
message = "Paperless-ng replaces Paperless, either disable Paperless or assign a new dataDir and port to one of them";
}
];

# Enable redis if no special url is set
services.redis.enable = mkIf (! hasAttr "PAPERLESS_REDIS" env) (mkDefault true);

systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
"d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
(if cfg.consumptionDirIsPublic then
"d '${cfg.consumptionDir}' 777 - - - -"
else
"d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
)
];

systemd.services.paperless-ng-consumer = {
description = "Paperless document consumer";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = "${cfg.package}/bin/paperless-ng document_consumer";
Restart = "on-failure";
};
environment = env;
after = [ "systemd-tmpfiles-setup.service" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
ln -sf ${manage} ${cfg.dataDir}/paperless-ng-manage
# Auto-migrate on first run or if the package has changed
versionFile="${cfg.dataDir}/src-version"
if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
${cfg.package}/bin/paperless-ng migrate
echo ${cfg.package} > "$versionFile"
fi
'';
};

systemd.services.paperless-ng-server = {
description = "Paperless document server";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
Restart = "on-failure";
};
environment = env;
# Bind to `paperless-ng-consumer` so that the server never runs
# during migrations
bindsTo = [ "paperless-ng-consumer.service" ];
after = [ "paperless-ng-consumer.service" ];
wantedBy = [ "multi-user.target" ];
};

systemd.services.paperless-ng-web = {
description = "Paperless web server";
serviceConfig = defaultServiceConfig // {
User = cfg.user;
ExecStart = ''
${pkgs.python3Packages.gunicorn}/bin/gunicorn \
-c ${cfg.package}/lib/paperless-ng/gunicorn.conf.py paperless.asgi:application
'';
Restart = "on-failure";

AmbientCapabilities = "CAP_NET_BIND_SERVICE";
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
# gunicorn needs setuid
SystemCallFilter = [ "@system-service" "~@privileged @resources @keyring" "@setuid" ];
};
environment = env // {
PATH = mkForce cfg.package.path;
PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
};
# Bind to `paperless-ng-consumer` so that the server never runs
# during migrations
bindsTo = [ "paperless-ng-consumer.service" ];
after = [ "paperless-ng-consumer.service" ];
wantedBy = [ "multi-user.target" ];
};

users = optionalAttrs (cfg.user == defaultUser) {
users.${defaultUser} = {
group = defaultUser;
uid = config.ids.uids.paperless;
home = cfg.dataDir;
};

groups.${defaultUser} = {
gid = config.ids.gids.paperless;
};
};
};
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ in
pam-u2f = handleTest ./pam-u2f.nix {};
pantheon = handleTest ./pantheon.nix {};
paperless = handleTest ./paperless.nix {};
paperless-ng = handleTest ./paperless-ng.nix {};
pdns-recursor = handleTest ./pdns-recursor.nix {};
peerflix = handleTest ./peerflix.nix {};
pgjwt = handleTest ./pgjwt.nix {};
Expand Down
38 changes: 38 additions & 0 deletions nixos/tests/paperless-ng.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import ./make-test-python.nix ({ lib, ... }: {
name = "paperless-ng";
meta = {
maintainers = with lib.maintainers; [ Flakebi ];
};

nodes.machine = { pkgs, ... }: {
environment.systemPackages = with pkgs; [ imagemagick jq ];
services.redis.enable = true;
services.paperless-ng.enable = true;
virtualisation.memorySize = 1024;
};

testScript = ''
machine.wait_for_unit("paperless-ng-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"
)
with subtest("Service gets ready"):
machine.wait_for_unit("paperless-ng-web.service")
# Wait until server accepts connections
machine.wait_until_succeeds("curl -fs localhost:28981")
# Create admin user
machine.succeed("echo \"from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@localhost', 'admin')\" | /var/lib/paperless/paperless-ng-manage shell")
with subtest("Test document is consumed"):
machine.wait_until_succeeds(
"(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 1))"
)
assert "2005-10-16" in machine.succeed(
"curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
)
'';
})

0 comments on commit 7cb1a20

Please sign in to comment.