This commit is contained in:
2025-12-05 10:55:37 +01:00
commit 396e290394
1423 changed files with 825479 additions and 0 deletions

34
.codecov.yml Normal file
View File

@@ -0,0 +1,34 @@
codecov:
require_ci_to_pass: true
# https://docs.codecov.com/docs/components
component_management:
individual_components:
- component_id: backend
paths:
- src/**
- component_id: frontend
paths:
- src-ui/**
# https://docs.codecov.com/docs/pull-request-comments
comment:
layout: "header, diff, components, flags, files"
# https://docs.codecov.com/docs/javascript-bundle-analysis
require_bundle_changes: true
bundle_change_threshold: "50Kb"
coverage:
status:
project:
default:
# https://docs.codecov.com/docs/commit-status#threshold
threshold: 1%
patch:
default:
# For the changed lines only, target 100% covered, but
# allow as low as 75%
target: 100%
threshold: 25%
# https://docs.codecov.com/docs/javascript-bundle-analysis
bundle_analysis:
# Fail if the bundle size increases by more than 1MB
warning_threshold: "1MB"
status: true

175
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,175 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim as main-app
ARG DEBIAN_FRONTEND=noninteractive
# Buildx provided, must be defined to use though
ARG TARGETARCH
# Can be workflow provided, defaults set for manual building
ARG JBIG2ENC_VERSION=0.29
ARG QPDF_VERSION=11.9.0
ARG GS_VERSION=10.03.1
# Set Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Ignore warning from Whitenoise
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1
#
# Begin installation and configuration
# Order the steps below from least often changed to most
#
# Packages need for running
ARG RUNTIME_PACKAGES="\
# General utils
curl \
# Docker specific
gosu \
# Timezones support
tzdata \
# fonts for text file thumbnail generation
fonts-liberation \
gettext \
ghostscript \
gnupg \
icc-profiles-free \
imagemagick \
# PostgreSQL
postgresql-client \
# MySQL / MariaDB
mariadb-client \
# OCRmyPDF dependencies
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-deu \
tesseract-ocr-fra \
tesseract-ocr-ita \
tesseract-ocr-spa \
unpaper \
pngquant \
jbig2dec \
# lxml
libxml2 \
libxslt1.1 \
# itself
qpdf \
# Mime type detection
file \
libmagic1 \
media-types \
zlib1g \
# Barcode splitter
libzbar0 \
poppler-utils \
htop \
sudo"
# Install basic runtime packages.
# These change very infrequently
RUN set -eux \
echo "Installing system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES}
ARG PYTHON_PACKAGES="ca-certificates"
RUN set -eux \
echo "Installing python packages" \
&& apt-get update \
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
COPY --from=ghcr.io/astral-sh/uv:0.7.8 /uv /bin/uv
RUN set -eux \
&& echo "Installing pre-built updates" \
&& echo "Installing qpdf ${QPDF_VERSION}" \
&& curl --fail --silent --show-error --location \
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& curl --fail --silent --show-error --location \
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& echo "Installing Ghostscript ${GS_VERSION}" \
&& curl --fail --silent --show-error --location \
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& curl --fail --silent --show-error --location \
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& curl --fail --silent --show-error --location \
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& echo "Installing jbig2enc" \
&& curl --fail --silent --show-error --location \
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb
# setup docker-specific things
# These change sometimes, but rarely
WORKDIR /usr/src/paperless/src/docker/
COPY [ \
"docker/rootfs/etc/ImageMagick-6/paperless-policy.xml", \
"./" \
]
RUN set -eux \
&& echo "Configuring ImageMagick" \
&& mv paperless-policy.xml /etc/ImageMagick-6/policy.xml
# Packages needed only for building a few quick Python
# dependencies
ARG BUILD_PACKAGES="\
build-essential \
git \
# https://www.psycopg.org/docs/install.html#prerequisites
libpq-dev \
# https://github.com/PyMySQL/mysqlclient#linux
default-libmysqlclient-dev \
pkg-config"
# hadolint ignore=DL3042
RUN --mount=type=cache,target=/root/.cache/uv,id=pip-cache \
set -eux \
&& echo "Installing build system packages" \
&& apt-get update \
&& apt-get install --yes --quiet ${BUILD_PACKAGES}
RUN set -eux \
&& npm update -g pnpm
# add users, setup scripts
# Mount the compiled frontend to expected location
RUN set -eux \
&& echo "Setting up user/group" \
&& groupmod --new-name paperless node \
&& usermod --login paperless --home /usr/src/paperless node \
&& usermod -s /bin/bash paperless \
&& echo "paperless ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
&& echo "Creating volume directories" \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/data \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/media \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/consume \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/export \
&& mkdir --parents --verbose /usr/src/paperless/paperless-ngx/.venv \
&& echo "Adjusting all permissions" \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless
VOLUME ["/usr/src/paperless/paperless-ngx/data", \
"/usr/src/paperless/paperless-ngx/media", \
"/usr/src/paperless/paperless-ngx/consume", \
"/usr/src/paperless/paperless-ngx/export", \
"/usr/src/paperless/paperless-ngx/.venv"]

94
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,94 @@
# Paperless-ngx Development Environment
## Overview
Welcome to the Paperless-ngx development environment! This setup uses VSCode DevContainers to provide a consistent and seamless development experience.
### What are DevContainers?
DevContainers are a feature in VSCode that allows you to develop within a Docker container. This ensures that your development environment is consistent across different machines and setups. By defining a containerized environment, you can eliminate the "works on my machine" problem.
### Advantages of DevContainers
- **Consistency**: Same environment for all developers.
- **Isolation**: Separate development environment from your local machine.
- **Reproducibility**: Easily recreate the environment on any machine.
- **Pre-configured Tools**: Include all necessary tools and dependencies in the container.
## DevContainer Setup
The DevContainer configuration provides up all the necessary services for Paperless-ngx, including:
- Redis
- Gotenberg
- Tika
Data is stored using Docker volumes to ensure persistence across container restarts.
## Configuration Files
The setup includes debugging configurations (`launch.json`) and tasks (`tasks.json`) to help you manage and debug various parts of the project:
- **Backend Debugging:**
- `manage.py runserver`
- `manage.py document-consumer`
- `celery`
- **Maintenance Tasks:**
- Create superuser
- Run migrations
- Recreate virtual environment (`.venv` with `uv`)
- Compile frontend assets
## Getting Started
### Step 1: Running the DevContainer
To start the DevContainer:
1. Open VSCode.
2. Open the project folder.
3. Open the command palette and choose `Dev Containers: Rebuild and Reopen in Container`.
VSCode will build and start the DevContainer environment.
### Step 2: Initial Setup
Once the DevContainer is up and running, run the `Project Setup: Run all Init Tasks` task to initialize the project.
Alternatively, the Project Setup can be done with individual tasks:
1. **Compile Frontend Assets**: `Maintenance: Compile frontend for production`.
2. **Run Database Migrations**: `Maintenance: manage.py migrate`.
3. **Create Superuser**: `Maintenance: manage.py createsuperuser`.
### Debugging and Running Services
You can start and debug backend services either as debugging sessions via `launch.json` or as tasks.
#### Using `launch.json`
1. Press `F5` or go to the **Run and Debug** view in VSCode.
2. Select the desired configuration:
- `Runserver`
- `Document Consumer`
- `Celery`
#### Using Tasks
1. Open the command palette and select `Tasks: Run Task`.
2. Choose the desired task:
- `Runserver`
- `Document Consumer`
- `Celery`
### Additional Maintenance Tasks
Additional tasks are available for common maintenance operations:
- **Recreate .venv**: For setting up the virtual environment using `uv`.
- **Migrate Database**: To apply database migrations.
- **Create Superuser**: To create an admin user for the application.
## Let's Get Started!
Follow the steps above to get your development environment up and running. Happy coding!

View File

@@ -0,0 +1,28 @@
{
"name": "Paperless Development",
"dockerComposeFile": "docker-compose.devcontainer.sqlite-tika.yml",
"service": "paperless-development",
"workspaceFolder": "/usr/src/paperless/paperless-ngx",
"postCreateCommand": "/bin/bash -c 'rm -rf .venv/.* && uv sync --group dev && uv run pre-commit install'",
"customizations": {
"vscode": {
"extensions": [
"mhutchie.git-graph",
"ms-python.python",
"ms-vscode.js-debug-nightly",
"eamodio.gitlens",
"yzhang.markdown-all-in-one"
],
"settings": {
"python.defaultInterpreterPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python",
"python.pythonPath": "/usr/src/paperless/paperless-ngx/.venv/bin/python",
"python.terminal.activateEnvInCurrentTerminal": true,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
}
}
},
"remoteUser": "paperless"
}

View File

@@ -0,0 +1,77 @@
# Docker Compose file for developing Paperless NGX in VSCode DevContainers.
# This file contains everything Paperless NGX needs to run.
# Paperless supports amd64, arm, and arm64 hardware.
# All compose files of Paperless configure it in the following way:
#
# - Paperless is (re)started on system boot if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# In addition, this Docker Compose file adds the following optional
# configurations:
#
# - Apache Tika and Gotenberg servers are started with Paperless NGX and Paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint, and their LibreOffice counterparts).
#
# This file is intended only to be used through VSCOde devcontainers. See README.md
# in the folder .devcontainer.
services:
broker:
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- ./redisdata:/data
# No ports need to be exposed; the VSCode DevContainer plugin manages them.
paperless-development:
image: paperless-ngx
build:
context: ../ # Dockerfile cannot access files from parent directories if context is not set.
dockerfile: ./.devcontainer/Dockerfile
restart: unless-stopped
depends_on:
- broker
- gotenberg
- tika
volumes:
- ..:/usr/src/paperless/paperless-ngx:delegated
- ../.devcontainer/vscode:/usr/src/paperless/paperless-ngx/.vscode:delegated # VSCode config files
- virtualenv:/usr/src/paperless/paperless-ngx/.venv # Virtual environment persisted in volume
- /usr/src/paperless/paperless-ngx/src/documents/static/frontend # Static frontend files exist only in container
- /usr/src/paperless/paperless-ngx/src/.pytest_cache
- /usr/src/paperless/paperless-ngx/.ruff_cache
- /usr/src/paperless/paperless-ngx/htmlcov
- /usr/src/paperless/paperless-ngx/.coverage
- ./data:/usr/src/paperless/paperless-ngx/data
- ./media:/usr/src/paperless/paperless-ngx/media
- ./consume:/usr/src/paperless/paperless-ngx/consume
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
PAPERLESS_STATICDIR: ./src/documents/static
PAPERLESS_DEBUG: true
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "chown -R paperless:paperless /usr/src/paperless/paperless-ngx/src/documents/static/frontend && chown -R paperless:paperless /usr/src/paperless/paperless-ngx/.ruff_cache && while sleep 1000; do :; done"
gotenberg:
image: docker.io/gotenberg/gotenberg:8.17
restart: unless-stopped
# The Gotenberg Chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even JavaScript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:
redisdata:
virtualenv:

View File

@@ -0,0 +1,58 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome: Debug Angular Frontend",
"description": "Debug the Angular Dev Frontend in Chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:4200",
"webRoot": "${workspaceFolder}/src-ui",
"preLaunchTask": "Start: Frontend Angular"
},
{
"name": "Debug: Backend Server (manage.py runserver)",
"description": "Debug the Django Backend Server",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/manage.py",
"args": [
"runserver"
],
"django": true,
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
},
"python": "${workspaceFolder}/.venv/bin/python"
},
{
"name": "Debug: Consumer Service (manage.py document_consumer)",
"description": "Debug the Consumer Service which processes files from a directory",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/manage.py",
"args": [
"document_consumer"
],
"django": true,
"console": "integratedTerminal",
"env": {
"PYTHONPATH": "${workspaceFolder}/src"
},
"python": "${workspaceFolder}/.venv/bin/python"
}
],
"compounds": [
{
"name": "Debug: FullStack",
"description": "Debug run the Angular dev frontend, Django backend, and consumer service",
"configurations": [
"Chrome: Debug Angular Frontend",
"Debug: Backend Server (manage.py runserver)",
"Debug: Consumer Service (manage.py document_consumer)"
],
"preLaunchTask": "Start: Celery Worker"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"files.watcherExclude": {
"**/.venv/**": true,
"**/pytest_cache/**": true
},
"python.testing.cwd": "${workspaceFolder}/src"
}

View File

@@ -0,0 +1,223 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start: Celery Worker",
"description": "Start the Celery Worker which processes background and consume tasks",
"type": "shell",
"command": "uv run celery --app paperless worker -l DEBUG",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}/src"
},
"problemMatcher": [
{
"owner": "custom",
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "celery.*",
"endsPattern": "ready"
}
}
]
},
{
"label": "Start: Frontend Angular",
"description": "Start the Frontend Angular Dev Server",
"type": "shell",
"command": "pnpm start",
"isBackground": true,
"options": {
"cwd": "${workspaceFolder}/src-ui"
},
"problemMatcher": [
{
"owner": "custom",
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Compiled successfully"
}
}
]
},
{
"label": "Start: Consumer Service (manage.py document_consumer)",
"description": "Start the Consumer Service which processes files from a directory",
"type": "shell",
"command": "uv run python manage.py document_consumer",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Start: Backend Server (manage.py runserver)",
"description": "Start the Backend Server which serves the Django API and the compiled Angular frontend",
"type": "shell",
"command": "uv run python manage.py runserver",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Maintenance: manage.py migrate",
"description": "Apply database migrations",
"type": "shell",
"command": "uv run python manage.py migrate",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Maintenance: Build Documentation",
"description": "Build the documentation with MkDocs",
"type": "shell",
"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Maintenance: manage.py createsuperuser",
"description": "Create a superuser",
"type": "shell",
"command": "uv run python manage.py createsuperuser",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src"
}
},
{
"label": "Maintenance: recreate .venv",
"description": "Recreate the python virtual environment and install python dependencies",
"type": "shell",
"command": "rm -rf .venv && uv venv && uv sync --dev",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}"
}
},
{
"label": "Maintenance: Install Frontend Dependencies",
"description": "Install frontend (pnpm) dependencies",
"type": "pnpm",
"script": "install",
"path": "src-ui",
"group": "clean",
"problemMatcher": [],
"detail": "install dependencies from package"
},
{
"description": "Clean install frontend dependencies and build the frontend for production",
"label": "Maintenance: Compile frontend for production",
"type": "shell",
"command": "pnpm install && ./node_modules/.bin/ng build --configuration production",
"group": "none",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": false,
"clear": true,
"revealProblems": "onProblem"
},
"options": {
"cwd": "${workspaceFolder}/src-ui"
}
},
{
"label": "Project Setup: Run all Init Tasks",
"description": "Runs all init tasks to setup the project including migrate the database, create a superuser and compile the frontend for production",
"dependsOrder": "sequence",
"dependsOn": [
"Maintenance: manage.py migrate",
"Maintenance: manage.py createsuperuser",
"Maintenance: Compile frontend for production"
]
},
{
"label": "Project Start: Run all Services",
"description": "Runs all services required to start the project including the Celery Worker, the Consumer Service and the Backend Server",
"dependsOn": [
"Start: Celery Worker",
"Start: Consumer Service (manage.py document_consumer)",
"Start: Backend Server (manage.py runserver)"
]
}
]
}

30
.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
# Tool caches
**/__pycache__
**/.ruff_cache/
**/.mypy_cache/
# Virtual environment & similar
.venv/
./src-ui/node_modules
./src-ui/dist
# IDE folders
.idea/
.vscode/
./src-ui/.vscode
# VCS
.git
# Test related
**/.pytest_cache
**/tests
**/*.spec.ts
**/htmlcov
# Local folders
./export
./consume
./media
./data
./docs
./dist
./scripts
./resources
# Other stuff
**/*.drawio.png

37
.editorconfig Normal file
View File

@@ -0,0 +1,37 @@
# EditorConfig: http://EditorConfig.org
root = true
[*]
indent_style = tab
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 79
[{*.html,*.css,*.js}]
max_line_length = off
[*.py]
indent_size = 4
indent_style = space
[*.{yml,yaml}]
indent_style = space
[*.rst]
indent_style = space
[*.md]
indent_style = space
# Tests don't get a line width restriction. It's still a good idea to follow
# the 79 character rule, but in the interests of clarity, tests often need to
# violate it.
[**/test_*.py]
max_line_length = off
[Dockerfile*]
indent_style = space

