patristic-be / src /lib /storage /base.py
Mario33333's picture
deploy: reader-access security hardening (feature/audiobook@06a5ed8)
7c2d250 verified
Raw
History Blame Contribute Delete
2.78 kB
"""Storage protocol β€” what every backend must implement."""
from __future__ import annotations
from pathlib import Path
from typing import Protocol
class Storage(Protocol):
"""Backend-agnostic PDF store keyed by book_id.
All methods raise on hard failures (missing creds, network, etc.) β€” the
caller decides whether to retry. Callers should never construct paths
themselves; route everything through this interface.
"""
name: str
def ensure_local(self, book_id: str) -> Path:
"""Return a local Path the caller can hand to PyMuPDF / open().
Local backend: returns the canonical raw_dir path; raises if the
file is missing.
R2 backend: checks the local cache; if missing, downloads from R2
first, then returns the cached path. Subsequent calls are free.
"""
...
def put_pdf(self, book_id: str, src: bytes | Path) -> str:
"""Store a PDF. `src` may be raw bytes (from a file_uploader) or a
local Path. Returns a URI describing where it landed
(`file://...` or `r2://bucket/key`)."""
...
def exists(self, book_id: str) -> bool:
"""Cheap existence check at the backend (no download)."""
...
def delete(self, book_id: str) -> None:
"""Remove the PDF from storage. No-op if absent."""
...
def list_book_ids(self) -> list[str]:
"""Every book_id that has a stored PDF β€” used by the migration tool
to verify the source/destination inventories agree."""
...
def ensure_text_local(self, book_id: str) -> None:
"""Materialise the per-page OCR/clean JSON for ``book_id`` onto the
local filesystem (``cfg.paths.ocr_dir`` / ``clean_dir``).
Local backend: no-op β€” the JSON is already canonical on disk.
R2 backend: downloads the per-book text bundle(s) and extracts the
``page_NNNN.json`` files so the disk-reading code (page endpoints,
PDF export builder) works unchanged on an ephemeral cloud disk.
Idempotent and cheap on repeat calls. A book with no text bundle
(extraction never run, or no clean stage) is left untouched.
"""
...
def put_text(self, book_id: str) -> list[str]:
"""Push the local OCR/clean per-page JSON for ``book_id`` to storage.
Local backend: no-op (disk is canonical) β€” returns ``[]``.
R2 backend: zips ``ocr_dir/{book_id}`` and ``clean_dir/{book_id}``
into one object each and uploads them, returning the storage URIs.
Mirrors :meth:`put_pdf` so promoting a book to the cloud carries its
text layer, not just the PDF.
"""
...