"""Gitea: репозитории, raw-файлы, деплой compose на сервер.""" from __future__ import annotations from typing import List, Optional import httpx from app.core.ssh import SSHClient from app.db.database import get_session from app.db.models import Setting DEFAULT_URL = "http://192.168.1.171:3000" def _get(key: str) -> str: with get_session() as s: row = s.get(Setting, key) return row.value if row else "" def _set(key: str, value: str) -> None: with get_session() as s: row = s.get(Setting, key) or Setting(key=key) row.value = value s.add(row) s.commit() def get_config() -> dict: return {"url": _get("gitea_url") or DEFAULT_URL, "token": _get("gitea_token")} def save_config(url: str, token: str) -> None: _set("gitea_url", url.rstrip("/")) _set("gitea_token", token) async def _client() -> httpx.AsyncClient: cfg = get_config() headers = {"Authorization": f"token {cfg['token']}"} if cfg["token"] else {} return httpx.AsyncClient(base_url=cfg["url"], headers=headers, timeout=20) async def list_repos() -> List[dict]: async with await _client() as cl: r = await cl.get("/api/v1/user/repos", params={"limit": 50}) r.raise_for_status() return [{"name": x["name"], "full_name": x["full_name"], "clone_url": x["clone_url"], "default_branch": x["default_branch"]} for x in r.json()] async def get_file(repo: str, path: str, ref: str = "main") -> str: async with await _client() as cl: r = await cl.get(f"/api/v1/repos/{repo}/raw/{path}", params={"ref": ref}) r.raise_for_status() return r.text async def test_connection() -> tuple[bool, str]: try: async with await _client() as cl: r = await cl.get("/api/v1/version") if r.status_code == 200: return True, f"Gitea {r.json().get('version')}" return False, f"HTTP {r.status_code}" except Exception as e: return False, str(e) # ---------- деплой compose ---------- async def deploy(c: SSHClient, clone_url: str, repo_name: str, branch: str, compose_path: str, env_content: Optional[str], log) -> None: """git clone/pull в /opt/stacks/ + docker compose up -d.""" base = "/opt/stacks" workdir = f"{base}/{repo_name}" await c.sudo(f"mkdir -p {base}") r = await c.sudo(f"test -d {workdir}/.git && echo exists || echo missing") if "exists" in r.stdout: log(f"git pull в {workdir}") r = await c.sudo(f"git -C {workdir} fetch --all && git -C {workdir} checkout {branch} " f"&& git -C {workdir} pull origin {branch}", timeout=300) else: log(f"git clone {clone_url} → {workdir}") url = clone_url r = await c.sudo(f"git clone -b {branch} {url} {workdir}", timeout=300) log(r.stdout[-1000:] + r.stderr[-500:]) if not r.ok: raise RuntimeError("git операция не удалась") if env_content is not None: import base64 b64 = base64.b64encode(env_content.encode()).decode() await c.sudo(f"echo {b64} | base64 -d > {workdir}/.env") log(".env записан") log("docker compose up -d") r = await c.sudo(f"docker compose -f {workdir}/{compose_path} up -d", timeout=900) log(r.stdout + r.stderr) if not r.ok: raise RuntimeError("compose up не удался") async def stack_action(c: SSHClient, repo_name: str, compose_path: str, action: str) -> str: assert action in ("up -d", "down", "restart", "pull", "logs --tail 100", "ps") r = await c.sudo(f"docker compose -f /opt/stacks/{repo_name}/{compose_path} {action}", timeout=600) return r.stdout + r.stderr