Aniworld/src/server/api/backup.py
2025-10-22 09:20:35 +02:00

305 lines
8.6 KiB
Python

"""Backup management API endpoints."""
import logging
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from src.server.services.backup_service import BackupService, get_backup_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/backup", tags=["backup"])
class BackupCreateRequest(BaseModel):
"""Request to create a backup."""
backup_type: str # 'config', 'database', 'full'
description: Optional[str] = None
class BackupResponse(BaseModel):
"""Response for backup creation."""
success: bool
message: str
backup_name: Optional[str] = None
size_bytes: Optional[int] = None
class BackupListResponse(BaseModel):
"""Response for listing backups."""
backups: List[Dict[str, Any]]
total_count: int
class RestoreRequest(BaseModel):
"""Request to restore from backup."""
backup_name: str
class RestoreResponse(BaseModel):
"""Response for restore operation."""
success: bool
message: str
def get_backup_service_dep() -> BackupService:
"""Dependency to get backup service."""
return get_backup_service()
@router.post("/create", response_model=BackupResponse)
async def create_backup(
request: BackupCreateRequest,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> BackupResponse:
"""Create a new backup.
Args:
request: Backup creation request.
backup_service: Backup service dependency.
Returns:
BackupResponse: Result of backup creation.
"""
try:
backup_info = None
if request.backup_type == "config":
backup_info = backup_service.backup_configuration(
request.description or ""
)
elif request.backup_type == "database":
backup_info = backup_service.backup_database(
request.description or ""
)
elif request.backup_type == "full":
backup_info = backup_service.backup_full(
request.description or ""
)
else:
raise ValueError(f"Invalid backup type: {request.backup_type}")
if backup_info is None:
return BackupResponse(
success=False,
message=f"Failed to create {request.backup_type} backup",
)
return BackupResponse(
success=True,
message=(
f"{request.backup_type.capitalize()} backup created "
"successfully"
),
backup_name=backup_info.name,
size_bytes=backup_info.size_bytes,
)
except Exception as e:
logger.error(f"Failed to create backup: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/list", response_model=BackupListResponse)
async def list_backups(
backup_type: Optional[str] = None,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> BackupListResponse:
"""List available backups.
Args:
backup_type: Optional filter by backup type.
backup_service: Backup service dependency.
Returns:
BackupListResponse: List of available backups.
"""
try:
backups = backup_service.list_backups(backup_type)
return BackupListResponse(backups=backups, total_count=len(backups))
except Exception as e:
logger.error(f"Failed to list backups: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/restore", response_model=RestoreResponse)
async def restore_backup(
request: RestoreRequest,
backup_type: Optional[str] = None,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> RestoreResponse:
"""Restore from a backup.
Args:
request: Restore request.
backup_type: Type of backup to restore.
backup_service: Backup service dependency.
Returns:
RestoreResponse: Result of restore operation.
"""
try:
# Determine backup type from filename if not provided
if backup_type is None:
if "config" in request.backup_name:
backup_type = "config"
elif "database" in request.backup_name:
backup_type = "database"
else:
backup_type = "full"
success = False
if backup_type == "config":
success = backup_service.restore_configuration(
request.backup_name
)
elif backup_type == "database":
success = backup_service.restore_database(request.backup_name)
else:
raise ValueError(f"Cannot restore backup type: {backup_type}")
if not success:
return RestoreResponse(
success=False,
message=f"Failed to restore {backup_type} backup",
)
return RestoreResponse(
success=True,
message=f"{backup_type.capitalize()} backup restored successfully",
)
except Exception as e:
logger.error(f"Failed to restore backup: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{backup_name}", response_model=Dict[str, Any])
async def delete_backup(
backup_name: str,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> Dict[str, Any]:
"""Delete a backup.
Args:
backup_name: Name of the backup to delete.
backup_service: Backup service dependency.
Returns:
dict: Result of delete operation.
"""
try:
success = backup_service.delete_backup(backup_name)
if not success:
raise HTTPException(status_code=404, detail="Backup not found")
return {"success": True, "message": "Backup deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete backup: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/cleanup", response_model=Dict[str, Any])
async def cleanup_backups(
max_backups: int = 10,
backup_type: Optional[str] = None,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> Dict[str, Any]:
"""Clean up old backups.
Args:
max_backups: Maximum number of backups to keep.
backup_type: Optional filter by backup type.
backup_service: Backup service dependency.
Returns:
dict: Number of backups deleted.
"""
try:
deleted_count = backup_service.cleanup_old_backups(
max_backups, backup_type
)
return {
"success": True,
"message": "Cleanup completed",
"deleted_count": deleted_count,
}
except Exception as e:
logger.error(f"Failed to cleanup backups: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/export/anime", response_model=Dict[str, Any])
async def export_anime_data(
backup_service: BackupService = Depends(get_backup_service_dep),
) -> Dict[str, Any]:
"""Export anime library data.
Args:
backup_service: Backup service dependency.
Returns:
dict: Result of export operation.
"""
try:
output_file = "data/backups/anime_export.json"
success = backup_service.export_anime_data(output_file)
if not success:
raise HTTPException(
status_code=500, detail="Failed to export anime data"
)
return {
"success": True,
"message": "Anime data exported successfully",
"export_file": output_file,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to export anime data: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/import/anime", response_model=Dict[str, Any])
async def import_anime_data(
import_file: str,
backup_service: BackupService = Depends(get_backup_service_dep),
) -> Dict[str, Any]:
"""Import anime library data.
Args:
import_file: Path to import file.
backup_service: Backup service dependency.
Returns:
dict: Result of import operation.
"""
try:
success = backup_service.import_anime_data(import_file)
if not success:
raise HTTPException(
status_code=400, detail="Failed to import anime data"
)
return {
"success": True,
"message": "Anime data imported successfully",
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to import anime data: {e}")
raise HTTPException(status_code=500, detail=str(e))