fix db sync bugs

This commit is contained in:
2026-08-05 15:10:55 +02:00
parent 85f6e79a7f
commit 12e36b2f17
4 changed files with 61 additions and 27 deletions
+22 -8
View File
@@ -89,8 +89,10 @@ class CardDAVClient:
def sync_collection(self, collection_url: str, sync_token: str | None):
"""
Führt REPORT sync-collection aus. Gibt (changed_or_new_vcards, deleted_hrefs, new_sync_token) zurück.
changed_or_new_vcards: list[str] roher vCard-Text
Führt REPORT sync-collection aus.
Gibt (changed_vcards, etags, deleted_hrefs, new_sync_token) zurück.
changed_vcards: list[str] roher vCard-Text
etags: list[str|None] ETag pro vCard (None wenn nicht vorhanden)
deleted_hrefs: list[str] Hrefs von gelöschten Kontakten (status 404)
"""
token_element = f"<d:sync-token>{sync_token}</d:sync-token>" if sync_token else "<d:sync-token/>"
@@ -102,7 +104,7 @@ class CardDAVClient:
</d:sync-collection>"""
root = self._request("REPORT", collection_url, body, depth="1")
vcards, deleted_hrefs = [], []
vcards, etags, deleted_hrefs = [], [], []
for response in root.findall("d:response", NS):
status_el = response.find(".//d:status", NS)
status_text = status_el.text if status_el is not None else ""
@@ -114,28 +116,40 @@ class CardDAVClient:
deleted_hrefs.append(href)
continue
if not any(s in status_text for s in ("200", "207")):
logger.warning("Unerwarteter Status %s für %s, überspringe", status_text, href)
continue
etag_el = response.find("d:getetag", NS)
etag = etag_el.text if etag_el is not None else None
data = response.find(".//card:address-data", NS)
if data is not None and data.text:
vcards.append(data.text)
etags.append(etag)
new_token_el = root.find("d:sync-token", NS)
new_token = new_token_el.text if new_token_el is not None else None
return vcards, deleted_hrefs, new_token
return vcards, etags, deleted_hrefs, new_token
def fetch_all_vcards(self, collection_url: str) -> list[str]:
"""Fallback für den allerersten, vollen Abruf über addressbook-query."""
def fetch_all_vcards(self, collection_url: str) -> tuple[list[str], list[str | None]]:
"""Fallback für den allerersten, vollen Abruf über addressbook-query.
Gibt (vcards, etags) zurück."""
body = """<?xml version="1.0" encoding="utf-8" ?>
<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
<d:prop><d:getetag/><card:address-data/></d:prop>
<card:filter/>
</card:addressbook-query>"""
root = self._request("REPORT", collection_url, body, depth="1")
vcards = []
vcards, etags = [], []
for response in root.findall("d:response", NS):
etag_el = response.find("d:getetag", NS)
etag = etag_el.text if etag_el is not None else None
data = response.find(".//card:address-data", NS)
if data is not None and data.text:
vcards.append(data.text)
return vcards
etags.append(etag)
return vcards, etags
def discover_collection(self) -> str:
principal = self.discover_principal()
+3 -1
View File
@@ -5,7 +5,7 @@ import logging
import os
import uuid
from contextlib import contextmanager
from datetime import date
from datetime import date, datetime
import pymysql
from pymysql.cursors import DictCursor
@@ -95,9 +95,11 @@ def finish_sync_run(conn, run_id: str, status: str, upserted: int = None, delete
def upsert_contacts(conn, contacts: list[dict], run_id: str):
if not contacts:
return
now = datetime.now()
with conn.cursor() as cur:
for c in contacts:
c["sync_run_id"] = run_id
c["last_synced_at"] = now
c = _sanitize_contact(c)
cols = list(c.keys())
placeholders = ", ".join(["%s"] * len(cols))
+34 -16
View File
@@ -9,10 +9,10 @@ vollständiger Re-Sync."""
import logging
import sys
from config import Config
from carddav_client import CardDAVClient, ICLOUD_BASE_URL, SyncTokenInvalid
from vcard_parser import parse_vcard
import db
from carddav_client import ICLOUD_BASE_URL, CardDAVClient, SyncTokenInvalid
from config import Config
from vcard_parser import parse_vcard
logging.basicConfig(level=Config.LOG_LEVEL, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("sync")
@@ -29,35 +29,53 @@ def sync_account(conn, account, href_to_uid_cache: dict):
try:
if not stored_token:
logger.info("[%s] Kein sync-token vorhanden, führe initialen Full-Sync aus", account.name)
raw_vcards = client.fetch_all_vcards(collection_url)
contacts = [c for c in (parse_vcard(v, account.name) for v in raw_vcards) if c]
raw_vcards, raw_etags = client.fetch_all_vcards(collection_url)
contacts = []
for v, etag in zip(raw_vcards, raw_etags):
c = parse_vcard(v, account.name, etag=etag)
if c:
contacts.append(c)
else:
logger.warning("[%s] vCard konnte nicht geparst werden, überspringe", account.name)
db.replace_all_contacts_for_account(conn, account.name, contacts, run_id)
_, _, new_token = client.sync_collection(collection_url, None)
if new_token:
db.save_sync_token(conn, account.name, new_token)
db.finish_sync_run(conn, run_id, "success", upserted=len(contacts), deleted=0)
logger.info("[%s] Initialer Sync abgeschlossen: %d Kontakte", account.name, len(contacts))
return
try:
changed_vcards, deleted_hrefs, new_token = client.sync_collection(collection_url, stored_token)
changed_vcards, etags, deleted_hrefs, new_token = client.sync_collection(collection_url, stored_token)
except SyncTokenInvalid:
logger.warning("[%s] sync-token vom Server abgelehnt, führe vollen Re-Sync aus", account.name)
db.clear_sync_token(conn, account.name)
raw_vcards = client.fetch_all_vcards(collection_url)
contacts = [c for c in (parse_vcard(v, account.name) for v in raw_vcards) if c]
raw_vcards, raw_etags = client.fetch_all_vcards(collection_url)
contacts = []
for v, etag in zip(raw_vcards, raw_etags):
c = parse_vcard(v, account.name, etag=etag)
if c:
contacts.append(c)
else:
logger.warning("[%s] vCard konnte nicht geparst werden, überspringe", account.name)
db.replace_all_contacts_for_account(conn, account.name, contacts, run_id)
_, _, new_token = client.sync_collection(collection_url, None)
if new_token:
db.save_sync_token(conn, account.name, new_token)
db.finish_sync_run(conn, run_id, "success", upserted=len(contacts), deleted=0)
logger.info("[%s] Re-Sync abgeschlossen: %d Kontakte", account.name, len(contacts))
return
contacts = [c for c in (parse_vcard(v, account.name) for v in changed_vcards) if c]
contacts = []
for v, etag in zip(changed_vcards, etags):
c = parse_vcard(v, account.name, etag=etag)
if c:
contacts.append(c)
else:
logger.warning("[%s] vCard konnte nicht geparst werden, überspringe", account.name)
db.upsert_contacts(conn, contacts, run_id)
deleted_uids = [href.rstrip("/").rsplit("/", 1)[-1].replace(".vcf", "") for href in deleted_hrefs]
deleted_uids = []
for href in deleted_hrefs:
uid = href.rstrip("/").rsplit("/", 1)[-1].replace(".vcf", "")
if uid:
deleted_uids.append(uid)
else:
logger.warning("[%s] Konnte UID nicht aus href extrahieren: %s", account.name, href)
db.delete_contacts_by_href_uids(conn, account.name, deleted_uids)
if new_token:
+2 -2
View File
@@ -33,7 +33,7 @@ def _type_str(obj) -> str:
return "other"
def parse_vcard(raw_text: str, account: str) -> dict | None:
def parse_vcard(raw_text: str, account: str, etag: str | None = None) -> dict | None:
try:
vcard = vobject.readOne(raw_text)
except Exception as exc:
@@ -81,7 +81,7 @@ def parse_vcard(raw_text: str, account: str) -> dict | None:
return {
"account": account,
"uid": uid,
"etag": None,
"etag": etag,
"full_name": _scalar(_get(vcard, "fn")),
"given_name": given_name,
"family_name": family_name,