git.iko.soy/typeable/nixpkgs
on master help sign in
paperless-ng: init at 1.4.5
commit95f2dc650d8c6c9f65efd85d915246ab94c4de6e authorFlakebi <flakebi@t-online.de> dateThu 13 May 2021, 16:42 (6 years ago) parent21d05e6
6 files changed+541
nixos/modules/module-list.nix+1 nixos/modules/services/misc/paperless-ng.nix+304 nixos/tests/all-tests.nix+1 nixos/tests/paperless-ng.nix+36 pkgs/applications/office/paperless-ng/default.nix+197 pkgs/top-level/all-packages.nix+2
nixos/modules/module-list.nix+1
@@ -551,6 +551,7 @@
551551 ./services/misc/osrm.nix
552552 ./services/misc/packagekit.nix
553553 ./services/misc/paperless.nix
554+ ./services/misc/paperless-ng.nix
554555 ./services/misc/parsoid.nix
555556 ./services/misc/plex.nix
556557 ./services/misc/plikd.nix
nixos/modules/services/misc/paperless-ng.nixnew file+304
@@ -0,0 +1,304 @@
1+{ config, pkgs, lib, ... }:
2+
3+with lib;
4+let
5+ cfg = config.services.paperless-ng;
6+
7+ defaultUser = "paperless";
8+
9+ env = {
10+ PAPERLESS_DATA_DIR = cfg.dataDir;
11+ PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
12+ PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
13+ GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
14+ } // lib.mapAttrs (_: toString) cfg.extraConfig;
15+
16+ manage = let
17+ setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
18+ in pkgs.writeShellScript "manage" ''
19+ ${setupEnv}
20+ exec ${cfg.package}/bin/paperless-ng "$@"
21+ '';
22+
23+ # Secure the services
24+ defaultServiceConfig = {
25+ TemporaryFileSystem = "/:ro";
26+ BindReadOnlyPaths = [
27+ "/nix/store"
28+ "-/etc/resolv.conf"
29+ "-/etc/nsswitch.conf"
30+ "-/etc/hosts"
31+ "-/etc/localtime"
32+ ];
33+ BindPaths = [
34+ cfg.consumptionDir
35+ cfg.dataDir
36+ cfg.mediaDir
37+ ];
38+ CapabilityBoundingSet = "";
39+ # ProtectClock adds DeviceAllow=char-rtc r
40+ DeviceAllow = "";
41+ LockPersonality = true;
42+ MemoryDenyWriteExecute = true;
43+ NoNewPrivileges = true;
44+ PrivateDevices = true;
45+ PrivateMounts = true;
46+ # Needs to connect to redis
47+ # PrivateNetwork = true;
48+ PrivateTmp = true;
49+ PrivateUsers = true;
50+ ProcSubset = "pid";
51+ ProtectClock = true;
52+ # Breaks if the home dir of the user is in /home
53+ # Also does not add much value in combination with the TemporaryFileSystem.
54+ # ProtectHome = true;
55+ ProtectHostname = true;
56+ # Would re-mount paths ignored by temporary root
57+ #ProtectSystem = "strict";
58+ ProtectControlGroups = true;
59+ ProtectKernelLogs = true;
60+ ProtectKernelModules = true;
61+ ProtectKernelTunables = true;
62+ ProtectProc = "invisible";
63+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
64+ RestrictNamespaces = true;
65+ RestrictRealtime = true;
66+ RestrictSUIDSGID = true;
67+ SystemCallArchitectures = "native";
68+ SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
69+ # Does not work well with the temporary root
70+ #UMask = "0066";
71+ };
72+in
73+{
74+ meta.maintainers = with maintainers; [ earvstedt Flakebi ];
75+
76+ options.services.paperless-ng = {
77+ enable = mkOption {
78+ type = lib.types.bool;
79+ default = false;
80+ description = ''
81+ Enable Paperless-ng.
82+
83+ When started, the Paperless database is automatically created if it doesn't
84+ exist and updated if the Paperless package has changed.
85+ Both tasks are achieved by running a Django migration.
86+
87+ A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
88+ <literal>''${dataDir}/paperless-ng-manage</literal>.
89+ '';
90+ };
91+
92+ dataDir = mkOption {
93+ type = types.str;
94+ default = "/var/lib/paperless";
95+ description = "Directory to store the Paperless data.";
96+ };
97+
98+ mediaDir = mkOption {
99+ type = types.str;
100+ default = "${cfg.dataDir}/media";
101+ defaultText = "\${dataDir}/consume";
102+ description = "Directory to store the Paperless documents.";
103+ };
104+
105+ consumptionDir = mkOption {
106+ type = types.str;
107+ default = "${cfg.dataDir}/consume";
108+ defaultText = "\${dataDir}/consume";
109+ description = "Directory from which new documents are imported.";
110+ };
111+
112+ consumptionDirIsPublic = mkOption {
113+ type = types.bool;
114+ default = false;
115+ description = "Whether all users can write to the consumption dir.";
116+ };
117+
118+ passwordFile = mkOption {
119+ type = types.nullOr types.path;
120+ default = null;
121+ example = "/run/keys/paperless-ng-password";
122+ description = ''
123+ A file containing the superuser password.
124+
125+ A superuser is required to access the web interface.
126+ If unset, you can create a superuser manually by running
127+ <literal>''${dataDir}/paperless-ng-manage createsuperuser</literal>.
128+
129+ The default superuser name is <literal>admin</literal>. To change it, set
130+ option <option>extraConfig.PAPERLESS_ADMIN_USER</option>.
131+ WARNING: When changing the superuser name after the initial setup, the old superuser
132+ will continue to exist.
133+
134+ To disable login for the web interface, set the following:
135+ <literal>extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";</literal>.
136+ WARNING: Only use this on a trusted system without internet access to Paperless.
137+ '';
138+ };
139+
140+ address = mkOption {
141+ type = types.str;
142+ default = "localhost";
143+ description = "Web interface address.";
144+ };
145+
146+ port = mkOption {
147+ type = types.port;
148+ default = 28981;
149+ description = "Web interface port.";
150+ };
151+
152+ extraConfig = mkOption {
153+ type = types.attrs;
154+ default = {};
155+ description = ''
156+ Extra paperless-ng config options.
157+
158+ See <link xlink:href="https://paperless-ng.readthedocs.io/en/latest/configuration.html">the documentation</link>
159+ for available options.
160+ '';
161+ example = literalExample ''
162+ {
163+ PAPERLESS_OCR_LANGUAGE = "deu+eng";
164+ }
165+ '';
166+ };
167+
168+ user = mkOption {
169+ type = types.str;
170+ default = defaultUser;
171+ description = "User under which Paperless runs.";
172+ };
173+
174+ package = mkOption {
175+ type = types.package;
176+ default = pkgs.paperless-ng;
177+ defaultText = "pkgs.paperless-ng";
178+ description = "The Paperless package to use.";
179+ };
180+ };
181+
182+ config = mkIf cfg.enable {
183+ assertions = [
184+ {
185+ assertion = config.services.paperless.enable ->
186+ (config.services.paperless.dataDir != cfg.dataDir && config.services.paperless.port != cfg.port);
187+ message = "Paperless-ng replaces Paperless, either disable Paperless or assign a new dataDir and port to one of them";
188+ }
189+ ];
190+
191+ # Enable redis if no special url is set
192+ services.redis.enable = mkIf (!hasAttr "PAPERLESS_REDIS" env) true;
193+
194+ systemd.tmpfiles.rules = [
195+ "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
196+ "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
197+ (if cfg.consumptionDirIsPublic then
198+ "d '${cfg.consumptionDir}' 777 - - - -"
199+ else
200+ "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
201+ )
202+ ];
203+
204+ systemd.services.paperless-ng-server = {
205+ description = "Paperless document server";
206+ serviceConfig = defaultServiceConfig // {
207+ User = cfg.user;
208+ ExecStart = "${cfg.package}/bin/paperless-ng qcluster";
209+ Restart = "on-failure";
210+ };
211+ environment = env;
212+ wantedBy = [ "multi-user.target" ];
213+ wants = [ "paperless-ng-consumer.service" "paperless-ng-web.service" ];
214+
215+ preStart = ''
216+ ln -sf ${manage} ${cfg.dataDir}/paperless-ng-manage
217+
218+ # Auto-migrate on first run or if the package has changed
219+ versionFile="${cfg.dataDir}/src-version"
220+ if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
221+ ${cfg.package}/bin/paperless-ng migrate
222+ echo ${cfg.package} > "$versionFile"
223+ fi
224+ ''
225+ + optionalString (cfg.passwordFile != null) ''
226+ export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
227+ export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
228+ superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
229+ superuserStateFile="${cfg.dataDir}/superuser-state"
230+
231+ if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
232+ ${cfg.package}/bin/paperless-ng manage_superuser
233+ echo "$superuserState" > "$superuserStateFile"
234+ fi
235+ '';
236+ };
237+
238+ # Password copying can't be implemented as a privileged preStart script
239+ # in 'paperless-ng-server' because 'defaultServiceConfig' limits the filesystem
240+ # paths accessible by the service.
241+ systemd.services.paperless-ng-copy-password = mkIf (cfg.passwordFile != null) {
242+ requiredBy = [ "paperless-ng-server.service" ];
243+ before = [ "paperless-ng-server.service" ];
244+ serviceConfig = {
245+ ExecStart = ''
246+ ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
247+ '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
248+ '';
249+ Type = "oneshot";
250+ };
251+ };
252+
253+ systemd.services.paperless-ng-consumer = {
254+ description = "Paperless document consumer";
255+ serviceConfig = defaultServiceConfig // {
256+ User = cfg.user;
257+ ExecStart = "${cfg.package}/bin/paperless-ng document_consumer";
258+ Restart = "on-failure";
259+ };
260+ environment = env;
261+ # Bind to `paperless-ng-server` so that the consumer never runs
262+ # during migrations
263+ bindsTo = [ "paperless-ng-server.service" ];
264+ after = [ "paperless-ng-server.service" ];
265+ };
266+
267+ systemd.services.paperless-ng-web = {
268+ description = "Paperless web server";
269+ serviceConfig = defaultServiceConfig // {
270+ User = cfg.user;
271+ ExecStart = ''
272+ ${pkgs.python3Packages.gunicorn}/bin/gunicorn \
273+ -c ${cfg.package}/lib/paperless-ng/gunicorn.conf.py paperless.asgi:application
274+ '';
275+ Restart = "on-failure";
276+
277+ AmbientCapabilities = "CAP_NET_BIND_SERVICE";
278+ CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
279+ # gunicorn needs setuid
280+ SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid" ];
281+ };
282+ environment = env // {
283+ PATH = mkForce cfg.package.path;
284+ PYTHONPATH = "${cfg.package.pythonPath}:${cfg.package}/lib/paperless-ng/src";
285+ };
286+ # Bind to `paperless-ng-server` so that the web server never runs
287+ # during migrations
288+ bindsTo = [ "paperless-ng-server.service" ];
289+ after = [ "paperless-ng-server.service" ];
290+ };
291+
292+ users = optionalAttrs (cfg.user == defaultUser) {
293+ users.${defaultUser} = {
294+ group = defaultUser;
295+ uid = config.ids.uids.paperless;
296+ home = cfg.dataDir;
297+ };
298+
299+ groups.${defaultUser} = {
300+ gid = config.ids.gids.paperless;
301+ };
302+ };
303+ };
304+}
nixos/tests/all-tests.nix+1
@@ -335,6 +335,7 @@ in
335335 pam-u2f = handleTest ./pam-u2f.nix {};
336336 pantheon = handleTest ./pantheon.nix {};
337337 paperless = handleTest ./paperless.nix {};
338+ paperless-ng = handleTest ./paperless-ng.nix {};
338339 pdns-recursor = handleTest ./pdns-recursor.nix {};
339340 peerflix = handleTest ./peerflix.nix {};
340341 pgjwt = handleTest ./pgjwt.nix {};
nixos/tests/paperless-ng.nixnew file+36
@@ -0,0 +1,36 @@
1+import ./make-test-python.nix ({ lib, ... }: {
2+ name = "paperless-ng";
3+ meta.maintainers = with lib.maintainers; [ earvstedt Flakebi ];
4+
5+ nodes.machine = { pkgs, ... }: {
6+ environment.systemPackages = with pkgs; [ imagemagick jq ];
7+ services.paperless-ng = {
8+ enable = true;
9+ passwordFile = builtins.toFile "password" "admin";
10+ };
11+ virtualisation.memorySize = 1024;
12+ };
13+
14+ testScript = ''
15+ machine.wait_for_unit("paperless-ng-consumer.service")
16+
17+ with subtest("Create test doc"):
18+ machine.succeed(
19+ "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
20+ "-annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png"
21+ )
22+
23+ with subtest("Web interface gets ready"):
24+ machine.wait_for_unit("paperless-ng-web.service")
25+ # Wait until server accepts connections
26+ machine.wait_until_succeeds("curl -fs localhost:28981")
27+
28+ with subtest("Document is consumed"):
29+ machine.wait_until_succeeds(
30+ "(($(curl -u admin:admin -fs localhost:28981/api/documents/ | jq .count) == 1))"
31+ )
32+ assert "2005-10-16" in machine.succeed(
33+ "curl -u admin:admin -fs localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
34+ )
35+ '';
36+})
pkgs/applications/office/paperless-ng/default.nixnew file+197
@@ -0,0 +1,197 @@
1+{ lib
2+, fetchurl
3+, nixosTests
4+, python3
5+, ghostscript
6+, imagemagick
7+, jbig2enc
8+, ocrmypdf
9+, optipng
10+, pngquant
11+, qpdf
12+, tesseract4
13+, unpaper
14+, liberation_ttf
15+}:
16+
17+let
18+ py = python3.override {
19+ packageOverrides = self: super: {
20+ django = super.django_3;
21+ django-picklefield = super.django-picklefield.overrideAttrs (oldAttrs: {
22+ # Checks do not pass with django 3
23+ doInstallCheck = false;
24+ });
25+ # Avoid warning in django-q versions > 1.3.4
26+ # https://github.com/jonaswinkler/paperless-ng/issues/857
27+ # https://github.com/Koed00/django-q/issues/526
28+ django-q = super.django-q.overridePythonAttrs (oldAttrs: rec {
29+ version = "1.3.4";
30+ src = super.fetchPypi {
31+ inherit (oldAttrs) pname;
32+ inherit version;
33+ sha256 = "Uj1U3PG2YVLBtlj5FPAO07UYo0MqnezUiYc4yo274Q8=";
34+ };
35+ });
36+ };
37+ };
38+
39+ path = lib.makeBinPath [ ghostscript imagemagick jbig2enc optipng pngquant qpdf tesseract4 unpaper ];
40+in
41+py.pkgs.pythonPackages.buildPythonApplication rec {
42+ pname = "paperless-ng";
43+ version = "1.4.5";
44+
45+ src = fetchurl {
46+ url = "https://github.com/jonaswinkler/paperless-ng/releases/download/ng-${version}/${pname}-${version}.tar.xz";
47+ sha256 = "2PJb8j3oimlfiJ3gqjK6uTemzFdtAP2Mlm5RH09bx/E=";
48+ };
49+
50+ format = "other";
51+
52+ # Make bind address configurable
53+ # Fix tests with Pillow 8.3.1: https://github.com/jonaswinkler/paperless-ng/pull/1183
54+ prePatch = ''
55+ substituteInPlace gunicorn.conf.py --replace "bind = '0.0.0.0:8000'" ""
56+ substituteInPlace src/paperless_tesseract/parsers.py --replace "return x" "return round(x)"
57+ '';
58+
59+ propagatedBuildInputs = with py.pkgs.pythonPackages; [
60+ aioredis
61+ arrow
62+ asgiref
63+ async-timeout
64+ attrs
65+ autobahn
66+ automat
67+ blessed
68+ certifi
69+ cffi
70+ channels-redis
71+ channels
72+ chardet
73+ click
74+ coloredlogs
75+ concurrent-log-handler
76+ constantly
77+ cryptography
78+ daphne
79+ dateparser
80+ django-cors-headers
81+ django_extensions
82+ django-filter
83+ django-picklefield
84+ django-q
85+ django
86+ djangorestframework
87+ filelock
88+ fuzzywuzzy
89+ gunicorn
90+ h11
91+ hiredis
92+ httptools
93+ humanfriendly
94+ hyperlink
95+ idna
96+ imap-tools
97+ img2pdf
98+ incremental
99+ inotify-simple
100+ inotifyrecursive
101+ joblib
102+ langdetect
103+ lxml
104+ msgpack
105+ numpy
106+ ocrmypdf
107+ pathvalidate
108+ pdfminer
109+ pikepdf
110+ pillow
111+ pluggy
112+ portalocker
113+ psycopg2
114+ pyasn1-modules
115+ pyasn1
116+ pycparser
117+ pyopenssl
118+ python-dateutil
119+ python-dotenv
120+ python-gnupg
121+ python-Levenshtein
122+ python_magic
123+ pytz
124+ pyyaml
125+ redis
126+ regex
127+ reportlab
128+ requests
129+ scikit-learn
130+ scipy
131+ service-identity
132+ six
133+ sortedcontainers
134+ sqlparse
135+ threadpoolctl
136+ tika
137+ tqdm
138+ twisted.extras.tls
139+ txaio
140+ tzlocal
141+ urllib3
142+ uvicorn
143+ uvloop
144+ watchdog
145+ watchgod
146+ wcwidth
147+ websockets
148+ whitenoise
149+ whoosh
150+ zope_interface
151+ ];
152+
153+ doCheck = true;
154+ checkInputs = with py.pkgs.pythonPackages; [
155+ pytest
156+ pytest-cov
157+ pytest-django
158+ pytest-env
159+ pytest-sugar
160+ pytest-xdist
161+ factory_boy
162+ ];
163+
164+ # The tests require:
165+ # - PATH with runtime binaries
166+ # - A temporary HOME directory for gnupg
167+ # - XDG_DATA_DIRS with test-specific fonts
168+ checkPhase = ''
169+ pushd src
170+ PATH="${path}:$PATH" HOME=$(mktemp -d) XDG_DATA_DIRS="${liberation_ttf}/share:$XDG_DATA_DIRS" pytest
171+ popd
172+ '';
173+
174+ installPhase = ''
175+ mkdir -p $out/lib
176+ cp -r . $out/lib/paperless-ng
177+ chmod +x $out/lib/paperless-ng/src/manage.py
178+ makeWrapper $out/lib/paperless-ng/src/manage.py $out/bin/paperless-ng \
179+ --prefix PYTHONPATH : "$PYTHONPATH" \
180+ --prefix PATH : "${path}"
181+ '';
182+
183+ passthru = {
184+ # PYTHONPATH of all dependencies used by the package
185+ pythonPath = python3.pkgs.makePythonPath propagatedBuildInputs;
186+ inherit path;
187+
188+ tests = { inherit (nixosTests) paperless-ng; };
189+ };
190+
191+ meta = with lib; {
192+ description = "A supercharged version of paperless: scan, index, and archive all of your physical documents";
193+ homepage = "https://paperless-ng.readthedocs.io/en/latest/";
194+ license = licenses.gpl3Only;
195+ maintainers = with maintainers; [ earvstedt Flakebi ];
196+ };
197+}
pkgs/top-level/all-packages.nix+2
@@ -7891,6 +7891,8 @@ with pkgs;
78917891
78927892 paperless = callPackage ../applications/office/paperless { };
78937893
7894+ paperless-ng = callPackage ../applications/office/paperless-ng { };
7895+
78947896 paperwork = callPackage ../applications/office/paperwork/paperwork-gtk.nix { };
78957897
78967898 papertrail = callPackage ../tools/text/papertrail { };