File size: 2,776 Bytes
7c2d250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""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.

        """
        ...