From 9c8494f20cb75f8fc5dba2e2bbad9be834708565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Gro=C3=9Fmann?= Date: Thu, 18 Jan 2024 00:39:25 +0100 Subject: [PATCH] feat: paperless --- flake.lock | 12 +-- hosts/elisabeth/guests.nix | 35 +++++++ .../generated/paperless-admin-passwd.age | 15 +++ hosts/elisabeth/secrets/paperless/host.pub | 1 + modules/config/users.nix | 2 + modules/services/paperless.nix | 49 ++++++++++ modules/services/samba.nix | 90 ++++++++++++++---- secrets/secrets.nix.age | Bin 4968 -> 5013 bytes 8 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 hosts/elisabeth/secrets/paperless/generated/paperless-admin-passwd.age create mode 100644 hosts/elisabeth/secrets/paperless/host.pub create mode 100644 modules/services/paperless.nix diff --git a/flake.lock b/flake.lock index 2706421..e39e73e 100644 --- a/flake.lock +++ b/flake.lock @@ -1054,12 +1054,12 @@ "pre-commit-hooks": "pre-commit-hooks_2" }, "locked": { - "lastModified": 1705283066, - "narHash": "sha256-uYvo7hr28saTQuzZ+t0v2dPAxfcVLs4WirMuFl/ykAA=", - "owner": "oddlama", - "repo": "nixos-extra-modules", - "rev": "cab2f4b0408cc072a8f9405daa542298b11ea87b", - "type": "github" + "dirtyRev": "3057e049e731190def8e9c5bea5467b3edcdd93e-dirty", + "dirtyShortRev": "3057e04-dirty", + "lastModified": 1705280248, + "narHash": "sha256-c/BOXNxZ2Yf6FntFUxYROlxkxJ8kKfpjcwCxdwb9UAw=", + "type": "git", + "url": "file:///home/patrick/repos/nix/nixos-extra-modules" }, "original": { "owner": "oddlama", diff --git a/hosts/elisabeth/guests.nix b/hosts/elisabeth/guests.nix index bd83185..beb5434 100644 --- a/hosts/elisabeth/guests.nix +++ b/hosts/elisabeth/guests.nix @@ -11,6 +11,7 @@ nextclouddomain = "nc.${config.secrets.secrets.global.domains.web}"; giteadomain = "git.${config.secrets.secrets.global.domains.web}"; vaultwardendomain = "pw.${config.secrets.secrets.global.domains.web}"; + paperlessdomain = "ppl.${config.secrets.secrets.global.domains.web}"; ipOf = hostName: lib.net.cidr.host config.secrets.secrets.global.net.ips."${config.guests.${hostName}.nodeName}" config.secrets.secrets.global.net.privateSubnet; in { services.nginx = { @@ -35,6 +36,7 @@ in { client_max_body_size 1G ; ''; }; + upstreams.gitea = { servers."${ipOf "gitea"}:3000" = {}; @@ -54,6 +56,7 @@ in { client_max_body_size 1G ; ''; }; + upstreams.adguardhome = { servers."${ipOf "adguardhome"}:3000" = {}; @@ -74,6 +77,27 @@ in { deny all; ''; }; + upstreams.paperless = { + servers."${ipOf "paperless"}:3000" = {}; + + extraConfig = '' + zone paperless 64k ; + keepalive 5 ; + ''; + }; + virtualHosts.${paperlessdomain} = { + forceSSL = true; + useACMEHost = "web"; + locations."/" = { + proxyPass = "http://paperless"; + proxyWebsockets = true; + X-Frame-Options = "SAMEORIGIN"; + }; + extraConfig = '' + client_max_body_size 4G ; + ''; + }; + upstreams.nextcloud = { servers."${ipOf "nextcloud"}:80" = {}; @@ -96,6 +120,7 @@ in { enablePanzer ? false, enableRenaultFT ? false, enableBunker ? false, + enableSharedPaperless ? false, ... }: { autostart = true; @@ -119,6 +144,11 @@ in { pool = "panzer"; dataset = "bunker/guests/${guestName}"; }; + zfs."/paperless" = lib.mkIf enableSharedPaperless { + pool = "panzer"; + dataset = "bunker/shared/paperless"; + shared = true; + }; modules = [ ../../modules/config ../../modules/services/${guestName}.nix @@ -175,11 +205,16 @@ in { // mkContainer "nextcloud" { enablePanzer = true; } + // mkContainer "paperless" { + enableSharedPaperless = true; + } // mkContainer "gitea" { enablePanzer = true; } // mkContainer "samba" { enablePanzer = true; enableRenaultFT = true; + enableBunker = true; + enableSharedPaperless = true; }; } diff --git a/hosts/elisabeth/secrets/paperless/generated/paperless-admin-passwd.age b/hosts/elisabeth/secrets/paperless/generated/paperless-admin-passwd.age new file mode 100644 index 0000000..31960c0 --- /dev/null +++ b/hosts/elisabeth/secrets/paperless/generated/paperless-admin-passwd.age @@ -0,0 +1,15 @@ +age-encryption.org/v1 +-> X25519 Upc2gcbuYjkXNn8+FezCTEaWFjJQJHHVTIhrP0SUMkc +izickp9vEY4orPFyxda6xbdE4EK0vRSs+XmltAyKdKQ +-> piv-p256 XTQkUA A2buYDo2nM4vNAl9eZSejFa9d+ggM2v8OpyXxHw/GuU7 +dMOYZy/v1YiLnik+BgPn2Tb7X2DsmvSNRzVO9/gaOlk +-> piv-p256 ZFgiIw AuRRB6mIoiLiNdapfZxkHgvWFhq6Qkvnhu4CEy6H5OI9 +y13F4MuE/ZyMZYNfIIi6JIbsmHoR9UoDpW91GqZCgCI +-> piv-p256 5vmPtQ Az6XCDWVi7x6xKpbtNw7hZV831oHmaeP5qSejHFeEHRt +N809G8zW1IrVyU32mOuaC1TRAy6jkUJberP+JZzqa+A +-> piv-p256 ZFgiIw AjyP/+324TJKG5PqIiUTHv52OBNikuxJNndjx7AQVz6y +Zt2h5oLPD+M4PZFTL1NgDgtdByWa5lbrak+KJf+XIGA +-> *[Fh)-grease $,TX 2 5"~'m +rjXg4EA11HFFPHXjz9YcQOOrwlsfMTDy +--- QT0gtk2OI2ggsBiGPA38oUkT/8jWEt5yHoo8RCpN+P8 +MYy"!J'cW\\a$3zN#m2jQ{K TsK&vCV%{IP \ No newline at end of file diff --git a/hosts/elisabeth/secrets/paperless/host.pub b/hosts/elisabeth/secrets/paperless/host.pub new file mode 100644 index 0000000..1d0e17f --- /dev/null +++ b/hosts/elisabeth/secrets/paperless/host.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICZxPatFKKVFCC7JUsgvWb7/rb5IO+nttAm4v0Rzv++W diff --git a/modules/config/users.nix b/modules/config/users.nix index 772af05..c213937 100644 --- a/modules/config/users.nix +++ b/modules/config/users.nix @@ -23,6 +23,8 @@ radicale = uidGid 215; gitea = uidGid 215; vaultwarden = uidGid 215; + redis-paperless = uidGid 216; + paperless = uidGid 315; systemd-oom = uidGid 300; systemd-coredump = uidGid 301; patrick = uidGid 1000; diff --git a/modules/services/paperless.nix b/modules/services/paperless.nix new file mode 100644 index 0000000..100fd34 --- /dev/null +++ b/modules/services/paperless.nix @@ -0,0 +1,49 @@ +{ + config, + lib, + ... +}: let + paperlessdomain = "ppl.${config.secrets.secrets.global.domains.web}"; +in { + networking.firewall.allowedTCPPorts = [3000]; + age.secrets.paperless-admin-passwd = { + generator.script = "alnum"; + mode = "440"; + group = "paperless"; + }; + users.users.paperless.isSystemUser = true; + services.paperless = { + enable = true; + address = "0.0.0.0"; + port = 3000; + passwordFile = config.age.secrets.paperless-admin-passwd.path; + consumptionDir = "/paperless/consume"; + mediaDir = "/paperless/media"; + settings = { + PAPERLESS_URL = "https://${paperlessdomain}"; + PAPERLESS_ALLOWED_HOSTS = paperlessdomain; + PAPERLESS_CORS_ALLOWED_HOSTS = "https://${paperlessdomain}"; + PAPERLESS_TRUSTED_PROXIES = lib.net.cidr.host config.secrets.secrets.global.net.ips.elisabeth config.secrets.secrets.global.net.privateSubnet; + + # let nginx do all the compression + PAPERLESS_ENABLE_COMPRESSION = false; + PAPERLESS_CONSUMER_ENABLE_BARCODES = true; + PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE = true; + PAPERLESS_CONSUMER_BARCODE_SCANNER = "ZXING"; + PAPERLESS_CONSUMER_RECURSIVE = true; + PAPERLESS_FILENAME_FORMAT = "{owner_username}/{created_year}-{created_month}-{created_day}_{asn}_{title}"; + PAPERLESS_NUMBER_OF_SUGESSTED_DATES = 11; + PAPERLESS_OCR_LANGUAGE = "deu+eng"; + PAPERLESS_TASK_WORKERS = 4; + PAPERLESS_WEBSERVER_WORKERS = 4; + }; + }; + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/paperless"; + user = "paperless"; + group = "paperless"; + mode = "0750"; + } + ]; +} diff --git a/modules/services/samba.nix b/modules/services/samba.nix index ec106dd..43ca8e4 100644 --- a/modules/services/samba.nix +++ b/modules/services/samba.nix @@ -91,17 +91,20 @@ user ? "smb", group ? "smb", hasBunker ? false, + hasPaperless ? false, persistRoot ? "/panzer", }: cfg: let config = { "#persistRoot" = persistRoot; + "#user" = user; + "#group" = group; "read only" = "no"; "guest ok" = "no"; "create mask" = "0740"; "directory mask" = "0750"; - "force user" = "${user}"; - "force group" = "${group}"; + "force user" = user; + "force group" = group; "valid users" = "${user} @${group}"; "force create mode" = "0660"; "force directory mode" = "0770"; @@ -123,6 +126,19 @@ "path" = "/media/smb/${name}-important"; "#persistRoot" = "/bunker"; }; + } + // lib.optionalAttrs hasPaperless + { + "${name}-paperless" = + config + // { + "path" = "/media/smb/${name}-paperless"; + "#paperless" = true; + "force user" = "paperless"; + "force group" = "paperless"; + # Empty to prevent imperamence setting a persistence folder + "#persistRoot" = ""; + }; }; in lib.mkMerge [ @@ -137,6 +153,7 @@ user = "patrick"; group = "patrick"; hasBunker = true; + hasPaperless = true; } {}) (mkShare { name = "helen-data"; @@ -176,16 +193,19 @@ users = lib.unique (lib.mapAttrsToList (_: val: val."force user") config.services.samba.shares); groups = lib.unique (users ++ (lib.mapAttrsToList (_: val: val."force group") config.services.samba.shares)); in { - users = lib.mkMerge (lib.flip map users (user: { - ${user} = { - isNormalUser = true; - home = "/var/empty"; - createHome = false; - useDefaultShell = false; - autoSubUidGidRange = false; - group = "${user}"; - }; - })); + users = lib.mkMerge ((lib.flip map users (user: { + ${user} = { + isNormalUser = true; + home = "/var/empty"; + createHome = false; + useDefaultShell = false; + autoSubUidGidRange = false; + group = "${user}"; + }; + })) + ++ [ + {paperless.isNormalUser = lib.mkForce false;} + ]); groups = lib.mkMerge ((lib.flip map groups (group: { ${group} = { }; @@ -193,14 +213,42 @@ ++ [{family.members = ["patrick" "david" "helen" "ggr"];}]); }; - environment.persistence = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: { - ${v."#persistRoot"}.directories = [ - { - directory = "${v.path}"; - user = "${v."force user"}"; - group = "${v."force group"}"; + fileSystems = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: + lib.optionalAttrs ((v ? "#paperless") && v."#paperless") { + "${v.path}/consume" = { + fsType = "none"; + options = ["bind"]; + device = "/paperless/consume/${v."#user"}"; + }; + "${v.path}/media" = { + fsType = "none "; + options = ["bind"]; + device = "/paperless/media/${v."#user"}"; + }; + })); + + systemd.tmpfiles.settings = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: + lib.optionalAttrs ((v ? "#paperless") && v."#paperless") { + "10-smb-paperless"."/paperless/consume/${v."#user"}".d = { + user = "paperless"; + group = "paperless"; mode = "0770"; - } - ]; - })); + }; + "10-smb-paperless"."/paperless/media/${v."#user"}".d = { + user = "paperless"; + group = "paperless"; + mode = "0770"; + }; + })); + environment.persistence = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: + lib.optionalAttrs ((v ? "#persistRoot") && (v."#persistRoot" != "")) { + ${v."#persistRoot"}.directories = [ + { + directory = "${v.path}"; + user = "${v."force user"}"; + group = "${v."force group"}"; + mode = "0770"; + } + ]; + })); } diff --git a/secrets/secrets.nix.age b/secrets/secrets.nix.age index ad1e141291b5c4b62b508c92462e78abe6d5dc6f..bf36d7468788d9aa6461db5a2553f30ba4cc070c 100644 GIT binary patch delta 5010 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