feat(infra): dav_delete_resource — DELETE de recurso CardDAV/CalDAV (grupo dav)
Completa el CRUD del grupo dav (put/get/list/get-collection/delete). HTTP DELETE con Basic auth, If-Match opcional para borrado condicional, maneja 404 como idempotente. Solo stdlib. 7 tests deterministas (monkeypatch urlopen). Probado contra Xandikos real durante la limpieza del ciclo de sync OSINT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""Borra (DELETE) un recurso DAV individual via HTTP Basic auth.
|
||||
|
||||
Funcion impura: hace una peticion HTTP DELETE. Construye el header
|
||||
`Authorization: Basic base64(user:pass)` a mano con stdlib. El resource_path
|
||||
puede ser un href absoluto (como los que devuelve dav_list_resources /
|
||||
dav_get_collection) o una ruta relativa al base_url. Opcionalmente envia el
|
||||
header `If-Match: <etag>` para un borrado condicional (solo borra si el etag
|
||||
coincide, evita pisar una edicion concurrente). Maneja errores sin lanzar.
|
||||
Solo usa stdlib (urllib, base64, ssl).
|
||||
|
||||
ATENCION: DELETE es DESTRUCTIVO e IRREVERSIBLE en el servidor. Usar con
|
||||
confirmacion explicita del caller (nunca a ciegas en un bucle de sync). Pensado
|
||||
para limpiar recursos de prueba o retirar contactos obsoletos de forma
|
||||
controlada.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import ssl
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _basic_auth_header(username: str, password: str) -> str:
|
||||
raw = ("%s:%s" % (username, password)).encode("utf-8")
|
||||
return "Basic " + base64.b64encode(raw).decode("ascii")
|
||||
|
||||
|
||||
def _resolve_url(base_url: str, resource_path: str) -> str:
|
||||
if resource_path.startswith("http://") or resource_path.startswith("https://"):
|
||||
return resource_path
|
||||
return base_url.rstrip("/") + "/" + resource_path.lstrip("/")
|
||||
|
||||
|
||||
def dav_delete_resource(
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
resource_path: str,
|
||||
*,
|
||||
etag: str = "",
|
||||
timeout_s: float = 20.0,
|
||||
verify_tls: bool = True,
|
||||
) -> dict:
|
||||
"""Borra un recurso DAV (DELETE). DESTRUCTIVO e IRREVERSIBLE.
|
||||
|
||||
Args:
|
||||
base_url: URL base del servidor DAV. Se ignora si resource_path ya es
|
||||
una URL absoluta.
|
||||
username: usuario para HTTP Basic auth.
|
||||
password: contrasena para HTTP Basic auth. Resolver desde pass.
|
||||
resource_path: href absoluto (p.ej. '/enmanuel/contacts/addressbook/x.vcf')
|
||||
o URL completa del recurso a borrar. Acepta directamente los hrefs
|
||||
que devuelven dav_list_resources / dav_get_collection.
|
||||
etag: si se da, se envia como header If-Match para un borrado
|
||||
condicional (el servidor solo borra si el etag actual coincide;
|
||||
devuelve 412 Precondition Failed si cambio). Vacio = borrado
|
||||
incondicional.
|
||||
timeout_s: timeout de la peticion en segundos. Default 20.0.
|
||||
verify_tls: si True (default) verifica el certificado TLS.
|
||||
|
||||
Returns:
|
||||
dict. En exito: {status:'ok', http_status:int, url:str} (DELETE devuelve
|
||||
normalmente 204 No Content o 200). En error (sin lanzar):
|
||||
{status:'error', error:str, http_status:int|None}. Un 404 (ya no existe)
|
||||
se devuelve como error con http_status=404; el caller puede tratarlo
|
||||
como idempotente (ya borrado).
|
||||
"""
|
||||
url = _resolve_url(base_url, resource_path)
|
||||
headers = {"Authorization": _basic_auth_header(username, password)}
|
||||
if etag:
|
||||
headers["If-Match"] = etag
|
||||
req = urllib.request.Request(url, method="DELETE", headers=headers)
|
||||
|
||||
context = None if verify_tls else ssl._create_unverified_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout_s, context=context) as resp:
|
||||
return {"status": "ok", "http_status": resp.status, "url": url}
|
||||
except urllib.error.HTTPError as e:
|
||||
return {"status": "error", "error": "http %s" % e.code, "http_status": e.code}
|
||||
except urllib.error.URLError as e:
|
||||
return {"status": "error", "error": str(e.reason), "http_status": None}
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"status": "error", "error": str(e), "http_status": None}
|
||||
Reference in New Issue
Block a user