spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3# Project: Toit-Ka4# -----------------------------------------------------------------------------5# replicate.py : réplication LECTURE SEULE des BD de prod vers ce nœud.6#7# Lou-Ka tourne sur M3U96b (~/apps/lou-ka/data/louka.db)8# Immo-Ka tourne sur M4M64a (~/apps/immo-ka/data/immoka.db)9# Toit-Ka tourne sur M3U96a et TIRE des snapshots cohérents via le LAN :10# 1. ssh <node> "sqlite3 <db> '.backup /tmp/<nom>-snap.db'" (snapshot sûr11# même en cours d'écriture — jamais un rsync direct d'une DB WAL vivante)12# 2. rsync du snapshot vers data/replicas/<nom>.db (échange atomique)13#14# Config .env (absente en dev -> replicate ne fait rien, l'ETL lit les BD15# locales pointées par TOITKA_LOUKA_DB / TOITKA_IMMOKA_DB) :16# TOITKA_REPLICA_SOURCES=louka=192.168.2.82:apps/lou-ka/data/louka.db,immoka=192.168.2.83:apps/immo-ka/data/immoka.db17# TOITKA_REPLICA_USER=simon-pierreboucher18# -----------------------------------------------------------------------------19from __future__ import annotations2021import os22import subprocess23import time24from pathlib import Path2526from .db import DATA2728REPLICAS = DATA / "replicas"29SSH_OPTS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=10",30 "-o", "StrictHostKeyChecking=accept-new"]313233def _sources() -> list[tuple[str, str, str]]:34 """[(nom, hôte, chemin distant relatif au $HOME), …] depuis l'environnement."""35 raw = os.environ.get("TOITKA_REPLICA_SOURCES", "").strip()36 out = []37 for part in filter(None, (p.strip() for p in raw.split(","))):38 name, _, rest = part.partition("=")39 host, _, path = rest.partition(":")40 if name and host and path:41 out.append((name.strip(), host.strip(), path.strip()))42 return out434445def _pull(name: str, host: str, remote_db: str, user: str) -> bool:46 t0 = time.time()47 snap = f"/tmp/toitka-{name}-snap.db"48 dest = REPLICAS / f"{name}.db"49 tmp = REPLICAS / f"{name}.db.part"50 # 1. snapshot cohérent côté source (sqlite .backup gère le WAL en cours)51 r = subprocess.run(52 ["ssh", *SSH_OPTS, f"{user}@{host}",53 f"sqlite3 ~/{remote_db} \".backup {snap}\""],54 capture_output=True, text=True, timeout=600)55 if r.returncode != 0:56 print(f"[replicate] {name} : snapshot ÉCHOUÉ — {r.stderr.strip()[:300]}")57 return False58 # 2. transfert (rsync incrémental) puis échange atomique59 r = subprocess.run(60 ["rsync", "-a", "--partial", "-e", "ssh " + " ".join(SSH_OPTS),61 f"{user}@{host}:{snap}", str(tmp)],62 capture_output=True, text=True, timeout=1800)63 if r.returncode != 0 or not tmp.exists():64 print(f"[replicate] {name} : rsync ÉCHOUÉ — {r.stderr.strip()[:300]}")65 tmp.unlink(missing_ok=True)66 return False67 os.replace(tmp, dest)68 subprocess.run(["ssh", *SSH_OPTS, f"{user}@{host}", f"rm -f {snap}"],69 capture_output=True, timeout=60)70 mb = dest.stat().st_size / 1e671 print(f"[replicate] {name} : {mb:.0f} Mo en {time.time() - t0:.1f} s")72 return True737475def run() -> None:76 sources = _sources()77 if not sources:78 return # dev : rien à répliquer, l'ETL lit en local79 REPLICAS.mkdir(parents=True, exist_ok=True)80 user = os.environ.get("TOITKA_REPLICA_USER", os.environ.get("USER", ""))81 for name, host, path in sources:82 try:83 _pull(name, host, path, user)84 except subprocess.TimeoutExpired:85 print(f"[replicate] {name} : délai dépassé")86 except Exception as e:87 print(f"[replicate] {name} : {type(e).__name__}: {e}")88