at 24.11-pre 14 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4let 5 cfg = config.services.paperless; 6 7 defaultUser = "paperless"; 8 defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf"; 9 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; 13 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); 36 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 ''; 42 43 # Secure the services 44 defaultServiceConfig = { 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 LockPersonality = true; 55 MemoryDenyWriteExecute = true; 56 NoNewPrivileges = true; 57 PrivateDevices = true; 58 PrivateMounts = true; 59 PrivateNetwork = true; 60 PrivateTmp = true; 61 PrivateUsers = true; 62 ProtectClock = true; 63 # Breaks if the home dir of the user is in /home 64 # ProtectHome = true; 65 ProtectHostname = true; 66 ProtectSystem = "strict"; 67 ProtectControlGroups = true; 68 ProtectKernelLogs = true; 69 ProtectKernelModules = true; 70 ProtectKernelTunables = true; 71 ProtectProc = "invisible"; 72 # Don't restrict ProcSubset because django-q requires read access to /proc/stat 73 # to query CPU and memory information. 74 # Note that /proc only contains processes of user `paperless`, so this is safe. 75 # ProcSubset = "pid"; 76 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 77 RestrictNamespaces = true; 78 RestrictRealtime = true; 79 RestrictSUIDSGID = true; 80 SupplementaryGroups = optional enableRedis redisServer.user; 81 SystemCallArchitectures = "native"; 82 SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ]; 83 UMask = "0066"; 84 }; 85in 86{ 87 meta.maintainers = with maintainers; [ erikarvstedt Flakebi leona ]; 88 89 imports = [ 90 (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ]) 91 (mkRenamedOptionModule [ "services" "paperless" "extraConfig" ] [ "services" "paperless" "settings" ]) 92 ]; 93 94 options.services.paperless = { 95 enable = mkOption { 96 type = lib.types.bool; 97 default = false; 98 description = '' 99 Enable Paperless. 100 101 When started, the Paperless database is automatically created if it doesn't 102 exist and updated if the Paperless package has changed. 103 Both tasks are achieved by running a Django migration. 104 105 A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to 106 `''${dataDir}/paperless-manage`. 107 ''; 108 }; 109 110 dataDir = mkOption { 111 type = types.str; 112 default = "/var/lib/paperless"; 113 description = "Directory to store the Paperless data."; 114 }; 115 116 mediaDir = mkOption { 117 type = types.str; 118 default = "${cfg.dataDir}/media"; 119 defaultText = literalExpression ''"''${dataDir}/media"''; 120 description = "Directory to store the Paperless documents."; 121 }; 122 123 consumptionDir = mkOption { 124 type = types.str; 125 default = "${cfg.dataDir}/consume"; 126 defaultText = literalExpression ''"''${dataDir}/consume"''; 127 description = "Directory from which new documents are imported."; 128 }; 129 130 consumptionDirIsPublic = mkOption { 131 type = types.bool; 132 default = false; 133 description = "Whether all users can write to the consumption dir."; 134 }; 135 136 passwordFile = mkOption { 137 type = types.nullOr types.path; 138 default = null; 139 example = "/run/keys/paperless-password"; 140 description = '' 141 A file containing the superuser password. 142 143 A superuser is required to access the web interface. 144 If unset, you can create a superuser manually by running 145 `''${dataDir}/paperless-manage createsuperuser`. 146 147 The default superuser name is `admin`. To change it, set 148 option {option}`settings.PAPERLESS_ADMIN_USER`. 149 WARNING: When changing the superuser name after the initial setup, the old superuser 150 will continue to exist. 151 152 To disable login for the web interface, set the following: 153 `settings.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`. 154 WARNING: Only use this on a trusted system without internet access to Paperless. 155 ''; 156 }; 157 158 address = mkOption { 159 type = types.str; 160 default = "localhost"; 161 description = "Web interface address."; 162 }; 163 164 port = mkOption { 165 type = types.port; 166 default = 28981; 167 description = "Web interface port."; 168 }; 169 170 settings = mkOption { 171 type = lib.types.submodule { 172 freeformType = with lib.types; attrsOf (let 173 typeList = [ bool float int str path package ]; 174 in oneOf (typeList ++ [ (listOf (oneOf typeList)) (attrsOf (oneOf typeList)) ])); 175 }; 176 default = { }; 177 description = '' 178 Extra paperless config options. 179 180 See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options. 181 182 Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values. 183 Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience. 184 ''; 185 example = { 186 PAPERLESS_OCR_LANGUAGE = "deu+eng"; 187 PAPERLESS_DBHOST = "/run/postgresql"; 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 }; 195 196 user = mkOption { 197 type = types.str; 198 default = defaultUser; 199 description = "User under which Paperless runs."; 200 }; 201 202 package = 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 lists.unique ( 209 [ "equ" "osd" "eng" ] 210 ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE 211 ) 212 else null; 213 }; 214 }; 215 }; 216 217 openMPThreadingWorkaround = mkEnableOption '' 218 a workaround for document classifier timeouts. 219 220 Paperless uses OpenBLAS via scikit-learn for document classification. 221 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. 226 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 '' // mkOption { default = true; }; 230 }; 231 232 config = mkIf cfg.enable { 233 services.redis.servers.paperless.enable = mkIf enableRedis true; 234 235 systemd.tmpfiles.settings."10-paperless" = let 236 defaultRule = { 237 inherit (cfg) user; 238 inherit (config.users.users.${cfg.user}) group; 239 }; 240 in { 241 "${cfg.dataDir}".d = defaultRule; 242 "${cfg.mediaDir}".d = defaultRule; 243 "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule; 244 }; 245 246 systemd.services.paperless-scheduler = { 247 description = "Paperless Celery Beat"; 248 wantedBy = [ "multi-user.target" ]; 249 wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ]; 250 serviceConfig = defaultServiceConfig // { 251 User = cfg.user; 252 ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO"; 253 Restart = "on-failure"; 254 LoadCredential = lib.optionalString (cfg.passwordFile != null) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}"; 255 }; 256 environment = env; 257 258 preStart = '' 259 ln -sf ${manage} ${cfg.dataDir}/paperless-manage 260 261 # Auto-migrate on first run or if the package has changed 262 versionFile="${cfg.dataDir}/src-version" 263 version=$(cat "$versionFile" 2>/dev/null || echo 0) 264 265 if [[ $version != ${cfg.package.version} ]]; then 266 ${cfg.package}/bin/paperless-ngx migrate 267 268 # Parse old version string format for backwards compatibility 269 version=$(echo "$version" | grep -ohP '[^-]+$') 270 271 versionLessThan() { 272 target=$1 273 [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]] 274 } 275 276 if versionLessThan 1.12.0; then 277 # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1 278 echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade." 279 ${cfg.package}/bin/paperless-ngx document_index reindex 280 fi 281 282 echo ${cfg.package.version} > "$versionFile" 283 fi 284 '' 285 + optionalString (cfg.passwordFile != null) '' 286 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}" 287 export PAPERLESS_ADMIN_PASSWORD=$(cat $CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD) 288 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD" 289 superuserStateFile="${cfg.dataDir}/superuser-state" 290 291 if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then 292 ${cfg.package}/bin/paperless-ngx manage_superuser 293 echo "$superuserState" > "$superuserStateFile" 294 fi 295 ''; 296 } // optionalAttrs enableRedis { 297 after = [ "redis-paperless.service" ]; 298 }; 299 300 systemd.services.paperless-task-queue = { 301 description = "Paperless Celery Workers"; 302 after = [ "paperless-scheduler.service" ]; 303 serviceConfig = defaultServiceConfig // { 304 User = cfg.user; 305 ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO"; 306 Restart = "on-failure"; 307 # The `mbind` syscall is needed for running the classifier. 308 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ]; 309 # Needs to talk to mail server for automated import rules 310 PrivateNetwork = false; 311 }; 312 environment = env; 313 }; 314 315 systemd.services.paperless-consumer = { 316 description = "Paperless document consumer"; 317 # Bind to `paperless-scheduler` so that the consumer never runs 318 # during migrations 319 bindsTo = [ "paperless-scheduler.service" ]; 320 after = [ "paperless-scheduler.service" ]; 321 serviceConfig = defaultServiceConfig // { 322 User = cfg.user; 323 ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer"; 324 Restart = "on-failure"; 325 }; 326 environment = env; 327 # Allow the consumer to access the private /tmp directory of the server. 328 # This is required to support consuming files via a local folder. 329 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; 330 }; 331 332 systemd.services.paperless-web = { 333 description = "Paperless web server"; 334 # Bind to `paperless-scheduler` so that the web server never runs 335 # during migrations 336 bindsTo = [ "paperless-scheduler.service" ]; 337 after = [ "paperless-scheduler.service" ]; 338 # Setup PAPERLESS_SECRET_KEY. 339 # If this environment variable is left unset, paperless-ngx defaults 340 # to a well-known value, which is insecure. 341 script = let 342 secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key"; 343 in '' 344 if [[ ! -f '${secretKeyFile}' ]]; then 345 ( 346 umask 0377 347 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}' 348 ) 349 fi 350 export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}') 351 if [[ ! $PAPERLESS_SECRET_KEY ]]; then 352 echo "PAPERLESS_SECRET_KEY is empty, refusing to start." 353 exit 1 354 fi 355 exec ${cfg.package.python.pkgs.gunicorn}/bin/gunicorn \ 356 -c ${cfg.package}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application 357 ''; 358 serviceConfig = defaultServiceConfig // { 359 User = cfg.user; 360 Restart = "on-failure"; 361 362 LimitNOFILE = 65536; 363 # gunicorn needs setuid, liblapack needs mbind 364 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ]; 365 # Needs to serve web page 366 PrivateNetwork = false; 367 } // lib.optionalAttrs (cfg.port < 1024) { 368 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 369 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 370 }; 371 environment = env // { 372 PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src"; 373 }; 374 # Allow the web interface to access the private /tmp directory of the server. 375 # This is required to support uploading files via the web interface. 376 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; 377 }; 378 379 users = optionalAttrs (cfg.user == defaultUser) { 380 users.${defaultUser} = { 381 group = defaultUser; 382 uid = config.ids.uids.paperless; 383 home = cfg.dataDir; 384 }; 385 386 groups.${defaultUser} = { 387 gid = config.ids.gids.paperless; 388 }; 389 }; 390 }; 391}