1
.env Normal file
View File

@@ -0,0 +1 @@
COMPOSE_PROJECT_NAME=paperless

View File

@@ -0,0 +1,14 @@
title: "[Feature Request] "
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what you would like to see.
validations:
required: true
- type: textarea
id: other
attributes:
label: Other
description: Add any other context or information about the feature request here.

55
.github/DISCUSSION_TEMPLATE/support.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
title: "[Support] "
body:
- type: textarea
id: description
attributes:
label: What's your question or issue?
description: Provide a clear and concise description of what you're trying to do, and what's going wrong.
placeholder: |
I'm trying to...
[Include screenshots if helpful]
validations:
required: true
- type: textarea
id: steps
attributes:
label: What have you tried?
description: Describe any steps you've already taken to troubleshoot or solve the issue.
placeholder: |
- I checked the logs and saw...
- I followed the install guide and tried...
- type: input
id: version
attributes:
label: Paperless-ngx version
placeholder: e.g. 1.14.0
validations:
required: true
- type: input
id: host-os
attributes:
label: Host OS
description: Include architecture if relevant.
placeholder: e.g. Ubuntu 22.04 / Raspberry Pi arm64
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- Docker - official image
- Docker - linuxserver.io image
- Bare metal
- Other (please describe above)
- type: textarea
id: system-status
attributes:
label: System status
description: If available, copy & paste the system status output from Settings > System Status > Copy
render: json
- type: textarea
id: logs
attributes:
label: Relevant logs or output
description: If you have logs, errors that might help, paste it here.
render: bash

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: [shamoon, stumpylog]

