374 lines
12 KiB
Python
374 lines
12 KiB
Python
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
|
|
from src.server.models.config import AppConfig, ConfigUpdate, ValidationResult
|
|
from src.server.services.config_service import (
|
|
ConfigBackupError,
|
|
ConfigServiceError,
|
|
ConfigValidationError,
|
|
get_config_service,
|
|
)
|
|
from src.server.utils.dependencies import require_auth
|
|
|
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
|
|
|
|
|
@router.get("", response_model=AppConfig)
|
|
def get_config(auth: Optional[dict] = Depends(require_auth)) -> AppConfig:
|
|
"""Return current application configuration."""
|
|
try:
|
|
config_service = get_config_service()
|
|
return config_service.load_config()
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to load config: {e}"
|
|
) from e
|
|
|
|
|
|
@router.put("", response_model=AppConfig)
|
|
def update_config(
|
|
update: ConfigUpdate, auth: dict = Depends(require_auth)
|
|
) -> AppConfig:
|
|
"""Apply an update to the configuration and persist it.
|
|
|
|
Creates automatic backup before applying changes.
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
return config_service.update_config(update)
|
|
except ConfigValidationError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid configuration: {e}"
|
|
) from e
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update config: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/validate", response_model=ValidationResult)
|
|
def validate_config(
|
|
cfg: AppConfig, auth: dict = Depends(require_auth) # noqa: ARG001
|
|
) -> ValidationResult:
|
|
"""Validate a provided AppConfig without applying it.
|
|
|
|
Returns ValidationResult with any validation errors.
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
return config_service.validate_config(cfg)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=str(e)
|
|
) from e
|
|
|
|
|
|
@router.get("/backups", response_model=List[Dict[str, object]])
|
|
def list_backups(
|
|
auth: dict = Depends(require_auth)
|
|
) -> List[Dict[str, object]]:
|
|
"""List all available configuration backups.
|
|
|
|
Returns list of backup metadata including name, size, and created time.
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
return config_service.list_backups()
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to list backups: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/backups", response_model=Dict[str, str])
|
|
def create_backup(
|
|
name: Optional[str] = None, auth: dict = Depends(require_auth)
|
|
) -> Dict[str, str]:
|
|
"""Create a backup of the current configuration.
|
|
|
|
Args:
|
|
name: Optional custom backup name (timestamp used if not provided)
|
|
|
|
Returns:
|
|
Dictionary with backup name and message
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
backup_path = config_service.create_backup(name)
|
|
return {
|
|
"name": backup_path.name,
|
|
"message": "Backup created successfully"
|
|
}
|
|
except ConfigBackupError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Failed to create backup: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/backups/{backup_name}/restore", response_model=AppConfig)
|
|
def restore_backup(
|
|
backup_name: str, auth: dict = Depends(require_auth)
|
|
) -> AppConfig:
|
|
"""Restore configuration from a backup.
|
|
|
|
Creates backup of current config before restoring.
|
|
|
|
Args:
|
|
backup_name: Name of backup file to restore
|
|
|
|
Returns:
|
|
Restored configuration
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
return config_service.restore_backup(backup_name)
|
|
except ConfigBackupError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Failed to restore backup: {e}"
|
|
) from e
|
|
|
|
|
|
@router.delete("/backups/{backup_name}")
|
|
def delete_backup(
|
|
backup_name: str, auth: dict = Depends(require_auth)
|
|
) -> Dict[str, str]:
|
|
"""Delete a configuration backup.
|
|
|
|
Args:
|
|
backup_name: Name of backup file to delete
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
config_service.delete_backup(backup_name)
|
|
return {"message": f"Backup '{backup_name}' deleted successfully"}
|
|
except ConfigBackupError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Failed to delete backup: {e}"
|
|
) from e
|
|
|
|
|
|
@router.get("/section/advanced", response_model=Dict[str, object])
|
|
def get_advanced_config(
|
|
auth: Optional[dict] = Depends(require_auth)
|
|
) -> Dict[str, object]:
|
|
"""Get advanced configuration section.
|
|
|
|
Returns:
|
|
Dictionary with advanced configuration settings
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
app_config = config_service.load_config()
|
|
return app_config.other.get("advanced", {})
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to load advanced config: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/section/advanced", response_model=Dict[str, str])
|
|
def update_advanced_config(
|
|
config: Dict[str, object], auth: dict = Depends(require_auth)
|
|
) -> Dict[str, str]:
|
|
"""Update advanced configuration section.
|
|
|
|
Args:
|
|
config: Advanced configuration settings
|
|
auth: Authentication token (required)
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
app_config = config_service.load_config()
|
|
|
|
# Update advanced section in other
|
|
if "advanced" not in app_config.other:
|
|
app_config.other["advanced"] = {}
|
|
app_config.other["advanced"].update(config)
|
|
|
|
config_service.save_config(app_config)
|
|
return {"message": "Advanced configuration updated successfully"}
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update advanced config: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/directory", response_model=Dict[str, Any])
|
|
async def update_directory(
|
|
directory_config: Dict[str, str], auth: dict = Depends(require_auth)
|
|
) -> Dict[str, Any]:
|
|
"""Update anime directory configuration.
|
|
|
|
Args:
|
|
directory_config: Dictionary with 'directory' key
|
|
auth: Authentication token (required)
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
try:
|
|
directory = directory_config.get("directory")
|
|
if not directory:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Directory path is required"
|
|
)
|
|
|
|
config_service = get_config_service()
|
|
app_config = config_service.load_config()
|
|
|
|
# Store directory in other section
|
|
app_config.other["anime_directory"] = directory
|
|
|
|
config_service.save_config(app_config)
|
|
|
|
# Sync series from data files to database
|
|
sync_count = 0
|
|
try:
|
|
import structlog
|
|
|
|
from src.server.services.anime_service import sync_series_from_data_files
|
|
logger = structlog.get_logger(__name__)
|
|
sync_count = await sync_series_from_data_files(directory, logger)
|
|
logger.info(
|
|
"Directory updated: synced series from data files",
|
|
directory=directory,
|
|
count=sync_count
|
|
)
|
|
except Exception as e:
|
|
# Log but don't fail the directory update if sync fails
|
|
import structlog
|
|
structlog.get_logger(__name__).warning(
|
|
"Failed to sync series after directory update",
|
|
error=str(e)
|
|
)
|
|
|
|
response: Dict[str, Any] = {
|
|
"message": "Anime directory updated successfully",
|
|
"synced_series": sync_count
|
|
}
|
|
|
|
return response
|
|
except ConfigServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to update directory: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/export")
|
|
async def export_config(
|
|
export_options: Dict[str, bool], auth: dict = Depends(require_auth)
|
|
):
|
|
"""Export configuration to JSON file.
|
|
|
|
Args:
|
|
export_options: Options for export (include_sensitive, etc.)
|
|
auth: Authentication token (required)
|
|
|
|
Returns:
|
|
JSON file download response
|
|
"""
|
|
try:
|
|
import json
|
|
|
|
from fastapi.responses import Response
|
|
|
|
config_service = get_config_service()
|
|
app_config = config_service.load_config()
|
|
|
|
# Convert to dict
|
|
config_dict = app_config.model_dump()
|
|
|
|
# Optionally remove sensitive data
|
|
if not export_options.get("include_sensitive", False):
|
|
# Remove sensitive fields if present
|
|
config_dict.pop("password_salt", None)
|
|
config_dict.pop("password_hash", None)
|
|
|
|
# Create filename with timestamp
|
|
from datetime import datetime
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename = f"aniworld_config_{timestamp}.json"
|
|
|
|
# Return as downloadable JSON
|
|
content = json.dumps(config_dict, indent=2)
|
|
return Response(
|
|
content=content,
|
|
media_type="application/json",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{filename}"'
|
|
}
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to export config: {e}"
|
|
) from e
|
|
|
|
|
|
@router.post("/reset", response_model=Dict[str, str])
|
|
def reset_config(
|
|
reset_options: Dict[str, bool], auth: dict = Depends(require_auth)
|
|
) -> Dict[str, str]:
|
|
"""Reset configuration to defaults.
|
|
|
|
Args:
|
|
reset_options: Options for reset (preserve_security, etc.)
|
|
auth: Authentication token (required)
|
|
|
|
Returns:
|
|
Success message
|
|
"""
|
|
try:
|
|
config_service = get_config_service()
|
|
|
|
# Create backup before resetting
|
|
config_service.create_backup("pre_reset")
|
|
|
|
# Load default config
|
|
default_config = AppConfig()
|
|
|
|
# If preserve_security is True, keep authentication settings
|
|
if reset_options.get("preserve_security", True):
|
|
current_config = config_service.load_config()
|
|
# Preserve security-related fields from other
|
|
if "password_salt" in current_config.other:
|
|
default_config.other["password_salt"] = (
|
|
current_config.other["password_salt"]
|
|
)
|
|
if "password_hash" in current_config.other:
|
|
default_config.other["password_hash"] = (
|
|
current_config.other["password_hash"]
|
|
)
|
|
|
|
# Save default config
|
|
config_service.save_config(default_config)
|
|
|
|
return {
|
|
"message": "Configuration reset to defaults successfully"
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to reset config: {e}"
|
|
) from e
|
|
|