progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
88 lines
4.2 KiB
Python
88 lines
4.2 KiB
Python
"""End-to-end check of the sync against a LOCAL diora server (never a real account):
|
|
login, protected credentials, book list, lazy download, a position going out, another device
|
|
reading later, an offline catch-up that must lose. Throwaway state in a temp folder.
|
|
|
|
python tools/smoke_server.py http://127.0.0.1:8001 <username> <password>
|
|
|
|
It can run again on the same database (the furthest place only ever grows).
|
|
|
|
The server is the diora-web checkout run with its own throwaway database, seeded like
|
|
diora-android's tools/seed_testserver.py does.
|
|
"""
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
home = tempfile.mkdtemp(prefix="diora-tui-smoke-")
|
|
for var in ("LOCALAPPDATA", "APPDATA", "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME"):
|
|
os.environ[var] = home
|
|
os.environ["HOME"] = home
|
|
os.environ["DIORA_TUI_HOME"] = home
|
|
|
|
from diora_tui import api, blocks, config, crypto, epub, login, progress, remote # noqa: E402
|
|
|
|
server, username, password = sys.argv[1].rstrip("/"), sys.argv[2], sys.argv[3]
|
|
if "127.0.0.1" not in server and "localhost" not in server:
|
|
sys.exit("only against a local test server")
|
|
|
|
|
|
def check(what, ok, detail=""):
|
|
print(("OK " if ok else "FAIL ") + what + (f" [{detail}]" if detail else ""))
|
|
if not ok:
|
|
sys.exit(1)
|
|
|
|
|
|
token, name = login.web_login(server, username, password)
|
|
key_b64 = crypto.derive_key_b64(name, password)
|
|
cfg = config.RemoteConfig(server, token, key_b64, name)
|
|
config.save(cfg)
|
|
back = config.load()
|
|
check("credentials stored protected and read back",
|
|
back == cfg and (sys.platform != "win32" or "dpapi:" in Path(config._CONFIG_FILE).read_text()))
|
|
check("no token or key in the config file in the clear",
|
|
token not in Path(config._CONFIG_FILE).read_text() and key_b64 not in Path(config._CONFIG_FILE).read_text())
|
|
|
|
lib = Path(home) / "lib"
|
|
res = remote.sync_library(lib, cfg)
|
|
check("book list pulled, nothing downloaded", res.books >= 1 and not list(lib.glob("*.epub")), f"{res.books} books")
|
|
store = progress.default()
|
|
check("server clock offset learnt", abs(store.offset) < 5000, f"{store.offset} ms")
|
|
|
|
entry = sorted(__import__("diora_tui.library_meta", fromlist=["x"]).remote_entries().items())[0]
|
|
skey, info = entry
|
|
path = remote.download_book(lib, cfg, info["server_id"], info["title"])
|
|
check("book downloaded on demand and keyed by the server id", path.exists() and epub._book_id(path) == skey, path.name)
|
|
book = blocks.load_flat_book(path)
|
|
|
|
# the reader arrives at block 12 now
|
|
now = progress.now_ms()
|
|
a1 = blocks.format_anchor(12, 0.5)
|
|
store.write(skey, a1, blocks.fraction_for_anchor(book, 12, 0.5), now)
|
|
check("position queued", store.get(skey).pending is not None)
|
|
check("flushed", remote.flush_pending(cfg) == 1 and store.get(skey).pending is None)
|
|
row = [b for b in api.fetch_books(server, token)[0] if b["id"] == info["server_id"]][0]
|
|
check("server holds it with device and a real fraction",
|
|
row["position_anchor"] == a1 and row["device"] == "diora TUI" and 0 < row["scroll_fraction"] < 1
|
|
and abs(row["read_at"] - now) < 10_000, f"fraction {row['scroll_fraction']:.4f}, read_at diff {row['read_at'] - now} ms")
|
|
|
|
# another device reads somewhere else, later
|
|
time.sleep(0.05)
|
|
other = blocks.format_anchor(3, 0.0)
|
|
api.post_progress(server, token, info["server_id"], anchor=other, fraction=0.02, read_at=progress.now_ms(), device="Handy")
|
|
remote.sync_library(lib, cfg)
|
|
st = store.get(skey)
|
|
check("a pull shows the later position of the other device", st.anchor == other and st.device == "Handy")
|
|
check("the furthest place is kept (never behind what was read)",
|
|
progress.cmp_progress(st.furthest_anchor, 0, a1, 0) >= 0, st.furthest_anchor)
|
|
|
|
# an offline catch-up from before that must not become the continue point
|
|
store.write(skey, blocks.format_anchor(30, 0.0), 0.5, progress.now_ms() - 120_000)
|
|
remote.flush_pending(cfg)
|
|
row = [b for b in api.fetch_books(server, token)[0] if b["id"] == info["server_id"]][0]
|
|
check("an older write loses on the server", row["position_anchor"] == other and row["device"] == "Handy")
|
|
check("...and locally", store.get(skey).anchor == other and store.get(skey).pending is None)
|
|
check("...but raises the furthest place",
|
|
progress.cmp_progress(row["furthest_anchor"], 0, blocks.format_anchor(30, 0.0), 0) >= 0, row["furthest_anchor"])
|
|
print("all good")
|