Files
ObsiGate/backend/routers/conflicts.py
T

74 lines
3.4 KiB
Python

"""Syncthing conflict endpoints (ROADMAP #85, tranche 8).
Handlers déplacés depuis :mod:`backend.main` sans changement de
comportement : mêmes chemins (``/api/conflicts*``), mêmes modèles de
réponse, mêmes dépendances d'authentification.
Adaptations strictement équivalentes :
- ``_resolve_safe_path`` / ``_backup_file`` → :mod:`backend.services.paths`
et :mod:`backend.services.backups` (pass-through).
"""
import logging
import shutil
from pathlib import Path
from fastapi import APIRouter, Body, Depends, HTTPException
from backend.audit import log_file_delete
from backend.auth.middleware import check_vault_access, require_auth
from backend.indexer import get_conflicts, get_vault_data, remove_single_file
from backend.schemas import ConflictResolveResponse, ConflictsResponse
from backend.services.backups import create_backup
from backend.services.paths import resolve_safe_path
from backend.sse import sse_manager
logger = logging.getLogger("obsigate")
router = APIRouter(tags=["conflicts"])
@router.get("/api/conflicts", response_model=ConflictsResponse)
async def api_conflicts(current_user=Depends(require_auth)):
"""List sync-conflict files across accessible vaults."""
user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", [])
all_conflicts = get_conflicts()
if "*" not in user_vaults:
all_conflicts = [c for c in all_conflicts if c["vault"] in user_vaults]
return {"conflicts": all_conflicts, "total": len(all_conflicts)}
@router.post("/api/conflicts/resolve", response_model=ConflictResolveResponse)
async def api_conflict_resolve(body: dict = Body(...), current_user=Depends(require_auth)):
"""Resolve a conflict: keep_local (delete conflict file) or keep_conflict (replace original)."""
vault_name = body.get("vault")
conflict_path = body.get("conflict_path")
original_path = body.get("original_path")
action = body.get("action") # "keep_local" or "keep_conflict"
# mypy: narrow down from dict values
assert isinstance(vault_name, str), "'vault' is required and must be a string"
assert isinstance(conflict_path, str), "'conflict_path' is required and must be a string"
assert isinstance(original_path, str), "'original_path' is required and must be a string"
if not check_vault_access(vault_name, current_user):
raise HTTPException(403, f"Accès refusé à la vault '{vault_name}'")
vault_data = get_vault_data(vault_name)
if not vault_data:
raise HTTPException(404, "Vault not found")
vault_root = Path(vault_data["path"])
conf_file = resolve_safe_path(vault_root, conflict_path)
orig_file = resolve_safe_path(vault_root, original_path)
if not conf_file.exists():
raise HTTPException(404, "Conflict file not found")
try:
if action == "keep_conflict":
create_backup(orig_file, vault_name, original_path)
shutil.copy2(conf_file, orig_file)
logger.info(f"Conflict resolved (keep_conflict): {conflict_path} → {original_path}")
conf_file.unlink()
await remove_single_file(vault_name, conflict_path)
log_file_delete(current_user["username"], vault_name, conflict_path)
await sse_manager.broadcast("file_deleted", {"vault": vault_name, "path": conflict_path})
return {"status": "resolved", "action": action}
except Exception as e:
raise HTTPException(500, f"Error resolving conflict: {e!s}")