Extract documents from Google Takeout archives for import into Paperless-ngx.
Google Takeout dumps everything — documents, photos, videos, Google Photos metadata — into multi-volume archives with deeply nested directories. This tool:
- Scans your Takeout archives in all formats Google provides
- Extracts only document files you care about
- Skips unwanted files based on a single config file
- Flattens (or preserves) output directories
- Optionally fingerprints filenames with their original directory path
- Handles name collisions with rename / skip / overwrite strategies
- Prints a detailed summary report
| Format | Extension(s) | Backend | Notes |
|---|---|---|---|
| ZIP | .zip |
zipfile (stdlib) |
True streaming |
| TAR (gzip) | .tar.gz, .tgz |
tarfile (stdlib) |
True streaming |
| TAR (bzip2) | .tar.bz2 |
tarfile (stdlib) |
True streaming |
| TAR (xz) | .tar.xz |
tarfile (stdlib) |
True streaming |
| TAR (uncomp) | .tar |
tarfile (stdlib) |
True streaming |
| 7-Zip | .7z |
py7zr |
Extracts to scratch disk first (~1× archive size) |
git clone https://github.com/viscory/paperless-py.git
cd paperless-py
uv sync
# Copy the example config and edit it to your needs
cp config/example.toml config.toml
# Run it
uv run python -m paperless_py
# or, from anywhere:
paperless-py --config /path/to/config.tomlRequires Python 3.14+ and uv.
Everything is driven by config.toml. The only CLI argument is an
optional --config PATH.
A fully annotated example lives at config/example.toml —
copy it and customise.
# ── Where to read and write ──────────────────────────────────────
[paths]
input_dir = "."
output_dir = "paperless_ready"
# ── What to extract ──────────────────────────────────────────────
[include]
extensions = [".pdf", ".docx", ".doc", ".xlsx", ".xls", ".csv", ".txt"]
# ── What to skip ───────────────────────────────────────────────
[exclude]
patterns = [
"(?i)(?:^|/)google photos(?:/|$)",
"(?i)(?:^|/)trash(?:/|$)",
]
# ── How files are written ──────────────────────────────────────
[output]
dry_run = false
fingerprint = false
fingerprint_delimiter = "_"
flatten = true
collision = "rename"
# ── Execution behaviour ────────────────────────────────────────────
[runtime]
log_level = "INFO"| Section | Key | Type | Default | Description |
|---|---|---|---|---|
[paths] |
input_dir |
string | "." |
Directory containing your archive files |
[paths] |
output_dir |
string | "paperless_ready" |
Where to place extracted documents |
[include] |
extensions |
list | .pdf, .docx, … |
File extensions to extract (case-insensitive) |
[exclude] |
patterns |
list of strings | (?i)google photos, (?i)trash |
Regex patterns checked against filename and full archive path |
[output] |
dry_run |
boolean | false |
When true, no files are written |
[output] |
fingerprint |
boolean | false |
Encode original directory path into filename |
[output] |
fingerprint_delimiter |
string | "_" |
String joining path components when fingerprinting |
[output] |
flatten |
boolean | true |
When true, all files land directly in output_dir; when false, original directory structure is preserved |
[output] |
collision |
string | "rename" |
What to do when output filename exists: rename (append _N), skip, or overwrite |
[runtime] |
log_level |
string | "INFO" |
Verbosity: DEBUG, INFO, WARN, ERROR |
- No global case-insensitive flag — add
(?i)at the start of your pattern if you want case-insensitive matching. - Directory patterns should use
(?:^|/)name(?:/|$)so they match only as path components. - Filename patterns should use
^anchors to avoid matching inside a full path.
[output]
fingerprint = true
fingerprint_delimiter = "_"Takeout/Drive/Documents/report.pdf → Takeout_Drive_Documents_report.pdf
Takeout/Drive/Invoices/report.pdf → Takeout_Drive_Invoices_report.pdf
[output]
flatten = falseTakeout/Drive/Documents/report.pdf → paperless_ready/Takeout/Drive/Documents/report.pdf
[exclude]
patterns = [
"(?i)^\\d{4}_[a-z]\\d{2}_(?:qp|ms)\\.pdf$",
"(?i)^thumb_.*\\.jpg$",
][output]
collision = "skip"- Dotfiles and extensionless files —
Path("report").suffixis"", so a file literally named.pdf(or one with no extension) never matchesinclude.extensionsand is silently skipped. Use a regex inexclude.patternsif you need to handle such files explicitly.
uv sync --extra dev
# Run tests
uv run pytest -v
# Type-check
uv run mypy src/
# Lint
uv run ruff check src/ tests/paperless-py/
├── config.toml # Your local config (gitignored)
├── config/
│ └── example.toml # Annotated example with every option explained
├── src/paperless_py/
│ ├── __init__.py
│ ├── __main__.py # python -m paperless_py
│ ├── config.py # Config loading & validation (TOML)
│ ├── archive.py # Unified .zip / .tar.* / .7z reader
│ ├── extractor.py # Core extraction + fingerprint logic
│ ├── reporter.py # Rich report rendering
│ └── cli.py # Entry point
├── tests/
│ ├── conftest.py # Generates small archives in all 3 formats
│ ├── test_config.py
│ ├── test_archive.py
│ └── test_extractor.py
└── pyproject.toml