Files
desktop-planer/backend/app/main.py
2026-09-22 13:14:06 +02:00

101 lines
3.2 KiB
Python

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from . import db
from .config import BASE_COLLECTIONS, COLLECTIONS, SYNC_TOKEN
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("app")
@asynccontextmanager
async def lifespan(_app):
if not SYNC_TOKEN:
log.warning("SYNC_TOKEN ist leer, jede Anfrage wird abgelehnt.")
db.init()
yield
app = FastAPI(title="Desktop Sync", lifespan=lifespan)
def check_token(authorization: str):
if not SYNC_TOKEN or authorization != f"Bearer {SYNC_TOKEN}":
raise HTTPException(status_code=401, detail="Token fehlt oder ist falsch")
class SyncIn(BaseModel):
since: int = 0
events: list[dict] | None = None
todos: list[dict] | None = None
notes: list[dict] | None = None
folders: list[dict] | None = None
files: list[dict] | None = None
# Zurück kommen die Sammlungen der Anfrage. Ohne Sammlungen die vier Basis-Sammlungen,
# Dateien nur auf ausdrückliche Anfrage, sonst zieht das Handy Sitzungen mit.
@property
def requested(self):
named = [c for c in COLLECTIONS if getattr(self, c) is not None]
return named or list(BASE_COLLECTIONS)
@app.get("/api/health")
def health():
return {"ok": True}
@app.post("/api/sync")
def sync(body: SyncIn, authorization: str = Header(default="")):
"""Nimmt Änderungen des Clients an und gibt alles zurück, was sich seit seinem Stand geändert hat.
Konflikte: der Eintrag mit dem neueren updated gewinnt. Verliert der Client, bekommt er den
Server-Stand zurück. Die Antwort enthält auch die gerade angenommenen Einträge; der Client
erkennt sie am gleichen updated und überspringt sie.
"""
check_token(authorization)
with db.connect() as conn:
accepted = 0
rejected = {collection: [] for collection in COLLECTIONS}
for collection in COLLECTIONS:
for item in getattr(body, collection) or []:
ok, stored = db.merge(conn, collection, item)
if ok:
accepted += 1
else:
rejected[collection].append(stored)
out = {}
for collection in body.requested:
changed = db.since(conn, collection, body.since)
ids = {item["id"] for item in changed}
out[collection] = changed + [item for item in rejected[collection] if item["id"] not in ids]
out["cursor"] = db.cursor(conn)
if accepted:
log.info("sync: %d Einträge übernommen", accepted)
return out
# Lesezugriff für andere Anwendungen
@app.get("/api/events")
def events(authorization: str = Header(default="")):
check_token(authorization)
with db.connect() as conn:
return db.alive(conn, "events")
@app.get("/api/todos")
def todos(authorization: str = Header(default="")):
check_token(authorization)
with db.connect() as conn:
return db.alive(conn, "todos")
@app.get("/api/notes")
def notes(authorization: str = Header(default="")):
check_token(authorization)
with db.connect() as conn:
return db.alive(conn, "notes")