From 15dd872713c0a6d0d6ba7af711f0e85a62446e49 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Sat, 8 Oct 2022 06:07:22 +0200 Subject: [PATCH 1/7] paperless: improved the configuration, added tika and gotenberg, and running as user for per-user paperlesses --- roles/paperless-ngx/files/common_consume.py | 27 ++++ roles/paperless-ngx/files/post-consume.py | 26 +++ roles/paperless-ngx/files/pre-consume.py | 14 ++ roles/paperless-ngx/tasks/paperless-ngx.yml | 167 +++++++++++++++++--- 4 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 roles/paperless-ngx/files/common_consume.py create mode 100755 roles/paperless-ngx/files/post-consume.py create mode 100755 roles/paperless-ngx/files/pre-consume.py diff --git a/roles/paperless-ngx/files/common_consume.py b/roles/paperless-ngx/files/common_consume.py new file mode 100644 index 0000000..147bd67 --- /dev/null +++ b/roles/paperless-ngx/files/common_consume.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import json +from os import environ, path +from datetime import datetime + +DATA_DIR = environ.get("PAPERLESS_DATA_DIR", "../data/") +LOGGING_DIR = environ.get("PAPERLESS_LOGGING_DIR", path.join(DATA_DIR, "log/")) + + +def logger(env_vars, consume_stage): + # paperless-ngx has a hardcoded log file name anyay + log_path = path.join(LOGGING_DIR, "consume.log") + + log_item = {k.lower(): environ.get(k) for k in env_vars} + log_item.update({ + "timestamp": datetime.now().isoformat(), + "user": environ.get("PAPERLESS_USER"), + "log_path": log_path, + "paperless_consume_stage": consume_stage + + }) + + with open(log_path, 'a') as f: + j = json.dumps(log_item, indent=2) + f.write(j) + f.write("\n") diff --git a/roles/paperless-ngx/files/post-consume.py b/roles/paperless-ngx/files/post-consume.py new file mode 100755 index 0000000..8c5cecf --- /dev/null +++ b/roles/paperless-ngx/files/post-consume.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from common_consume import logger + + +def main(): + post_consume_vars = [ + "DOCUMENT_ID", + "DOCUMENT_FILE_NAME", + "DOCUMENT_CREATED", + "DOCUMENT_MODIFIED", + "DOCUMENT_ADDED", + "DOCUMENT_SOURCE_PATH", + "DOCUMENT_ARCHIVE_PATH", + "DOCUMENT_THUMBNAIL_PATH", + "DOCUMENT_DOWNLOAD_URL", + "DOCUMENT_THUMBNAIL_URL", + "DOCUMENT_CORRESPONDENT", + "DOCUMENT_TAGS", + "DOCUMENT_ORIGINAL_FILENAME" + ] + logger(post_consume_vars, "post") + + +if __name__ == "__main__": + main() diff --git a/roles/paperless-ngx/files/pre-consume.py b/roles/paperless-ngx/files/pre-consume.py new file mode 100755 index 0000000..5b4a76b --- /dev/null +++ b/roles/paperless-ngx/files/pre-consume.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from common_consume import logger + + +def main(): + pre_consume_vars = [ + "DOCUMENT_SOURCE_PATH" + ] + logger(pre_consume_vars, "pre") + + +if __name__ == "__main__": + main() diff --git a/roles/paperless-ngx/tasks/paperless-ngx.yml b/roles/paperless-ngx/tasks/paperless-ngx.yml index 78d8843..014ca49 100644 --- a/roles/paperless-ngx/tasks/paperless-ngx.yml +++ b/roles/paperless-ngx/tasks/paperless-ngx.yml @@ -39,30 +39,80 @@ tags: - mariadb-users -- name: create dir structure +- name: create dir structure for paperless-ngx file: path: "{{ systemuserlist.paperless.home }}/{{ item.name }}" state: directory mode: 0775 owner: "{{ item.owner|default('paperless') }}" group: "{{ item.group|default('paperless') }}" + tags: + - paperless-dirs with_items: # checked dockerfile: https://github.com/docker-library/redis/blob/master/7.0/Dockerfile - name: redis owner: 999 group: 999 - - name: redis/data + - name: redis/data-{{ paperless_user }} owner: 999 group: 999 - name: paperless-ngx - - name: paperless-ngx/data - - name: paperless-ngx/media - - name: paperless-ngx/export - - name: paperless-ngx/consume + - name: paperless-ngx/bin -- name: redis container for paperless-nx +- name: ensure {{ paperless_users_path }} exists + file: + path: "{{ paperless_users_path }}" + state: directory + mode: 0755 + owner: paperless + group: paperless + tags: + - paperless-dirs + +- name: ensure {{ paperless_users_path }}/{{ paperless_user }} exists + file: + path: "{{ paperless_users_path }}/{{ paperless_user }}" + state: directory + mode: 0750 + owner: "{{ paperless_user }}" + group: "{{ paperless_user }}" + tags: + - paperless-dirs + +- name: create dir structure for user in {{ paperless_users_path }}/{{ paperless_user }}} + file: + path: "{{ paperless_users_path }}/{{ paperless_user }}/{{ item }}" + state: directory + mode: 0750 + owner: "{{ paperless_user }}" + group: "{{ paperless_user }}" + tags: + - paperless-dirs + with_items: + - data + - media + - media/trash + - export + - consume + +- name: paperless scripts + copy: + src: "{{ item }}" + dest: "{{ systemuserlist.paperless.home }}/paperless-ngx/bin/{{ item }}" + owner: paperless + group: paperless + mode: 0775 + with_items: + - common_consume.py + - post-consume.py + - pre-consume.py + tags: + - paperless-scripts + - paperless-bin + +- name: redis container for paperless-ngx user {{ paperless_user }} docker_container: - name: paperless-ngx-redis + name: paperless-ngx-redis-{{ paperless_user }} image: "redis:latest" restart_policy: "unless-stopped" auto_remove: false @@ -71,7 +121,7 @@ state: started container_default_behavior: compatibility env: - REDIS_HOST: paperless-ngx-redis + REDIS_HOST: paperless-ngx-redis-{{ paperless_user }} networks_cli_compatible: false networks: - name: bridgewithdns @@ -82,7 +132,7 @@ test: "redis-cli --raw incr ping" mounts: - type: bind - source: "{{ systemuserlist.paperless.home }}/redis/data" + source: "{{ systemuserlist.paperless.home }}/redis/data-{{ paperless_user }}" target: /data tags: - paperless-containers @@ -91,6 +141,53 @@ - paperless-ngx-redis - redis + # https://tika.apache.org/ + # used to convert office documents +- name: tika container for paperless-nx + docker_container: + name: paperless-ngx-tika + image: "ghcr.io/paperless-ngx/tika:latest" + restart_policy: "unless-stopped" + auto_remove: false + detach: true + pull: true + state: started + container_default_behavior: compatibility + networks_cli_compatible: false + networks: + - name: bridgewithdns + tags: + - paperless-containers + - paperless-ngx-containers + - docker-containers + - paperless-ngx-tika + - tika-container + + # https://gotenberg.dev/ + # also used for office documents, converting them +- name: gotenberg container for paperless-nx + docker_container: + name: paperless-ngx-gotenberg + image: "docker.io/gotenberg/gotenberg:7" + restart_policy: "unless-stopped" + auto_remove: false + detach: true + pull: true + state: started + container_default_behavior: compatibility + networks_cli_compatible: false + networks: + - name: bridgewithdns + env: + CHROMIUM_DISABLE_ROUTES: "1" + tags: + - paperless-containers + - paperless-ngx-containers + - docker-containers + - paperless-ngx-gotenberg + - gotenberg-container + + # https://github.com/paperless-ngx/paperless-ngx/blob/main/Dockerfile # uid stuff docs: https://paperless-ngx.readthedocs.io/en/latest/setup.html?highlight=usermap # uid stuff source: https://github.com/paperless-ngx/paperless-ngx/blob/main/docker/docker-entrypoint.sh#L37 @@ -124,25 +221,25 @@ ipv4_address: "{{ bridgewithdns['paperless-ngx-webserver'] }}" mounts: - type: bind - source: "{{ systemuserlist.paperless.home }}/paperless-ngx/data" + source: "{{ paperless_users_path }}/{{ paperless_user }}/data" target: /usr/src/paperless/data - type: bind - source: "{{ systemuserlist.paperless.home }}/paperless-ngx/media" + source: "{{ paperless_users_path }}/{{ paperless_user }}/media" target: /usr/src/paperless/media - type: bind - source: "{{ systemuserlist.paperless.home }}/paperless-ngx/export" + source: "{{ paperless_users_path }}/{{ paperless_user }}/export" target: /usr/src/paperless/export - type: bind - source: "{{ systemuserlist.paperless.home }}/paperless-ngx/consume" + source: "{{ paperless_users_path }}/{{ paperless_user }}/consume" target: /usr/src/paperless/consume + - type: bind + source: "{{ systemuserlist.paperless.home }}/paperless-ngx/bin" + target: /usr/src/paperless/bin/ env: - USERMAP_UID: "{{ systemuserlist.paperless.uid }}" - USERMAP_GID: "{{ systemuserlist.paperless.gid }}" + USERMAP_UID: "{{ userlist[paperless_user]['uid'] }}" + USERMAP_GID: "{{ userlist[paperless_user]['gid'] }}" PAPERLESS_URL: "https://{{ paperless_url }}" PAPERLESS_SECRET_KEY: "{{ paperless_secret_key }}" - PAPERLESS_OCR_LANGUAGES: "{{ paperless_ocr_langs }}" - PAPERLESS_OCR_LANGUAGE: "{{ paperless_ocr_default_lang }}" - PAPERLESS_REDIS: redis://paperless-ngx-redis:6379 PAPERLESS_DBENGINE: mariadb PAPERLESS_DBHOST: "{{ mariadb_host }}" PAPERLESS_DBNAME: "{{ mariadb_db }}" @@ -150,12 +247,42 @@ PAPERLESS_DBPASS: "{{ systemuserlist.paperless.mariadb_pass }}" PAPERLESS_DBPORT: "3306" PAPERLESS_TIME_ZONE: UTC + # FILES + PAPERLESS_FILENAME_FORMAT_REMOVE_NONE: "true" + PAPERLESS_TRASH_DIR: "../media/trash" + PAPERLESS_FILENAME_FORMAT: "{{ paperless_filename_format }}" + # OCR + # see: https://ocrmypdf.readthedocs.io/en/latest/api.html#reference + # PAPERLESS_OCR_USER_ARGS= + PAPERLESS_OCR_CLEAN: "clean" + PAPERLESS_OCR_MODE: "{{ paperless_ocr_mode }}" + # lang codes: https://www.loc.gov/standards/iso639-2/php/code_list.php + PAPERLESS_OCR_LANGUAGES: "{{ paperless_ocr_langs|join(' ') }}" + PAPERLESS_OCR_LANGUAGE: "{{ paperless_ocr_langs|join('+') }}" + # INITIAL ADMIN USER PAPERLESS_ADMIN_USER: "{{ paperless_admin_user }}" PAPERLESS_ADMIN_MAIL: "{{ paperless_admin_email }}" PAPERLESS_ADMIN_PASSWORD: "{{ paperless_admin_passwd }}" + # DATES + PAPERLESS_IGNORE_DATES: "{{ userlist[paperless_user]['birthday'] }},1970-01-01" + PAPERLESS_NUMBER_OF_SUGGESTED_DATES: "5" # AUTH PAPERLESS_ENABLE_HTTP_REMOTE_USER: "true" PAPERLESS_LOGOUT_REDIRECT_URL: "https://{{ authelia_login_url }}/logout" + # CONSUMER + PAPERLESS_POST_CONSUME_SCRIPT: /usr/src/paperless/bin/post-consume.py + PAPERLESS_PRE_CONSUME_SCRIPT: /usr/src/paperless/bin/pre-consume.py + PAPERLESS_CONSUMER_RECURSIVE: "true" + PAPERLESS_CONSUMER_SUBDIRS_AS_TAG: "true" + # (default) leave duplicates + PAPERLESS_CONSUMER_DELETE_DUPLICATES: "false" + # REDIS, TIKA, GOTENBERG + PAPERLESS_REDIS: redis://paperless-ngx-redis-{{ paperless_user }}:6379 + PAPERLESS_TIKA_ENABLED: "true" + PAPERLESS_TIKA_ENDPOINT: "http://paperless-ngx-tika:9998" + PAPERLESS_TIKA_GOTENBERG_ENDPOINT: "http://paperless-ngx-gotenberg:3000" + # CUSTOM + PAPERLESS_USER: "{{ paperless_user }}" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000"] interval: 30s @@ -163,6 +290,8 @@ retries: 5 tags: - paperless-containers + - paperless-container + - paperless-config - paperless-ngx-containers - paperless-ngx-container - docker-containers -- 2.40.1 From 3632e7c819d6b41efc17f730c1751955ac3d1c80 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Sat, 8 Oct 2022 06:25:56 +0200 Subject: [PATCH 2/7] nginx config kind of working --- roles/paperless-ngx/tasks/paperless-ngx.yml | 12 ++++++++---- roles/paperless-ngx/templates/01-paperless.j2 | 12 +++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/roles/paperless-ngx/tasks/paperless-ngx.yml b/roles/paperless-ngx/tasks/paperless-ngx.yml index 014ca49..ce975f9 100644 --- a/roles/paperless-ngx/tasks/paperless-ngx.yml +++ b/roles/paperless-ngx/tasks/paperless-ngx.yml @@ -58,6 +58,7 @@ group: 999 - name: paperless-ngx - name: paperless-ngx/bin + - name: paperless-www - name: ensure {{ paperless_users_path }} exists file: @@ -143,7 +144,7 @@ # https://tika.apache.org/ # used to convert office documents -- name: tika container for paperless-nx +- name: tika container for paperless-ngx docker_container: name: paperless-ngx-tika image: "ghcr.io/paperless-ngx/tika:latest" @@ -165,7 +166,7 @@ # https://gotenberg.dev/ # also used for office documents, converting them -- name: gotenberg container for paperless-nx +- name: gotenberg container for paperless-ngx docker_container: name: paperless-ngx-gotenberg image: "docker.io/gotenberg/gotenberg:7" @@ -236,8 +237,6 @@ source: "{{ systemuserlist.paperless.home }}/paperless-ngx/bin" target: /usr/src/paperless/bin/ env: - USERMAP_UID: "{{ userlist[paperless_user]['uid'] }}" - USERMAP_GID: "{{ userlist[paperless_user]['gid'] }}" PAPERLESS_URL: "https://{{ paperless_url }}" PAPERLESS_SECRET_KEY: "{{ paperless_secret_key }}" PAPERLESS_DBENGINE: mariadb @@ -247,6 +246,11 @@ PAPERLESS_DBPASS: "{{ systemuserlist.paperless.mariadb_pass }}" PAPERLESS_DBPORT: "3306" PAPERLESS_TIME_ZONE: UTC + # USER + USERMAP_UID: "{{ userlist[paperless_user]['uid'] }}" + USERMAP_GID: "{{ userlist[paperless_user]['gid'] }}" + PAPERLESS_FORCE_SCRIPT_NAME: "/{{ paperless_user }}" + PAPERLESS_STATIC_URL: "/{{ paperless_user }}/static/" # FILES PAPERLESS_FILENAME_FORMAT_REMOVE_NONE: "true" PAPERLESS_TRASH_DIR: "../media/trash" diff --git a/roles/paperless-ngx/templates/01-paperless.j2 b/roles/paperless-ngx/templates/01-paperless.j2 index 6974354..e04afb7 100644 --- a/roles/paperless-ngx/templates/01-paperless.j2 +++ b/roles/paperless-ngx/templates/01-paperless.j2 @@ -19,7 +19,17 @@ server { # real_ip_header X-Forwarded-For; # real_ip_recursive on; - location / { + + location = / { + + include /etc/nginx/require_auth.conf; + include /etc/nginx/require_auth_proxy.conf; + # temporary redirects dont get remembered by the browser + # and redirect issues are no fun + return 302 https://{{ paperless_url }}/{{ paperless_user }}/; + } + + location /{{ paperless_user }} { include /etc/nginx/require_auth.conf; include /etc/nginx/require_auth_proxy.conf; -- 2.40.1 From 6fbdf3f8540275edf9ed15ec54b41880baf3f532 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Sat, 8 Oct 2022 06:28:30 +0200 Subject: [PATCH 3/7] cleanup --- roles/paperless-ngx/tasks/paperless-ngx.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/paperless-ngx/tasks/paperless-ngx.yml b/roles/paperless-ngx/tasks/paperless-ngx.yml index ce975f9..ae1b23d 100644 --- a/roles/paperless-ngx/tasks/paperless-ngx.yml +++ b/roles/paperless-ngx/tasks/paperless-ngx.yml @@ -58,7 +58,6 @@ group: 999 - name: paperless-ngx - name: paperless-ngx/bin - - name: paperless-www - name: ensure {{ paperless_users_path }} exists file: @@ -141,6 +140,7 @@ - docker-containers - paperless-ngx-redis - redis + when: false # https://tika.apache.org/ # used to convert office documents -- 2.40.1 From d13b9ca0bc90b2844447a648f1c7b9761e877fc3 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Sat, 8 Oct 2022 06:29:13 +0200 Subject: [PATCH 4/7] more cleanup --- roles/paperless-ngx/tasks/paperless-ngx.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/paperless-ngx/tasks/paperless-ngx.yml b/roles/paperless-ngx/tasks/paperless-ngx.yml index ae1b23d..d9032fa 100644 --- a/roles/paperless-ngx/tasks/paperless-ngx.yml +++ b/roles/paperless-ngx/tasks/paperless-ngx.yml @@ -140,7 +140,6 @@ - docker-containers - paperless-ngx-redis - redis - when: false # https://tika.apache.org/ # used to convert office documents -- 2.40.1 From a3ce47433d83fe8b533836854883b53e11c5d8fb Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Fri, 14 Oct 2022 21:49:03 +0200 Subject: [PATCH 5/7] multiple paperless-ngx installs work --- roles/paperless-ngx/files/common_consume.py | 5 +- roles/paperless-ngx/files/favicon.ico | Bin 0 -> 111014 bytes roles/paperless-ngx/handlers/main.yml | 5 ++ roles/paperless-ngx/tasks/main.yml | 4 +- roles/paperless-ngx/tasks/paperless-ngx.yml | 83 ++++++++++++++++-- roles/paperless-ngx/templates/01-paperless.j2 | 65 +++++++++++--- .../templates/filebeat-paperless.yml.j2 | 38 ++++++++ .../templates/paperless_user.html.j2 | 41 +++++++++ roles/paperless-ngx/templates/whoami.json.j2 | 1 + 9 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 roles/paperless-ngx/files/favicon.ico create mode 100644 roles/paperless-ngx/templates/filebeat-paperless.yml.j2 create mode 100644 roles/paperless-ngx/templates/paperless_user.html.j2 create mode 100644 roles/paperless-ngx/templates/whoami.json.j2 diff --git a/roles/paperless-ngx/files/common_consume.py b/roles/paperless-ngx/files/common_consume.py index 147bd67..6e3209a 100644 --- a/roles/paperless-ngx/files/common_consume.py +++ b/roles/paperless-ngx/files/common_consume.py @@ -15,13 +15,14 @@ def logger(env_vars, consume_stage): log_item = {k.lower(): environ.get(k) for k in env_vars} log_item.update({ "timestamp": datetime.now().isoformat(), - "user": environ.get("PAPERLESS_USER"), + "paperless_user": environ.get("PAPERLESS_USER"), "log_path": log_path, "paperless_consume_stage": consume_stage }) with open(log_path, 'a') as f: - j = json.dumps(log_item, indent=2) + #j = json.dumps(log_item, indent=2) + j = json.dumps(log_item) f.write(j) f.write("\n") diff --git a/roles/paperless-ngx/files/favicon.ico b/roles/paperless-ngx/files/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cb57d8b2b1e8b91c9aba08f0b59caa0f4f8808dc GIT binary patch literal 111014 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 @@ -314,10 +318,67 @@ with_items: - "{{ paperless_url }}" +- name: make www dirs + file: + state: directory + path: /var/www/{{ item }} + owner: www-data + group: www-data + mode: 0755 + loop_control: + label: /var/www/{{ item }} + with_items: + - "{{ paperless_url }}" + tags: + - paperless-nginx + +- name: template index file for user if enabled + template: + src: paperless_user.html.j2 + dest: /var/www/{{ paperless_url }}/{{ paperless_user }}.html + owner: www-data + group: www-data + mode: 0755 + tags: + - paperless-nginx + when: + - paperless_user_index_file|default(true) + +- name: remove index files for user if not enabled + file: + state: absent + dest: /var/www/{{ paperless_url }}/{{ paperless_user }}.html + tags: + - paperless-nginx + when: + - not paperless_user_index_file|default(true) + +- name: template whoami.json + template: + src: "{{ item }}.j2" + dest: /var/www/{{ paperless_url }}/{{ item }} + owner: www-data + group: www-data + mode: 0644 + with_items: + - whoami.json + tags: + - paperless-nginx + +- name: add favicon + copy: + src: favicon.ico + dest: /var/www/{{ paperless_url }}/favicon.ico + owner: www-data + group: www-data + mode: 0755 + tags: + - paperless-nginx + - name: template nginx vhost for paperless template: src: 01-paperless.j2 - dest: /etc/nginx/sites-enabled/01-paperless + dest: /etc/nginx/sites-enabled/01-{{ paperless_url }} owner: root group: root mode: 0644 @@ -325,3 +386,15 @@ - nginx - paperless-nginx notify: reload nginx + +- name: template filebeat config + template: + src: filebeat-paperless.yml.j2 + dest: "/etc/filebeat/inputs.d/paperless-{{ paperless_user }}.yml" + owner: root + group: root + mode: 0644 + tags: + - filebeat + - filebeat-paperless-ngx + notify: restart filebeat diff --git a/roles/paperless-ngx/templates/01-paperless.j2 b/roles/paperless-ngx/templates/01-paperless.j2 index e04afb7..c962745 100644 --- a/roles/paperless-ngx/templates/01-paperless.j2 +++ b/roles/paperless-ngx/templates/01-paperless.j2 @@ -1,3 +1,14 @@ +map $paper $paperless_upstream { + 'ben' {{ bridgewithdns['paperless-ngx-webserver'] }}:8000; + #default localhost:8000; +} + +map $paper $paper_url { + 'ben' 'ben/'; + default ''; +} + + server { listen 443 ssl http2; {% if inventory_hostname in wg_clients -%} @@ -5,12 +16,12 @@ server { {% endif -%} include /etc/nginx/authelia_internal.conf; - include listen-proxy-protocol.conf; include /etc/nginx/sudo-known.conf; server_name {{ paperless_url }}; + resolver {{ pihole_dns }} ipv6=off; # set_real_ip_from 10.0.0.0/8; # set_real_ip_from 172.16.0.0/12; @@ -20,21 +31,53 @@ server { # real_ip_recursive on; - location = / { + root /var/www/{{ paperless_url }}; + + include /etc/nginx/require_auth.conf; + location = / { + add_before_body /.sudo-known/header.html; + add_after_body /.sudo-known/footer.html; + + #set $paper $authelia_user; + add_header "paperless-user" $authelia_user always; + add_header "paperless-proxy" "false" always; + add_header "paperless-default" "false" always; + #try_files $uri /_redirect?paper_user=$authelia_user; + try_files /$authelia_user.html $uri /_redirect?paper_user=$authelia_user; + + } + location / { + # this block serves files from the www root (/whoami, mostly). + # it doesnt have the try_files directive, so it will not forward + # or redirect. e.g. /foo.html, /foo will 404. + add_header "paperless-user" $authelia_user always; + add_header "paperless-proxy" "false" always; + add_header "paperless-default" "true" always; - include /etc/nginx/require_auth.conf; - include /etc/nginx/require_auth_proxy.conf; - # temporary redirects dont get remembered by the browser - # and redirect issues are no fun - return 302 https://{{ paperless_url }}/{{ paperless_user }}/; } - location /{{ paperless_user }} { - include /etc/nginx/require_auth.conf; + location /_redirect { + internal; + return 302 /$arg_paper_user/; + } + + + # {{ paperless_users }} + + location ~* ^/(\w+)/(.*)$ { include /etc/nginx/require_auth_proxy.conf; - proxy_pass http://{{ bridgewithdns['paperless-ngx-webserver'] }}:8000; + # both work! + #set $paper $authelia_user; + set $paper $1; + add_header "paperless-user" $paper always; + add_header "paperless-proxy" "true" always; + # this also works! (but not if you use return) + #add_header "paperless-authelia-user" $authelia_user always; + + # rewrite ^ $request_uri; + rewrite '^/\w*(/ws/.*)$' $1 break; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -44,6 +87,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; + + proxy_pass http://$paperless_upstream; } access_log /var/log/nginx/access_{{ paperless_url }}.log main; diff --git a/roles/paperless-ngx/templates/filebeat-paperless.yml.j2 b/roles/paperless-ngx/templates/filebeat-paperless.yml.j2 new file mode 100644 index 0000000..2dee0be --- /dev/null +++ b/roles/paperless-ngx/templates/filebeat-paperless.yml.j2 @@ -0,0 +1,38 @@ +- type: filestream + paths: + - "{{ systemuserlist.paperless.home }}/paperless-ngx/data/{{ paperless_user }}/log/consume.log" + + scan_frequency: 10s + enabled: true + + parsers: + - ndjson: + keys_under_root: true + add_error_key: true + + fields_under_root: true + fields: + service.type: paperless + consume: true + #paperless_user: "{{ paperless_user }}" + + tags: + - paperless + - consumer + + +- type: filestream + paths: + - "{{ systemuserlist.paperless.home }}/paperless-ngx/data/{{ paperless_user }}/log/paperless.log" + - "{{ systemuserlist.paperless.home }}/paperless-ngx/data/{{ paperless_user }}/log/mail.log" + + scan_frequency: 10s + enabled: true + + fields_under_root: true + fields: + service.type: paperless + paperless_user: "{{ paperless_user }}" + + tags: + - paperless diff --git a/roles/paperless-ngx/templates/paperless_user.html.j2 b/roles/paperless-ngx/templates/paperless_user.html.j2 new file mode 100644 index 0000000..0e28fd5 --- /dev/null +++ b/roles/paperless-ngx/templates/paperless_user.html.j2 @@ -0,0 +1,41 @@ + +
+
{{ paperless_url }} | {{ paperless_user }}
+ + +
+ +
+
> shared
+ +
+ +{{ inventory_hostname }} + + diff --git a/roles/paperless-ngx/templates/whoami.json.j2 b/roles/paperless-ngx/templates/whoami.json.j2 new file mode 100644 index 0000000..c744123 --- /dev/null +++ b/roles/paperless-ngx/templates/whoami.json.j2 @@ -0,0 +1 @@ +{{ {} | to_nice_json() }} -- 2.40.1 From 2ee93778ce6ee388d184a7444c8627dc86975c2f Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Fri, 14 Oct 2022 22:22:22 +0200 Subject: [PATCH 6/7] add header and footer to .sudo-known for add_after_body and add_before_body --- roles/nginx/tasks/nginx.yml | 1 + roles/nginx/templates/00-default.j2 | 24 ++++++++++++------------ roles/nginx/templates/nginx.conf.j2 | 12 ++++++++++++ roles/nginx/templates/sudo-known.conf.j2 | 9 +++++++++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/roles/nginx/tasks/nginx.yml b/roles/nginx/tasks/nginx.yml index a6e564d..f8357c5 100644 --- a/roles/nginx/tasks/nginx.yml +++ b/roles/nginx/tasks/nginx.yml @@ -86,6 +86,7 @@ - nginx-conf - authelia-nginx - well-known + - nginx-well-known - gitea-nginx notify: reload nginx diff --git a/roles/nginx/templates/00-default.j2 b/roles/nginx/templates/00-default.j2 index b01c46f..6289d39 100644 --- a/roles/nginx/templates/00-default.j2 +++ b/roles/nginx/templates/00-default.j2 @@ -14,20 +14,20 @@ server { server_name {{ inventory_hostname }}; include /etc/nginx/authelia_internal.conf; - location = /server_status { - stub_status; + location = /server_status { + stub_status; - access_log off; + access_log off; - allow 127.0.0.1; - {% if 'address' in ansible_default_ipv4 -%} - allow {{ ansible_default_ipv4.address }}; - {% endif -%} - {% if ansible_default_ipv6 is defined and 'address' in ansible_default_ipv6 -%} - allow {{ ansible_default_ipv6.address }}; - {% endif -%} - allow {{ bridgewithdns_cidr }}; - deny all; + allow 127.0.0.1; + {% if 'address' in ansible_default_ipv4 -%} + allow {{ ansible_default_ipv4.address }}; + {% endif -%} + {% if ansible_default_ipv6 is defined and 'address' in ansible_default_ipv6 -%} + allow {{ ansible_default_ipv6.address }}; + {% endif -%} + allow {{ bridgewithdns_cidr }}; + deny all; } diff --git a/roles/nginx/templates/nginx.conf.j2 b/roles/nginx/templates/nginx.conf.j2 index 0230d49..204f999 100644 --- a/roles/nginx/templates/nginx.conf.j2 +++ b/roles/nginx/templates/nginx.conf.j2 @@ -18,6 +18,15 @@ http { # nginx hack # if $authelia_user doesnt exist, set it to empty string # if $authelia_user does exist, do nothing + # map $host $authelia_user { + # "" ""; + # default $authelia_user; + + # } + # map $host $authelia_groups { + # "" ""; + # default $authelia_groups; + # } map $host $authelia_user { default ""; } @@ -33,6 +42,9 @@ http { ' "request": "$request", ' ' "request_method": "$request_method", ' ' "request_uri": "$request_uri", ' + ' "uri": "$uri", ' + ' "http_connection": "$http_connection", ' + ' "http_upgrade": "$http_upgrade", ' ' "server_name": "$server_name", ' ' "server_port": "$server_port", ' ' "status": "$status", ' diff --git a/roles/nginx/templates/sudo-known.conf.j2 b/roles/nginx/templates/sudo-known.conf.j2 index a4f40ce..09fa18c 100644 --- a/roles/nginx/templates/sudo-known.conf.j2 +++ b/roles/nginx/templates/sudo-known.conf.j2 @@ -23,3 +23,12 @@ location = /.sudo-known/info.html { default_type text/html; return 200 '\n\n '; } + +location = /.sudo-known/header.html { + default_type text/html; + alias /var/www/shared/header.html; +} +location = /.sudo-known/footer.html { + default_type text/html; + alias /var/www/shared/footer.html; +} -- 2.40.1 From 708c383d0758b786865fe5b805761048ed5e4024 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Wed, 26 Oct 2022 01:27:03 +0200 Subject: [PATCH 7/7] use env file, refined the 'redirect to paperless' logic --- roles/paperless-ngx/defaults/main.yml | 3 + roles/paperless-ngx/handlers/main.yml | 6 ++ roles/paperless-ngx/tasks/paperless-ngx.yml | 71 ++++------------ .../templates/01-paperless-same-urls.j2 | 80 +++++++++++++++++++ roles/paperless-ngx/templates/01-paperless.j2 | 74 ++++++++--------- .../templates/paperless-ngx.env.j2 | 52 ++++++++++++ 6 files changed, 195 insertions(+), 91 deletions(-) create mode 100644 roles/paperless-ngx/defaults/main.yml create mode 100644 roles/paperless-ngx/templates/01-paperless-same-urls.j2 create mode 100644 roles/paperless-ngx/templates/paperless-ngx.env.j2 diff --git a/roles/paperless-ngx/defaults/main.yml b/roles/paperless-ngx/defaults/main.yml new file mode 100644 index 0000000..868d459 --- /dev/null +++ b/roles/paperless-ngx/defaults/main.yml @@ -0,0 +1,3 @@ +--- + +paperless_user_specific_urls: true diff --git a/roles/paperless-ngx/handlers/main.yml b/roles/paperless-ngx/handlers/main.yml index 6392a1b..96a1772 100644 --- a/roles/paperless-ngx/handlers/main.yml +++ b/roles/paperless-ngx/handlers/main.yml @@ -9,3 +9,9 @@ service: name: filebeat state: restarted + +- name: restart paperless-ngx + docker_container: + name: paperless-ngx-user-{{ paperless_user }} + state: started + restart: true diff --git a/roles/paperless-ngx/tasks/paperless-ngx.yml b/roles/paperless-ngx/tasks/paperless-ngx.yml index b46a64f..cd38c92 100644 --- a/roles/paperless-ngx/tasks/paperless-ngx.yml +++ b/roles/paperless-ngx/tasks/paperless-ngx.yml @@ -190,6 +190,15 @@ - paperless-ngx-gotenberg - gotenberg-container +- name: template {{ paperless_user }}.env + template: + src: paperless-ngx.env.j2 + dest: "{{ systemuserlist.paperless.home }}/paperless-ngx/{{ paperless_user }}.env" + owner: root + group: root + mode: 0750 + tags: + - paperless-config # https://github.com/paperless-ngx/paperless-ngx/blob/main/Dockerfile # uid stuff docs: https://paperless-ngx.readthedocs.io/en/latest/setup.html?highlight=usermap @@ -239,57 +248,7 @@ - type: bind source: "{{ systemuserlist.paperless.home }}/paperless-ngx/bin" target: /usr/src/paperless/bin/ - env: - PAPERLESS_URL: "https://{{ paperless_url }}" - PAPERLESS_SECRET_KEY: "{{ paperless_secret_key }}" - PAPERLESS_DBENGINE: mariadb - PAPERLESS_DBHOST: "{{ mariadb_host }}" - PAPERLESS_DBNAME: "{{ mariadb_db }}" - PAPERLESS_DBUSER: "{{ systemuserlist.paperless.username }}" - PAPERLESS_DBPASS: "{{ systemuserlist.paperless.mariadb_pass }}" - PAPERLESS_DBPORT: "3306" - PAPERLESS_TIME_ZONE: UTC - # USER - USERMAP_UID: "{{ userlist[paperless_user]['uid'] }}" - USERMAP_GID: "{{ userlist[paperless_user]['gid'] }}" - PAPERLESS_FORCE_SCRIPT_NAME: "/{{ paperless_user }}" - PAPERLESS_STATIC_URL: "/{{ paperless_user }}/static/" - # FILES - PAPERLESS_FILENAME_FORMAT_REMOVE_NONE: "true" - PAPERLESS_TRASH_DIR: "../media/trash" - #PAPERLESS_FILENAME_FORMAT: "{{ paperless_filename_format }}" - # OCR - # see: https://ocrmypdf.readthedocs.io/en/latest/api.html#reference - # PAPERLESS_OCR_USER_ARGS= - PAPERLESS_OCR_CLEAN: "clean" - PAPERLESS_OCR_MODE: "{{ paperless_ocr_mode }}" - # lang codes: https://www.loc.gov/standards/iso639-2/php/code_list.php - PAPERLESS_OCR_LANGUAGES: "{{ paperless_ocr_langs|join(' ') }}" - PAPERLESS_OCR_LANGUAGE: "{{ paperless_ocr_langs|join('+') }}" - # INITIAL ADMIN USER - PAPERLESS_ADMIN_USER: "{{ paperless_admin_user }}" - PAPERLESS_ADMIN_MAIL: "{{ paperless_admin_email }}" - PAPERLESS_ADMIN_PASSWORD: "{{ paperless_admin_passwd }}" - # DATES - PAPERLESS_IGNORE_DATES: "{{ userlist[paperless_user]['birthday'] }},1970-01-01" - PAPERLESS_NUMBER_OF_SUGGESTED_DATES: "5" - # AUTH - PAPERLESS_ENABLE_HTTP_REMOTE_USER: "true" - PAPERLESS_LOGOUT_REDIRECT_URL: "https://{{ authelia_login_url }}/logout" - # CONSUMER - PAPERLESS_POST_CONSUME_SCRIPT: /usr/src/paperless/bin/post-consume.py - PAPERLESS_PRE_CONSUME_SCRIPT: /usr/src/paperless/bin/pre-consume.py - PAPERLESS_CONSUMER_RECURSIVE: "true" - PAPERLESS_CONSUMER_SUBDIRS_AS_TAG: "true" - # (default) leave duplicates - PAPERLESS_CONSUMER_DELETE_DUPLICATES: "false" - # REDIS, TIKA, GOTENBERG - PAPERLESS_REDIS: redis://paperless-ngx-redis-{{ paperless_user }}:6379 - PAPERLESS_TIKA_ENABLED: "true" - PAPERLESS_TIKA_ENDPOINT: "http://paperless-ngx-tika:9998" - PAPERLESS_TIKA_GOTENBERG_ENDPOINT: "http://paperless-ngx-gotenberg:3000" - # CUSTOM - PAPERLESS_USER: "{{ paperless_user }}" + env_file: "{{ systemuserlist.paperless.home }}/paperless-ngx/{{ paperless_user }}.env" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000"] interval: 30s @@ -329,10 +288,12 @@ label: /var/www/{{ item }} with_items: - "{{ paperless_url }}" + # helper dir for try_file + - "{{ paperless_url }}/{{ paperless_user }}" tags: - paperless-nginx -- name: template index file for user if enabled +- name: template index file for user if user specific urls template: src: paperless_user.html.j2 dest: /var/www/{{ paperless_url }}/{{ paperless_user }}.html @@ -342,16 +303,16 @@ tags: - paperless-nginx when: - - paperless_user_index_file|default(true) + - paperless_user_specific_urls -- name: remove index files for user if not enabled +- name: remove index files for user if not user specific urls file: state: absent dest: /var/www/{{ paperless_url }}/{{ paperless_user }}.html tags: - paperless-nginx when: - - not paperless_user_index_file|default(true) + - not paperless_user_specific_urls - name: template whoami.json template: diff --git a/roles/paperless-ngx/templates/01-paperless-same-urls.j2 b/roles/paperless-ngx/templates/01-paperless-same-urls.j2 new file mode 100644 index 0000000..ff37370 --- /dev/null +++ b/roles/paperless-ngx/templates/01-paperless-same-urls.j2 @@ -0,0 +1,80 @@ +map $authelia_user $paperless_upstream { + ben {{ bridgewithdns['paperless-ngx-webserver'] }}:8000; + #default localhost:8000; +} + +# cant use variables in the regex of a map +map $uri $paperless_uri { + '/$authelia_user' '/$authelia_user/'; +} + +server { + listen 443 ssl http2; + {% if inventory_hostname in wg_clients -%} + listen {{ wg_clients[inventory_hostname].ip }}:443 ssl http2; + {% endif -%} + + root /var/www/{{ paperless_url }}; + server_name {{ paperless_url }}; + + include listen-proxy-protocol.conf; + include /etc/nginx/authelia_internal.conf; + include /etc/nginx/sudo-known.conf; + + resolver {{ pihole_dns }} ipv6=off; + + # set_real_ip_from 10.0.0.0/8; + # set_real_ip_from 172.16.0.0/12; + # set_real_ip_from 192.168.0.0/16; + # set_real_ip_from fc00::/7; + # real_ip_header X-Forwarded-For; + # real_ip_recursive on; + + + include /etc/nginx/require_auth.conf; + if ($paperless_uri) + { + rewrite ^/(\w+)$ $1/ last; + } + + location / { + include /etc/nginx/require_auth_proxy.conf; + + # both work! + set $paperless_user $authelia_user; + #set $paperless_user $1; + + # this also works! (but not if you use return) + #add_header "paperless-authelia-user" $authelia_user always; + + set $paperless_user $authelia_user; + add_header "paperless-user" $authelia_user always; + add_header "paperless-uri" $uri always; + add_header "paperless-proxy" "true" always; + add_header "paperless-location-root" "true" always; + add_header "paperless-upstream" $paperless_upstream always; + + # rewrite ^ $request_uri; + #rewrite '^/\w*(/ws/.*)$' $1 break; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + proxy_pass http://$paperless_upstream; + } + + access_log /var/log/nginx/access_{{ paperless_url }}.log main; + error_log /var/log/nginx/error_{{ paperless_url }}.log warn; + + ssl_session_timeout 5m; + ssl_certificate /usr/local/etc/certs/{{ paperless_url }}/fullchain.pem; + ssl_certificate_key /usr/local/etc/certs/{{ paperless_url }}/privkey.pem; + + fastcgi_hide_header X-Powered-By; +} diff --git a/roles/paperless-ngx/templates/01-paperless.j2 b/roles/paperless-ngx/templates/01-paperless.j2 index c962745..90b1cbd 100644 --- a/roles/paperless-ngx/templates/01-paperless.j2 +++ b/roles/paperless-ngx/templates/01-paperless.j2 @@ -1,26 +1,20 @@ -map $paper $paperless_upstream { - 'ben' {{ bridgewithdns['paperless-ngx-webserver'] }}:8000; - #default localhost:8000; +map $authelia_user $paperless_upstream { + {{ paperless_user }} {{ bridgewithdns['paperless-ngx-webserver'] }}:8000; } -map $paper $paper_url { - 'ben' 'ben/'; - default ''; -} - - server { listen 443 ssl http2; {% if inventory_hostname in wg_clients -%} listen {{ wg_clients[inventory_hostname].ip }}:443 ssl http2; {% endif -%} - include /etc/nginx/authelia_internal.conf; - include listen-proxy-protocol.conf; - include /etc/nginx/sudo-known.conf; - + root /var/www/{{ paperless_url }}; server_name {{ paperless_url }}; + include listen-proxy-protocol.conf; + include /etc/nginx/authelia_internal.conf; + include /etc/nginx/sudo-known.conf; + resolver {{ pihole_dns }} ipv6=off; # set_real_ip_from 10.0.0.0/8; @@ -30,54 +24,57 @@ server { # real_ip_header X-Forwarded-For; # real_ip_recursive on; - - root /var/www/{{ paperless_url }}; - include /etc/nginx/require_auth.conf; + location = / { add_before_body /.sudo-known/header.html; add_after_body /.sudo-known/footer.html; - #set $paper $authelia_user; add_header "paperless-user" $authelia_user always; + add_header "paperless-uri" $uri always; add_header "paperless-proxy" "false" always; - add_header "paperless-default" "false" always; - #try_files $uri /_redirect?paper_user=$authelia_user; - try_files /$authelia_user.html $uri /_redirect?paper_user=$authelia_user; + add_header "paperless-location-root" "true" always; + + # if there is no file '$authelia_user.html', nginx issues + # a redirect to /$authelia_user/ instead (via an internal + # location) + try_files /$authelia_user.html /_redirect?user=$authelia_user; } location / { - # this block serves files from the www root (/whoami, mostly). - # it doesnt have the try_files directive, so it will not forward - # or redirect. e.g. /foo.html, /foo will 404. - add_header "paperless-user" $authelia_user always; - add_header "paperless-proxy" "false" always; - add_header "paperless-default" "true" always; + # this block serves files from the www root (/whoami, mostly), unless + # there is a directory with the same name as $uri is looking for (without + # the leading /, then it gets caught by the regexp location), then it will + # redirect to $uri/ which should be caught by the regexp block, otherwise + # a 404 is returned. + # theres no logic in the nginx config for this, it just depends on try_files + # finding a dir with the matching name, then nginx will issue a redirect, and + # is probably expecting to serve up files from that dir next. + add_header "paperless-user" $authelia_user always; + add_header "paperless-uri" $uri always; + add_header "paperless-proxy" "false" always; + add_header "paperless-location-root" "false" always; + + try_files $uri $uri/ =404; } location /_redirect { internal; - return 302 /$arg_paper_user/; } - - # {{ paperless_users }} - - location ~* ^/(\w+)/(.*)$ { + location ~* ^/(?\w+)/(.*)$ { include /etc/nginx/require_auth_proxy.conf; # both work! - #set $paper $authelia_user; - set $paper $1; - add_header "paperless-user" $paper always; - add_header "paperless-proxy" "true" always; - + #set $paperless_user $authelia_user; + #set $paperless_user $1; # this also works! (but not if you use return) #add_header "paperless-authelia-user" $authelia_user always; # rewrite ^ $request_uri; rewrite '^/\w*(/ws/.*)$' $1 break; + proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -88,6 +85,11 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; + add_header "paperless-user" $authelia_user always; + add_header "paperless-uri" $uri always; + add_header "paperless-proxy" "true" always; + add_header "paperless-upstream" $paperless_upstream always; + proxy_pass http://$paperless_upstream; } diff --git a/roles/paperless-ngx/templates/paperless-ngx.env.j2 b/roles/paperless-ngx/templates/paperless-ngx.env.j2 new file mode 100644 index 0000000..8422d0b --- /dev/null +++ b/roles/paperless-ngx/templates/paperless-ngx.env.j2 @@ -0,0 +1,52 @@ +PAPERLESS_URL=https://{{ paperless_url }} +PAPERLESS_SECRET_KEY={{ paperless_secret_key }} +PAPERLESS_DBENGINE=mariadb +PAPERLESS_DBHOST={{ mariadb_host }} +PAPERLESS_DBNAME={{ mariadb_db }} +PAPERLESS_DBUSER={{ systemuserlist.paperless.username }} +PAPERLESS_DBPASS={{ systemuserlist.paperless.mariadb_pass }} +PAPERLESS_DBPORT=3306 +PAPERLESS_TIME_ZONE=UTC +# USER +USERMAP_UID={{ userlist[paperless_user]['uid'] }} +USERMAP_GID={{ userlist[paperless_user]['gid'] }} +{% if paperless_user_specific_urls -%} +PAPERLESS_FORCE_SCRIPT_NAME=/{{ paperless_user }} +PAPERLESS_STATIC_URL=/{{ paperless_user }}/static/ +{% endif %} +# FILES +PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=true +PAPERLESS_TRASH_DIR=../media/trash +#PAPERLESS_FILENAME_FORMAT={{ paperless_filename_format }} +# OCR +# see=https://ocrmypdf.readthedocs.io/en/latest/api.html#reference +# PAPERLESS_OCR_USER_ARGS= +PAPERLESS_OCR_CLEAN=clean +PAPERLESS_OCR_MODE={{ paperless_ocr_mode }} +# lang codes=https://www.loc.gov/standards/iso639-2/php/code_list.php +PAPERLESS_OCR_LANGUAGES={{ paperless_ocr_langs|join(' ') }} +PAPERLESS_OCR_LANGUAGE={{ paperless_ocr_langs|join('+') }} +# INITIAL ADMIN USER +PAPERLESS_ADMIN_USER={{ paperless_admin_user }} +PAPERLESS_ADMIN_MAIL={{ paperless_admin_email }} +PAPERLESS_ADMIN_PASSWORD={{ paperless_admin_passwd }} +# DATES +PAPERLESS_IGNORE_DATES={{ userlist[paperless_user]['birthday'] }},1970-01-01 +PAPERLESS_NUMBER_OF_SUGGESTED_DATES=5 +# AUTH +PAPERLESS_ENABLE_HTTP_REMOTE_USER=true +PAPERLESS_LOGOUT_REDIRECT_URL=https://{{ authelia_login_url }}/logout +# CONSUMER +PAPERLESS_POST_CONSUME_SCRIPT=/usr/src/paperless/bin/post-consume.py +PAPERLESS_PRE_CONSUME_SCRIPT=/usr/src/paperless/bin/pre-consume.py +PAPERLESS_CONSUMER_RECURSIVE=true +PAPERLESS_CONSUMER_SUBDIRS_AS_TAG=true +# (default) leave duplicates +PAPERLESS_CONSUMER_DELETE_DUPLICATES=false +# REDIS, TIKA, GOTENBERG +PAPERLESS_REDIS=redis://paperless-ngx-redis-{{ paperless_user }}:6379 +PAPERLESS_TIKA_ENABLED=true +PAPERLESS_TIKA_ENDPOINT=http://paperless-ngx-tika:9998 +PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://paperless-ngx-gotenberg:3000 +# CUSTOM +PAPERLESS_USER={{ paperless_user }} -- 2.40.1