paperless-export
Overview
A thin scheduled wrapper around Paperless-ngx's
built-in document_exporter, plus the one thing it doesn't do: a materialized
_Steuer/YYYY/ tax view built from your Steuer-YYYY tags.
Paperless's exporter already produces the full no-lock-in export — every
document laid out by your storage-path template, originals and PDF/A archive
versions, and a complete manifest.json (tags, correspondents, types, custom
fields). This tool deliberately does not rebuild any of that. It:
- runs
document_exporter <target> --use-filename-format --compare-checksums --delete(each flag toggleable), streams progress with bounded diagnostics, and falls back to a flat export with a clear warning when a path exceeds the OS limit, - optionally embeds metadata into each distinct exported original/archive PDF before any derived copies are created,
- validates every manifest path inside the export root, then builds
_Steuer/<YYYY>/— one original-based symlink (or copy) per document taggedSteuer-YYYY— plus a greppable_Steuer/INDEX.csv, - publishes the derived view only after every required source and staged output
validates; a failed replacement leaves the prior complete
_Steuercurrent.
This is a readable escape copy, not a replacement for tested backups of Paperless and its database.
export/
Bescheid/Finanzamt/2024-05-01 Steuerbescheid.pdf # ← document_exporter
manifest.json # ← document_exporter
_Steuer/
2024/2024-05-01 Steuerbescheid.pdf → ../../Bescheid/Finanzamt/…
INDEX.csv # year,title,correspondent,created,original_path
Status
The latest verified public release is 1.2.2 across GitHub Releases, PyPI, and the Fileworks Homebrew tap.
Install
pipx install paperless-export # + 'paperless-export[pdf]' for --embed-tags
# or
brew install fileworks/tap/paperless-export
Released 1.2.2 on
PyPI,
GitHub, and
fileworks/tap (verified 2026-08-12).
Quick start
# from the directory holding your Paperless compose file
paperless-export run --export-dir ~/paperless-export
The first run performs a full export. Later runs reuse what is already there and rebuild only what changed.
Usage
# the nightly job (run from the directory containing your compose file):
paperless-export run --export-dir /volume1/paperless/export
# protected secret-file path only; the passphrase value never enters argv/env:
paperless-export run \
--export-dir /volume1/paperless/export \
--passphrase-file /run/secrets/paperless-export-passphrase
# rebuild only the tax view from an existing export:
paperless-export tax-view --export-dir /volume1/paperless/export
# FAT/exFAT or cloud targets that don't preserve symlinks:
paperless-export run --export-dir ./export --copy
# embed originals and archive PDFs without creating _Steuer:
paperless-export run --export-dir ./export --no-tax-view --embed-tags
Notes:
--exporter-target(default../export) is the path as the exporter process sees it inside the container;--export-diris the same directory on this host. With the standard compose setup they're the same bind mount.PAPERLESS_URL+PAPERLESS_TOKEN(env or flags) enable a preflight check so a bad token fails fast with a clear message — they're optional because the exporter itself runs inside the container and needs no API access.PAPERLESS_EXPORT_PASSPHRASE_FILEis a path-only alias for--passphrase-file.-reads the passphrase once from standard input. Protected files must be regular, not symlinks, and mode0600on POSIX.PAPERLESS_EXPORT_LOG_FILEis the environment alias for--log-file. Without it, a bounded rotatingpaperless-export.logis written beside the export directory for unattended-job diagnostics.PAPERLESS_EXPORT_TIMEOUT_SECONDSis the environment alias for--exporter-timeout. The reviewed default is six hours; expiry terminates the child cleanly, then force-kills only if it will not exit.- Passphrase transport is supported for the default
docker compose exec -T webserver document_exporteradapter. A custom exporter command is rejected when a passphrase is configured unless this project gains a separately reviewed stdin adapter for it. --embed-tagsrewrites each distinct original PDF and Paperless PDF/A archive. Non-PDF originals are skipped. Embedding happens before_Steuercopies, so copy mode receives the updated bytes. Rewrites change checksums, so Paperless can re-export those files on the next--compare-checksumsrun.
Passphrase and export security
Without a passphrase, the command warns before launching Paperless because Paperless-ngx 2.20.x may store supported account secrets in plaintext. Paperless's native passphrase protects these fields:
- mail-account
passwordandrefresh_token; - social-token
tokenandtoken_secret.
The passphrase does not encrypt the entire manifest.json, exported
documents, or other metadata. Paperless records the cryptographic parameters
in metadata.json, and a later import needs the same passphrase. Keep the
passphrase in a secret-manager-mounted file (or provide it over stdin), make
the export directory accessible only to the backup account, and protect backup
copies with storage-level encryption. Do not put the secret value in a shell
argument or environment variable.
Every original and archive path read from manifest.json is treated as
untrusted. Absolute paths, Windows drive/UNC forms, empty/malformed components,
parent traversal, and symlink escapes are rejected before _Steuer is cleared
or any PDF is opened. The same confinement is repeated immediately before
file operations. Do not modify the export tree concurrently with
post-processing.
Behavior guarantees
- Read-only against Paperless — writes only into the export directory.
- Atomic derived view —
_Steuer/is built in a confined same-filesystem sibling stage and journalled through publication. Missing sources, copy/link failures, and interruptions preserve the last complete view. - Verifiable — after a run,
_Steuer/2025/contains exactly the documents taggedSteuer-2025;INDEX.csvmatches a manifest query. - Honest failures — exporter, unavailable infrastructure, unsafe output, and incomplete PDF metadata have separate stable exit categories. A missing tax-view source is a fatal output error because publishing a partial view is forbidden.
- Never silent —
document_exporter's output is relayed live rather than buffered until the end. Final errors repeat only the last 64 KiB diagnostic tail; path-too-long detection covers the entire stream independently.
Exit codes
immich-export, paperless-export and unpacksort share one exit-code
vocabulary, so a script can branch on the code without knowing which tool it
ran. The class of outcome is the same everywhere; the specific condition is
this tool's, and the table below is what it means here.
| Code | Name | Meaning |
|---|---|---|
| 0 | SUCCESS |
everything asked for was done |
| 1 | PARTIAL |
the exporter succeeded but requested post-processing is incomplete |
| 2 | USAGE |
bad flags, a path that is missing or malformed, or credentials the server rejected — nothing was attempted |
| 3 | CONFLICT |
the Paperless API, or its Docker/Compose service or container, is unavailable |
| 4 | FATAL |
unexpected failure, or a write that failed after work had begun (re-run with --verbose) |
| 130 | INTERRUPTED |
cancelled by the operator |
The line between 2 and 4 is was anything attempted. A --export-dir that
is missing when you type the command is 2 in every tool: nothing ran, and
nothing was written. A write that fails part-way through leaves the export in a
state nobody can characterise, and that is 4.
document_exporter's own child code stays in the diagnostics line.
Scheduling on a Synology (DSM Task Scheduler)
Use the checked-in POSIX/BusyBox-compatible wrapper so Task Scheduler does not depend on an interactive shell profile:
PAPERLESS_COMPOSE_DIR=/volume1/docker/paperless \
PAPERLESS_EXPORT_DIR=/volume1/paperless/export \
/bin/sh /volume1/docker/paperless-export/scripts/synology-task.sh
Nightly, after the Paperless backup window; the export target should live on a
share covered by your backup chain. Run the task as a dedicated account that can
read the Compose project and write only the export directory. The wrapper sets
umask 077, validates absolute/readable locations before launch, and is syntax-
checked with a strict POSIX shell in CI.
Configuration
Configuration is by flag and environment variable; nothing is stored between runs.
| Setting | Purpose |
|---|---|
--exporter-cmd |
Exporter invocation; defaults to Docker Compose in the working directory |
--exporter-target |
Export path as seen inside the exporter container |
--passphrase-file |
File holding the export passphrase. The value never enters argv or the environment |
--copy |
Use verified copies in _Steuer instead of symlinks |
--no-tax-view |
Skip building _Steuer |
--log-file |
Bounded rotating logfile path |
--exporter-timeout |
Stop a stuck child after 21600 seconds by default |
--help is authoritative.
Troubleshooting
The export appears to hang. Child output is streamed and a five-second heartbeat reaches both the terminal and rotating logfile during silent work. The configured exporter timeout stops a genuinely stuck child.
Exit code 2 for a directory you expected to work. tax-view rebuilds the
views of an export it did not produce, so --export-dir has to exist already;
run paperless-export run first. run will create the directory itself, but
only its last segment, and only when the parent is already there — so a mistyped
path fails loudly instead of quietly becoming a new empty tree.
Exit code 1 or 4. One means document_exporter succeeded but requested
post-processing (PDF metadata embedding, the _Steuer view) is incomplete; the
report names each part that is missing. Four means an unexpected failure, or
output that could not be written once the run was under way — including a
manifest path pointing outside the export root. Both leave Paperless originals
untouched, and a failed tax-view publication retains the previous complete
_Steuer.
Symlinks fail on the target. Use --copy; the tax view is then built
from copies.
Development
uv sync --locked --all-extras --all-groups
uv run ruff check . && uv run ruff format --check . # lint
uv run mypy # strict types
uv run pytest # tests
uv build # sdist + wheel
Pull requests run linting, strict types, tests, and builds on Python 3.12, 3.13, and 3.14, plus Linux/macOS/Windows portability, the Synology contract, documentation links, dependency auditing, and an installed-wheel smoke test. Renovate batches routine dependency changes into one Monday PR and automerge is enabled; a per-repository concurrency limit of one prevents stacked dependency branches or PRs.
A separate Sunday workflow indexes production-format manifests at 50,000,
100,000, and 500,000 entries, keeping schedule-only work out of pull-request
and main CI checks.
Conventional Commits drive python-semantic-release. After CI passes on a
main push, the release workflow pins the tested SHA, verifies the staged
source, lock, tag, distributions, and installed CLI, then atomically pushes the
release commit and tag. Publication proceeds through the protected
github-release, pypi, and homebrew environments in that order; PyPI uses
trusted publishing and GitHub Releases carry the verified wheel and sdist.
For per-clone paths, commands, or preferences, create an ignored
CLAUDE.local.md at the repository root. Do not put credentials or other
secrets in it.
Security
Report vulnerabilities privately as described in SECURITY.md.
Registered secrets — the Paperless token and the passphrase — are redacted from
the logfile and from captured exporter output, including values split across
chunk boundaries. A passphrase is accepted only from a protected file or
standard input, never from argv or the environment.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paperless_export-2.0.0.tar.gz.
File metadata
- Download URL: paperless_export-2.0.0.tar.gz
- Upload date:
- Size: 155.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8cc7772001893af23513f1feb925752a8d82168d21f8456dda7f29e3fd5f7c88
|
|
| MD5 |
2bc98d926b40bd72b3103f1eba4c0db4
|
|
| BLAKE2b-256 |
f44631588130f6674a7776edf87e8a45ea8637b8ff721e345a55e9e9499cc353
|
Provenance
The following attestation bundles were made for paperless_export-2.0.0.tar.gz:
Publisher:
release.yml on fileworks/paperless-export
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paperless_export-2.0.0.tar.gz -
Subject digest:
8cc7772001893af23513f1feb925752a8d82168d21f8456dda7f29e3fd5f7c88 - Sigstore transparency entry: 2440179263
- Sigstore integration time:
-
Permalink:
fileworks/paperless-export@cd6ea67ab8d4eb831d7cd1bf7567fb80dff3644c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/fileworks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd6ea67ab8d4eb831d7cd1bf7567fb80dff3644c -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file paperless_export-2.0.0-py3-none-any.whl.
File metadata
- Download URL: paperless_export-2.0.0-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6bfd82da21b94f67bdfa30e321b697515ee4eb01f3773d16dd2182e8e44cf2a2
|
|
| MD5 |
365fe21cf2dc60b4fc6798ad3534212d
|
|
| BLAKE2b-256 |
2ee548d24936cb6d7ec8c1507e4ed7ed4b4ac62b6b04773b86b64d30af111aa1
|
Provenance
The following attestation bundles were made for paperless_export-2.0.0-py3-none-any.whl:
Publisher:
release.yml on fileworks/paperless-export
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
paperless_export-2.0.0-py3-none-any.whl -
Subject digest:
6bfd82da21b94f67bdfa30e321b697515ee4eb01f3773d16dd2182e8e44cf2a2 - Sigstore transparency entry: 2440179569
- Sigstore integration time:
-
Permalink:
fileworks/paperless-export@cd6ea67ab8d4eb831d7cd1bf7567fb80dff3644c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/fileworks
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cd6ea67ab8d4eb831d7cd1bf7567fb80dff3644c -
Trigger Event:
workflow_run
-
Statement type: