fluffychat: 1.22.1 -> 1.23.0 (#364091)
[NixPkgs.git] / nixos / modules / services / misc / paperless.nix
blobb1401b1b470daf4c63942946efb3faa40d57fef9
1 { config, pkgs, lib, ... }:
2 let
3   cfg = config.services.paperless;
5   defaultUser = "paperless";
6   defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf";
8   # Don't start a redis instance if the user sets a custom redis connection
9   enableRedis = !(cfg.settings ? PAPERLESS_REDIS);
10   redisServer = config.services.redis.servers.paperless;
12   env = {
13     PAPERLESS_DATA_DIR = cfg.dataDir;
14     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
15     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
16     PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont;
17     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
18   } // lib.optionalAttrs (config.time.timeZone != null) {
19     PAPERLESS_TIME_ZONE = config.time.timeZone;
20   } // lib.optionalAttrs enableRedis {
21     PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
22   } // lib.optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) {
23     PAPERLESS_NLTK_DIR = pkgs.symlinkJoin {
24       name = "paperless_ngx_nltk_data";
25       paths = cfg.package.nltkData;
26     };
27   } // lib.optionalAttrs (cfg.openMPThreadingWorkaround) {
28     OMP_NUM_THREADS = "1";
29   } // (lib.mapAttrs (_: s:
30     if (lib.isAttrs s || lib.isList s) then builtins.toJSON s
31     else if lib.isBool s then lib.boolToString s
32     else toString s
33   ) cfg.settings);
35   manage = pkgs.writeShellScript "manage" ''
36     set -o allexport # Export the following env vars
37     ${lib.toShellVars env}
38     ${lib.optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"}
39     exec ${cfg.package}/bin/paperless-ngx "$@"
40   '';
42   defaultServiceConfig = {
43     Slice = "system-paperless.slice";
44     # Secure the services
45     ReadWritePaths = [
46       cfg.consumptionDir
47       cfg.dataDir
48       cfg.mediaDir
49     ];
50     CacheDirectory = "paperless";
51     CapabilityBoundingSet = "";
52     # ProtectClock adds DeviceAllow=char-rtc r
53     DeviceAllow = "";
54     EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
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 = lib.optional enableRedis redisServer.user;
82     SystemCallArchitectures = "native";
83     SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
84     UMask = "0066";
85   };
88   meta.maintainers = with lib.maintainers; [ leona SuperSandro2000 erikarvstedt ];
90   imports = [
91     (lib.mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
92     (lib.mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ])
93   ];
95   options.services.paperless = {
96     enable = lib.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 = lib.mkOption {
112       type = lib.types.str;
113       default = "/var/lib/paperless";
114       description = "Directory to store the Paperless data.";
115     };
117     mediaDir = lib.mkOption {
118       type = lib.types.str;
119       default = "${cfg.dataDir}/media";
120       defaultText = lib.literalExpression ''"''${dataDir}/media"'';
121       description = "Directory to store the Paperless documents.";
122     };
124     consumptionDir = lib.mkOption {
125       type = lib.types.str;
126       default = "${cfg.dataDir}/consume";
127       defaultText = lib.literalExpression ''"''${dataDir}/consume"'';
128       description = "Directory from which new documents are imported.";
129     };
131     consumptionDirIsPublic = lib.mkOption {
132       type = lib.types.bool;
133       default = false;
134       description = "Whether all users can write to the consumption dir.";
135     };
137     passwordFile = lib.mkOption {
138       type = lib.types.nullOr lib.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 = lib.mkOption {
160       type = lib.types.str;
161       default = "localhost";
162       description = "Web interface address.";
163     };
165     port = lib.mkOption {
166       type = lib.types.port;
167       default = 28981;
168       description = "Web interface port.";
169     };
171     settings = lib.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_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ];
189         PAPERLESS_OCR_USER_ARGS = {
190           optimize = 1;
191           pdfa_image_compression = "lossless";
192         };
193       };
194     };
196     user = lib.mkOption {
197       type = lib.types.str;
198       default = defaultUser;
199       description = "User under which Paperless runs.";
200     };
202     package = lib.mkPackageOption pkgs "paperless-ngx" { } // {
203       apply = pkg: pkg.override {
204         tesseract5 = pkg.tesseract5.override {
205           # always enable detection modules
206           # tesseract fails to build when eng is not present
207           enableLanguages = if cfg.settings ? PAPERLESS_OCR_LANGUAGE then
208             lib.lists.unique (
209               [ "equ" "osd" "eng" ]
210               ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE
211             )
212           else null;
213         };
214       };
215     };
217     openMPThreadingWorkaround = lib.mkEnableOption ''
218       a workaround for document classifier timeouts.
220       Paperless uses OpenBLAS via scikit-learn for document classification.
222       The default is to use threading for OpenMP but this would cause the
223       document classifier to spin on one core seemingly indefinitely if there
224       are large amounts of classes per classification; causing it to
225       effectively never complete due to running into timeouts.
227       This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See
228       https://github.com/NixOS/nixpkgs/issues/240591 for more information
229     '' // lib.mkOption { default = true; };
231     environmentFile = lib.mkOption {
232       type = lib.types.nullOr lib.types.path;
233       default = null;
234       example = "/run/secrets/paperless";
235       description = ''
236         Path to a file containing extra paperless config options in the systemd `EnvironmentFile`
237         format. Refer to the [documentation](https://docs.paperless-ngx.com/configuration/) for
238         config options.
240         This can be used to pass secrets to paperless without putting them in the Nix store.
242         To set a database password, point `environmentFile` at a file containing:
243         ```
244         PAPERLESS_DBPASS=<pass>
245         ```
246       '';
247     };
249     database = {
250       createLocally = lib.mkOption {
251         type = lib.types.bool;
252         default = false;
253         description = ''
254           Configure local PostgreSQL database server for Paperless.
255         '';
256       };
257     };
258   };
260   config = lib.mkIf cfg.enable {
261     services.redis.servers.paperless.enable = lib.mkIf enableRedis true;
263     services.postgresql = lib.mkIf cfg.database.createLocally {
264       enable = true;
265       ensureDatabases = [ "paperless" ];
266       ensureUsers = [{
267         name = config.services.paperless.user;
268         ensureDBOwnership = true;
269       }];
270     };
272     services.paperless.settings = lib.mkIf cfg.database.createLocally {
273       PAPERLESS_DBENGINE = "postgresql";
274       PAPERLESS_DBHOST = "/run/postgresql";
275       PAPERLESS_DBNAME = "paperless";
276       PAPERLESS_DBUSER = "paperless";
277     };
279     systemd.slices.system-paperless = {
280       description = "Paperless Document Management System Slice";
281       documentation = [ "https://docs.paperless-ngx.com" ];
282     };
284     systemd.tmpfiles.settings."10-paperless" = let
285       defaultRule = {
286         inherit (cfg) user;
287         inherit (config.users.users.${cfg.user}) group;
288       };
289     in {
290       "${cfg.dataDir}".d = defaultRule;
291       "${cfg.mediaDir}".d = defaultRule;
292       "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule;
293     };
295     systemd.services.paperless-scheduler = {
296       description = "Paperless Celery Beat";
297       wantedBy = [ "multi-user.target" ];
298       wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
299       serviceConfig = defaultServiceConfig // {
300         User = cfg.user;
301         ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO";
302         Restart = "on-failure";
303         LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}";
304       };
305       environment = env;
307       preStart = ''
308         ln -sf ${manage} ${cfg.dataDir}/paperless-manage
310         # Auto-migrate on first run or if the package has changed
311         versionFile="${cfg.dataDir}/src-version"
312         version=$(cat "$versionFile" 2>/dev/null || echo 0)
314         if [[ $version != ${cfg.package.version} ]]; then
315           ${cfg.package}/bin/paperless-ngx migrate
317           # Parse old version string format for backwards compatibility
318           version=$(echo "$version" | grep -ohP '[^-]+$')
320           versionLessThan() {
321             target=$1
322             [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
323           }
325           if versionLessThan 1.12.0; then
326             # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
327             echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
328             ${cfg.package}/bin/paperless-ngx document_index reindex
329           fi
331           echo ${cfg.package.version} > "$versionFile"
332         fi
333       ''
334       + lib.optionalString (cfg.passwordFile != null) ''
335         export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
336         PAPERLESS_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD")
337         export PAPERLESS_ADMIN_PASSWORD
338         superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
339         superuserStateFile="${cfg.dataDir}/superuser-state"
341         if [[ $(cat "$superuserStateFile" 2>/dev/null) != "$superuserState" ]]; then
342           ${cfg.package}/bin/paperless-ngx manage_superuser
343           echo "$superuserState" > "$superuserStateFile"
344         fi
345       '';
346       requires = lib.optional cfg.database.createLocally "postgresql.service";
347       after = lib.optional enableRedis "redis-paperless.service"
348         ++ lib.optional cfg.database.createLocally "postgresql.service";
349     };
351     systemd.services.paperless-task-queue = {
352       description = "Paperless Celery Workers";
353       requires = lib.optional cfg.database.createLocally "postgresql.service";
354       after = [ "paperless-scheduler.service" ]
355         ++ lib.optional cfg.database.createLocally "postgresql.service";
356       serviceConfig = defaultServiceConfig // {
357         User = cfg.user;
358         ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO";
359         Restart = "on-failure";
360         # The `mbind` syscall is needed for running the classifier.
361         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
362         # Needs to talk to mail server for automated import rules
363         PrivateNetwork = false;
364       };
365       environment = env;
366     };
368     systemd.services.paperless-consumer = {
369       description = "Paperless document consumer";
370       # Bind to `paperless-scheduler` so that the consumer never runs
371       # during migrations
372       bindsTo = [ "paperless-scheduler.service" ];
373       requires = lib.optional cfg.database.createLocally "postgresql.service";
374       after = [ "paperless-scheduler.service" ]
375         ++ lib.optional cfg.database.createLocally "postgresql.service";
376       serviceConfig = defaultServiceConfig // {
377         User = cfg.user;
378         ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer";
379         Restart = "on-failure";
380       };
381       environment = env;
382       # Allow the consumer to access the private /tmp directory of the server.
383       # This is required to support consuming files via a local folder.
384       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
385     };
387     systemd.services.paperless-web = {
388       description = "Paperless web server";
389       # Bind to `paperless-scheduler` so that the web server never runs
390       # during migrations
391       bindsTo = [ "paperless-scheduler.service" ];
392       requires = lib.optional cfg.database.createLocally "postgresql.service";
393       after = [ "paperless-scheduler.service" ]
394         ++ lib.optional cfg.database.createLocally "postgresql.service";
395       # Setup PAPERLESS_SECRET_KEY.
396       # If this environment variable is left unset, paperless-ngx defaults
397       # to a well-known value, which is insecure.
398       script = let
399         secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key";
400       in ''
401         if [[ ! -f '${secretKeyFile}' ]]; then
402           (
403             umask 0377
404             tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}'
405           )
406         fi
407         PAPERLESS_SECRET_KEY="$(cat '${secretKeyFile}')"
408         export PAPERLESS_SECRET_KEY
409         if [[ ! $PAPERLESS_SECRET_KEY ]]; then
410           echo "PAPERLESS_SECRET_KEY is empty, refusing to start."
411           exit 1
412         fi
413         exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \
414           -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
415       '';
416       serviceConfig = defaultServiceConfig // {
417         User = cfg.user;
418         Restart = "on-failure";
420         LimitNOFILE = 65536;
421         # gunicorn needs setuid, liblapack needs mbind
422         SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
423         # Needs to serve web page
424         PrivateNetwork = false;
425       };
426       environment = env // {
427         PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src";
428       };
429       # Allow the web interface to access the private /tmp directory of the server.
430       # This is required to support uploading files via the web interface.
431       unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
432     };
434     users = lib.optionalAttrs (cfg.user == defaultUser) {
435       users.${defaultUser} = {
436         group = defaultUser;
437         uid = config.ids.uids.paperless;
438         home = cfg.dataDir;
439       };
441       groups.${defaultUser} = {
442         gid = config.ids.gids.paperless;
443       };
444     };
445   };