Neuer `sync`-Subcommand: authentifiziert über den Personal-Access-Token gegen GET /api/sync/ + GET /books/<id>/data/, entschlüsselt Metadaten und Buchbytes lokal mit AES-256-GCM (diora_tui/crypto.py, kompatibel zu static/js/app.js' encryptBytes/decryptBytes) und legt EPUBs als normale Dateien in der Library ab — PDFs werden übersprungen. Zugangsdaten (Server-URL, Token, Base64-Key) landen auf Wunsch in ~/.config/diora-tui/config.json (0600), sonst interaktive Abfrage pro Lauf. Lesefortschritt wird bewusst noch nicht synced: der Server verankert Position als "blockIndex:innerFraction" in der Absatz-Nummerierung des Web-Readers, die nicht 1:1 auf das Kapitel-basierte scroll_fraction dieser TUI abbildet — ein naiver Abgleich würde falsche Positionen liefern. Getestet: AES-GCM-Rundreise + Falsch-Schlüssel-Ablehnung, vollständiger sync-Lauf gegen einen echten Dev-Server (Token/Key-Abfrage, Download, Entschlüsselung, Dedup bei erneutem Lauf, Fehlerfälle bei falschem Token/Key, --save inkl. 0600-Datei und Wiederverwendung ohne Prompt).
34 lines
1.2 KiB
Python
34 lines
1.2 KiB
Python
"""HTTP client for diora's sync API (see the repo's CLAUDE.md, "Sync API for
|
|
local clients"). Status: that API is provisional — expect endpoint/field
|
|
changes as the server-side implementation settles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import requests
|
|
|
|
|
|
class ApiError(Exception):
|
|
pass
|
|
|
|
|
|
def _headers(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def fetch_sync_snapshot(server_url: str, token: str) -> dict:
|
|
resp = requests.get(f"{server_url}/api/sync/", headers=_headers(token), timeout=30)
|
|
if resp.status_code == 401:
|
|
raise ApiError("Authentifizierung fehlgeschlagen — Token falsch oder abgelaufen?")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def fetch_book_data(server_url: str, token: str, book_id: int) -> dict:
|
|
resp = requests.get(f"{server_url}/books/{book_id}/data/", headers=_headers(token), timeout=120)
|
|
if resp.status_code == 401:
|
|
raise ApiError("Authentifizierung fehlgeschlagen — Token falsch oder abgelaufen?")
|
|
if resp.status_code == 404:
|
|
raise ApiError(f"Buch {book_id} nicht gefunden (falscher Owner oder gelöscht?)")
|
|
resp.raise_for_status()
|
|
return resp.json()
|