from __future__ import annotations import json import os import shutil from datetime import datetime from pathlib import Path from fastapi import APIRouter, Body, HTTPException, UploadFile, File from fastapi.responses import FileResponse from backend.app.core.config import BackendConfig, CONFIG_PATH, load_config from backend.app.db.database import DEFAULT_DATABASE_PATH router = APIRouter(prefix="/config", tags=["config"]) # Chemins possibles vers la config frontend (dev + docker) FRONTEND_CONFIG_PATH = ( Path(__file__).resolve().parent.parent.parent.parent / "frontend" / "public" / "config_frontend.json" ) FRONTEND_CONFIG_FALLBACK_PATH = ( Path(__file__).resolve().parent.parent.parent.parent / "backend" / "config_frontend.json" ) def _get_frontend_config_path() -> Path | None: if FRONTEND_CONFIG_PATH.exists(): return FRONTEND_CONFIG_PATH if FRONTEND_CONFIG_FALLBACK_PATH.exists(): return FRONTEND_CONFIG_FALLBACK_PATH return None @router.get("/backend", response_model=BackendConfig) def read_backend_config() -> BackendConfig: # expose la configuration backend en lecture seule return load_config() @router.put("/backend", response_model=BackendConfig) def update_backend_config(payload: dict = Body(...)) -> BackendConfig: current = load_config() try: # Fusion profonde des configs (nécessaire pour les modèles imbriqués Pydantic v2) def deep_merge(base: dict, update: dict) -> dict: result = base.copy() for key, value in update.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = deep_merge(result[key], value) else: result[key] = value return result # Convertir en dict, fusionner, puis revalider current_dict = current.model_dump() merged = deep_merge(current_dict, payload) updated = BackendConfig.model_validate(merged) CONFIG_PATH.write_text(updated.model_dump_json(indent=2), encoding="utf-8") load_config.cache_clear() return load_config() except Exception as exc: # pragma: no cover raise HTTPException(status_code=400, detail=str(exc)) @router.get("/frontend") def read_frontend_config() -> dict: """Retourne la configuration frontend.""" config_path = _get_frontend_config_path() if not config_path: raise HTTPException(status_code=404, detail="Config frontend introuvable") return json.loads(config_path.read_text(encoding="utf-8")) @router.put("/frontend") def update_frontend_config(payload: dict = Body(...)) -> dict: """Met à jour la configuration frontend.""" try: # Charger la config actuelle current = {} config_path = _get_frontend_config_path() if config_path and config_path.exists(): current = json.loads(config_path.read_text(encoding="utf-8")) # Fusion profonde des configs def deep_merge(base: dict, update: dict) -> dict: result = base.copy() for key, value in update.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = deep_merge(result[key], value) else: result[key] = value return result updated = deep_merge(current, payload) target_paths = [] if FRONTEND_CONFIG_PATH.parent.exists(): target_paths.append(FRONTEND_CONFIG_PATH) if FRONTEND_CONFIG_FALLBACK_PATH.parent.exists(): target_paths.append(FRONTEND_CONFIG_FALLBACK_PATH) if not target_paths: target_paths.append(FRONTEND_CONFIG_FALLBACK_PATH) for target in target_paths: target.write_text( json.dumps(updated, indent=2, ensure_ascii=False), encoding="utf-8", ) return updated except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) # ==================== Database Backup ==================== @router.get("/database/info") def database_info() -> dict: """Retourne les informations sur la base de données.""" if not DEFAULT_DATABASE_PATH.exists(): raise HTTPException(status_code=404, detail="Base de données introuvable") stat = DEFAULT_DATABASE_PATH.stat() return { "path": str(DEFAULT_DATABASE_PATH), "filename": DEFAULT_DATABASE_PATH.name, "size_bytes": stat.st_size, "size_mb": round(stat.st_size / (1024 * 1024), 2), "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(), } @router.get("/database/backup") def download_database(): """Télécharge une copie de la base de données.""" if not DEFAULT_DATABASE_PATH.exists(): raise HTTPException(status_code=404, detail="Base de données introuvable") # Créer une copie temporaire pour éviter les problèmes de lock backup_path = DEFAULT_DATABASE_PATH.parent / f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db" shutil.copy2(DEFAULT_DATABASE_PATH, backup_path) return FileResponse( path=backup_path, filename=f"suivi_produit_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db", media_type="application/x-sqlite3", background=None, # Nettoyer après envoi ) @router.post("/database/restore") async def restore_database(file: UploadFile = File(...)) -> dict: """Restaure la base de données depuis un fichier uploadé.""" if not file.filename.endswith(".db"): raise HTTPException(status_code=400, detail="Le fichier doit être un .db") # Vérifier la taille (max 100MB) content = await file.read() if len(content) > 100 * 1024 * 1024: raise HTTPException(status_code=400, detail="Fichier trop volumineux (max 100MB)") # Créer un backup avant restauration if DEFAULT_DATABASE_PATH.exists(): backup_before = DEFAULT_DATABASE_PATH.parent / f"before_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db" shutil.copy2(DEFAULT_DATABASE_PATH, backup_before) try: # Écrire le nouveau fichier with open(DEFAULT_DATABASE_PATH, "wb") as f: f.write(content) return { "success": True, "message": "Base de données restaurée avec succès", "size_bytes": len(content), } except Exception as exc: raise HTTPException(status_code=500, detail=f"Erreur lors de la restauration: {exc}")