305 lines
8.6 KiB
Python
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))
|