paperless-ng: init at 1.4.5

This commit is contained in:
Flakebi 2021-05-13 16:42:22 +02:00
parent 21d05e6643
commit 95f2dc650d
No known key found for this signature in database
GPG key ID: 38E7ED984D7DCD02
6 changed files with 541 additions and 0 deletions

View file

@ -551,6 +551,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

View file

@ -0,0 +1,304 @@
{ 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 ];
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 {
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) 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;
};
};
};
}

View file

@ -335,6 +335,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 {};

View file

@ -0,0 +1,36 @@
import ./make-test-python.nix ({ lib, ... }: {
name = "paperless-ng";
meta.maintainers = with lib.maintainers; [ earvstedt Flakebi ];
nodes.machine = { pkgs, ... }: {
environment.systemPackages = with pkgs; [ imagemagick jq ];
services.paperless-ng = {
enable = true;
passwordFile = builtins.toFile "password" "admin";
};
virtualisation.memorySize = 1024;
};
testScript = ''
machine.wait_for_unit("paperless-ng-consumer.service")
with subtest("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("Web interface gets ready"):
machine.wait_for_unit("paperless-ng-web.service")
# Wait until server accepts connections
machine.wait_until_succeeds("curl -fs localhost:28981")
with subtest("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'"
)
'';
})

View file

@ -0,0 +1,197 @@
{ lib
, fetchurl
, nixosTests
, python3
, ghostscript
, imagemagick
, jbig2enc
, ocrmypdf
, optipng
, pngquant
, qpdf
, tesseract4
, unpaper
, liberation_ttf
}:
let
py = python3.override {
packageOverrides = self: super: {
django = super.django_3;
django-picklefield = super.django-picklefield.overrideAttrs (oldAttrs: {
# Checks do not pass with django 3
doInstallCheck = false;
});
# Avoid warning in django-q versions > 1.3.4
# https://github.com/jonaswinkler/paperless-ng/issues/857
# https://github.com/Koed00/django-q/issues/526
django-q = super.django-q.overridePythonAttrs (oldAttrs: rec {
version = "1.3.4";
src = super.fetchPypi {
inherit (oldAttrs) pname;
inherit version;
sha256 = "Uj1U3PG2YVLBtlj5FPAO07UYo0MqnezUiYc4yo274Q8=";
};
});
};
};
path = lib.makeBinPath [ ghostscript imagemagick jbig2enc optipng pngquant qpdf tesseract4 unpaper ];
in
py.pkgs.pythonPackages.buildPythonApplication rec {
pname = "paperless-ng";
version = "1.4.5";
src = fetchurl {
url = "https://github.com/jonaswinkler/paperless-ng/releases/download/ng-${version}/${pname}-${version}.tar.xz";
sha256 = "2PJb8j3oimlfiJ3gqjK6uTemzFdtAP2Mlm5RH09bx/E=";
};
format = "other";
# Make bind address configurable
# Fix tests with Pillow 8.3.1: https://github.com/jonaswinkler/paperless-ng/pull/1183
prePatch = ''
substituteInPlace gunicorn.conf.py --replace "bind = '0.0.0.0:8000'" ""
substituteInPlace src/paperless_tesseract/parsers.py --replace "return x" "return round(x)"
'';
propagatedBuildInputs = with py.pkgs.pythonPackages; [
aioredis
arrow
asgiref
async-timeout
attrs
autobahn
automat
blessed
certifi
cffi
channels-redis
channels
chardet
click
coloredlogs
concurrent-log-handler
constantly
cryptography
daphne
dateparser
django-cors-headers
django_extensions
django-filter
django-picklefield
django-q
django
djangorestframework
filelock
fuzzywuzzy
gunicorn
h11
hiredis
httptools
humanfriendly
hyperlink
idna
imap-tools
img2pdf
incremental
inotify-simple
inotifyrecursive
joblib
langdetect
lxml
msgpack
numpy
ocrmypdf
pathvalidate
pdfminer
pikepdf
pillow
pluggy
portalocker
psycopg2
pyasn1-modules
pyasn1
pycparser
pyopenssl
python-dateutil
python-dotenv
python-gnupg
python-Levenshtein
python_magic
pytz
pyyaml
redis
regex
reportlab
requests
scikit-learn
scipy
service-identity
six
sortedcontainers
sqlparse
threadpoolctl
tika
tqdm
twisted.extras.tls
txaio
tzlocal
urllib3
uvicorn
uvloop
watchdog
watchgod
wcwidth
websockets
whitenoise
whoosh
zope_interface
];
doCheck = true;
checkInputs = with py.pkgs.pythonPackages; [
pytest
pytest-cov
pytest-django
pytest-env
pytest-sugar
pytest-xdist
factory_boy
];
# The tests require:
# - PATH with runtime binaries
# - A temporary HOME directory for gnupg
# - XDG_DATA_DIRS with test-specific fonts
checkPhase = ''
pushd src
PATH="${path}:$PATH" HOME=$(mktemp -d) XDG_DATA_DIRS="${liberation_ttf}/share:$XDG_DATA_DIRS" pytest
popd
'';
installPhase = ''
mkdir -p $out/lib
cp -r . $out/lib/paperless-ng
chmod +x $out/lib/paperless-ng/src/manage.py
makeWrapper $out/lib/paperless-ng/src/manage.py $out/bin/paperless-ng \
--prefix PYTHONPATH : "$PYTHONPATH" \
--prefix PATH : "${path}"
'';
passthru = {
# PYTHONPATH of all dependencies used by the package
pythonPath = python3.pkgs.makePythonPath propagatedBuildInputs;
inherit path;
tests = { inherit (nixosTests) paperless-ng; };
};
meta = with lib; {
description = "A supercharged version of paperless: scan, index, and archive all of your physical documents";
homepage = "https://paperless-ng.readthedocs.io/en/latest/";
license = licenses.gpl3Only;
maintainers = with maintainers; [ earvstedt Flakebi ];
};
}

View file

@ -7891,6 +7891,8 @@ with pkgs;
paperless = callPackage ../applications/office/paperless { };
paperless-ng = callPackage ../applications/office/paperless-ng { };
paperwork = callPackage ../applications/office/paperwork/paperwork-gtk.nix { };
papertrail = callPackage ../tools/text/papertrail { };