ForcePilot/server/utils/migrate.py

273 lines
9.6 KiB
Python
Raw Normal View History

"""
数据库迁移系统
"""
2025-09-18 20:32:33 +08:00
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path
from src.utils import logger
class DatabaseMigrator:
"""数据库迁移器"""
2025-09-18 20:32:33 +08:00
def __init__(self, db_path: str):
self.db_path = db_path
self.backup_dir = os.path.join(os.path.dirname(db_path), "backups")
self.migration_version_key = "migration_version"
2025-09-18 20:32:33 +08:00
def ensure_backup_dir(self):
"""确保备份目录存在"""
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
2025-09-18 20:32:33 +08:00
def backup_database(self) -> str:
"""备份数据库文件"""
if not os.path.exists(self.db_path):
logger.info("数据库文件不存在,无需备份")
return ""
2025-09-18 20:32:33 +08:00
self.ensure_backup_dir()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"server_backup_{timestamp}.db"
backup_path = os.path.join(self.backup_dir, backup_filename)
2025-09-18 20:32:33 +08:00
try:
shutil.copy2(self.db_path, backup_path)
logger.info(f"数据库已备份到: {backup_path}")
return backup_path
except Exception as e:
logger.error(f"数据库备份失败: {e}")
raise
2025-09-18 20:32:33 +08:00
def get_current_version(self) -> int:
"""获取当前数据库版本"""
if not os.path.exists(self.db_path):
return 0
2025-09-18 20:32:33 +08:00
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
2025-09-18 20:32:33 +08:00
# 检查版本表是否存在
cursor.execute("""
2025-09-18 20:32:33 +08:00
SELECT name FROM sqlite_master
WHERE type='table' AND name='migration_versions' """)
if not cursor.fetchone():
# 版本表不存在,检查是否为旧版本数据库
cursor.execute("""
2025-09-18 20:32:33 +08:00
SELECT name FROM sqlite_master
WHERE type='table' AND name='users'
""")
if cursor.fetchone():
# 用户表存在但版本表不存在,说明是旧版本
return 0
else:
# 全新数据库
return 0
2025-09-18 20:32:33 +08:00
# 获取当前版本
cursor.execute("SELECT version FROM migration_versions ORDER BY version DESC LIMIT 1")
result = cursor.fetchone()
return result[0] if result else 0
2025-09-18 20:32:33 +08:00
except Exception as e:
logger.error(f"获取数据库版本失败: {e}")
return 0
finally:
2025-09-18 20:32:33 +08:00
if "conn" in locals():
conn.close()
2025-09-18 20:32:33 +08:00
def set_version(self, version: int):
"""设置数据库版本"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
2025-09-18 20:32:33 +08:00
# 创建版本表
cursor.execute("""
CREATE TABLE IF NOT EXISTS migration_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version INTEGER NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
description TEXT
)
""")
2025-09-18 20:32:33 +08:00
# 插入版本记录
2025-09-18 20:32:33 +08:00
cursor.execute(
"""
INSERT INTO migration_versions (version, description)
VALUES (?, ?)
2025-09-18 20:32:33 +08:00
""",
(version, f"Migration to version {version}"),
)
conn.commit()
logger.info(f"数据库版本设置为: {version}")
2025-09-18 20:32:33 +08:00
except Exception as e:
logger.error(f"设置数据库版本失败: {e}")
raise
finally:
2025-09-18 20:32:33 +08:00
if "conn" in locals():
conn.close()
2025-09-18 20:32:33 +08:00
def execute_migration(self, version: int, description: str, sql_commands: list[str]):
"""执行迁移"""
logger.info(f"执行迁移 v{version}: {description}")
2025-09-18 20:32:33 +08:00
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
2025-09-18 20:32:33 +08:00
# 执行迁移SQL命令
for sql in sql_commands:
if sql.strip(): # 跳过空命令
logger.info(f"执行SQL: {sql}")
cursor.execute(sql)
2025-09-18 20:32:33 +08:00
conn.commit()
logger.info(f"迁移 v{version} 执行成功")
2025-09-18 20:32:33 +08:00
except Exception as e:
logger.error(f"迁移 v{version} 执行失败: {e}")
raise
finally:
2025-09-18 20:32:33 +08:00
if "conn" in locals():
conn.close()
2025-09-18 20:32:33 +08:00
def check_column_exists(self, table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
2025-09-18 20:32:33 +08:00
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [column[1] for column in cursor.fetchall()]
return column_name in columns
2025-09-18 20:32:33 +08:00
except Exception:
return False
finally:
2025-09-18 20:32:33 +08:00
if "conn" in locals():
conn.close()
2025-09-18 20:32:33 +08:00
def run_migrations(self):
"""运行所有待执行的迁移"""
current_version = self.get_current_version()
latest_version = self.get_latest_migration_version()
2025-09-18 20:32:33 +08:00
# 如果数据库已存在但没有版本表,创建版本表并设置为最新版本
if current_version == 0 and latest_version > 0 and os.path.exists(self.db_path):
# 检查users表是否已有新字段如果有说明是通过SQLAlchemy创建的
2025-09-18 20:32:33 +08:00
if (
self.check_column_exists("users", "login_failed_count")
and self.check_column_exists("users", "last_failed_login")
and self.check_column_exists("users", "login_locked_until")
):
# 字段已存在,直接设置为最新版本
logger.info(f"检测到现有数据库已包含最新字段,设置版本为 v{latest_version}")
self.set_version(latest_version)
return
2025-09-18 20:32:33 +08:00
if current_version >= latest_version:
logger.info(f"数据库已是最新版本 v{current_version}")
return
2025-09-18 20:32:33 +08:00
logger.info(f"开始数据库迁移: v{current_version} -> v{latest_version}")
2025-09-18 20:32:33 +08:00
# 备份数据库
backup_path = self.backup_database()
2025-09-18 20:32:33 +08:00
try:
# 执行迁移
migrations = self.get_migrations()
has_executed_migrations = False
2025-09-18 20:32:33 +08:00
for version, description, sql_commands in migrations:
if version > current_version:
if sql_commands: # 只有当有SQL命令时才执行迁移
self.execute_migration(version, description, sql_commands)
has_executed_migrations = True
else:
logger.info(f"迁移 v{version}: {description} - 无需执行,字段已存在")
2025-09-18 20:32:33 +08:00
# 无论是否有SQL命令都设置版本
self.set_version(version)
2025-09-18 20:32:33 +08:00
if has_executed_migrations:
logger.info("数据库迁移完成")
else:
logger.info("数据库结构已是最新,仅更新版本记录")
2025-09-18 20:32:33 +08:00
except Exception as e:
logger.error(f"数据库迁移失败: {e}")
if backup_path and os.path.exists(backup_path):
logger.info(f"尝试从备份恢复: {backup_path}")
try:
shutil.copy2(backup_path, self.db_path)
logger.info("数据库已从备份恢复")
except Exception as restore_error:
logger.error(f"数据库恢复失败: {restore_error}")
raise
2025-09-18 20:32:33 +08:00
def get_latest_migration_version(self) -> int:
"""获取最新迁移版本号"""
# 这里返回硬编码的最新版本号,不依赖迁移定义
# 因为迁移定义可能为空(字段已存在)
return 1 # 当前最新版本是 v1
2025-09-18 20:32:33 +08:00
def get_migrations(self) -> list[tuple[int, str, list[str]]]:
"""获取所有迁移定义
返回格式: [(version, description, [sql_commands])]
"""
migrations = []
2025-09-18 20:32:33 +08:00
# 迁移 v1: 为 users 表添加登录失败限制字段
# 使用条件检查来避免重复添加字段
v1_commands = []
2025-09-18 20:32:33 +08:00
# 检查并添加 login_failed_count 字段
2025-09-18 20:32:33 +08:00
if not self.check_column_exists("users", "login_failed_count"):
v1_commands.append("ALTER TABLE users ADD COLUMN login_failed_count INTEGER NOT NULL DEFAULT 0")
2025-09-18 20:32:33 +08:00
# 检查并添加 last_failed_login 字段
2025-09-18 20:32:33 +08:00
if not self.check_column_exists("users", "last_failed_login"):
v1_commands.append("ALTER TABLE users ADD COLUMN last_failed_login DATETIME")
2025-09-18 20:32:33 +08:00
# 检查并添加 login_locked_until 字段
2025-09-18 20:32:33 +08:00
if not self.check_column_exists("users", "login_locked_until"):
v1_commands.append("ALTER TABLE users ADD COLUMN login_locked_until DATETIME")
2025-09-18 20:32:33 +08:00
# 如果有命令需要执行,才添加迁移
if v1_commands:
migrations.append((1, "为用户表添加登录失败限制字段", v1_commands))
2025-09-18 20:32:33 +08:00
# 未来的迁移可以在这里添加
# migrations.append((
# 2,
# "添加新功能相关表",
# [
# "CREATE TABLE new_feature (...)",
# "ALTER TABLE existing_table ADD COLUMN new_field ..."
# ]
# ))
2025-09-18 20:32:33 +08:00
return migrations
def check_and_migrate(db_path: str):
"""检查并执行数据库迁移"""
migrator = DatabaseMigrator(db_path)
2025-09-18 20:32:33 +08:00
try:
migrator.run_migrations()
return True
except Exception as e:
logger.error(f"数据库迁移过程中发生错误: {e}")
2025-09-18 20:32:33 +08:00
return False