| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- """API routes for GitHub profile backup."""
- import logging
- from fastapi import APIRouter, Depends, HTTPException, Query
- from sqlalchemy import delete, desc, select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.core.database import get_db
- from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog
- from backend.app.schemas.github_backup import (
- GitHubBackupConfigCreate,
- GitHubBackupConfigResponse,
- GitHubBackupConfigUpdate,
- GitHubBackupLogResponse,
- GitHubBackupStatus,
- GitHubBackupTriggerResponse,
- GitHubTestConnectionResponse,
- )
- from backend.app.services.github_backup import github_backup_service
- logger = logging.getLogger(__name__)
- router = APIRouter(prefix="/github-backup", tags=["github-backup"])
- def _config_to_response(config: GitHubBackupConfig) -> dict:
- """Convert config model to response dict."""
- return {
- "id": config.id,
- "repository_url": config.repository_url,
- "has_token": bool(config.access_token),
- "branch": config.branch,
- "schedule_enabled": config.schedule_enabled,
- "schedule_type": config.schedule_type,
- "backup_kprofiles": config.backup_kprofiles,
- "backup_cloud_profiles": config.backup_cloud_profiles,
- "backup_settings": config.backup_settings,
- "enabled": config.enabled,
- "last_backup_at": config.last_backup_at,
- "last_backup_status": config.last_backup_status,
- "last_backup_message": config.last_backup_message,
- "last_backup_commit_sha": config.last_backup_commit_sha,
- "next_scheduled_run": config.next_scheduled_run,
- "created_at": config.created_at,
- "updated_at": config.updated_at,
- }
- @router.get("/config", response_model=GitHubBackupConfigResponse | None)
- async def get_config(db: AsyncSession = Depends(get_db)):
- """Get the current GitHub backup configuration."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- return None
- return _config_to_response(config)
- @router.post("/config", response_model=GitHubBackupConfigResponse)
- async def save_config(
- config_data: GitHubBackupConfigCreate,
- db: AsyncSession = Depends(get_db),
- ):
- """Create or update GitHub backup configuration.
- Only one configuration is supported. If one exists, it will be updated.
- """
- # Check for existing config
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if config:
- # Update existing
- config.repository_url = config_data.repository_url
- config.access_token = config_data.access_token
- config.branch = config_data.branch
- config.schedule_enabled = config_data.schedule_enabled
- config.schedule_type = config_data.schedule_type.value
- config.backup_kprofiles = config_data.backup_kprofiles
- config.backup_cloud_profiles = config_data.backup_cloud_profiles
- config.backup_settings = config_data.backup_settings
- config.enabled = config_data.enabled
- # Calculate next scheduled run if enabled
- if config.schedule_enabled:
- config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type)
- else:
- config.next_scheduled_run = None
- logger.info(f"Updated GitHub backup config: {config.repository_url}")
- else:
- # Create new
- config = GitHubBackupConfig(
- repository_url=config_data.repository_url,
- access_token=config_data.access_token,
- branch=config_data.branch,
- schedule_enabled=config_data.schedule_enabled,
- schedule_type=config_data.schedule_type.value,
- backup_kprofiles=config_data.backup_kprofiles,
- backup_cloud_profiles=config_data.backup_cloud_profiles,
- backup_settings=config_data.backup_settings,
- enabled=config_data.enabled,
- )
- if config.schedule_enabled:
- config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type)
- db.add(config)
- logger.info(f"Created GitHub backup config: {config.repository_url}")
- await db.commit()
- await db.refresh(config)
- return _config_to_response(config)
- @router.patch("/config", response_model=GitHubBackupConfigResponse)
- async def update_config(
- update_data: GitHubBackupConfigUpdate,
- db: AsyncSession = Depends(get_db),
- ):
- """Partially update GitHub backup configuration."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- raise HTTPException(status_code=404, detail="No configuration found")
- update_dict = update_data.model_dump(exclude_unset=True)
- for key, value in update_dict.items():
- if key == "schedule_type" and value is not None:
- setattr(config, key, value.value)
- else:
- setattr(config, key, value)
- # Recalculate next scheduled run if schedule settings changed
- if "schedule_enabled" in update_dict or "schedule_type" in update_dict:
- if config.schedule_enabled:
- config.next_scheduled_run = github_backup_service._calculate_next_run(config.schedule_type)
- else:
- config.next_scheduled_run = None
- await db.commit()
- await db.refresh(config)
- logger.info(f"Updated GitHub backup config: {config.repository_url}")
- return _config_to_response(config)
- @router.delete("/config")
- async def delete_config(db: AsyncSession = Depends(get_db)):
- """Delete the GitHub backup configuration and all logs."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- raise HTTPException(status_code=404, detail="No configuration found")
- await db.delete(config)
- await db.commit()
- logger.info("Deleted GitHub backup config")
- return {"message": "Configuration deleted"}
- @router.post("/test", response_model=GitHubTestConnectionResponse)
- async def test_connection(
- repo_url: str = Query(..., description="GitHub repository URL"),
- token: str = Query(..., description="Personal Access Token"),
- ):
- """Test GitHub connection with provided credentials."""
- result = await github_backup_service.test_connection(repo_url, token)
- return GitHubTestConnectionResponse(**result)
- @router.post("/test-stored", response_model=GitHubTestConnectionResponse)
- async def test_stored_connection(db: AsyncSession = Depends(get_db)):
- """Test GitHub connection using stored configuration."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- raise HTTPException(status_code=404, detail="No configuration found")
- if not config.access_token:
- raise HTTPException(status_code=400, detail="No access token configured")
- test_result = await github_backup_service.test_connection(config.repository_url, config.access_token)
- return GitHubTestConnectionResponse(**test_result)
- @router.post("/run", response_model=GitHubBackupTriggerResponse)
- async def trigger_backup(db: AsyncSession = Depends(get_db)):
- """Manually trigger a backup."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- raise HTTPException(status_code=404, detail="No configuration found. Configure backup first.")
- if not config.enabled:
- raise HTTPException(status_code=400, detail="Backup is disabled")
- backup_result = await github_backup_service.run_backup(config.id, trigger="manual")
- return GitHubBackupTriggerResponse(**backup_result)
- @router.get("/status", response_model=GitHubBackupStatus)
- async def get_status(db: AsyncSession = Depends(get_db)):
- """Get current backup status."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- return GitHubBackupStatus(
- configured=False,
- enabled=False,
- is_running=False,
- progress=None,
- last_backup_at=None,
- last_backup_status=None,
- next_scheduled_run=None,
- )
- return GitHubBackupStatus(
- configured=True,
- enabled=config.enabled,
- is_running=github_backup_service.is_running,
- progress=github_backup_service.progress,
- last_backup_at=config.last_backup_at,
- last_backup_status=config.last_backup_status,
- next_scheduled_run=config.next_scheduled_run,
- )
- @router.get("/logs", response_model=list[GitHubBackupLogResponse])
- async def get_logs(
- limit: int = Query(default=50, ge=1, le=200),
- offset: int = Query(default=0, ge=0),
- db: AsyncSession = Depends(get_db),
- ):
- """Get backup logs."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- return []
- logs_result = await db.execute(
- select(GitHubBackupLog)
- .where(GitHubBackupLog.config_id == config.id)
- .order_by(desc(GitHubBackupLog.started_at))
- .offset(offset)
- .limit(limit)
- )
- logs = logs_result.scalars().all()
- return [
- GitHubBackupLogResponse(
- id=log.id,
- config_id=log.config_id,
- started_at=log.started_at,
- completed_at=log.completed_at,
- status=log.status,
- trigger=log.trigger,
- commit_sha=log.commit_sha,
- files_changed=log.files_changed,
- error_message=log.error_message,
- )
- for log in logs
- ]
- @router.delete("/logs")
- async def clear_logs(
- keep_last: int = Query(default=10, ge=0, le=100, description="Number of recent logs to keep"),
- db: AsyncSession = Depends(get_db),
- ):
- """Clear backup logs, optionally keeping the most recent entries."""
- result = await db.execute(select(GitHubBackupConfig).limit(1))
- config = result.scalar_one_or_none()
- if not config:
- return {"deleted": 0, "message": "No configuration found"}
- if keep_last > 0:
- # Get IDs to keep
- keep_result = await db.execute(
- select(GitHubBackupLog.id)
- .where(GitHubBackupLog.config_id == config.id)
- .order_by(desc(GitHubBackupLog.started_at))
- .limit(keep_last)
- )
- keep_ids = [row[0] for row in keep_result.fetchall()]
- if keep_ids:
- delete_result = await db.execute(
- delete(GitHubBackupLog).where(
- GitHubBackupLog.config_id == config.id, GitHubBackupLog.id.not_in(keep_ids)
- )
- )
- else:
- delete_result = await db.execute(delete(GitHubBackupLog).where(GitHubBackupLog.config_id == config.id))
- else:
- delete_result = await db.execute(delete(GitHubBackupLog).where(GitHubBackupLog.config_id == config.id))
- await db.commit()
- deleted_count = delete_result.rowcount
- logger.info(f"Deleted {deleted_count} GitHub backup logs (kept {keep_last})")
- return {"deleted": deleted_count, "message": f"Deleted {deleted_count} logs"}
|