118
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Bug report
description: Something is not working
title: "[BUG] Concise description of the issue"
labels: ["bug", "unconfirmed"]
body:
- type: markdown
attributes:
value: |
### ⚠️ Please remember: issues are for *bugs*
That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below.
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
- type: markdown
attributes:
value: |
#### Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperlessngx:matrix.org).
#### Before opening an issue, please double check:
- [The troubleshooting documentation](https://docs.paperless-ngx.com/troubleshooting/).
- [The installation instructions](https://docs.paperless-ngx.com/setup/#installation).
- [Existing issues and discussions](https://github.com/paperless-ngx/paperless-ngx/search?q=&type=issues).
- Disable any custom container initialization scripts, if using
If you encounter issues while installing or configuring Paperless-ngx, please post in the ["Support" section of the discussions](https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=support).
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem.
placeholder: |
Currently Paperless does not work when...
[Screenshot if applicable]
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. See error
validations:
required: true
- type: textarea
id: logs
attributes:
label: Webserver logs
description: Logs from the web server related to your issue.
render: bash
validations:
required: true
- type: textarea
id: logs_browser
attributes:
label: Browser logs
description: Logs from the web browser related to your issue, if needed
render: bash
- type: input
id: version
attributes:
label: Paperless-ngx version
placeholder: e.g. 1.6.0
validations:
required: true
- type: input
id: host-os
attributes:
label: Host OS
description: Host OS of the machine running paperless-ngx. Please add the architecture (uname -m) if applicable.
placeholder: e.g. Archlinux / Ubuntu 20.04 / Raspberry Pi `arm64`
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- Docker - official image
- Docker - linuxserver.io image
- Bare metal
- Other (please describe above)
description: Note there are significant differences from the official image and linuxserver.io, please check if your issue is specific to the third-party image.
validations:
required: true
- type: textarea
id: system-status
attributes:
label: System status
description: If available, copy & paste the system status output from Settings > System Status > Copy
render: json
- type: input
id: browser
attributes:
label: Browser
description: Which browser you are using, if relevant.
placeholder: e.g. Chrome, Safari
- type: textarea
id: config-changes
attributes:
label: Configuration changes
description: Any configuration changes you made in `docker-compose.yml`, `docker-compose.env` or `paperless.conf`.
- type: checkboxes
id: required-checks
attributes:
label: Please confirm the following
options:
- label: I believe this issue is a bug that affects all users of Paperless-ngx, not something specific to my installation.
required: true
- label: This issue is not about the OCR or archive creation of a specific file(s). Otherwise, please see above regarding OCR tools.
required: true
- label: I have already searched for relevant existing issues and discussions before opening this report.
required: true
- label: I have updated the title field above with a concise description.
required: true

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Questions and Help
url: https://github.com/paperless-ngx/paperless-ngx/discussions
about: General questions or support for using Paperless-ngx.
- name: 💬 Chat
url: https://matrix.to/#/#paperlessngx:matrix.org
about: Want to discuss Paperless-ngx with others? Check out our chat.
- name: 🚀 Feature Request
url: https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=feature-requests
about: Remember to search for existing feature requests and "up-vote" those that you like.

42
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,42 @@
<!--
Note: All PRs with code changes should be targeted to the `dev` branch, pure documentation changes can target `main`
-->
## Proposed change
<!--
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your proposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
-->
<!--
⚠️ Important: Pull requests that implement a new feature or enhancement *should almost always target an existing feature request* with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. If that is not currently the case, please open a feature request instead of this PR to gather feedback from both users and the project maintainers.
-->
Closes #(issue or discussion)
## Type of change
<!--
What type of change does your PR introduce to Paperless-ngx?
NOTE: Please check only one box!
-->
- [ ] Bug fix: non-breaking change which fixes an issue.
- [ ] New feature / Enhancement: non-breaking change which adds functionality. _Please read the important note above._
- [ ] Breaking change: fix or feature that would cause existing functionality to not work as expected.
- [ ] Documentation only.
- [ ] Other. Please explain:
## Checklist:
<!--
NOTE: PRs that do not address the following will not be merged, please do not skip any relevant items.
-->
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
- [ ] I have made corresponding changes to the documentation as needed.
- [ ] I have checked my modifications for any breaking changes.

124
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,124 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
# Required for uv support for now
enable-beta-ecosystems: true
updates:
# Enable version updates for pnpm
- package-ecosystem: "npm"
target-branch: "dev"
# Look for `pnpm-lock.yaml` file in the `/src-ui` directory
directory: "/src-ui"
open-pull-requests-limit: 10
schedule:
interval: "monthly"
labels:
- "frontend"
- "dependencies"
groups:
frontend-angular-dependencies:
patterns:
- "@angular*"
- "@ng-*"
- "ngx-*"
- "ng2-pdf-viewer"
frontend-jest-dependencies:
patterns:
- "@types/jest"
- "jest*"
frontend-eslint-dependencies:
patterns:
- "@typescript-eslint*"
- "eslint"
# Enable version updates for Python
- package-ecosystem: "uv"
target-branch: "dev"
directory: "/"
# Check for updates once a week
schedule:
interval: "weekly"
labels:
- "backend"
- "dependencies"
groups:
development:
patterns:
- "*pytest*"
- "ruff"
- "mkdocs-material"
- "pre-commit*"
django:
patterns:
- "*django*"
- "drf-*"
major-versions:
update-types:
- "major"
small-changes:
update-types:
- "minor"
- "patch"
exclude-patterns:
- "*django*"
- "drf-*"
pre-built:
patterns:
- psycopg*
- zxing-cpp
# Enable updates for GitHub Actions
- package-ecosystem: "github-actions"
target-branch: "dev"
directory: "/"
schedule:
# Check for updates to GitHub Actions every month
interval: "monthly"
labels:
- "ci-cd"
- "dependencies"
groups:
actions:
update-types:
- "major"
- "minor"
- "patch"
# Update Dockerfile in root directory
- package-ecosystem: "docker"
directories:
- "/"
- "/.devcontainer/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
commit-message:
prefix: "docker"
include: "scope"
# Update Docker Compose files in docker/compose directory
- package-ecosystem: "docker-compose"
directory: "/docker/compose/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
commit-message:
prefix: "docker-compose"
include: "scope"
groups:
# Individual groups for each image
gotenberg:
patterns:
- "docker.io/gotenberg/gotenberg*"
tika:
patterns:
- "docker.io/apache/tika*"
redis:
patterns:
- "docker.io/library/redis*"
mariadb:
patterns:
- "docker.io/library/mariadb*"
postgres:
patterns:
- "docker.io/library/postgres*"

26
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
backend:
- changed-files:
- any-glob-to-any-file:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'requirements.txt'
frontend:
- changed-files:
- any-glob-to-any-file:
- 'src-ui/**'
documentation:
- changed-files:
- any-glob-to-any-file:
- 'docs/**'
ci-cd:
- changed-files:
- any-glob-to-any-file:
- '.github/**'
# pr types
bug:
- head-branch:
- ['^fix']
enhancement:
- head-branch:
- ['^feature']

53
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
categories:
- title: 'Breaking Changes'
labels:
- 'breaking-change'
- title: 'Notable Changes'
labels:
- 'notable'
- title: 'Features / Enhancements'
labels:
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'bug'
- title: 'Documentation'
labels:
- 'documentation'
- title: 'Maintenance'
labels:
- 'chore'
- 'deployment'
- 'translation'
- 'ci-cd'
- title: 'Dependencies'
collapse-after: 3
labels:
- 'dependencies'
- title: 'All App Changes'
labels:
- 'frontend'
- 'backend'
collapse-after: 1
include-labels:
- 'enhancement'
- 'bug'
- 'chore'
- 'deployment'
- 'translation'
- 'dependencies'
- 'documentation'
- 'frontend'
- 'backend'
- 'ci-cd'
- 'breaking-change'
- 'notable'
exclude-labels:
- 'skip-changelog'
category-template: '### $TITLE'
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
change-title-escapes: '\<*_&#@'
template: |
## paperless-ngx $RESOLVED_VERSION
$CHANGES

673
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,673 @@
name: ci
on:
push:
tags:
# https://semver.org/#spec-item-2
- 'v[0-9]+.[0-9]+.[0-9]+'
# https://semver.org/#spec-item-9
- 'v[0-9]+.[0-9]+.[0-9]+-beta.rc[0-9]+'
branches-ignore:
- 'translations**'
pull_request:
branches-ignore:
- 'translations**'
env:
DEFAULT_UV_VERSION: "0.8.x"
# This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.11"
NLTK_DATA: "/usr/share/nltk_data"
jobs:
detect-duplicate:
name: Detect Duplicate Run
runs-on: ubuntu-24.04
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- name: Check if workflow should run
id: check
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
if (context.eventName !== 'push') {
core.info('Not a push event; running workflow.');
core.setOutput('should_run', 'true');
return;
}
const ref = context.ref || '';
if (!ref.startsWith('refs/heads/')) {
core.info('Push is not to a branch; running workflow.');
core.setOutput('should_run', 'true');
return;
}
const branch = ref.substring('refs/heads/'.length);
const { owner, repo } = context.repo;
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: 'open',
head: `${owner}:${branch}`,
per_page: 100,
});
if (prs.length === 0) {
core.info(`No open PR found for ${branch}; running workflow.`);
core.setOutput('should_run', 'true');
} else {
core.info(`Found ${prs.length} open PR(s) for ${branch}; skipping duplicate push run.`);
core.setOutput('should_run', 'false');
}
pre-commit:
needs:
- detect-duplicate
if: needs.detect-duplicate.outputs.should_run == 'true'
name: Linting Checks
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Check files
uses: pre-commit/action@v3.0.1
documentation:
name: "Build & Deploy Documentation"
runs-on: ubuntu-24.04
needs:
- pre-commit
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install Python dependencies
run: |
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
- name: Make documentation
run: |
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
--dev \
--frozen \
mkdocs build --config-file ./mkdocs.yml
- name: Deploy documentation
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
--dev \
--frozen \
mkdocs gh-deploy --force --no-history
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: documentation
path: site/
retention-days: 7
tests-backend:
name: "Backend Tests (Python ${{ matrix.python-version }})"
runs-on: ubuntu-24.04
needs:
- pre-commit
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Start containers
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils
- name: Configure ImageMagick
run: |
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
- name: Install Python dependencies
run: |
uv sync \
--python ${{ steps.setup-python.outputs.python-version }} \
--group testing \
--frozen
- name: List installed Python dependencies
run: |
uv pip list
- name: Install or update NLTK dependencies
run: uv run python -m nltk.downloader punkt punkt_tab snowball_data stopwords -d ${{ env.NLTK_DATA }}
- name: Tests
env:
NLTK_DATA: ${{ env.NLTK_DATA }}
PAPERLESS_CI_TEST: 1
# Enable paperless_mail testing against real server
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
run: |
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
--dev \
--frozen \
pytest
- name: Upload backend test results to Codecov
if: always()
uses: codecov/test-results-action@v1
with:
flags: backend-python-${{ matrix.python-version }}
files: junit.xml
- name: Upload backend coverage to Codecov
uses: codecov/codecov-action@v5
with:
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
- name: Stop containers
if: always()
run: |
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml logs
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml down
install-frontend-dependencies:
name: "Install Frontend Dependencies"
runs-on: ubuntu-24.04
needs:
- pre-commit
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v5
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Install dependencies
run: cd src-ui && pnpm install
tests-frontend:
name: "Frontend Unit Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04
needs:
- install-frontend-dependencies
strategy:
fail-fast: false
matrix:
node-version: [20.x]
shard-index: [1, 2, 3, 4]
shard-count: [4]
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v5
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Linting checks
run: cd src-ui && pnpm run lint
- name: Run Jest unit tests
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload frontend test results to Codecov
uses: codecov/test-results-action@v1
if: always()
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/
- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@v5
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/
tests-frontend-e2e:
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
runs-on: ubuntu-24.04
needs:
- install-frontend-dependencies
strategy:
fail-fast: false
matrix:
node-version: [20.x]
shard-index: [1, 2]
shard-count: [2]
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v5
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright system dependencies
run: npx playwright install-deps
- name: Install dependencies
run: cd src-ui && pnpm install --no-frozen-lockfile
- name: Install Playwright
run: cd src-ui && pnpm exec playwright install
- name: Run Playwright e2e tests
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
frontend-bundle-analysis:
name: "Frontend Bundle Analysis"
runs-on: ubuntu-24.04
needs:
- tests-frontend
- tests-frontend-e2e
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v5
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/package-lock.json') }}
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Build frontend and upload analysis
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && pnpm run build --configuration=production
build-docker-image:
name: Build Docker image for ${{ github.ref_name }}
runs-on: ubuntu-24.04
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/l10n_'))
concurrency:
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
cancel-in-progress: true
needs:
- tests-backend
- tests-frontend
- tests-frontend-e2e
steps:
- name: Check pushing to Docker Hub
id: push-other-places
# Only push to Dockerhub from the main repo AND the ref is either:
# main
# dev
# beta
# a tag
# Otherwise forks would require a Docker Hub account and secrets setup
run: |
if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
echo "Enabling DockerHub image push"
echo "enable=true" >> $GITHUB_OUTPUT
else
echo "Not pushing to DockerHub"
echo "enable=false" >> $GITHUB_OUTPUT
fi
- name: Set ghcr repository name
id: set-ghcr-repository
run: |
ghcr_name=$(echo "${{ github.repository }}" | awk '{ print tolower($0) }')
echo "Name is ${ghcr_name}"
echo "ghcr-repository=${ghcr_name}" >> $GITHUB_OUTPUT
- name: Gather Docker metadata
id: docker-meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}
name=paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
name=quay.io/paperlessngx/paperless-ngx,enable=${{ steps.push-other-places.outputs.enable }}
tags: |
# Tag branches with branch name
type=ref,event=branch
# Process semver tags
# For a tag x.y.z or vX.Y.Z, output an x.y.z and x.y image tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Checkout
uses: actions/checkout@v5
# If https://github.com/docker/buildx/issues/1044 is resolved,
# the append input with a native arm64 arch could be used to
# significantly speed up building
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
# Don't attempt to login if not pushing to Docker Hub
if: steps.push-other-places.outputs.enable == 'true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
uses: docker/login-action@v3
# Don't attempt to login if not pushing to Quay.io
if: steps.push-other-places.outputs.enable == 'true'
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
build-args: |
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
# Get cache layers from this branch, then dev
# This allows new branches to get at least some cache benefits, generally from dev
cache-from: |
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
cache-to: |
type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
- name: Export frontend artifact from docker
run: |
docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: frontend-compiled
path: src/documents/static/frontend/
retention-days: 7
build-release:
name: "Build Release"
needs:
- build-docker-image
- documentation
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install Python dependencies
run: |
uv sync --python ${{ steps.setup-python.outputs.python-version }} --dev --frozen
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext liblept5
- name: Download frontend artifact
uses: actions/download-artifact@v5
with:
name: frontend-compiled
path: src/documents/static/frontend/
- name: Download documentation artifact
uses: actions/download-artifact@v5
with:
name: documentation
path: docs/_build/html/
- name: Generate requirements file
run: |
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
- name: Compile messages
run: |
cd src/
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
manage.py compilemessages
- name: Collect static files
run: |
cd src/
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
manage.py collectstatic --no-input
- name: Move files
run: |
echo "Making dist folders"
for directory in dist \
dist/paperless-ngx \
dist/paperless-ngx/scripts;
do
mkdir --verbose --parents ${directory}
done
echo "Copying basic files"
for file_name in .dockerignore \
.env \
Dockerfile \
pyproject.toml \
uv.lock \
requirements.txt \
LICENSE \
README.md \
paperless.conf.example
do
cp --verbose ${file_name} dist/paperless-ngx/
done
mv --verbose dist/paperless-ngx/paperless.conf.example dist/paperless-ngx/paperless.conf
echo "Copying Docker related files"
cp --recursive docker/ dist/paperless-ngx/docker
echo "Copying startup scripts"
cp --verbose scripts/*.service scripts/*.sh scripts/*.socket dist/paperless-ngx/scripts/
echo "Copying source files"
cp --recursive src/ dist/paperless-ngx/src
echo "Copying documentation"
cp --recursive docs/_build/html/ dist/paperless-ngx/docs
mv --verbose static dist/paperless-ngx
- name: Make release package
run: |
echo "Creating release archive"
cd dist
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release
path: dist/paperless-ngx.tar.xz
retention-days: 7
publish-release:
name: "Publish Release"
runs-on: ubuntu-24.04
outputs:
prerelease: ${{ steps.get_version.outputs.prerelease }}
changelog: ${{ steps.create-release.outputs.body }}
version: ${{ steps.get_version.outputs.version }}
needs:
- build-release
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc'))
steps:
- name: Download release artifact
uses: actions/download-artifact@v5
with:
name: release
path: ./
- name: Get version
id: get_version
run: |
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
if [[ ${{ contains(github.ref_name, '-beta.rc') }} == 'true' ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Create Release and Changelog
id: create-release
uses: release-drafter/release-drafter@v6
with:
name: Paperless-ngx ${{ steps.get_version.outputs.version }}
tag: ${{ steps.get_version.outputs.version }}
version: ${{ steps.get_version.outputs.version }}
prerelease: ${{ steps.get_version.outputs.prerelease }}
publish: true # ensures release is not marked as draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
id: upload-release-asset
uses: shogo82148/actions-upload-release-asset@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
asset_path: ./paperless-ngx.tar.xz
asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz
asset_content_type: application/x-xz
append-changelog:
name: "Append Changelog"
runs-on: ubuntu-24.04
needs:
- publish-release
if: needs.publish-release.outputs.prerelease == 'false'
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: main
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Append Changelog to docs
id: append-Changelog
working-directory: docs
run: |
git branch ${{ needs.publish-release.outputs.version }}-changelog
git checkout ${{ needs.publish-release.outputs.version }}-changelog
echo -e "# Changelog\n\n${{ needs.publish-release.outputs.changelog }}\n" > changelog-new.md
echo "Manually linking usernames"
sed -i -r 's|@([a-zA-Z0-9_]+) \(\[#|[@\1](https://github.com/\1) ([#|g' changelog-new.md
echo "Removing unneeded comment tags"
sed -i -r 's|@<!---->|@|g' changelog-new.md
CURRENT_CHANGELOG=`tail --lines +2 changelog.md`
echo -e "$CURRENT_CHANGELOG" >> changelog-new.md
mv changelog-new.md changelog.md
uv run \
--python ${{ steps.setup-python.outputs.python-version }} \
--dev \
pre-commit run --files changelog.md || true
git config --global user.name "github-actions"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
git push origin ${{ needs.publish-release.outputs.version }}-changelog
- name: Create Pull Request
uses: actions/github-script@v8
with:
script: |
const { repo, owner } = context.repo;
const result = await github.rest.pulls.create({
title: 'Documentation: Add ${{ needs.publish-release.outputs.version }} changelog',
owner,
repo,
head: '${{ needs.publish-release.outputs.version }}-changelog',
base: 'main',
body: 'This PR is auto-generated by CI.'
});
github.rest.issues.addLabels({
owner,
repo,
issue_number: result.data.number,
labels: ['documentation', 'skip-changelog']
});

62
.github/workflows/cleanup-tags.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
# This workflow runs on certain conditions to check for and potentially
# delete container images from the GHCR which no longer have an associated
# code branch.
# Requires a PAT with the correct scope set in the secrets.
#
# This workflow will not trigger runs on forked repos.
name: Cleanup Image Tags
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
concurrency:
group: registry-tags-cleanup
cancel-in-progress: false
jobs:
cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.11.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "${{ matrix.primary-name }}"
scheme: "branch"
repo_name: "paperless-ngx"
match_regex: "(feature|fix)"
do_delete: "true"
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
needs:
- cleanup-images
strategy:
fail-fast: false
matrix:
primary-name: ["paperless-ngx", "paperless-ngx/builder/cache/app"]
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.GHA_CONTAINER_DELETE_TOKEN }}
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.11.0
with:
token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}"
is_org: "true"
package_name: "${{ matrix.primary-name }}"
do_delete: "true"

48
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [main, dev]
pull_request:
# The branches below must be a subset of the branches above
branches: [dev]
schedule:
- cron: '28 13 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript', 'python']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

30
.github/workflows/crowdin.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Crowdin Action
on:
workflow_dispatch:
schedule:
- cron: '2 */12 * * *'
push:
paths: ['src/locale/**', 'src-ui/messages.xlf', 'src-ui/src/locale/**']
branches: [dev]
jobs:
synchronize-with-crowdin:
name: Crowdin Sync
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v5
with:
token: ${{ secrets.PNGX_BOT_PAT }}
- name: crowdin action
uses: crowdin/github-action@v2
with:
upload_translations: false
download_translations: true
crowdin_branch_name: 'dev'
localization_branch_name: l10n_dev
pull_request_labels: 'skip-changelog, translation'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

