"""Plugin de formato: ESCRITOR de Caddy (SDD 29).""" import os, sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from formatos import escritor # noqa: E402 def _direcciones(s): """En nginx dos `server` con el MISMO nombre se distinguen por su `listen`; en Caddy, por el ESQUEMA de la dirección. Sin traducir eso, los bloques colisionan y caddy rechaza el fichero entero con «ambiguous site definition» — encontrado validando con el binario real, no leyendo.""" solo_80 = bool(s.puertos) and all(p == "80" for p in s.puertos) pref = "http://" if solo_80 else "" return ", ".join(pref + n for n in s.nombres) or (":80" if solo_80 else ":443") def _emitir_acciones(acciones, ind): L = [] for a in acciones: if a.tipo == "proxy": L.append(f"{ind}reverse_proxy {a.valor.rstrip('/').replace('http://','').replace('https://','')}") elif a.tipo == "archivos": L.append(f"{ind}root * {a.valor}") elif a.tipo == "intentar": L.append(f"{ind}try_files {a.valor}") elif a.tipo == "redir": p = a.valor.split() code = p[0] if p and p[0].isdigit() else "" dest = " ".join(p[1:]) if code else a.valor L.append(f"{ind}redir {dest} {code}".rstrip() if dest else f"{ind}respond {code}") return L @escritor("caddy", familia="web") def escribir(cfg): A = cfg.acta L = [f"# Generado por scripts/mudanza/traducir.py desde una configuración de {cfg.formato_origen}.", "# ⚠ NO es una traducción completa. Abajo va el ACTA de lo que NO se pudo traducir, con su", "# línea de origen. Desplegar esto sin resolverlo es desplegar un servidor incompleto.", ""] for s in cfg.sitios: L.append(f"{_direcciones(s)} {{") L.append(f"\t# desde la línea {s.origen} del {cfg.formato_origen} original") if s.tls.get("cert"): L.append(f"\ttls {s.tls.get('cert','')} {s.tls.get('key','')}") if s.raiz: L.append(f"\troot * {s.raiz}") L += _emitir_acciones(s.acciones, "\t") for r in s.rutas: if r.clase == "regex": # Caddy no tiene un equivalente directo de `location ~`. Se DECLARA en el acta en vez # de emitir algo parecido: un `handle` de prefijo no es lo mismo que una regex, y # parecerse es peor que faltar. A.anota(r.origen, "SIN-TRADUCIR", f"ruta por regex: {r.patron}", "Caddy no tiene equivalente directo de `location ~`; se escribe con " "`@matcher path_regexp` y hay que revisar el orden de evaluación") continue cuerpo = _emitir_acciones(r.acciones, "\t\t") if cuerpo: L.append(f"\thandle {r.patron} {{ # línea {r.origen}") L += cuerpo L.append("\t}") if s.raiz: L.append("\tfile_server") L.append("}") L.append("") pend = A.sin_traducir() if pend: L.append("# ══ ACTA: lo que NO se tradujo — hay que resolverlo a mano ══") for e in pend: L.append(f"# línea {e['linea']:>4} {e['directiva'][:70]}") if e["motivo"]: L.append(f"# └─ {e['motivo'][:96]}") return "\n".join(L) + "\n"