310 lines
9.2 KiB
Python
310 lines
9.2 KiB
Python
"""Startup migration runner for data file to database migration.
|
|
|
|
This module provides functions to run the data file migration automatically
|
|
during application startup. The migration checks for existing data files
|
|
in the anime directory and migrates them to the database.
|
|
|
|
Usage:
|
|
This module is intended to be called from the FastAPI lifespan context.
|
|
|
|
Example:
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# ... initialization ...
|
|
await ensure_migration_on_startup()
|
|
yield
|
|
# ... cleanup ...
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from src.server.database.connection import get_db_session
|
|
from src.server.services.auth_service import auth_service
|
|
from src.server.services.config_service import ConfigService
|
|
from src.server.services.data_migration_service import (
|
|
MigrationResult,
|
|
get_data_migration_service,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def run_startup_migration(anime_directory: str) -> MigrationResult:
|
|
"""Run data file migration for the given anime directory.
|
|
|
|
Checks if there are data files to migrate and runs the migration
|
|
if needed. This function is idempotent - running it multiple times
|
|
will only migrate files that haven't been migrated yet.
|
|
|
|
Args:
|
|
anime_directory: Path to the anime directory containing
|
|
series folders with data files
|
|
|
|
Returns:
|
|
MigrationResult: Results of the migration operation,
|
|
including counts of migrated, skipped, and failed items
|
|
|
|
Note:
|
|
This function creates its own database session and commits
|
|
the transaction at the end of the migration.
|
|
"""
|
|
service = get_data_migration_service()
|
|
|
|
# Check if migration is needed
|
|
if not service.is_migration_needed(anime_directory):
|
|
logger.info(
|
|
"No data files found to migrate in: %s",
|
|
anime_directory
|
|
)
|
|
return MigrationResult(total_found=0)
|
|
|
|
logger.info(
|
|
"Starting data file migration from: %s",
|
|
anime_directory
|
|
)
|
|
|
|
# Get database session and run migration
|
|
async with get_db_session() as db:
|
|
result = await service.migrate_all(anime_directory, db)
|
|
|
|
# Log results
|
|
if result.migrated > 0 or result.failed > 0:
|
|
logger.info(
|
|
"Migration complete: %d migrated, %d skipped, %d failed",
|
|
result.migrated,
|
|
result.skipped,
|
|
result.failed
|
|
)
|
|
|
|
if result.errors:
|
|
for error in result.errors:
|
|
logger.warning("Migration error: %s", error)
|
|
|
|
return result
|
|
|
|
|
|
def _get_anime_directory_from_config() -> Optional[str]:
|
|
"""Get anime directory from application configuration.
|
|
|
|
Attempts to load the configuration file and extract the
|
|
anime_directory setting from the 'other' config section.
|
|
|
|
Returns:
|
|
Anime directory path if configured, None otherwise
|
|
"""
|
|
try:
|
|
config_service = ConfigService()
|
|
config = config_service.load_config()
|
|
|
|
# anime_directory is stored in the 'other' dict
|
|
anime_dir = config.other.get("anime_directory")
|
|
|
|
if anime_dir:
|
|
anime_dir = str(anime_dir).strip()
|
|
if anime_dir:
|
|
return anime_dir
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Could not load anime directory from config: %s",
|
|
e
|
|
)
|
|
return None
|
|
|
|
|
|
def _is_setup_complete() -> bool:
|
|
"""Check if the application setup is complete.
|
|
|
|
Setup is complete when:
|
|
1. Master password is configured
|
|
2. Configuration file exists and is valid
|
|
|
|
Returns:
|
|
True if setup is complete, False otherwise
|
|
"""
|
|
# Check if master password is configured
|
|
if not auth_service.is_configured():
|
|
return False
|
|
|
|
# Check if config exists and is valid
|
|
try:
|
|
config_service = ConfigService()
|
|
config = config_service.load_config()
|
|
|
|
# Validate the loaded config
|
|
validation = config.validate()
|
|
if not validation.valid:
|
|
return False
|
|
|
|
except Exception:
|
|
# If we can't load or validate config, setup is not complete
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
async def ensure_migration_on_startup() -> Optional[MigrationResult]:
|
|
"""Ensure data file migration runs during application startup.
|
|
|
|
This function should be called during FastAPI application startup.
|
|
It loads the anime directory from configuration and runs the
|
|
migration if the directory is configured and contains data files.
|
|
|
|
Migration will only run if setup is complete (master password
|
|
configured and valid configuration exists).
|
|
|
|
Returns:
|
|
MigrationResult if migration was run, None if skipped
|
|
(e.g., when no anime directory is configured)
|
|
|
|
Behavior:
|
|
- Returns None if anime_directory is not configured (first run)
|
|
- Returns None if anime_directory does not exist
|
|
- Returns MigrationResult with total_found=0 if no data files exist
|
|
- Returns MigrationResult with migration counts if migration ran
|
|
|
|
Note:
|
|
This function catches and logs all exceptions without re-raising,
|
|
ensuring that startup migration failures don't block application
|
|
startup. Check the logs for any migration errors.
|
|
|
|
Example:
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await init_db()
|
|
|
|
try:
|
|
result = await ensure_migration_on_startup()
|
|
if result:
|
|
logger.info(
|
|
"Migration: %d migrated, %d failed",
|
|
result.migrated,
|
|
result.failed
|
|
)
|
|
except Exception as e:
|
|
logger.error("Migration failed: %s", e)
|
|
|
|
yield
|
|
await close_db()
|
|
"""
|
|
# Check if setup is complete before running migration
|
|
if not _is_setup_complete():
|
|
logger.debug(
|
|
"Setup not complete, skipping startup migration"
|
|
)
|
|
return None
|
|
|
|
# Get anime directory from config
|
|
anime_directory = _get_anime_directory_from_config()
|
|
|
|
if not anime_directory:
|
|
logger.debug(
|
|
"No anime directory configured, skipping migration"
|
|
)
|
|
return None
|
|
|
|
# Validate directory exists
|
|
anime_path = Path(anime_directory)
|
|
if not anime_path.exists():
|
|
logger.warning(
|
|
"Anime directory does not exist: %s, skipping migration",
|
|
anime_directory
|
|
)
|
|
return None
|
|
|
|
if not anime_path.is_dir():
|
|
logger.warning(
|
|
"Anime directory path is not a directory: %s, skipping migration",
|
|
anime_directory
|
|
)
|
|
return None
|
|
|
|
logger.info(
|
|
"Checking for data files to migrate in: %s",
|
|
anime_directory
|
|
)
|
|
|
|
try:
|
|
result = await run_startup_migration(anime_directory)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"Data file migration failed: %s",
|
|
e,
|
|
exc_info=True
|
|
)
|
|
# Return empty result rather than None to indicate we attempted
|
|
return MigrationResult(
|
|
total_found=0,
|
|
failed=1,
|
|
errors=[f"Migration failed: {str(e)}"]
|
|
)
|
|
|
|
|
|
async def run_migration_for_directory(
|
|
anime_directory: str
|
|
) -> Optional[MigrationResult]:
|
|
"""Run data file migration for a specific directory.
|
|
|
|
This function can be called after setup is complete to migrate
|
|
data files from the specified anime directory to the database.
|
|
Unlike ensure_migration_on_startup, this does not check setup
|
|
status as it's intended to be called after setup is complete.
|
|
|
|
Args:
|
|
anime_directory: Path to the anime directory containing
|
|
series folders with data files
|
|
|
|
Returns:
|
|
MigrationResult if migration was run, None if directory invalid
|
|
"""
|
|
if not anime_directory or not anime_directory.strip():
|
|
logger.debug("Empty anime directory provided, skipping migration")
|
|
return None
|
|
|
|
anime_directory = anime_directory.strip()
|
|
|
|
# Validate directory exists
|
|
anime_path = Path(anime_directory)
|
|
if not anime_path.exists():
|
|
logger.warning(
|
|
"Anime directory does not exist: %s, skipping migration",
|
|
anime_directory
|
|
)
|
|
return None
|
|
|
|
if not anime_path.is_dir():
|
|
logger.warning(
|
|
"Anime directory path is not a directory: %s",
|
|
anime_directory
|
|
)
|
|
return None
|
|
|
|
logger.info(
|
|
"Running migration for directory: %s",
|
|
anime_directory
|
|
)
|
|
|
|
try:
|
|
result = await run_startup_migration(anime_directory)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"Data file migration failed for %s: %s",
|
|
anime_directory,
|
|
e,
|
|
exc_info=True
|
|
)
|
|
return MigrationResult(
|
|
total_found=0,
|
|
failed=1,
|
|
errors=[f"Migration failed: {str(e)}"]
|
|
)
|