From 1a479ceb247789e8e07e5a0280e05c1071a84118 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 21 Aug 2026 03:09:04 +0000 Subject: [PATCH] Add --dbip CLI flag to auto-download/update the DB-IP MMDB database. Downloads the latest dbip-city-lite-YYYY-MM.mmdb.gz before starting the server, skipping when the local database is current, falling back to the previous month on 404, and removing older databases after an update. Promotes httpx to a runtime dependency. --- docs/analytics.md | 6 +++- pagerite/__main__.py | 69 +++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 5 ++-- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/analytics.md b/docs/analytics.md index 40783de..ec3cc37 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -57,7 +57,11 @@ The client (`pagerite.js`) POSTs fire-and-forget pings to `/_a` with `country`. The MMDB lookup and the reverse-DNS lookup run in background tasks after the visit is stored, so the `/ _a` response is never delayed. The decompressed `dbip-*.mmdb` file is kept in the repository root and - ignored by git. + ignored by git. The CLI flag `--dbip` (`uv run pagerite --dbip`) downloads + the latest `dbip-city-lite-YYYY-MM.mmdb.gz` from DB-IP before the server + starts, skipping the download when the local database is already current and + removing older versions after an update; without the flag only an existing + file is used. - **Crawler hits**: every document GET is queued in RAM as a pending crawler hit. If a ping from the same (IP, User-Agent) pair arrives within 10 seconds the hit is discarded; otherwise it is written to `crawlers`. diff --git a/pagerite/__main__.py b/pagerite/__main__.py index f5c02a4..dafe3f2 100644 --- a/pagerite/__main__.py +++ b/pagerite/__main__.py @@ -1,14 +1,74 @@ -# auto-upgrade@fastapi-vue-setup - remove this if you modify this file """Command-line entry point for running the backend server.""" import argparse +import gzip import os +import sys +from datetime import date +from pathlib import Path +import httpx from fastapi_vue import server DEFAULT_PORT = 8100 DEVMODE = os.getenv("PAGERITE_DEV") == "1" +# Repository root (pagerite/__main__.py -> ..), where the MMDB lives. +_REPO_ROOT = Path(__file__).resolve().parent.parent + +DBIP_URL = "https://download.db-ip.com/free/dbip-city-lite-{month}.mmdb.gz" + + +def _download_dbip() -> None: + """Download the latest dbip-city-lite MMDB if ours is missing or older.""" + today = date.today() + months = [f"{today:%Y-%m}"] + # The current month's file may not be published yet; fall back to last month. + prev = (today.replace(day=1) - date.resolution).replace(day=1) + months.append(f"{prev:%Y-%m}") + + existing = sorted( + p.stem.removeprefix("dbip-city-lite-").removesuffix(".mmdb") + for p in _REPO_ROOT.glob("dbip-city-lite-*.mmdb*") + ) + if existing and existing[-1] >= months[0]: + print(f"pagerite: DB-IP database is current ({existing[-1]}), skipping download") + return + + for month in months: + url = DBIP_URL.format(month=month) + target = _REPO_ROOT / f"dbip-city-lite-{month}.mmdb.gz" + tmp = target.with_suffix(".mmdb.gz.tmp") + print(f"pagerite: downloading {url}") + try: + with httpx.stream("GET", url, follow_redirects=True, timeout=120) as r: + if r.status_code == 404: + continue + r.raise_for_status() + with open(tmp, "wb") as f: + for chunk in r.iter_bytes(): + f.write(chunk) + except httpx.HTTPError as e: + print(f"pagerite: DB-IP download failed: {e}", file=sys.stderr) + tmp.unlink(missing_ok=True) + continue + # Verify it is actually gzip data before installing it. + try: + with gzip.open(tmp, "rb") as f: + f.read(1) + except OSError: + print(f"pagerite: DB-IP download for {month} was not valid gzip", file=sys.stderr) + tmp.unlink(missing_ok=True) + continue + os.replace(tmp, target) + # Drop older databases so the app never picks up a stale one. + for old in _REPO_ROOT.glob("dbip-city-lite-*.mmdb*"): + if old.name != target.name: + old.unlink() + print(f"pagerite: DB-IP database updated to {target.name}") + return + print("pagerite: could not download a DB-IP database", file=sys.stderr) + def main() -> None: """Run the backend server with optional arguments.""" @@ -19,7 +79,14 @@ def main() -> None: action="append", help=(f"Endpoint (default: localhost:{DEFAULT_PORT})."), ) + parser.add_argument( + "--dbip", + action="store_true", + help="Download/update the DB-IP city lite database before starting.", + ) args = parser.parse_args() + if args.dbip: + _download_dbip() dev = {"reload": True, "reload_dirs": ["pagerite"]} if DEVMODE else {} server.run( "pagerite.app:app", diff --git a/pyproject.toml b/pyproject.toml index 02168d9..e9d23ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "fastapi-vue>=1.3.1", "fastapi[standard]>=0.141.1", "html5tagger>=2.0.0", + "httpx>=0.28.1", "kanta>=0.8.1", "markdown-it-py>=4.2.0", "maxminddb>=3.1.1", @@ -35,9 +36,7 @@ pagerite = "pagerite.__main__:main" Repository = "https://git.zi.fi/LeoVasanko/pagerite" [dependency-groups] -dev = [ - "httpx>=0.28.1", -] +dev = [] [tool.hatch.version] source = "vcs"