at 23.11-beta 14 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4let 5 cfg = config.services.paperless; 6 pkg = cfg.package; 7 8 defaultUser = "paperless"; 9 nltkDir = "/var/cache/paperless/nltk"; 10 defaultFont = "${pkgs.liberation_ttf}/share/fonts/truetype/LiberationSerif-Regular.ttf"; 11 12 # Don't start a redis instance if the user sets a custom redis connection 13 enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig; 14 redisServer = config.services.redis.servers.paperless; 15 16 env = { 17 PAPERLESS_DATA_DIR = cfg.dataDir; 18 PAPERLESS_MEDIA_ROOT = cfg.mediaDir; 19 PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir; 20 PAPERLESS_NLTK_DIR = nltkDir; 21 PAPERLESS_THUMBNAIL_FONT_NAME = defaultFont; 22 GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}"; 23 } // optionalAttrs (config.time.timeZone != null) { 24 PAPERLESS_TIME_ZONE = config.time.timeZone; 25 } // optionalAttrs enableRedis { 26 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}"; 27 } // ( 28 lib.mapAttrs (_: toString) cfg.extraConfig 29 ); 30 31 manage = pkgs.writeShellScript "manage" '' 32 set -o allexport # Export the following env vars 33 ${lib.toShellVars env} 34 exec ${pkg}/bin/paperless-ngx "$@" 35 ''; 36 37 # Secure the services 38 defaultServiceConfig = { 39 ReadWritePaths = [ 40 cfg.consumptionDir 41 cfg.dataDir 42 cfg.mediaDir 43 ]; 44 CacheDirectory = "paperless"; 45 CapabilityBoundingSet = ""; 46 # ProtectClock adds DeviceAllow=char-rtc r 47 DeviceAllow = ""; 48 LockPersonality = true; 49 MemoryDenyWriteExecute = true; 50 NoNewPrivileges = true; 51 PrivateDevices = true; 52 PrivateMounts = true; 53 PrivateNetwork = true; 54 PrivateTmp = true; 55 PrivateUsers = true; 56 ProtectClock = true; 57 # Breaks if the home dir of the user is in /home 58 # ProtectHome = true; 59 ProtectHostname = true; 60 ProtectSystem = "strict"; 61 ProtectControlGroups = true; 62 ProtectKernelLogs = true; 63 ProtectKernelModules = true; 64 ProtectKernelTunables = true; 65 ProtectProc = "invisible"; 66 # Don't restrict ProcSubset because django-q requires read access to /proc/stat 67 # to query CPU and memory information. 68 # Note that /proc only contains processes of user `paperless`, so this is safe. 69 # ProcSubset = "pid"; 70 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 71 RestrictNamespaces = true; 72 RestrictRealtime = true; 73 RestrictSUIDSGID = true; 74 SupplementaryGroups = optional enableRedis redisServer.user; 75 SystemCallArchitectures = "native"; 76 SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ]; 77 UMask = "0066"; 78 }; 79in 80{ 81 meta.maintainers = with maintainers; [ erikarvstedt Flakebi leona ]; 82 83 imports = [ 84 (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ]) 85 ]; 86 87 options.services.paperless = { 88 enable = mkOption { 89 type = lib.types.bool; 90 default = false; 91 description = lib.mdDoc '' 92 Enable Paperless. 93 94 When started, the Paperless database is automatically created if it doesn't 95 exist and updated if the Paperless package has changed. 96 Both tasks are achieved by running a Django migration. 97 98 A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to 99 `''${dataDir}/paperless-manage`. 100 ''; 101 }; 102 103 dataDir = mkOption { 104 type = types.str; 105 default = "/var/lib/paperless"; 106 description = lib.mdDoc "Directory to store the Paperless data."; 107 }; 108 109 mediaDir = mkOption { 110 type = types.str; 111 default = "${cfg.dataDir}/media"; 112 defaultText = literalExpression ''"''${dataDir}/media"''; 113 description = lib.mdDoc "Directory to store the Paperless documents."; 114 }; 115 116 consumptionDir = mkOption { 117 type = types.str; 118 default = "${cfg.dataDir}/consume"; 119 defaultText = literalExpression ''"''${dataDir}/consume"''; 120 description = lib.mdDoc "Directory from which new documents are imported."; 121 }; 122 123 consumptionDirIsPublic = mkOption { 124 type = types.bool; 125 default = false; 126 description = lib.mdDoc "Whether all users can write to the consumption dir."; 127 }; 128 129 passwordFile = mkOption { 130 type = types.nullOr types.path; 131 default = null; 132 example = "/run/keys/paperless-password"; 133 description = lib.mdDoc '' 134 A file containing the superuser password. 135 136 A superuser is required to access the web interface. 137 If unset, you can create a superuser manually by running 138 `''${dataDir}/paperless-manage createsuperuser`. 139 140 The default superuser name is `admin`. To change it, set 141 option {option}`extraConfig.PAPERLESS_ADMIN_USER`. 142 WARNING: When changing the superuser name after the initial setup, the old superuser 143 will continue to exist. 144 145 To disable login for the web interface, set the following: 146 `extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`. 147 WARNING: Only use this on a trusted system without internet access to Paperless. 148 ''; 149 }; 150 151 address = mkOption { 152 type = types.str; 153 default = "localhost"; 154 description = lib.mdDoc "Web interface address."; 155 }; 156 157 port = mkOption { 158 type = types.port; 159 default = 28981; 160 description = lib.mdDoc "Web interface port."; 161 }; 162 163 # FIXME this should become an RFC42-style settings attr 164 extraConfig = mkOption { 165 type = types.attrs; 166 default = { }; 167 description = lib.mdDoc '' 168 Extra paperless config options. 169 170 See [the documentation](https://docs.paperless-ngx.com/configuration/) 171 for available options. 172 173 Note that some options such as `PAPERLESS_CONSUMER_IGNORE_PATTERN` expect JSON values. Use `builtins.toJSON` to ensure proper quoting. 174 ''; 175 example = literalExpression '' 176 { 177 PAPERLESS_OCR_LANGUAGE = "deu+eng"; 178 179 PAPERLESS_DBHOST = "/run/postgresql"; 180 181 PAPERLESS_CONSUMER_IGNORE_PATTERN = builtins.toJSON [ ".DS_STORE/*" "desktop.ini" ]; 182 183 PAPERLESS_OCR_USER_ARGS = builtins.toJSON { 184 optimize = 1; 185 pdfa_image_compression = "lossless"; 186 }; 187 }; 188 ''; 189 }; 190 191 user = mkOption { 192 type = types.str; 193 default = defaultUser; 194 description = lib.mdDoc "User under which Paperless runs."; 195 }; 196 197 package = mkOption { 198 type = types.package; 199 default = pkgs.paperless-ngx; 200 defaultText = literalExpression "pkgs.paperless-ngx"; 201 description = lib.mdDoc "The Paperless package to use."; 202 }; 203 }; 204 205 config = mkIf cfg.enable { 206 services.redis.servers.paperless.enable = mkIf enableRedis true; 207 208 systemd.tmpfiles.rules = [ 209 "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -" 210 "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -" 211 (if cfg.consumptionDirIsPublic then 212 "d '${cfg.consumptionDir}' 777 - - - -" 213 else 214 "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -" 215 ) 216 ]; 217 218 systemd.services.paperless-scheduler = { 219 description = "Paperless Celery Beat"; 220 wantedBy = [ "multi-user.target" ]; 221 wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ]; 222 serviceConfig = defaultServiceConfig // { 223 User = cfg.user; 224 ExecStart = "${pkg}/bin/celery --app paperless beat --loglevel INFO"; 225 Restart = "on-failure"; 226 }; 227 environment = env; 228 229 preStart = '' 230 ln -sf ${manage} ${cfg.dataDir}/paperless-manage 231 232 # Auto-migrate on first run or if the package has changed 233 versionFile="${cfg.dataDir}/src-version" 234 version=$(cat "$versionFile" 2>/dev/null || echo 0) 235 236 if [[ $version != ${pkg.version} ]]; then 237 ${pkg}/bin/paperless-ngx migrate 238 239 # Parse old version string format for backwards compatibility 240 version=$(echo "$version" | grep -ohP '[^-]+$') 241 242 versionLessThan() { 243 target=$1 244 [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]] 245 } 246 247 if versionLessThan 1.12.0; then 248 # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1 249 echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade." 250 ${pkg}/bin/paperless-ngx document_index reindex 251 fi 252 253 echo ${pkg.version} > "$versionFile" 254 fi 255 '' 256 + optionalString (cfg.passwordFile != null) '' 257 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}" 258 export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password") 259 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD" 260 superuserStateFile="${cfg.dataDir}/superuser-state" 261 262 if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then 263 ${pkg}/bin/paperless-ngx manage_superuser 264 echo "$superuserState" > "$superuserStateFile" 265 fi 266 ''; 267 } // optionalAttrs enableRedis { 268 after = [ "redis-paperless.service" ]; 269 }; 270 271 systemd.services.paperless-task-queue = { 272 description = "Paperless Celery Workers"; 273 after = [ "paperless-scheduler.service" ]; 274 serviceConfig = defaultServiceConfig // { 275 User = cfg.user; 276 ExecStart = "${pkg}/bin/celery --app paperless worker --loglevel INFO"; 277 Restart = "on-failure"; 278 # The `mbind` syscall is needed for running the classifier. 279 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ]; 280 # Needs to talk to mail server for automated import rules 281 PrivateNetwork = false; 282 }; 283 environment = env; 284 }; 285 286 # Reading the user-provided password file requires root access 287 systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) { 288 requiredBy = [ "paperless-scheduler.service" ]; 289 before = [ "paperless-scheduler.service" ]; 290 serviceConfig = { 291 ExecStart = '' 292 ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \ 293 '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password' 294 ''; 295 Type = "oneshot"; 296 }; 297 }; 298 299 # Download NLTK corpus data 300 systemd.services.paperless-download-nltk-data = { 301 wantedBy = [ "paperless-scheduler.service" ]; 302 before = [ "paperless-scheduler.service" ]; 303 after = [ "network-online.target" ]; 304 serviceConfig = defaultServiceConfig // { 305 User = cfg.user; 306 Type = "oneshot"; 307 # Enable internet access 308 PrivateNetwork = false; 309 ExecStart = let pythonWithNltk = pkg.python.withPackages (ps: [ ps.nltk ]); in '' 310 ${pythonWithNltk}/bin/python -m nltk.downloader -d '${nltkDir}' punkt snowball_data stopwords 311 ''; 312 }; 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 = "${pkg}/bin/paperless-ngx document_consumer"; 324 Restart = "on-failure"; 325 }; 326 environment = env; 327 }; 328 329 systemd.services.paperless-web = { 330 description = "Paperless web server"; 331 # Bind to `paperless-scheduler` so that the web server never runs 332 # during migrations 333 bindsTo = [ "paperless-scheduler.service" ]; 334 after = [ "paperless-scheduler.service" ]; 335 # Setup PAPERLESS_SECRET_KEY. 336 # If this environment variable is left unset, paperless-ngx defaults 337 # to a well-known value, which is insecure. 338 script = let 339 secretKeyFile = "${cfg.dataDir}/nixos-paperless-secret-key"; 340 in '' 341 if [[ ! -f '${secretKeyFile}' ]]; then 342 ( 343 umask 0377 344 tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge '${secretKeyFile}' 345 ) 346 fi 347 export PAPERLESS_SECRET_KEY=$(cat '${secretKeyFile}') 348 if [[ ! $PAPERLESS_SECRET_KEY ]]; then 349 echo "PAPERLESS_SECRET_KEY is empty, refusing to start." 350 exit 1 351 fi 352 exec ${pkg.python.pkgs.gunicorn}/bin/gunicorn \ 353 -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application 354 ''; 355 serviceConfig = defaultServiceConfig // { 356 User = cfg.user; 357 Restart = "on-failure"; 358 359 # gunicorn needs setuid, liblapack needs mbind 360 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ]; 361 # Needs to serve web page 362 PrivateNetwork = false; 363 } // lib.optionalAttrs (cfg.port < 1024) { 364 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 365 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 366 }; 367 environment = env // { 368 PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src"; 369 }; 370 # Allow the web interface to access the private /tmp directory of the server. 371 # This is required to support uploading files via the web interface. 372 unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; 373 }; 374 375 users = optionalAttrs (cfg.user == defaultUser) { 376 users.${defaultUser} = { 377 group = defaultUser; 378 uid = config.ids.uids.paperless; 379 home = cfg.dataDir; 380 }; 381 382 groups.${defaultUser} = { 383 gid = config.ids.gids.paperless; 384 }; 385 }; 386 }; 387}