from flask import Flask, request, jsonify import os import subprocess import base64 import requests import redis import shutil from threading import Lock from webdav3.client import Client from requests.structures import CaseInsensitiveDict from webdav3.client import WebDavClient def safe_download_file(self, remote_path, local_path, progress=None, progress_args=()): response = self.execute_request(action='download', path=remote_path, headers={'accept-encoding': 'identity'}) total = response.headers.get('content-length') if total is None: log_debug(f"⚠️ Skipping file without content-length: {remote_path}") return total = int(total) with open(local_path, 'wb') as f: for chunk in response.iter_content(chunk_size=1024): if chunk: f.write(chunk) log_debug(f"✅ Downloaded file: {remote_path} → {local_path}") WebDavClient.download_file = safe_download_file app = Flask(__name__) # Environment variables NEXTCLOUD_URL_DAV = os.getenv("NEXTCLOUD_URL_DAV") NEXTCLOUD_USER = os.getenv("NEXTCLOUD_USER") NEXTCLOUD_PASSWORD = os.getenv("NEXTCLOUD_PASSWORD") NEXTCLOUD_PATH = os.getenv("NEXTCLOUD_PATH") N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL", "https://n8n.n8n.svc.kube.ia86.cc/webhook/7950310f-e526-475a-82d1-63818da79339") DEBUG = bool(os.getenv("DEBUG", True)) redis_host = os.getenv("REDIS_HOST", "redis.redis.svc.cluster.local") redis_port = int(os.getenv("REDIS_PORT", 6379)) redis_client = redis.Redis(host=redis_host, port=redis_port, decode_responses=True) lock_prefix = "lock:mkdocs:" local_lock = Lock() def log_debug(msg): if DEBUG: print(f"🔍 DEBUG: {msg}", flush=True) def sync_from_nextcloud(website, tmp_path): remote_path = f"/{NEXTCLOUD_PATH}/@{website}/" local_path = tmp_path if os.path.exists(local_path): shutil.rmtree(local_path) os.makedirs(local_path, exist_ok=True) options = { 'webdav_hostname': NEXTCLOUD_URL_DAV, 'webdav_login': NEXTCLOUD_USER, 'webdav_password': NEXTCLOUD_PASSWORD } client = Client(options) log_debug(f"Starting WebDAV sync from '{remote_path}' to '{local_path}'") client.download_sync(remote_path=remote_path, local_path=local_path) log_debug("WebDAV sync completed successfully.") @app.route("/build", methods=["POST"]) def build_mkdocs(): log_debug("POST request received at /build") data = request.json website = data.get("WEBSITE") error_callback = data.get("ERROR_CALLBACK", N8N_WEBHOOK_URL) if not website: log_debug("Error: Missing 'WEBSITE' parameter in request.") return jsonify({"error": "WEBSITE parameter missing"}), 400 lock_key = f"{lock_prefix}{website}" log_debug(f"Attempting to acquire Redis lock: {lock_key}a") try: lock_acquired = redis_client.set(lock_key, "locked", nx=True, ex=60) except redis.exceptions.ConnectionError as e: log_debug(f"Redis connection failed: {e}") return jsonify({"error": "Redis connection error"}), 500 if not lock_acquired: log_debug(f"Build already active for website: {website}") return jsonify({"status": "busy", "message": f"Build already active: {website}"}), 429 tmp_path = f"/tmp/{website}" compile_path = f"{tmp_path}#compile" final_path = f"/srv/{website}" try: sync_from_nextcloud(website, tmp_path) src = os.path.join(tmp_path, "mkdocs.yml") log_debug(f"Checking if mkdocs.yml exists at {src}") if not os.path.exists(src): log_debug(f"{src} not found after sync") return jsonify({"error": f"{src} not found after sync"}), 404 log_debug(f"Running MkDocs build: {src} -> {compile_path}") cmd = f"mkdocs build --quiet --no-strict --config-file {src} --site-dir {compile_path}" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) if result.returncode != 0: build_error = base64.b64encode(result.stderr.encode()).decode() json_payload = {"site": website, "error": build_error} log_debug(f"MkDocs build failed: {result.stderr}") requests.post(error_callback, json=json_payload, headers={"Content-Type": "application/json"}) return jsonify({"status": "error", "message": "Build failed", "error": result.stderr}), 500 log_debug(f"Performing differential copy from {compile_path} to {final_path}") subprocess.run(f"rsync -a --delete {compile_path}/ {final_path}/", shell=True, check=True) log_debug(f"MkDocs build and sync successful for website: {website}") return jsonify({"status": "success", "message": "Build successful"}), 200 finally: redis_client.delete(lock_key) log_debug(f"Redis lock released for website: {website}") if __name__ == "__main__": log_debug("Starting Flask server on 0.0.0.0:80") app.run(host="0.0.0.0", port=80)