112
.github/workflows/pr-bot.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: PR Bot
on:
pull_request_target:
types: [opened]
permissions:
contents: read
pull-requests: write
jobs:
pr-bot:
name: Automated PR Bot
runs-on: ubuntu-latest
steps:
- name: Label PR by file path or branch name
# see .github/labeler.yml for the labeler config
uses: actions/labeler@v6
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Label by size
uses: Gascon1/pr-size-labeler@v1.3.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: 'small-change'
xs_diff: '9'
s_label: 'non-trivial'
s_diff: '99999'
fail_if_xl: 'false'
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
- name: Label by PR title
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const title = pr.title.toLowerCase();
const labels = [];
if (/^(fix|bugfix)/i.test(title)) {
labels.push('bug');
} else if (/^feature/i.test(title)) {
labels.push('enhancement');
} else if (!/^(dependabot)/i.test(title) && !/^(chore)/i.test(title)) {
labels.push('enhancement'); // Default fallback
}
if (labels.length) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels,
});
core.info(`Added labels based on title: ${labels.join(', ')}`);
}
- name: Label bot-generated PRs
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const user = pr.user.login.toLowerCase();
const labels = [];
if (user.includes('dependabot')) {
labels.push('dependencies');
}
if (user.includes('crowdin-bot')) {
labels.push('translation', 'skip-changelog');
}
if (labels.length) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels,
});
}
- name: Welcome comment
if: ${{ !contains(github.actor, 'bot') }}
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const user = pr.user.login;
const { data: members } = await github.rest.orgs.listMembers({
org: 'paperless-ngx',
});
const memberLogins = members.map(m => m.login.toLowerCase());
if (memberLogins.includes(user.toLowerCase())) {
core.info('Skipping comment: user is org member');
return;
}
const body =
"Hello @" + user + ",\n\n" +
"Thank you very much for submitting this PR to us!\n\n" +
"This is what will happen next:\n\n" +
"1. CI tests will run against your PR to ensure quality and consistency.\n" +
"2. Next, human contributors from paperless-ngx review your changes.\n" +
"3. Please address any issues that come up during the review as soon as you are able to.\n" +
"4. If accepted, your pull request will be merged into the `dev` branch and changes there will be tested further.\n" +
"5. Eventually, changes from you and other contributors will be merged into `main` and a new release will be made.\n\n" +
"You'll be hearing from us soon, and thank you again for contributing to our project.";
await github.rest.issues.createComment({
issue_number: pr.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});

