base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / modules / services / misc / paperless.nix
blob70cf4f9ff6c6c55932b904e63b454dcff55667a7
1 { config, pkgs, lib, ... }:
3 with lib;
4 let
5   cfg = config.services.paperless;
7   defaultUser = "paperless";
8   defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
10   # Don't start a redis instance if the user sets a custom redis connection
11   enableRedis = !(cfg.settings ? PAPERLESS_REDIS);
12   redisServer = config.services.redis.servers.paperless;
14   env = {
15     PAPERLESS_DATA_DIR = cfg.dataDir;
16     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
17     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
18     PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
19     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
20   } // optionalAttrs (config.time.timeZone != null) {
21     PAPERLESS_TIME_ZONE = config.time.timeZone;
22   } // optionalAttrs enableRedis {
23     PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
24   } // optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) {
25     PAPERLESS_NLTK_DIR = pkgs.symlinkJoin {
26       name = "paperless_ngx_nltk_data";
27       paths = cfg.package.nltkData;
28     };
29   } // optionalAttrs (cfg.openMPThreadingWorkaround) {
30     OMP_NUM_THREADS = "1";
31   } // (lib.mapAttrs (_: s:
32     if (lib.isAttrs s || lib.isList s) then builtins.toJSON s
33     else if lib.isBool s then lib.boolToString s
34     else toString s
35   ) cfg.settings);
37   manage = pkgs.writeShellScript "manage" ''
38     set -o allexport # Export the following env vars
39     ${lib.toShellVars env}
40     exec ${cfg.package}/bin/paperless-ngx "$@"
41   '';
43   defaultServiceConfig = {
44     Slice = "system-paperless.slice";
45     # Secure the services
46     ReadWritePaths = [
47       cfg.consumptionDir
48       cfg.dataDir
49       cfg.mediaDir
50     ];
51     CacheDirectory = "paperless";
52     CapabilityBoundingSet = "";
53     # ProtectClock adds DeviceAllow=char-rtc r
54     DeviceAllow = "";
55     LockPersonality = true;
56     MemoryDenyWriteExecute = true;
57     NoNewPrivileges = true;
58     PrivateDevices = true;
59     PrivateMounts = true;
60     PrivateNetwork = true;
61     PrivateTmp = true;
62     PrivateUsers = true;
63     ProtectClock = true;
64     # Breaks if the home dir of the user is in /home
65     # ProtectHome = true;
66     ProtectHostname = true;
67     ProtectSystem = "strict";
68     ProtectControlGroups = true;
69     ProtectKernelLogs = true;
70     ProtectKernelModules = true;
71     ProtectKernelTunables = true;
72     ProtectProc = "invisible";
73     # Don't restrict ProcSubset because django-q requires read access to /proc/stat
74     # to query CPU and memory information.
75     # Note that /proc only contains processes of user `paperless`, so this is safe.
76     # ProcSubset = "pid";
77     RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
78     RestrictNamespaces = true;
79     RestrictRealtime = true;
80     RestrictSUIDSGID = true;
81     SupplementaryGroups = optional enableRedis redisServer.user;
82     SystemCallArchitectures = "native";
83     SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
84     UMask = "0066";
85   };
88   meta.maintainers = with maintainers; [ leona SuperSandro2000 erikarvstedt ];
90   imports = [
91     (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
92     (mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ])
93   ];
95   options.services.paperless = {
96     enable = mkOption {
97       type = lib.types.bool;
98       default = false;
99       description = ''
100         Enable Paperless.
102         When started, the Paperless database is automatically created if it doesn't
103         exist and updated if the Paperless package has changed.
104         Both tasks are achieved by running a Django migration.
106         A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
107         `''${dataDir}/paperless-manage`.
108       '';
109     };
111     dataDir = mkOption {
112       type = types.str;
113       default = "/var/lib/paperless";
114       description = "Directory to store the Paperless data.";
115     };
117     mediaDir = mkOption {
118       type = types.str;
119       default = "${cfg.dataDir}/media";
120       defaultText = literalExpression ''"''${dataDir}/media"'';
121       description = "Directory to store the Paperless documents.";
122     };
124     consumptionDir = mkOption {
125       type = types.str;
126       default = "${cfg.dataDir}/consume";
127       defaultText = literalExpression ''"''${dataDir}/consume"'';
128       description = "Directory from which new documents are imported.";
129     };
131     consumptionDirIsPublic = mkOption {
132       type = types.bool;
133       default = false;
134       description = "Whether all users can write to the consumption dir.";
135     };
137     passwordFile = mkOption {
138       type = types.nullOr types.path;
139       default = null;
140       example = "/run/keys/paperless-password";
141       description = ''
142         A file containing the superuser password.
144         A superuser is required to access the web interface.
145         If unset, you can create a superuser manually by running
146         `''${dataDir}/paperless-manage createsuperuser`.
148         The default superuser name is `admin`. To change it, set
149         option {option}`settings.PAPERLESS_ADMIN_USER`.
150         WARNING: When changing the superuser name after the initial setup, the old superuser
151         will continue to exist.
153         To disable login for the web interface, set the following:
154         `settings.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`.
155         WARNING: Only use this on a trusted system without internet access to Paperless.
156       '';
157     };
159     address = mkOption {
160       type = types.str;
161       default = "localhost";
162       description = "Web interface address.";
163     };
165     port = mkOption {
166       type = types.port;
167       default = 28981;
168       description = "Web interface port.";
169     };
171     settings = mkOption {
172       type = lib.types.submodule {
173         freeformType = with lib.types; attrsOf (let
174           typeList = [ bool float int str path package ];
175         in oneOf (typeList ++ [ (listOf (oneOf typeList)) (attrsOf (oneOf typeList)) ]));
176       };
177       default = { };
178       description = ''
179         Extra paperless config options.
181         See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options.
183         Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values.
184         Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience.
185       '';
186       example = {
187         PAPERLESS_OCR_LANGUAGE = "deu+eng";
188         PAPERLESS_DBHOST = "/run/postgresql";
189         PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ];
190         PAPERLESS_OCR_USER_ARGS = {
191           optimize = 1;
192           pdfa_image_compression = "lossless";
193         };
194       };
195     };
197     user = mkOption {
198       type = types.str;
199       default = defaultUser;
200       description = "User under which Paperless runs.";
201     };
203     package = mkPackageOption pkgs "paperless-ngx" { } // {
204       apply = pkg: pkg.override {
205         tesseract5 = pkg.tesseract5.override {
206           # always enable detection modules
207           # tesseract fails to build when eng is not present
208           enableLanguages = if cfg.settings ? PAPERLESS_OCR_LANGUAGE then
209             lists.unique (
210               [ "equ" "osd" "eng" ]
211               ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE
212             )
213           else null;
214         };
215       };
216     };
218     openMPThreadingWorkaround = mkEnableOption ''
219       a workaround for document classifier timeouts.
221       Paperless uses OpenBLAS via scikit-learn for document classification.
223       The default is to use threading for OpenMP but this would cause the
224       document classifier to spin on one core seemingly indefinitely if there
225       are large amounts of classes per classification; causing it to
226       effectively never complete due to running into timeouts.
228       This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See
229       https://github.com/NixOS/nixpkgs/issues/240591 for more information
230     '' // mkOption { default = true; };
231   };
233   config = mkIf cfg.enable {
234     services.redis.servers.paperless.enable = mkIf enableRedis true;
236     systemd.slices.system-paperless = {
237       description = "Paperless Document Management System Slice";
238       documentation = [ "https://docs.paperless-ngx.com" ];
239     };
241     systemd.tmpfiles.settings."10-paperless" = let
242       defaultRule = {
243         inherit (cfg) user;
244         inherit (config.users.users.${cfg.user}) group;
245       };
246     in {
247       "${cfg.dataDir}".d = defaultRule;
248       "${cfg.mediaDir}".d = defaultRule;
249       "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
250     };
252     systemd.services.paperless-scheduler = {
253       description = "Paperless Celery Beat";
254       wantedBy = [ "multi-user.target" ];
255       wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
256       serviceConfig = defaultServiceConfig // {
257         User = cfg.user;
258         ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO";
259         Restart = "on-failure";
260         LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
261       };
262       environment = env;
264       preStart = ''
265         ln -sf ${manage} ${cfg.dataDir}/paperless-manage
267         # Auto-migrate on first run or if the package has changed
268         versionFile="${cfg.dataDir}/src-version"
269         version=$(cat "$versionFile" 2>/dev/null || echo 0)
271         if [[ $version != ${cfg.package.version} ]]; then
272           ${cfg.package}/bin/paperless-ngx migrate
274           # Parse old version string format for backwards compatibility
275           version=$(echo "$version" | grep -ohP '[^-]+$')
277           versionLessThan() {
278             target=$1
279             [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
280           }
282           if versionLessThan 1.12.0; then
283             # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
284             echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
285             ${cfg.package}/bin/paperless-ngx document_index reindex
286           fi
288           echo ${cfg.package.version} > "$versionFile"
289         fi
290       ''
291       + optionalString (cfg.passwordFile != null) ''
292         export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
293         PAPERLESS_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD")
294         export PAPERLESS_ADMIN_PASSWORD
295         superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
296         superuserStateFile="${cfg.dataDir}/superuser-state"
298         if [[ $(cat "$superuserStateFile" 2>/dev/null) != "$superuserState" ]]; then
299           ${cfg.package}/bin/paperless-ngx manage_superuser
300           echo "$superuserState" > "$superuserStateFile"
301         fi
302       '';
303     } // optionalAttrs enableRedis {
304       after = [ "redis-paperless.service" ];
305     };
307     systemd.services.paperless-task-queue = {
308       description = "Paperless Celery Workers";
309       after = [ "paperless-scheduler.service" ];
310       serviceConfig = defaultServiceConfig // {
311         User = cfg.user;
312         ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO";
313         Restart = "on-failure";
314         # The `mbind` syscall is needed for running the classifier.
315         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
316         # Needs to talk to mail server for automated import rules
317         PrivateNetwork = false;
318       };
319       environment = env;
320     };
322     systemd.services.paperless-consumer = {
323       description = "Paperless document consumer";
324       # Bind to `paperless-scheduler` so that the consumer never runs
325       # during migrations
326       bindsTo = [ "paperless-scheduler.service" ];
327       after = [ "paperless-scheduler.service" ];
328       serviceConfig = defaultServiceConfig // {
329         User = cfg.user;
330         ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
331         Restart = "on-failure";
332       };
333       environment = env;
334       # Allow the consumer to access the private /tmp directory of the server.
335       # This is required to support consuming files via a local folder.
336       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
337     };
339     systemd.services.paperless-web = {
340       description = "Paperless web server";
341       # Bind to `paperless-scheduler` so that the web server never runs
342       # during migrations
343       bindsTo = [ "paperless-scheduler.service" ];
344       after = [ "paperless-scheduler.service" ];
345       # Setup PAPERLESS_SECRET_KEY.
346       # If this environment variable is left unset, paperless-ngx defaults
347       # to a well-known value, which is insecure.
348       script = let
349         secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
350       in ''
351         if [[ ! -f '${secretKeyFile}' ]]; then
352           (
353             umask 0377
354             tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
355           )
356         fi
357         PAPERLESS_SECRET_KEY="$(cat '${secretKeyFile}')"
358         export PAPERLESS_SECRET_KEY
359         if [[ ! $PAPERLESS_SECRET_KEY ]]; then
360           echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
361           exit 1
362         fi
363         exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \
364           -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
365       '';
366       serviceConfig = defaultServiceConfig // {
367         User = cfg.user;
368         Restart = "on-failure";
370         LimitNOFILE = 65536;
371         # gunicorn needs setuid, liblapack needs mbind
372         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
373         # Needs to serve web page
374         PrivateNetwork = false;
375       };
376       environment = env // {
377         PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src";
378       };
379       # Allow the web interface to access the private /tmp directory of the server.
380       # This is required to support uploading files via the web interface.
381       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
382     };
384     users = optionalAttrs (cfg.user == defaultUser) {
385       users.${defaultUser} = {
386         group = defaultUser;
387         uid = config.ids.uids.paperless;
388         home = cfg.dataDir;
389       };
391       groups.${defaultUser} = {
392         gid = config.ids.gids.paperless;
393       };
394     };
395   };