feat: POST /api/push/dav-bulk — push masivo por disco + 1 commit (segundos vs minutos)
Vía rápida DB→Xandikos para operaciones masivas: genera todos los vCards de agenda desde DuckDB a un tmpdir, rsync de golpe al working tree de la colección en magnus (excluyendo .git/.xandikos), UN solo git commit, y 1 PROPFIND para capturar todos los etags en batch. ~0.5s vs ~6min del push HTTP (que hace N PUTs + N PROPFINDs + N commits). El push HTTP push_all_dav se mantiene como fallback (y para CalDAV). Config DAV_BULK_SSH_HOST/REMOTE_DIR. 22 tests verdes.
This commit is contained in:
@@ -28,6 +28,8 @@ endpoints de datos responden SIEMPRE 200 con status ok|error en el body):
|
||||
POST /api/query/named ejecuta una query del catálogo {name, max_rows}
|
||||
POST /api/ingest/vault re-escanea el vault y reconstruye maestras+derivadas
|
||||
POST /api/ingest/dav baja Xandikos y reconstruye contacts/events+derivadas
|
||||
POST /api/push/dav push masivo HTTP de contacts+events (N PUT, fallback sin SSH)
|
||||
POST /api/push/dav-bulk push masivo de contacts por DISCO (1 rsync + 1 commit, requiere SSH)
|
||||
POST /api/sync/dav-pull sync inverso incremental por etag (ediciones del móvil)
|
||||
POST /api/render/note ejecuta query y la upserta como bloque sentinel en una nota
|
||||
"""
|
||||
@@ -166,6 +168,24 @@ def create_app(cfg: Config) -> FastAPI:
|
||||
allowed_hosts=["127.0.0.1", "localhost", "testserver"],
|
||||
)
|
||||
|
||||
# Anti-CSRF de navegador: rechaza las peticiones mutantes que el navegador
|
||||
# marca como cross-site (header Sec-Fetch-Site). Cierra el hueco de las
|
||||
# peticiones "simples" (POST sin preflight CORS) que el TrustedHost no filtra
|
||||
# porque su Host sigue siendo 127.0.0.1. Los clientes server-to-server (urllib,
|
||||
# curl) y el frontend mismo-origen no envían 'cross-site', así que no se ven
|
||||
# afectados.
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
@app.middleware("http")
|
||||
async def _reject_cross_site(request, call_next):
|
||||
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
||||
if request.headers.get("sec-fetch-site") == "cross-site":
|
||||
return JSONResponse(
|
||||
status_code=403,
|
||||
content={"status": "error", "error": "petición cross-site rechazada"},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
def run_readonly(sql: str, params: list, max_rows: int) -> dict:
|
||||
"""Ejecuta un SELECT con la conexión read_only del registry, acotado."""
|
||||
max_rows = max(1, min(int(max_rows), 10000))
|
||||
@@ -360,6 +380,13 @@ def create_app(cfg: Config) -> FastAPI:
|
||||
def push_dav() -> dict:
|
||||
return _guard(lambda: writes.push_all_dav(cfg))
|
||||
|
||||
@app.post("/api/push/dav-bulk")
|
||||
def push_dav_bulk() -> dict:
|
||||
# Vía RÁPIDA del push de contactos: 1 rsync + 1 commit + 1 PROPFIND en
|
||||
# el host de Xandikos (requiere SSH por clave). Si no hay SSH, usar
|
||||
# /api/push/dav (HTTP) como fallback.
|
||||
return _guard(lambda: writes.push_all_dav_bulk(cfg))
|
||||
|
||||
@app.post("/api/sync/dav-pull")
|
||||
def sync_dav_pull() -> dict:
|
||||
# Sync inverso: trae a la DB las ediciones del móvil/DAVx5,
|
||||
|
||||
Reference in New Issue
Block a user