at 25.11-pre 22 kB view raw
1{ 2 config, 3 options, 4 pkgs, 5 lib, 6 ... 7}: 8let 9 cfg = config.services.paperless; 10 11 defaultUser = "paperless"; 12 defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf"; 13 14 # Don't start a redis instance if the user sets a custom redis connection 15 enableRedis = !(cfg.settings ? PAPERLESS_REDIS); 16 redisServer = config.services.redis.servers.paperless; 17 18 env = 19 { 20 PAPERLESS_DATA_DIR = cfg.dataDir; 21 PAPERLESS_MEDIA_ROOT = cfg.mediaDir; 22 PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir; 23 PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont; 24 GRANIAN_HOST = cfg.address; 25 GRANIAN_PORT = toString cfg.port; 26 } 27 // lib.optionalAttrs (config.time.timeZone != null) { 28 PAPERLESS_TIME_ZONE = config.time.timeZone; 29 } 30 // lib.optionalAttrs enableRedis { 31 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}"; 32 } 33 // lib.optionalAttrs (cfg.settings.PAPERLESS_ENABLE_NLTK or true) { 34 PAPERLESS_NLTK_DIR = pkgs.symlinkJoin { 35 name = "paperless_ngx_nltk_data"; 36 paths = cfg.package.nltkData; 37 }; 38 } 39 // lib.optionalAttrs (cfg.openMPThreadingWorkaround) { 40 OMP_NUM_THREADS = "1"; 41 } 42 // (lib.mapAttrs ( 43 _: s: 44 if (lib.isAttrs s || lib.isList s) then 45 builtins.toJSON s 46 else if lib.isBool s then 47 lib.boolToString s 48 else 49 toString s 50 ) cfg.settings); 51 52 manage = pkgs.writeShellScriptBin "paperless-manage" '' 53 set -o allexport # Export the following env vars 54 ${lib.toShellVars env} 55 ${lib.optionalString (cfg.environmentFile != null) "source ${cfg.environmentFile}"} 56 57 cd '${cfg.dataDir}' 58 sudo=exec 59 if [[ "$USER" != ${cfg.user} ]]; then 60 ${ 61 if config.security.sudo.enable then 62 "sudo='exec ${config.security.wrapperDir}/sudo -u ${cfg.user} -E'" 63 else 64 ">&2 echo 'Aborting, paperless-manage must be run as user `${cfg.user}`!'; exit 2" 65 } 66 fi 67 $sudo ${lib.getExe cfg.package} "$@" 68 ''; 69 70 defaultServiceConfig = { 71 Slice = "system-paperless.slice"; 72 # Secure the services 73 ReadWritePaths = [ 74 cfg.consumptionDir 75 cfg.dataDir 76 cfg.mediaDir 77 ]; 78 CacheDirectory = "paperless"; 79 CapabilityBoundingSet = ""; 80 # ProtectClock adds DeviceAllow=char-rtc r 81 DeviceAllow = ""; 82 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; 83 LockPersonality = true; 84 MemoryDenyWriteExecute = true; 85 NoNewPrivileges = true; 86 PrivateDevices = true; 87 PrivateMounts = true; 88 PrivateNetwork = true; 89 PrivateTmp = true; 90 PrivateUsers = true; 91 ProtectClock = true; 92 # Breaks if the home dir of the user is in /home 93 # ProtectHome = true; 94 ProtectHostname = true; 95 ProtectSystem = "strict"; 96 ProtectControlGroups = true; 97 ProtectKernelLogs = true; 98 ProtectKernelModules = true; 99 ProtectKernelTunables = true; 100 ProtectProc = "invisible"; 101 ProcSubset = "pid"; 102 RestrictAddressFamilies = [ 103 "AF_UNIX" 104 "AF_INET" 105 "AF_INET6" 106 ]; 107 RestrictNamespaces = true; 108 RestrictRealtime = true; 109 RestrictSUIDSGID = true; 110 SupplementaryGroups = lib.optional enableRedis redisServer.user; 111 SystemCallArchitectures = "native"; 112 SystemCallFilter = [ 113 "@system-service" 114 "~@privileged @setuid @keyring" 115 ]; 116 UMask = "0066"; 117 }; 118in 119{ 120 meta.maintainers = with lib.maintainers; [ 121 leona 122 SuperSandro2000 123 erikarvstedt 124 atemu 125 theuni 126 ]; 127 128 imports = [ 129 (lib.mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ]) 130 (lib.mkRenamedOptionModule 131 [ "services" "paperless" "extraConfig" ] 132 [ "services" "paperless" "settings" ] 133 ) 134 ]; 135 136 options.services.paperless = { 137 enable = lib.mkOption { 138 type = lib.types.bool; 139 default = false; 140 description = '' 141 Whether to enable Paperless-ngx. 142 143 When started, the Paperless database is automatically created if it doesn't exist 144 and updated if the Paperless package has changed. 145 Both tasks are achieved by running a Django migration. 146 147 A script to manage the Paperless-ngx instance (by wrapping Django's manage.py) is available as `paperless-manage`. 148 ''; 149 }; 150 151 dataDir = lib.mkOption { 152 type = lib.types.str; 153 default = "/var/lib/paperless"; 154 description = "Directory to store the Paperless data."; 155 }; 156 157 mediaDir = lib.mkOption { 158 type = lib.types.str; 159 default = "${cfg.dataDir}/media"; 160 defaultText = lib.literalExpression ''"''${dataDir}/media"''; 161 description = "Directory to store the Paperless documents."; 162 }; 163 164 consumptionDir = lib.mkOption { 165 type = lib.types.str; 166 default = "${cfg.dataDir}/consume"; 167 defaultText = lib.literalExpression ''"''${dataDir}/consume"''; 168 description = "Directory from which new documents are imported."; 169 }; 170 171 consumptionDirIsPublic = lib.mkOption { 172 type = lib.types.bool; 173 default = false; 174 description = "Whether all users can write to the consumption dir."; 175 }; 176 177 passwordFile = lib.mkOption { 178 type = lib.types.nullOr lib.types.path; 179 default = null; 180 example = "/run/keys/paperless-password"; 181 description = '' 182 A file containing the superuser password. 183 184 A superuser is required to access the web interface. 185 If unset, you can create a superuser manually by running `paperless-manage createsuperuser`. 186 187 The default superuser name is `admin`. To change it, set 188 option {option}`settings.PAPERLESS_ADMIN_USER`. 189 WARNING: When changing the superuser name after the initial setup, the old superuser 190 will continue to exist. 191 192 To disable login for the web interface, set the following: 193 `settings.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`. 194 WARNING: Only use this on a trusted system without internet access to Paperless. 195 ''; 196 }; 197 198 address = lib.mkOption { 199 type = lib.types.str; 200 default = "127.0.0.1"; 201 description = "Web interface address."; 202 }; 203 204 port = lib.mkOption { 205 type = lib.types.port; 206 default = 28981; 207 description = "Web interface port."; 208 }; 209 210 settings = lib.mkOption { 211 type = lib.types.submodule { 212 freeformType = 213 with lib.types; 214 attrsOf ( 215 let 216 typeList = [ 217 bool 218 float 219 int 220 str 221 path 222 package 223 ]; 224 in 225 oneOf ( 226 typeList 227 ++ [ 228 (listOf (oneOf typeList)) 229 (attrsOf (oneOf typeList)) 230 ] 231 ) 232 ); 233 }; 234 default = { }; 235 description = '' 236 Extra paperless config options. 237 238 See [the documentation](https://docs.paperless-ngx.com/configuration/) for available options. 239 240 Note that some settings such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values. 241 Settings declared as lists or attrsets will automatically be serialised into JSON strings for your convenience. 242 ''; 243 example = { 244 PAPERLESS_OCR_LANGUAGE = "deu+eng"; 245 PAPERLESS_CONSUMER_IGNORE_PATTERN = [ 246 ".DS_STORE/*" 247 "desktop.ini" 248 ]; 249 PAPERLESS_OCR_USER_ARGS = { 250 optimize = 1; 251 pdfa_image_compression = "lossless"; 252 }; 253 }; 254 }; 255 256 user = lib.mkOption { 257 type = lib.types.str; 258 default = defaultUser; 259 description = "User under which Paperless runs."; 260 }; 261 262 package = lib.mkPackageOption pkgs "paperless-ngx" { } // { 263 apply = 264 pkg: 265 pkg.override { 266 tesseract5 = pkg.tesseract5.override { 267 # always enable detection modules 268 # tesseract fails to build when eng is not present 269 enableLanguages = 270 if cfg.settings ? PAPERLESS_OCR_LANGUAGE then 271 lib.lists.unique ( 272 [ 273 "equ" 274 "osd" 275 "eng" 276 ] 277 ++ lib.splitString "+" cfg.settings.PAPERLESS_OCR_LANGUAGE 278 ) 279 else 280 null; 281 }; 282 }; 283 }; 284 285 openMPThreadingWorkaround = 286 lib.mkEnableOption '' 287 a workaround for document classifier timeouts. 288 289 Paperless uses OpenBLAS via scikit-learn for document classification. 290 291 The default is to use threading for OpenMP but this would cause the 292 document classifier to spin on one core seemingly indefinitely if there 293 are large amounts of classes per classification; causing it to 294 effectively never complete due to running into timeouts. 295 296 This sets `OMP_NUM_THREADS` to `1` in order to mitigate the issue. See 297 https://github.com/NixOS/nixpkgs/issues/240591 for more information 298 '' 299 // lib.mkOption { default = true; }; 300 301 environmentFile = lib.mkOption { 302 type = lib.types.nullOr lib.types.path; 303 default = null; 304 example = "/run/secrets/paperless"; 305 description = '' 306 Path to a file containing extra paperless config options in the systemd `EnvironmentFile` 307 format. Refer to the [documentation](https://docs.paperless-ngx.com/configuration/) for 308 config options. 309 310 This can be used to pass secrets to paperless without putting them in the Nix store. 311 312 To set a database password, point `environmentFile` at a file containing: 313 ``` 314 PAPERLESS_DBPASS=<pass> 315 ``` 316 ''; 317 }; 318 319 database = { 320 createLocally = lib.mkOption { 321 type = lib.types.bool; 322 default = false; 323 description = '' 324 Configure local PostgreSQL database server for Paperless. 325 ''; 326 }; 327 }; 328 329 exporter = { 330 enable = lib.mkEnableOption "regular automatic document exports"; 331 332 directory = lib.mkOption { 333 type = lib.types.str; 334 default = cfg.dataDir + "/export"; 335 defaultText = lib.literalExpression "\${config.services.paperless.dataDir}/export"; 336 description = "Directory to store export."; 337 }; 338 339 onCalendar = lib.mkOption { 340 type = lib.types.nullOr lib.types.str; 341 default = "01:30:00"; 342 description = '' 343 When to run the exporter. See {manpage}`systemd.time(7)`. 344 345 `null` disables the timer; allowing you to run the 346 `paperless-exporter` service through other means. 347 ''; 348 }; 349 350 settings = lib.mkOption { 351 type = with lib.types; attrsOf anything; 352 default = { 353 "no-progress-bar" = true; 354 "no-color" = true; 355 "compare-checksums" = true; 356 "delete" = true; 357 }; 358 description = "Settings to pass to the document exporter as CLI arguments."; 359 }; 360 }; 361 362 configureTika = lib.mkOption { 363 type = lib.types.bool; 364 default = false; 365 description = '' 366 Whether to configure Tika and Gotenberg to process Office and e-mail files with OCR. 367 ''; 368 }; 369 }; 370 371 config = lib.mkIf cfg.enable ( 372 lib.mkMerge [ 373 { 374 environment.systemPackages = [ manage ]; 375 376 services.redis.servers.paperless.enable = lib.mkIf enableRedis true; 377 378 services.postgresql = lib.mkIf cfg.database.createLocally { 379 enable = true; 380 ensureDatabases = [ "paperless" ]; 381 ensureUsers = [ 382 { 383 name = config.services.paperless.user; 384 ensureDBOwnership = true; 385 } 386 ]; 387 }; 388 389 services.paperless.settings = lib.mkMerge [ 390 (lib.mkIf cfg.database.createLocally { 391 PAPERLESS_DBENGINE = "postgresql"; 392 PAPERLESS_DBHOST = "/run/postgresql"; 393 PAPERLESS_DBNAME = "paperless"; 394 PAPERLESS_DBUSER = "paperless"; 395 }) 396 (lib.mkIf cfg.configureTika { 397 PAPERLESS_GOTENBERG_ENABLED = true; 398 PAPERLESS_TIKA_ENABLED = true; 399 }) 400 ]; 401 402 systemd.slices.system-paperless = { 403 description = "Paperless Document Management System Slice"; 404 documentation = [ "https://docs.paperless-ngx.com" ]; 405 }; 406 407 systemd.tmpfiles.settings."10-paperless" = 408 let 409 defaultRule = { 410 inherit (cfg) user; 411 inherit (config.users.users.${cfg.user}) group; 412 }; 413 in 414 { 415 "${cfg.dataDir}".d = defaultRule; 416 "${cfg.mediaDir}".d = defaultRule; 417 "${cfg.consumptionDir}".d = if cfg.consumptionDirIsPublic then { mode = "777"; } else defaultRule; 418 }; 419 420 systemd.services.paperless-scheduler = { 421 description = "Paperless Celery Beat"; 422 wantedBy = [ "multi-user.target" ]; 423 wants = [ 424 "paperless-consumer.service" 425 "paperless-web.service" 426 "paperless-task-queue.service" 427 ]; 428 serviceConfig = defaultServiceConfig // { 429 User = cfg.user; 430 ExecStart = "${cfg.package}/bin/celery --app paperless beat --loglevel INFO"; 431 Restart = "on-failure"; 432 LoadCredential = lib.optionalString ( 433 cfg.passwordFile != null 434 ) "PAPERLESS_ADMIN_PASSWORD:${cfg.passwordFile}"; 435 PrivateNetwork = cfg.database.createLocally; # defaultServiceConfig enables this by default, needs to be disabled for remote DBs 436 }; 437 environment = env; 438 439 preStart = '' 440 # remove old papaerless-manage symlink 441 # TODO: drop with NixOS 25.11 442 [[ -L '${cfg.dataDir}/paperless-manage' ]] && rm '${cfg.dataDir}/paperless-manage' 443 444 # Auto-migrate on first run or if the package has changed 445 versionFile="${cfg.dataDir}/src-version" 446 version=$(cat "$versionFile" 2>/dev/null || echo 0) 447 448 if [[ $version != ${cfg.package.version} ]]; then 449 ${cfg.package}/bin/paperless-ngx migrate 450 451 # Parse old version string format for backwards compatibility 452 version=$(echo "$version" | grep -ohP '[^-]+$') 453 454 versionLessThan() { 455 target=$1 456 [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]] 457 } 458 459 if versionLessThan 1.12.0; then 460 # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1 461 echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade." 462 ${cfg.package}/bin/paperless-ngx document_index reindex 463 fi 464 465 echo ${cfg.package.version} > "$versionFile" 466 fi 467 468 if ${lib.boolToString (cfg.passwordFile != null)} || [[ -n $PAPERLESS_ADMIN_PASSWORD ]]; then 469 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}" 470 if [[ -e $CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD ]]; then 471 PAPERLESS_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PAPERLESS_ADMIN_PASSWORD") 472 export PAPERLESS_ADMIN_PASSWORD 473 fi 474 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD" 475 superuserStateFile="${cfg.dataDir}/superuser-state" 476 477 if [[ $(cat "$superuserStateFile" 2>/dev/null) != "$superuserState" ]]; then 478 ${cfg.package}/bin/paperless-ngx manage_superuser 479 echo "$superuserState" > "$superuserStateFile" 480 fi 481 fi 482 ''; 483 requires = lib.optional cfg.database.createLocally "postgresql.service"; 484 after = 485 lib.optional enableRedis "redis-paperless.service" 486 ++ lib.optional cfg.database.createLocally "postgresql.service"; 487 }; 488 489 systemd.services.paperless-task-queue = { 490 description = "Paperless Celery Workers"; 491 requires = lib.optional cfg.database.createLocally "postgresql.service"; 492 after = [ 493 "paperless-scheduler.service" 494 ] ++ lib.optional cfg.database.createLocally "postgresql.service"; 495 serviceConfig = defaultServiceConfig // { 496 User = cfg.user; 497 ExecStart = "${cfg.package}/bin/celery --app paperless worker --loglevel INFO"; 498 Restart = "on-failure"; 499 # The `mbind` syscall is needed for running the classifier. 500 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ]; 501 # Needs to talk to mail server for automated import rules 502 PrivateNetwork = false; 503 }; 504 environment = env; 505 }; 506 507 systemd.services.paperless-consumer = { 508 description = "Paperless document consumer"; 509 # Bind to `paperless-scheduler` so that the consumer never runs 510 # during migrations 511 bindsTo = [ "paperless-scheduler.service" ]; 512 requires = lib.optional cfg.database.createLocally "postgresql.service"; 513 after = [ 514 "paperless-scheduler.service" 515 ] ++ lib.optional cfg.database.createLocally "postgresql.service"; 516 serviceConfig = defaultServiceConfig // { 517 User = cfg.user; 518 ExecStart = "${cfg.package}/bin/paperless-ngx document_consumer"; 519 Restart = "on-failure"; 520 PrivateNetwork = cfg.database.createLocally; # defaultServiceConfig enables this by default, needs to be disabled for remote DBs 521 }; 522 environment = env; 523 # Allow the consumer to access the private /tmp directory of the server. 524 # This is required to support consuming files via a local folder. 525 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; 526 }; 527 528 systemd.services.paperless-web = { 529 description = "Paperless web server"; 530 # Bind to `paperless-scheduler` so that the web server never runs 531 # during migrations 532 bindsTo = [ "paperless-scheduler.service" ]; 533 requires = lib.optional cfg.database.createLocally "postgresql.service"; 534 after = [ 535 "paperless-scheduler.service" 536 ] ++ lib.optional cfg.database.createLocally "postgresql.service"; 537 # Setup PAPERLESS_SECRET_KEY. 538 # If this environment variable is left unset, paperless-ngx defaults 539 # to a well-known value, which is insecure. 540 script = 541 let 542 secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key"; 543 in 544 '' 545 if [[ ! -f '${secretKeyFile}' ]]; then 546 ( 547 umask 0377 548 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}' 549 ) 550 fi 551 PAPERLESS_SECRET_KEY="$(cat '${secretKeyFile}')" 552 export PAPERLESS_SECRET_KEY 553 if [[ ! $PAPERLESS_SECRET_KEY ]]; then 554 echo "PAPERLESS_SECRET_KEY is empty, refusing to start." 555 exit 1 556 fi 557 exec ${lib.getExe cfg.package.python.pkgs.granian} --interface asginl --ws "paperless.asgi:application" 558 ''; 559 serviceConfig = defaultServiceConfig // { 560 User = cfg.user; 561 Restart = "on-failure"; 562 563 LimitNOFILE = 65536; 564 # liblapack needs mbind 565 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ]; 566 # Needs to serve web page 567 PrivateNetwork = false; 568 }; 569 environment = env // { 570 PYTHONPATH = "${cfg.package.python.pkgs.makePythonPath cfg.package.propagatedBuildInputs}:${cfg.package}/lib/paperless-ngx/src"; 571 }; 572 # Allow the web interface to access the private /tmp directory of the server. 573 # This is required to support uploading files via the web interface. 574 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; 575 }; 576 577 users = lib.optionalAttrs (cfg.user == defaultUser) { 578 users.${defaultUser} = { 579 group = defaultUser; 580 uid = config.ids.uids.paperless; 581 home = cfg.dataDir; 582 }; 583 584 groups.${defaultUser} = { 585 gid = config.ids.gids.paperless; 586 }; 587 }; 588 589 services.gotenberg = lib.mkIf cfg.configureTika { 590 enable = true; 591 # https://github.com/paperless-ngx/paperless-ngx/blob/v2.15.3/docker/compose/docker-compose.sqlite-tika.yml#L64-L69 592 chromium.disableJavascript = true; 593 extraArgs = [ "--chromium-allow-list=file:///tmp/.*" ]; 594 }; 595 596 services.tika = lib.mkIf cfg.configureTika { 597 enable = true; 598 enableOcr = true; 599 }; 600 } 601 602 (lib.mkIf cfg.exporter.enable { 603 systemd.tmpfiles.rules = [ 604 "d '${cfg.exporter.directory}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -" 605 ]; 606 607 services.paperless.exporter.settings = options.services.paperless.exporter.settings.default; 608 609 systemd.services.paperless-exporter = { 610 startAt = lib.defaultTo [ ] cfg.exporter.onCalendar; 611 serviceConfig = { 612 User = cfg.user; 613 WorkingDirectory = cfg.dataDir; 614 }; 615 unitConfig = 616 let 617 services = [ 618 "paperless-consumer.service" 619 "paperless-scheduler.service" 620 "paperless-task-queue.service" 621 "paperless-web.service" 622 ]; 623 in 624 { 625 # Shut down the paperless services while the exporter runs 626 Conflicts = services; 627 After = services; 628 # Bring them back up afterwards, regardless of pass/fail 629 OnFailure = services; 630 OnSuccess = services; 631 }; 632 enableStrictShellChecks = true; 633 path = [ manage ]; 634 script = '' 635 paperless-manage document_exporter ${cfg.exporter.directory} ${ 636 lib.cli.toGNUCommandLineShell { } cfg.exporter.settings 637 } 638 ''; 639 }; 640 }) 641 ] 642 ); 643}