74 lines
3.4 KiB
Python
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}")
|