Spaces:
Running
Running
| """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. | |
| """ | |
| ... | |