24
.github/workflows/project-actions.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Project Automations
on:
pull_request_target: #_target allows access to secrets
types:
- opened
- reopened
branches:
- main
- dev
permissions:
contents: read
jobs:
pr_opened_or_reopened:
name: pr_opened_or_reopened
runs-on: ubuntu-24.04
permissions:
# write permission is required for autolabeler
pull-requests: write
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

291
.github/workflows/repo-maintenance.yml vendored Normal file
View File

@@ -0,0 +1,291 @@
name: 'Repository Maintenance'
on:
schedule:
- cron: '0 3 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock
jobs:
stale:
name: 'Stale'
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@v10
with:
days-before-stale: 7
days-before-close: 14
any-of-issue-labels: 'cant-reproduce,not a bug'
stale-issue-label: stale
stale-issue-message: >
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: ""
stale-pr-label: stale
exempt-pr-labels: 'notable'
close-pr-message: >
This pull request has been automatically closed because it has not had recent activity. Thank you for your contributions. Please open a new pull request or discussion if you would like to continue working on this change.
lock-threads:
name: 'Lock Old Threads'
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '30'
pr-inactive-days: '30'
discussion-inactive-days: '30'
log-output: true
issue-comment: >
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion or issue for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
pr-comment: >
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion or issue for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
discussion-comment: >
This discussion has been automatically locked since there has not been any recent activity after it was closed. Please open a new discussion for related concerns. See our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.
close-answered-discussions:
name: 'Close Answered Discussions'
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const query = `query($owner:String!, $name:String!) {
repository(owner:$owner, name:$name){
discussions(first:100, answered:true, states:[OPEN]) {
nodes {
id,
number
}
}
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
}
const result = await github.graphql(query, variables)
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`)
for (const discussion of result.repository.discussions.nodes) {
console.log(`Closing discussion #${discussion.number} (${discussion.id})`)
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`;
const commentVariables = {
discussion: discussion.id,
body: 'This discussion has been automatically closed because it was marked as answered. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
}
await github.graphql(addCommentMutation, commentVariables)
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
clientMutationId
}
}`;
const closeVariables = {
discussion: discussion.id,
reason: "RESOLVED",
}
await github.graphql(closeDiscussionMutation, closeVariables)
await sleep(1000)
}
close-outdated-discussions:
name: 'Close Outdated Discussions'
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const CUTOFF_DAYS = 180;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS);
const query = `query(
$owner:String!,
$name:String!,
$supportCategory:ID!,
$generalCategory:ID!,
) {
supportDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$supportCategory,
last:50,
answered:false,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
},
},
generalDiscussions: repository(owner:$owner, name:$name){
discussions(
categoryId:$generalCategory,
last:50,
states:[OPEN],
) {
nodes {
id,
number,
updatedAt
}
}
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
supportCategory: "DIC_kwDOG1Zs184CBKWK",
generalCategory: "DIC_kwDOG1Zs184CBKWJ"
}
const result = await github.graphql(query, variables);
const combinedDiscussions = [
...result.supportDiscussions.discussions.nodes,
...result.generalDiscussions.discussions.nodes,
]
console.log(`Checking ${combinedDiscussions.length} open discussions`);
for (const discussion of combinedDiscussions) {
if (new Date(discussion.updatedAt) < cutoff) {
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`);
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`;
const commentVariables = {
discussion: discussion.id,
body: 'This discussion has been automatically closed due to inactivity. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
}
await github.graphql(addCommentMutation, commentVariables);
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
clientMutationId
}
}`;
const closeVariables = {
discussion: discussion.id,
reason: "OUTDATED",
}
await github.graphql(closeDiscussionMutation, closeVariables);
await sleep(1000);
}
}
close-unsupported-feature-requests:
name: 'Close Unsupported Feature Requests'
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const CUTOFF_MAX_COUNT = 80;
const CUTOFF_1_DAYS = 180;
const CUTOFF_1_COUNT = 5;
const CUTOFF_2_DAYS = 365;
const CUTOFF_2_COUNT = 20;
const CUTOFF_3_DAYS = 730;
const CUTOFF_3_COUNT = 40;
const cutoff1Date = new Date();
cutoff1Date.setDate(cutoff1Date.getDate() - CUTOFF_1_DAYS);
const cutoff2Date = new Date();
cutoff2Date.setDate(cutoff2Date.getDate() - CUTOFF_2_DAYS);
const cutoff3Date = new Date();
cutoff3Date.setDate(cutoff3Date.getDate() - CUTOFF_3_DAYS);
const query = `query(
$owner:String!,
$name:String!,
$featureRequestsCategory:ID!,
) {
repository(owner:$owner, name:$name){
discussions(
categoryId:$featureRequestsCategory,
last:100,
states:[OPEN],
) {
nodes {
id,
createdAt,
number,
updatedAt,
upvoteCount,
}
},
}
}`;
const variables = {
owner: context.repo.owner,
name: context.repo.repo,
featureRequestsCategory: "DIC_kwDOG1Zs184CBNr4"
}
const result = await github.graphql(query, variables);
for (const discussion of result.repository.discussions.nodes) {
const discussionUpdatedDate = new Date(discussion.updatedAt);
const discussionCreatedDate = new Date(discussion.createdAt);
if ((discussionUpdatedDate < cutoff1Date && discussion.upvoteCount < CUTOFF_MAX_COUNT) ||
(discussionCreatedDate < cutoff1Date && discussion.upvoteCount < CUTOFF_1_COUNT) ||
(discussionCreatedDate < cutoff2Date && discussion.upvoteCount < CUTOFF_2_COUNT) ||
(discussionCreatedDate < cutoff3Date && discussion.upvoteCount < CUTOFF_3_COUNT)) {
console.log(`Closing discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt} with votes ${discussion.upvoteCount}`);
const addCommentMutation = `mutation($discussion:ID!, $body:String!) {
addDiscussionComment(input:{discussionId:$discussion, body:$body}) {
clientMutationId
}
}`;
const commentVariables = {
discussion: discussion.id,
body: 'This discussion has been automatically closed due to lack of community support. Please see our [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/dev/CONTRIBUTING.md#automatic-repository-maintenance) for more details.',
}
await github.graphql(addCommentMutation, commentVariables);
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) {
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) {
clientMutationId
}
}`;
const closeVariables = {
discussion: discussion.id,
reason: "OUTDATED",
}
await github.graphql(closeDiscussionMutation, closeVariables);
await sleep(1000);
}
}

69
.github/workflows/translate-strings.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Generate Translation Strings
on:
push:
branches:
- dev
jobs:
generate-translate-strings:
name: Generate Translation Strings
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
token: ${{ secrets.PNGX_BOT_PAT }}
ref: ${{ github.head_ref }}
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Install backend python dependencies
run: |
uv sync \
--group dev \
--frozen
- name: Generate backend translation strings
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 20
uses: actions/setup-node@v5
with:
node-version: 20.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
~/.cache
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
- name: Install frontend dependencies
if: steps.cache-frontend-deps.outputs.cache-hit != 'true'
run: cd src-ui && pnpm install
- name: Re-link Angular cli
run: cd src-ui && pnpm link @angular/cli
- name: Generate frontend translation strings
run: |
cd src-ui
pnpm run ng extract-i18n
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v6
with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings"
commit_user_name: "GitHub Actions"
commit_author: "GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com>"

112
.gitignore vendored Normal file
View File

@@ -0,0 +1,112 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
#lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
/src/paperless_mail/templates/node_modules
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.pytest_cache
junit.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# MkDocs documentation
site/
# PyBuilder
target/
# PyCharm
.idea
# VS Code
.vscode
/src-ui/.vscode
/docs/.vscode
.vscode-server
*CommandMarker
# Other stuff that doesn't belong
.virtualenv
virtualenv
/venv
.venv/
/docker-compose.env
/docker-compose.yml
.ruff_cache/
# Used for development
scripts/import-for-development
scripts/nuke
# Static files collected by the collectstatic command
/static/
# Stored PDFs
/media/
/data/
/paperless.conf
/consume/
/export/
# this is where the compiled frontend is moved to.
/src/documents/static/frontend/
# mac os
.DS_Store
# celery schedule file
celerybeat-schedule*
# ignore .devcontainer sub folders
/.devcontainer/consume/
/.devcontainer/data/
/.devcontainer/media/
/.devcontainer/redisdata/
# ignore pnpm package store folder created when setting up the devcontainer
.pnpm-store/

8
.hadolint.yml Normal file
View File

@@ -0,0 +1,8 @@
failure-threshold: warning
ignored:
# https://github.com/hadolint/hadolint/wiki/DL3008
- DL3008
# https://github.com/hadolint/hadolint/wiki/DL3013
- DL3013
# https://github.com/hadolint/hadolint/wiki/DL3003
- DL3003

82
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,82 @@
# This file configures pre-commit hooks.
# See https://pre-commit.com/ for general information
# See https://pre-commit.com/hooks.html for a listing of possible hooks
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-docstring-first
- id: check-json
exclude: "tsconfig.*json"
- id: check-yaml
args:
- "--unsafe"
- id: check-toml
- id: check-executables-have-shebangs
- id: end-of-file-fixer
exclude_types:
- svg
- pofile
exclude: "(^LICENSE$|^src/documents/static/bootstrap.min.css$)"
- id: mixed-line-ending
args:
- "--fix=lf"
- id: trailing-whitespace
exclude_types:
- svg
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies: [tomli]
exclude_types:
- pofile
- json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.6.2'
hooks:
- id: prettier
types_or:
- javascript
- ts
- markdown
additional_dependencies:
- prettier@3.3.3
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.2
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.6.0"
hooks:
- id: pyproject-fmt
# Dockerfile hooks
- repo: https://github.com/AleksaC/hadolint-py
rev: v2.14.0
hooks:
- id: hadolint
# Shell script hooks
- repo: https://github.com/lovesegfault/beautysh
rev: v6.2.1
hooks:
- id: beautysh
additional_dependencies:
- setuptools
args:
- "--tab"
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: "v0.11.0.1"
hooks:
- id: shellcheck
- repo: https://github.com/google/yamlfmt
rev: v0.17.2
hooks:
- id: yamlfmt
exclude: "^src-ui/pnpm-lock.yaml"

19
.prettierrc.js Normal file
View File

@@ -0,0 +1,19 @@
const config = {
// https://prettier.io/docs/en/options.html#semicolons
semi: false,
// https://prettier.io/docs/en/options.html#quotes
singleQuote: true,
// https://prettier.io/docs/en/options.html#trailing-commas
trailingComma: 'es5',
overrides: [
{
files: ['docs/*.md'],
options: {
tabWidth: 4,
},
},
],
plugins: [require('prettier-plugin-organize-imports')],
}
module.exports = config

1
.yamlfmt Normal file
View File

@@ -0,0 +1 @@
line_ending: lf

10
CODEOWNERS Normal file
View File

@@ -0,0 +1,10 @@
/.github/workflows/ @paperless-ngx/ci-cd
/docker/ @paperless-ngx/ci-cd
/scripts/ @paperless-ngx/ci-cd
/src-ui/ @paperless-ngx/frontend
/src/ @paperless-ngx/backend
pyproject.toml @paperless-ngx/backend
uv.lock @paperless-ngx/backend
*.py @paperless-ngx/backend

268
Dockerfile Normal file
View File

@@ -0,0 +1,268 @@
# syntax=docker/dockerfile:1
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
# Stage: compile-frontend
# Purpose: Compiles the frontend
# Notes:
# - Does PNPM stuff with Typescript and such
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
COPY ./src-ui /src/src-ui
WORKDIR /src/src-ui
RUN set -eux \
&& npm update -g pnpm \
&& npm install -g corepack@latest \
&& corepack enable \
&& pnpm install
ARG PNGX_TAG_VERSION=
# Add the tag to the environment file if its a tagged dev build
RUN set -eux && \
case "${PNGX_TAG_VERSION}" in \
dev|beta|fix*|feature*) \
sed -i -E "s/tag: '([a-z\.]+)'/tag: '${PNGX_TAG_VERSION}'/g" /src/src-ui/src/environments/environment.prod.ts \
;; \
esac
RUN set -eux \
&& ./node_modules/.bin/ng build --configuration production
# Stage: s6-overlay-base
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.8.22-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6
# https://github.com/just-containers/s6-overlay#customizing-s6-overlay-behaviour
ENV \
S6_BEHAVIOUR_IF_STAGE2_FAILS=2 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_VERBOSITY=1 \
PATH=/command:$PATH
# Buildx provided, must be defined to use though
ARG TARGETARCH
ARG TARGETVARIANT
# Lock this version
ARG S6_OVERLAY_VERSION=3.2.1.0
ARG S6_BUILD_TIME_PKGS="curl \
xz-utils"
RUN set -eux \
&& echo "Installing build time packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${S6_BUILD_TIME_PKGS} \
&& echo "Determining arch" \
&& S6_ARCH="" \
&& if [ "${TARGETARCH}${TARGETVARIANT}" = "amd64" ]; then S6_ARCH="x86_64"; \
elif [ "${TARGETARCH}${TARGETVARIANT}" = "arm64" ]; then S6_ARCH="aarch64"; fi\
&& if [ -z "${S6_ARCH}" ]; then { echo "Error: Not able to determine arch"; exit 1; }; fi \
&& echo "Installing s6-overlay for ${S6_ARCH}" \
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz.sha256" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \
"https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz.sha256" \
&& echo "Validating s6-archive checksums" \
&& sha256sum --check ./*.sha256 \
&& echo "Unpacking archives" \
&& tar --directory / -Jxpf s6-overlay-noarch.tar.xz \
&& tar --directory / -Jxpf s6-overlay-${S6_ARCH}.tar.xz \
&& echo "Removing downloaded archives" \
&& rm ./*.tar.xz \
&& rm ./*.sha256 \
&& echo "Cleaning up image" \
&& apt-get --yes purge ${S6_BUILD_TIME_PKGS} \
&& apt-get --yes autoremove --purge \
&& rm -rf /var/lib/apt/lists/*
# Copy our service defs and filesystem
COPY ./docker/rootfs /
# Stage: main-app
# Purpose: The final image
# Comments:
# - Don't leave anything extra in here
FROM s6-overlay-base AS main-app
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
LABEL org.opencontainers.image.source="https://github.com/paperless-ngx/paperless-ngx"
LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-ngx"
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
ARG DEBIAN_FRONTEND=noninteractive
# Buildx provided, must be defined to use though
ARG TARGETARCH
# Can be workflow provided, defaults set for manual building
ARG JBIG2ENC_VERSION=0.30
ARG QPDF_VERSION=11.9.0
ARG GS_VERSION=10.03.1
# Set Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Ignore warning from Whitenoise about async iterators
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1 \
# https://docs.astral.sh/uv/reference/settings/#link-mode
UV_LINK_MODE=copy \
UV_CACHE_DIR=/cache/uv/
#
# Begin installation and configuration
# Order the steps below from least often changed to most
#
# Packages need for running
ARG RUNTIME_PACKAGES="\
# General utils
curl \
# Docker specific
gosu \
# Timezones support
tzdata \
# fonts for text file thumbnail generation
fonts-liberation \
gettext \
ghostscript \
gnupg \
icc-profiles-free \
imagemagick \
# PostgreSQL
postgresql-client \
# MySQL / MariaDB
mariadb-client \
# OCRmyPDF dependencies
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-deu \
tesseract-ocr-fra \
tesseract-ocr-ita \
tesseract-ocr-spa \
unpaper \
pngquant \
jbig2dec \
# lxml
libxml2 \
libxslt1.1 \
# itself
qpdf \
# Mime type detection
file \
libmagic1 \
media-types \
zlib1g \
# Barcode splitter
libzbar0 \
poppler-utils"
# Install basic runtime packages.
# These change very infrequently
RUN set -eux \
echo "Installing system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
&& echo "Installing pre-built updates" \
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& echo "Installing qpdf ${QPDF_VERSION}" \
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
&& echo "Installing Ghostscript ${GS_VERSION}" \
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
&& echo "Installing jbig2enc" \
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
&& echo "Configuring imagemagick" \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& echo "Cleaning up image layer" \
&& rm --force --verbose *.deb \
&& rm --recursive --force --verbose /var/lib/apt/lists/*
WORKDIR /usr/src/paperless/src/
# Python dependencies
# Change pretty frequently
COPY --chown=1000:1000 ["pyproject.toml", "uv.lock", "/usr/src/paperless/src/"]
# Packages needed only for building a few quick Python
# dependencies
ARG BUILD_PACKAGES="\
build-essential \
# https://github.com/PyMySQL/mysqlclient#linux
default-libmysqlclient-dev \
pkg-config"
# hadolint ignore=DL3042
RUN --mount=type=cache,target=${UV_CACHE_DIR},id=python-cache \
set -eux \
&& echo "Installing build system packages" \
&& apt-get update \
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
&& echo "Installing Python requirements" \
&& uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt \
&& uv pip install --system --no-python-downloads --python-preference system --requirements requirements.txt \
&& echo "Installing NLTK data" \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" punkt_tab \
&& echo "Cleaning up image" \
&& apt-get --yes purge ${BUILD_PACKAGES} \
&& apt-get --yes autoremove --purge \
&& apt-get clean --yes \
&& rm --recursive --force --verbose *.whl \
&& rm --recursive --force --verbose /var/lib/apt/lists/* \
&& rm --recursive --force --verbose /tmp/* \
&& rm --recursive --force --verbose /var/tmp/* \
&& rm --recursive --force --verbose /var/cache/apt/archives/* \
&& truncate --size 0 /var/log/*log
# copy backend
COPY --chown=1000:1000 ./src ./
# copy frontend
COPY --from=compile-frontend --chown=1000:1000 /src/src/documents/static/frontend/ ./documents/static/frontend/
# add users, setup scripts
# Mount the compiled frontend to expected location
RUN set -eux \
&& sed -i '1s|^#!/usr/bin/env python3|#!/command/with-contenv python3|' manage.py \
&& echo "Setting up user/group" \
&& addgroup --gid 1000 paperless \
&& useradd --uid 1000 --gid paperless --home-dir /usr/src/paperless paperless \
&& echo "Creating volume directories" \
&& mkdir --parents --verbose /usr/src/paperless/data \
&& mkdir --parents --verbose /usr/src/paperless/media \
&& mkdir --parents --verbose /usr/src/paperless/consume \
&& mkdir --parents --verbose /usr/src/paperless/export \
&& echo "Creating gnupg directory" \
&& mkdir -m700 --verbose /usr/src/paperless/.gnupg \
&& echo "Adjusting all permissions" \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
&& echo "Collecting static files" \
&& s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
&& s6-setuidgid paperless python3 manage.py compilemessages
VOLUME ["/usr/src/paperless/data", \
"/usr/src/paperless/media", \
"/usr/src/paperless/consume", \
"/usr/src/paperless/export"]
ENTRYPOINT ["/init"]
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=5 CMD [ "curl", "-fs", "-S", "-L", "--max-time", "2", "http://localhost:8000" ]

31
README.md Normal file
View File

@@ -0,0 +1,31 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/a9ddbc7b-e739-4db9-9a59-04ee6a8c8e66" alt="Linfa DMS - Eco-Friendly Document Management System" width="640">
</p>
---
## Linfa: Your Intelligent & Sustainable DMS
**Linfa** is the enhanced, **open-source** DMS solution, perfectly suited for businesses ready to finalize their **green digital transformation**.
Built upon the robust Paperless-ngx architecture, Linfa has been developed and meticulously optimized for **intensive use** in professional and consultancy environments, guaranteeing **maximum efficiency** and regulatory compliance.
The name *Linfa* (Italian for "Sap" or "Lifeblood") underlines our core mission: to provide a **vital, waste-free document flow**, drastically cutting down on paper consumption and physical archival costs.
---
### Why Linfa is the Business Choice:
* ** Intelligence and Speed:** It leverages **advanced OCR** for automatic document indexing and **full-text search**, making every file instantly searchable. Crucially, Linfa uses **Machine Learning** to assign correspondents, document types, and tags, **learning continuously** from every manual correction you make to further boost automation accuracy.
* ** Business Focus:** Linfa includes **tailored integrations** and customizations for managing specific workflows (e.g., electronic invoicing) and features **Business Intelligence dashboards** to track your digitalization KPIs and estimated environmental impact.
* ** Expert Support:** The system comes with dedicated consultancy, deployment, and technical support services provided by an **experienced IT Systems Engineer** (myself!). This guarantees the **stability and critical maintenance** your business depends on.
Stop burying valuable data in paper. Rely on **Linfa** for a secure, **eco-friendly**, and fully controlled digital archive.
---
Contact me at matteo@idrainformatica.it for more info about the support and dedicated hosting.

8
crowdin.yml Normal file
View File

@@ -0,0 +1,8 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
- source: /src/locale/en_US/LC_MESSAGES/django.po
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po
- source: /src-ui/messages.xlf
translation: /src-ui/src/locale/messages.%locale_with_underscore%.xlf

1
docker/compose/.env Normal file
View File

@@ -0,0 +1 @@
COMPOSE_PROJECT_NAME=paperless

View File

@@ -0,0 +1,25 @@
# Docker Compose file for running paperless testing with actual Gotenberg
# and Tika containers for a more end to end test of the Tika related functionality
# Can be used locally or by the CI to start the necessary containers with the
# correct networking for the tests
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.23
hostname: gotenberg
container_name: gotenberg
network_mode: host
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
- "--log-level=warn"
- "--log-format=text"
tika:
image: docker.io/apache/tika:latest
hostname: tika
container_name: tika
network_mode: host
restart: unless-stopped

View File

@@ -0,0 +1,37 @@
###############################################################################
# Paperless-ngx settings #
###############################################################################
# See http://docs.paperless-ngx.com/configuration/ for all available options.
# The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the
# consumption directory.
#USERMAP_UID=1000
#USERMAP_GID=1000
# See the documentation linked above for all options. A few commonly adjusted settings
# are provided below.
# This is required if you will be exposing Paperless-ngx on a public domain
# (if doing so please consider security measures such as reverse proxy)
#PAPERLESS_URL=https://paperless.example.com
# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
#PAPERLESS_SECRET_KEY=change-me
# Use this variable to set a timezone for the Paperless Docker containers. Defaults to UTC.
#PAPERLESS_TIME_ZONE=America/Los_Angeles
# The default language to use for OCR. Set this to the language most of your
# documents are written in.
#PAPERLESS_OCR_LANGUAGE=eng
# Additional languages to install for text recognition, separated by a whitespace.
# Note that this is different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines
# the language used for OCR.
# The container installs English, German, Italian, Spanish and French by default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names
# for available languages.
#PAPERLESS_OCR_LANGUAGES=tur ces

View File

@@ -0,0 +1,90 @@
# docker compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), MariaDB is used as the database server.
# - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter-
# parts).
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/mariadb:12
restart: unless-stopped
volumes:
- dbdata:/var/lib/mysql
environment:
MARIADB_HOST: paperless
MARIADB_DATABASE: paperless
MARIADB_USER: paperless
MARIADB_PASSWORD: paperless
MARIADB_ROOT_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
- gotenberg
- tika
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBENGINE: mariadb
PAPERLESS_DBHOST: db
PAPERLESS_DBUSER: paperless # only needed if non-default username
PAPERLESS_DBPASS: paperless # only needed if non-default password
PAPERLESS_DBPORT: 3306
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.23
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:
dbdata:
redisdata:

View File

@@ -0,0 +1,69 @@
# Docker Compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), MariaDB is used as the database server.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/mariadb:12
restart: unless-stopped
volumes:
- dbdata:/var/lib/mysql
environment:
MARIADB_HOST: paperless
MARIADB_DATABASE: paperless
MARIADB_USER: paperless
MARIADB_PASSWORD: paperless
MARIADB_ROOT_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBENGINE: mariadb
PAPERLESS_DBHOST: db
PAPERLESS_DBUSER: paperless # only needed if non-default username
PAPERLESS_DBPASS: paperless # only needed if non-default password
PAPERLESS_DBPORT: 3306
volumes:
data:
media:
dbdata:
redisdata:

View File

@@ -0,0 +1,65 @@
# Docker Compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8010.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
#
# To install and update paperless with this file, do the following:
#
# - Open portainer Stacks list and click 'Add stack'
# - Paste the contents of this file and assign a name, e.g. 'paperless'
# - Upload 'docker-compose.env' by clicking on 'Load variables from .env file'
# - Modify the environment variables as needed
# - Click 'Deploy the stack' and wait for it to be deployed
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "8010:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
env_file:
- stack.env
volumes:
data:
media:
pgdata:
redisdata:

View File

@@ -0,0 +1,84 @@
# Docker Compose file for running paperless from the docker container registry.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
# - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter-
# parts).
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
- gotenberg
- tika
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.23
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:
pgdata:
redisdata:

View File

@@ -0,0 +1,63 @@
# Docker Compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Instead of SQLite (default), PostgreSQL is used as the database server.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
db:
image: docker.io/library/postgres:18
restart: unless-stopped
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: paperless
POSTGRES_USER: paperless
POSTGRES_PASSWORD: paperless
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- db
- broker
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
volumes:
data:
media:
pgdata:
redisdata:

View File

@@ -0,0 +1,72 @@
# Docker Compose file for running paperless from the docker container registry.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# In addition to that, this Docker Compose file adds the following optional
# configurations:
#
# - Apache Tika and Gotenberg servers are started with paperless and paperless
# is configured to use these services. These provide support for consuming
# Office documents (Word, Excel, PowerPoint and their LibreOffice counter-
# parts).
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- broker
- gotenberg
- tika
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.23
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
data:
media:
redisdata:

View File

@@ -0,0 +1,48 @@
# Docker Compose file for running paperless from the Docker Hub.
# This file contains everything paperless needs to run.
# Paperless supports amd64, arm and arm64 hardware.
#
# All compose files of paperless configure paperless in the following way:
#
# - Paperless is (re)started on system boot, if it was running before shutdown.
# - Docker volumes for storing data are managed by Docker.
# - Folders for importing and exporting files are created in the same directory
# as this file and mounted to the correct folders inside the container.
# - Paperless listens on port 8000.
#
# SQLite is used as the database. The SQLite file is stored in the data volume.
#
# To install and update paperless with this file, do the following:
#
# - Copy this file as 'docker-compose.yml' and the files 'docker-compose.env'
# and '.env' into a folder.
# - Run 'docker compose pull'.
# - Run 'docker compose up -d'.
#
# For more extensive installation and update instructions, refer to the
# documentation.
services:
broker:
image: docker.io/library/redis:8
restart: unless-stopped
volumes:
- redisdata:/data
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- broker
ports:
- "8000:8000"
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
volumes:
data:
media:
redisdata:

BIN
docker/init-flow.drawio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Run this script to generate the management commands again (for example if a new command is create or the template is updated)
set -eu
for command in decrypt_documents \
document_archiver \
document_exporter \
document_importer \
mail_fetcher \
document_create_classifier \
document_index \
document_renamer \
document_retagger \
document_thumbnails \
document_sanity_checker \
document_fuzzy_match \
manage_superuser \
convert_mariadb_uuid \
prune_audit_logs \
createsuperuser;
do
echo "installing $command..."
sed "s/management_command/$command/g" management_script.sh >"$PWD/rootfs/usr/local/bin/$command"
chmod u=rwx,g=rwx,o=rx "$PWD/rootfs/usr/local/bin/$command"
done

14
docker/management_script.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
set -e
cd "${PAPERLESS_SRC_DIR}"
if [[ $(id -u) == 0 ]]; then
s6-setuidgid paperless python3 manage.py management_command "$@"
elif [[ $(id -un) == "paperless" ]]; then
python3 manage.py management_command "$@"
else
echo "Unknown user."
fi

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)+>
<!ATTLIST policymap xmlns CDATA #FIXED ''>
<!ELEMENT policy EMPTY>
<!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
]>
<!--
Configure ImageMagick policies.
Domains include system, delegate, coder, filter, path, or resource.
Rights include none, read, write, execute and all. Use | to combine them,
for example: "read | write" to permit read from, or write to, a path.
Use a glob expression as a pattern.
Suppose we do not want users to process MPEG video images:
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
Here we do not want users reading images from HTTP:
<policy domain="coder" rights="none" pattern="HTTP" />
The /repository file system is restricted to read only. We use a glob
expression to match all paths that start with /repository:
<policy domain="path" rights="read" pattern="/repository/*" />
Lets prevent users from executing any image filters:
<policy domain="filter" rights="none" pattern="*" />
Any large image is cached to disk rather than memory:
<policy domain="resource" name="area" value="1GP"/>
Define arguments for the memory, map, area, width, height and disk resources
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
exceeds policy maximum so memory limit is 1GB).
Rules are processed in order. Here we want to restrict ImageMagick to only
read or write a small subset of proven web-safe image types:
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
-->
<policymap>
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- <policy domain="system" name="memory-map" value="anonymous"/> -->
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
<policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="map" value="512MiB"/>
<policy domain="resource" name="width" value="16KP"/>
<policy domain="resource" name="height" value="16KP"/>
<!-- <policy domain="resource" name="list-length" value="128"/> -->
<policy domain="resource" name="area" value="128MB"/>
<policy domain="resource" name="disk" value="1GiB"/>
<!-- <policy domain="resource" name="file" value="768"/> -->
<!-- <policy domain="resource" name="thread" value="4"/> -->
<!-- <policy domain="resource" name="throttle" value="0"/> -->
<!-- <policy domain="resource" name="time" value="3600"/> -->
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
<!-- <policy domain="delegate" rights="none" pattern="HTTPS" /> -->
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/> -->
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- not needed due to the need to use explicitly by mvg: -->
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
<!-- use curl -->
<policy domain="delegate" rights="none" pattern="URL" />
<policy domain="delegate" rights="none" pattern="HTTPS" />
<policy domain="delegate" rights="none" pattern="HTTP" />
<!-- in order to avoid to get image with password text -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- disable ghostscript format types -->
<policy domain="coder" rights="none" pattern="PS" />
<policy domain="coder" rights="none" pattern="PS2" />
<policy domain="coder" rights="none" pattern="PS3" />
<policy domain="coder" rights="none" pattern="EPS" />
<policy domain="coder" rights="read|write" pattern="PDF" />
<policy domain="coder" rights="none" pattern="XPS" />
</policymap>

View File

@@ -0,0 +1,8 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-complete]"
declare -r end_time=$(date +%s)
declare -r start_time=${PAPERLESS_START_TIME_S}
echo "${log_prefix} paperless-ngx docker container init completed in $(($end_time-$start_time)) seconds"
echo "${log_prefix} Starting services"

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-complete/run

View File

@@ -0,0 +1,44 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[custom-init]"
# Mostly borrowed from the LinuxServer.io base image
# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
declare -r custom_script_dir="/custom-cont-init.d"
# Tamper checking.
# Don't run files which are owned by anyone except root
# Don't run files which are writeable by others
if [ -d "${custom_script_dir}" ]; then
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 ! -user root)" ]; then
echo "${log_prefix} **** Potential tampering with custom scripts detected ****"
echo "${log_prefix} **** The folder '${custom_script_dir}' must be owned by root ****"
exit 0
fi
if [ -n "$(/usr/bin/find "${custom_script_dir}" -maxdepth 1 -perm -o+w)" ]; then
echo "${log_prefix} **** The folder '${custom_script_dir}' or some of contents have write permissions for others, which is a security risk. ****"
echo "${log_prefix} **** Please review the permissions and their contents to make sure they are owned by root, and can only be modified by root. ****"
exit 0
fi
# Make sure custom init directory has files in it
if [ -n "$(/bin/ls --almost-all "${custom_script_dir}" 2>/dev/null)" ]; then
echo "${log_prefix} files found in ${custom_script_dir} executing"
# Loop over files in the directory
for SCRIPT in "${custom_script_dir}"/*; do
NAME="$(basename "${SCRIPT}")"
if [ -f "${SCRIPT}" ]; then
echo "${log_prefix} ${NAME}: executing..."
/command/with-contenv /bin/bash "${SCRIPT}"
echo "${log_prefix} ${NAME}: exited $?"
elif [ ! -f "${SCRIPT}" ]; then
echo "${log_prefix} ${NAME}: is not a file"
fi
done
else
echo "${log_prefix} no custom files found exiting..."
fi
else
echo "${log_prefix} ${custom_script_dir} doesn't exist, nothing to do"
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-custom-init/run

View File

@@ -0,0 +1,33 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[env-init]"
echo "${log_prefix} Checking for environment from files"
if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; then
for FILENAME in /run/s6/container_environment/*; do
if [[ "${FILENAME##*/}" == PAPERLESS_*_FILE ]]; then
# This should have been named different..
if [[ "${FILENAME##*/}" == "PAPERLESS_OCR_SKIP_ARCHIVE_FILE" || "${FILENAME##*/}" == "PAPERLESS_MODEL_FILE" ]]; then
continue
fi
SECRETFILE=$(cat "${FILENAME}")
# Check the file exists
if [[ -f ${SECRETFILE} ]]; then
# Trim off trailing _FILE
FILESTRIP=${FILENAME//_FILE/}
if [[ $(tail -n1 "${SECRETFILE}" | wc -l) != 0 ]]; then
echo "${log_prefix} Your secret: ${FILENAME##*/} contains a trailing newline and may not work as expected"
fi
# Set environment variable
cat "${SECRETFILE}" > "${FILESTRIP}"
echo "${log_prefix} ${FILESTRIP##*/} set from ${FILENAME##*/}"
else
echo "${log_prefix} cannot find secret in ${FILENAME##*/}"
fi
fi
done
else
echo "${log_prefix} No *_FILE environment found"
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-env-file/run

View File

@@ -0,0 +1,65 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-folders]"
declare -r export_dir="/usr/src/paperless/export"
declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
declare -r media_root_dir="${PAPERLESS_MEDIA_ROOT:-/usr/src/paperless/media}"
declare -r consume_dir="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
declare -r tmp_dir="${PAPERLESS_SCRATCH_DIR:=/tmp/paperless}"
declare -r main_dirs=(
"${export_dir}"
"${data_dir}"
"${media_root_dir}"
"${consume_dir}"
"${tmp_dir}"
)
declare -r extra_dirs=(
"${main_dirs[@]}"
"${data_dir}/index"
"${media_root_dir}/documents"
"${media_root_dir}/documents/originals"
"${media_root_dir}/documents/thumbnails"
)
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
# Non-root mode: Create directories as current user, warn about permission issues
echo "${log_prefix} Running in non-root mode, checking directories"
current_uid=$(id --user)
current_gid=$(id --group)
for dir in "${extra_dirs[@]}"; do
if [[ ! -d "${dir}" ]]; then
mkdir --parents --verbose "${dir}" || echo "${log_prefix} WARNING: Could not create ${dir} - permission denied"
fi
# Check permissions on existing directories too
if [[ -d "${dir}" && ! -w "${dir}" ]]; then
echo "${log_prefix} WARNING: No write permission to ${dir}"
fi
done
# Warn about ownership issues
for dir in "${main_dirs[@]}"; do
if [[ -d "${dir}" ]]; then
find "${dir}" -not \( -user ${current_uid} -and -group ${current_gid} \) -exec echo "${log_prefix} WARNING: Permission issue on {}: not owned by current user (${current_uid}:${current_gid})" \; 2>/dev/null || echo "${log_prefix} WARNING: Cannot check permissions on ${dir}"
fi
done
else
# Root mode: Create and fix permissions as needed
echo "${log_prefix} Running with root privileges, adjusting directories and permissions"
# First create directories
for dir in "${extra_dirs[@]}"; do
if [[ ! -d "${dir}" ]]; then
mkdir --parents --verbose "${dir}"
fi
done
# Then fix permissions on all directories
for dir in "${main_dirs[@]}"; do
find "${dir}" -not \( -user paperless -and -group paperless \) -exec chown --changes paperless:paperless {} +
done
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-folders/run

View File

@@ -0,0 +1,18 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-migrations]"
declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
echo "${log_prefix} Apply database migrations..."
cd "${PAPERLESS_SRC_DIR}"
# The whole migrate, with flock, needs to run as the right user
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
else
exec s6-setuidgid paperless \
s6-setlock -n "${data_dir}/migration_lock" \
python3 manage.py migrate --skip-checks --no-input
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-migrations/run

View File

@@ -0,0 +1,22 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-user]"
declare -r usermap_original_uid=$(id -u paperless)
declare -r usermap_original_gid=$(id -g paperless)
declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
declare -r usermap_new_gid=${USERMAP_GID:-${usermap_original_gid:-$usermap_new_uid}}
if [[ ${usermap_new_uid} != "${usermap_original_uid}" ]]; then
echo "${log_prefix} Mapping UID for paperless to $usermap_new_uid"
usermod --non-unique --uid "${usermap_new_uid}" paperless
else
echo "${log_prefix} No UID changes for paperless"
fi
if [[ ${usermap_new_gid} != "${usermap_original_gid}" ]]; then
echo "${log_prefix} Mapping GID for paperless to $usermap_new_gid"
groupmod --non-unique --gid "${usermap_new_gid}" paperless
else
echo "${log_prefix} No GID changes for paperless"
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-modify-user/run

View File

@@ -0,0 +1,28 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-index]"
declare -r index_version=9
declare -r data_dir="${PAPERLESS_DATA_DIR:-/usr/src/paperless/data}"
declare -r index_version_file="${data_dir}/.index_version"
update_index () {
echo "${log_prefix} Search index out of date. Updating..."
cd "${PAPERLESS_SRC_DIR}"
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
python3 manage.py document_index reindex --no-progress-bar
echo ${index_version} | tee "${index_version_file}" > /dev/null
else
s6-setuidgid paperless python3 manage.py document_index reindex --no-progress-bar
echo ${index_version} | s6-setuidgid paperless tee "${index_version_file}" > /dev/null
fi
}
if [[ (! -f "${index_version_file}") ]]; then
echo "${log_prefix} No index version file found"
update_index
elif [[ $(<"${index_version_file}") != "$index_version" ]]; then
echo "${log_prefix} index version updated"
update_index
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-search-index/run

View File

@@ -0,0 +1,20 @@
#!/command/with-contenv /usr/bin/bash
# shellcheck shell=bash
declare -r log_prefix="[init-start]"
echo "${log_prefix} paperless-ngx docker container starting..."
# Set some directories into environment for other steps to access via environment
# Sort of like variables for later
printf "/usr/src/paperless/src" > /var/run/s6/container_environment/PAPERLESS_SRC_DIR
echo $(date +%s) > /var/run/s6/container_environment/PAPERLESS_START_TIME_S
# Check if we're starting as a non-root user
if [ "$(id --user)" != "0" ]; then
printf "true" > /var/run/s6/container_environment/USER_IS_NON_ROOT
echo "${log_prefix} paperless-ngx docker container running under a user ($(id --user):$(id --group))"
else
printf "/usr/src/paperless" > /var/run/s6/container_environment/HOME
echo "${log_prefix} paperless-ngx docker container starting init as root"
fi

View File

@@ -0,0 +1 @@
oneshot

View File

@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/init-start/run

Some files were not shown because too many files have changed in this diff Show More