6e1421013a
The paperless project has moved on to paperless-ng and the original paperless package in Nixpkgs has stopped working recently (due to version incompatibility with the providede Django package). Instead of investing more time into the old module we should migrate all users to the new module instead.
304 lines
9.9 KiB
Nix
304 lines
9.9 KiB
Nix
{ 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}";
|
|
} // lib.mapAttrs (_: toString) 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 = {
|
|
TemporaryFileSystem = "/:ro";
|
|
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 = "";
|
|
LockPersonality = true;
|
|
MemoryDenyWriteExecute = true;
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
PrivateMounts = true;
|
|
# Needs to connect to redis
|
|
# PrivateNetwork = true;
|
|
PrivateTmp = true;
|
|
PrivateUsers = true;
|
|
ProcSubset = "pid";
|
|
ProtectClock = true;
|
|
# Breaks if the home dir of the user is in /home
|
|
# Also does not add much value in combination with the TemporaryFileSystem.
|
|
# ProtectHome = true;
|
|
ProtectHostname = true;
|
|
# Would re-mount paths ignored by temporary root
|
|
#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" ];
|
|
# Does not work well with the temporary root
|
|
#UMask = "0066";
|
|
};
|
|
in
|
|
{
|
|
meta.maintainers = with maintainers; [ earvstedt Flakebi ];
|
|
|
|
imports = [
|
|
(mkRemovedOptionModule [ "services" "paperless"] ''
|
|
The paperless module has been removed as the upstream project died.
|
|
Users should migrate to the paperless-ng module (services.paperless-ng).
|
|
More information can be found in the NixOS 21.11 release notes.
|
|
'')
|
|
];
|
|
|
|
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.";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = types.nullOr types.path;
|
|
default = null;
|
|
example = "/run/keys/paperless-ng-password";
|
|
description = ''
|
|
A file containing the superuser password.
|
|
|
|
A superuser is required to access the web interface.
|
|
If unset, you can create a superuser manually by running
|
|
<literal>''${dataDir}/paperless-ng-manage createsuperuser</literal>.
|
|
|
|
The default superuser name is <literal>admin</literal>. To change it, set
|
|
option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
|
|
WARNING: When changing the superuser name after the initial setup, the old superuser
|
|
will continue to exist.
|
|
|
|
To disable login for the web interface, set the following:
|
|
<literal>extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";</literal>.
|
|
WARNING: Only use this on a trusted system without internet access to Paperless.
|
|
'';
|
|
};
|
|
|
|
address = mkOption {
|
|
type = types.str;
|
|
default = "localhost";
|
|
description = "Web interface address.";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
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 {
|
|
# Enable redis if no special url is set
|
|
services.redis.enable = mkIf (!hasAttr "PAPERLESS_REDIS" env) 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-server = {
|
|
description = "Paperless document server";
|
|
serviceConfig = defaultServiceConfig // {
|
|
User = cfg.user;
|
|
ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
|
|
Restart = "on-failure";
|
|
};
|
|
environment = env;
|
|
wantedBy = [ "multi-user.target" ];
|
|
wants = [ "paperless-ng-consumer.service" "paperless-ng-web.service" ];
|
|
|
|
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
|
|
''
|
|
+ optionalString (cfg.passwordFile != null) ''
|
|
export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
|
|
export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
|
|
superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
|
|
superuserStateFile="${cfg.dataDir}/superuser-state"
|
|
|
|
if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
|
|
${cfg.package}/bin/paperless-ng manage_superuser
|
|
echo "$superuserState" > "$superuserStateFile"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# Password copying can't be implemented as a privileged preStart script
|
|
# in 'paperless-ng-server' because 'defaultServiceConfig' limits the filesystem
|
|
# paths accessible by the service.
|
|
systemd.services.paperless-ng-copy-password = mkIf (cfg.passwordFile != null) {
|
|
requiredBy = [ "paperless-ng-server.service" ];
|
|
before = [ "paperless-ng-server.service" ];
|
|
serviceConfig = {
|
|
ExecStart = ''
|
|
${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
|
|
'${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
|
|
'';
|
|
Type = "oneshot";
|
|
};
|
|
};
|
|
|
|
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;
|
|
# Bind to `paperless-ng-server` so that the consumer never runs
|
|
# during migrations
|
|
bindsTo = [ "paperless-ng-server.service" ];
|
|
after = [ "paperless-ng-server.service" ];
|
|
};
|
|
|
|
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 = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
|
|
};
|
|
environment = env // {
|
|
PATH = mkForce cfg.package.path;
|
|
PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
|
|
};
|
|
# Bind to `paperless-ng-server` so that the web server never runs
|
|
# during migrations
|
|
bindsTo = [ "paperless-ng-server.service" ];
|
|
after = [ "paperless-ng-server.service" ];
|
|
};
|
|
|
|
users = optionalAttrs (cfg.user == defaultUser) {
|
|
users.${defaultUser} = {
|
|
group = defaultUser;
|
|
uid = config.ids.uids.paperless;
|
|
home = cfg.dataDir;
|
|
};
|
|
|
|
groups.${defaultUser} = {
|
|
gid = config.ids.gids.paperless;
|
|
};
|
|
};
|
|
};
|
|
}
|