add banbanmini backend
This commit is contained in:
105
talkingq-url/scripts/backup_db.py
Normal file
105
talkingq-url/scripts/backup_db.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import aiomysql
|
||||
import subprocess
|
||||
import datetime
|
||||
import gzip
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from config import settings
|
||||
|
||||
BACKUP_DIR = os.path.join(Path(__file__).parent.parent, "backups")
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
|
||||
async def backup_database(name=None, compress=True):
|
||||
"""备份MySQL数据库"""
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
if name:
|
||||
backup_name = f"{name}_{timestamp}"
|
||||
else:
|
||||
backup_name = f"backup_{timestamp}"
|
||||
|
||||
output_file = os.path.join(BACKUP_DIR, f"{backup_name}.sql")
|
||||
|
||||
cmd = [
|
||||
"mysqldump",
|
||||
"-h", settings.db_host,
|
||||
"-P", str(settings.db_port),
|
||||
"-u", settings.db_user,
|
||||
f"-p{settings.db_password}",
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
"--events",
|
||||
settings.db_name
|
||||
]
|
||||
|
||||
try:
|
||||
print(f"开始备份数据库...")
|
||||
with open(output_file, 'w') as f:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=f,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
_, stderr = process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
print(f"备份失败: {stderr.decode()}")
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
return False
|
||||
|
||||
if compress:
|
||||
compressed_file = f"{output_file}.gz"
|
||||
print(f"压缩备份文件...")
|
||||
with open(output_file, 'rb') as f_in:
|
||||
with gzip.open(compressed_file, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
os.remove(output_file) # 删除未压缩的SQL文件
|
||||
output_file = compressed_file
|
||||
|
||||
file_size = os.path.getsize(output_file) / (1024 * 1024) # 转换为MB
|
||||
print(f"备份完成: {output_file} ({file_size:.2f} MB)")
|
||||
return output_file
|
||||
|
||||
except Exception as e:
|
||||
print(f"备份过程中出错: {e}")
|
||||
if os.path.exists(output_file):
|
||||
os.remove(output_file)
|
||||
return False
|
||||
|
||||
async def list_backups():
|
||||
"""列出所有可用的备份文件"""
|
||||
if not os.path.exists(BACKUP_DIR):
|
||||
return []
|
||||
|
||||
backups = [f for f in os.listdir(BACKUP_DIR) if f.endswith('.sql') or f.endswith('.sql.gz')]
|
||||
backups.sort(reverse=True) # 按文件名排序,最新的在前面
|
||||
|
||||
return backups
|
||||
|
||||
async def cleanup_old_backups(keep=10):
|
||||
"""清理旧备份,仅保留指定数量的最新备份"""
|
||||
backups = await list_backups()
|
||||
|
||||
if len(backups) <= keep:
|
||||
print(f"当前共有 {len(backups)} 个备份,未超过保留数量 {keep},不需要清理")
|
||||
return
|
||||
|
||||
to_delete = backups[keep:]
|
||||
print(f"将删除 {len(to_delete)} 个旧备份,保留 {keep} 个最新备份")
|
||||
|
||||
for backup in to_delete:
|
||||
try:
|
||||
file_path = os.path.join(BACKUP_DIR, backup)
|
||||
os.remove(file_path)
|
||||
print(f"已删除: {backup}")
|
||||
except Exception as e:
|
||||
print(f"删除备份 {backup} 时出错: {e}")
|
||||
385
talkingq-url/scripts/cleanup_db.py
Normal file
385
talkingq-url/scripts/cleanup_db.py
Normal file
@@ -0,0 +1,385 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import aiomysql
|
||||
import datetime
|
||||
import inspect
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from config import settings
|
||||
from database.models import Base
|
||||
from sqlalchemy import inspect as sqlalchemy_inspect
|
||||
from utils.logger import session_logger
|
||||
|
||||
async def get_mysql_connection():
|
||||
"""建立MySQL数据库连接"""
|
||||
try:
|
||||
conn = await aiomysql.connect(
|
||||
host=settings.db_host,
|
||||
port=settings.db_port,
|
||||
user=settings.db_user,
|
||||
password=settings.db_password,
|
||||
db=settings.db_name,
|
||||
autocommit=False
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
print(f"数据库连接失败: {e}")
|
||||
return None
|
||||
|
||||
async def get_all_tables(conn):
|
||||
"""获取数据库中所有表"""
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SHOW TABLES")
|
||||
tables = await cursor.fetchall()
|
||||
return [table[0] for table in tables]
|
||||
|
||||
async def get_model_tables():
|
||||
"""获取模型定义的所有表"""
|
||||
return [table.__tablename__ for table in Base.__subclasses__()]
|
||||
|
||||
async def get_table_columns(conn, table_name):
|
||||
"""获取表的所有列"""
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
|
||||
columns = await cursor.fetchall()
|
||||
return [column[0] for column in columns]
|
||||
|
||||
async def get_model_columns(model_class):
|
||||
"""获取模型定义的所有列"""
|
||||
inspector = sqlalchemy_inspect(model_class)
|
||||
return [column.key for column in inspector.mapper.column_attrs]
|
||||
|
||||
async def identify_unused_tables(conn):
|
||||
"""识别未使用的表"""
|
||||
db_tables = await get_all_tables(conn)
|
||||
model_tables = await get_model_tables()
|
||||
|
||||
unused_tables = [table for table in db_tables if table not in model_tables]
|
||||
return unused_tables
|
||||
|
||||
async def identify_unused_columns(conn):
|
||||
"""识别未使用的列"""
|
||||
results = {}
|
||||
model_dict = {model.__tablename__: model for model in Base.__subclasses__()}
|
||||
|
||||
for table_name, model_class in model_dict.items():
|
||||
try:
|
||||
db_columns = await get_table_columns(conn, table_name)
|
||||
model_columns = await get_model_columns(model_class)
|
||||
|
||||
unused_columns = [col for col in db_columns if col not in model_columns]
|
||||
if unused_columns:
|
||||
results[table_name] = unused_columns
|
||||
except Exception as e:
|
||||
print(f"检查表 {table_name} 列时出错: {e}")
|
||||
|
||||
return results
|
||||
|
||||
async def get_table_foreign_keys(conn, table_name):
|
||||
"""获取表的外键约束"""
|
||||
async with conn.cursor() as cursor:
|
||||
query = """
|
||||
SELECT
|
||||
CONSTRAINT_NAME,
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE
|
||||
REFERENCED_TABLE_NAME IS NOT NULL
|
||||
AND TABLE_SCHEMA = %s
|
||||
AND TABLE_NAME = %s
|
||||
"""
|
||||
await cursor.execute(query, (settings.db_name, table_name))
|
||||
foreign_keys = await cursor.fetchall()
|
||||
return foreign_keys
|
||||
|
||||
async def get_referenced_tables(conn, table_name):
|
||||
"""获取引用指定表的所有表"""
|
||||
async with conn.cursor() as cursor:
|
||||
query = """
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
CONSTRAINT_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE
|
||||
REFERENCED_TABLE_NAME = %s
|
||||
AND TABLE_SCHEMA = %s
|
||||
"""
|
||||
await cursor.execute(query, (table_name, settings.db_name))
|
||||
references = await cursor.fetchall()
|
||||
|
||||
if references:
|
||||
print(f"表 {table_name} 被以下表引用:")
|
||||
for ref in references:
|
||||
print(f" - {ref[0]} (约束: {ref[1]})")
|
||||
|
||||
return references
|
||||
|
||||
async def disable_foreign_key_checks(conn):
|
||||
"""临时禁用外键约束检查"""
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
|
||||
print("已临时禁用外键约束检查")
|
||||
|
||||
async def enable_foreign_key_checks(conn):
|
||||
"""重新启用外键约束检查"""
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
|
||||
print("已重新启用外键约束检查")
|
||||
|
||||
async def sort_tables_for_deletion(conn, tables):
|
||||
"""对表进行排序,以确保先删除引用者,再删除被引用者"""
|
||||
dependency_graph = {}
|
||||
for table in tables:
|
||||
references = await get_referenced_tables(conn, table)
|
||||
dependency_graph[table] = [ref[0] for ref in references if ref[0] in tables]
|
||||
|
||||
result = []
|
||||
visited = set()
|
||||
temp_mark = set()
|
||||
|
||||
def visit(node):
|
||||
if node in temp_mark:
|
||||
return
|
||||
if node not in visited:
|
||||
temp_mark.add(node)
|
||||
for dependent in dependency_graph.get(node, []):
|
||||
visit(dependent)
|
||||
temp_mark.remove(node)
|
||||
visited.add(node)
|
||||
result.append(node)
|
||||
|
||||
for table in tables:
|
||||
if table not in visited:
|
||||
visit(table)
|
||||
|
||||
return result[::-1]
|
||||
|
||||
async def drop_tables(conn, tables, dry_run=True):
|
||||
"""删除未使用的表"""
|
||||
if not tables:
|
||||
return True
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
print("禁用外键约束检查以确保安全删除所有表...")
|
||||
await disable_foreign_key_checks(conn)
|
||||
|
||||
for table in tables:
|
||||
try:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
|
||||
print(f"已删除表: {table}")
|
||||
except Exception as e:
|
||||
print(f"删除表 {table} 时出错: {e}")
|
||||
await conn.rollback()
|
||||
await enable_foreign_key_checks(conn)
|
||||
return False
|
||||
|
||||
await conn.commit()
|
||||
await enable_foreign_key_checks(conn)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"删除表时出错: {e}")
|
||||
try:
|
||||
await enable_foreign_key_checks(conn)
|
||||
await conn.rollback()
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
for table in tables:
|
||||
print(f"将删除表: {table}")
|
||||
return True
|
||||
|
||||
async def drop_columns(conn, column_dict, dry_run=True):
|
||||
"""删除未使用的列"""
|
||||
for table, columns in column_dict.items():
|
||||
try:
|
||||
for column in columns:
|
||||
if dry_run:
|
||||
print(f"将从表 {table} 删除列: {column}")
|
||||
else:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(f"ALTER TABLE `{table}` DROP COLUMN `{column}`")
|
||||
print(f"已从表 {table} 删除列: {column}")
|
||||
except Exception as e:
|
||||
print(f"从表 {table} 删除列时出错: {e}")
|
||||
if not dry_run:
|
||||
await conn.rollback()
|
||||
return False
|
||||
|
||||
if not dry_run:
|
||||
await conn.commit()
|
||||
return True
|
||||
|
||||
async def cleanup_database(dry_run=True):
|
||||
"""清理数据库中未使用的表和列"""
|
||||
conn = await get_mysql_connection()
|
||||
if not conn:
|
||||
return False
|
||||
|
||||
try:
|
||||
unused_tables = await identify_unused_tables(conn)
|
||||
unused_columns = await identify_unused_columns(conn)
|
||||
|
||||
print("\n=== 数据库清理报告 ===")
|
||||
|
||||
if unused_tables:
|
||||
print(f"\n发现 {len(unused_tables)} 个未使用的表:")
|
||||
for table in unused_tables:
|
||||
print(f" - {table}")
|
||||
|
||||
if not dry_run:
|
||||
success = await drop_tables(conn, unused_tables, dry_run=False)
|
||||
if not success:
|
||||
print("删除表失败,已回滚操作")
|
||||
return False
|
||||
else:
|
||||
print("\n未发现任何未使用的表")
|
||||
|
||||
if unused_columns:
|
||||
total_columns = sum(len(cols) for cols in unused_columns.values())
|
||||
print(f"\n发现 {total_columns} 个未使用的列:")
|
||||
for table, columns in unused_columns.items():
|
||||
for column in columns:
|
||||
print(f" - {table}.{column}")
|
||||
|
||||
if not dry_run:
|
||||
success = await drop_columns(conn, unused_columns, dry_run=False)
|
||||
if not success:
|
||||
print("删除列失败,已回滚操作")
|
||||
return False
|
||||
else:
|
||||
print("\n未发现任何未使用的列")
|
||||
|
||||
if dry_run:
|
||||
print("\n这是一次模拟运行,未执行任何实际更改。要执行更改,请使用 --execute 选项。")
|
||||
else:
|
||||
print("\n数据库清理成功完成!")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"清理数据库时发生错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
async def remove_orphaned_records(dry_run=True):
|
||||
"""删除孤立记录(有外键但对应的父记录不存在)"""
|
||||
conn = await get_mysql_connection()
|
||||
if not conn:
|
||||
return False
|
||||
|
||||
try:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT COUNT(*) FROM conversation_messages
|
||||
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
|
||||
""")
|
||||
count = (await cursor.fetchone())[0]
|
||||
|
||||
if count > 0:
|
||||
print(f"\n发现 {count} 条孤立的会话消息记录")
|
||||
if not dry_run:
|
||||
await cursor.execute("""
|
||||
DELETE FROM conversation_messages
|
||||
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
|
||||
""")
|
||||
print(f"已删除 {count} 条孤立的会话消息记录")
|
||||
else:
|
||||
print("\n未发现孤立的会话消息记录")
|
||||
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("""
|
||||
SELECT COUNT(*) FROM role_languages
|
||||
WHERE role_id NOT IN (SELECT id FROM roles)
|
||||
""")
|
||||
count = (await cursor.fetchone())[0]
|
||||
|
||||
if count > 0:
|
||||
print(f"\n发现 {count} 条孤立的角色语言记录")
|
||||
if not dry_run:
|
||||
await cursor.execute("""
|
||||
DELETE FROM role_languages
|
||||
WHERE role_id NOT IN (SELECT id FROM roles)
|
||||
""")
|
||||
print(f"已删除 {count} 条孤立的角色语言记录")
|
||||
else:
|
||||
print("\n未发现孤立的角色语言记录")
|
||||
|
||||
if not dry_run:
|
||||
await conn.commit()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"清理孤立记录时发生错误: {e}")
|
||||
if not dry_run:
|
||||
await conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
async def drop_specific_tables(conn, tables_to_drop, dry_run=True):
|
||||
"""删除指定的表,无论它们是否被使用"""
|
||||
if dry_run:
|
||||
print(f"\n将删除以下指定表:")
|
||||
for table in tables_to_drop:
|
||||
print(f" - {table}")
|
||||
return True
|
||||
|
||||
try:
|
||||
await disable_foreign_key_checks(conn)
|
||||
|
||||
for table in tables_to_drop:
|
||||
try:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
|
||||
print(f"已删除表: {table}")
|
||||
except Exception as e:
|
||||
print(f"删除表 {table} 时出错: {e}")
|
||||
|
||||
await conn.commit()
|
||||
print(f"指定表已成功删除")
|
||||
return True
|
||||
except Exception as e:
|
||||
await conn.rollback()
|
||||
print(f"删除指定表时出错: {e}")
|
||||
return False
|
||||
finally:
|
||||
await enable_foreign_key_checks(conn)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="数据库清理工具")
|
||||
parser.add_argument('--execute', action='store_true', help='执行实际更改(不使用此选项则只进行模拟运行)')
|
||||
parser.add_argument('--drop-tables', type=str, help='指定要删除的表,用逗号分隔')
|
||||
args = parser.parse_args()
|
||||
|
||||
dry_run = not args.execute
|
||||
|
||||
async def main():
|
||||
if args.drop_tables:
|
||||
conn = await get_mysql_connection()
|
||||
if not conn:
|
||||
return False
|
||||
|
||||
try:
|
||||
tables_to_drop = [t.strip() for t in args.drop_tables.split(',')]
|
||||
await drop_specific_tables(conn, tables_to_drop, dry_run)
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
result = await cleanup_database(dry_run)
|
||||
if result:
|
||||
await remove_orphaned_records(dry_run)
|
||||
|
||||
asyncio.run(main())
|
||||
92
talkingq-url/scripts/db_management.py
Normal file
92
talkingq-url/scripts/db_management.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from scripts.backup_db import backup_database, list_backups, cleanup_old_backups, BACKUP_DIR
|
||||
from scripts.restore_db import restore_database, interactive_restore
|
||||
from scripts.cleanup_db import cleanup_database, remove_orphaned_records
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description='数据库备份和恢复工具')
|
||||
subparsers = parser.add_subparsers(dest='command', help='子命令')
|
||||
|
||||
backup_parser = subparsers.add_parser('backup', help='备份数据库')
|
||||
backup_parser.add_argument('--name', type=str, help='备份名称,不含扩展名')
|
||||
backup_parser.add_argument('--no-compress', action='store_true', help='不压缩备份文件')
|
||||
backup_parser.add_argument('--keep', type=int, default=10, help='保留的备份数量')
|
||||
|
||||
restore_parser = subparsers.add_parser('restore', help='恢复数据库')
|
||||
restore_parser.add_argument('--file', type=str, help='要恢复的备份文件')
|
||||
|
||||
list_parser = subparsers.add_parser('list', help='列出所有备份')
|
||||
|
||||
cleanup_parser = subparsers.add_parser('cleanup', help='清理旧备份')
|
||||
cleanup_parser.add_argument('--keep', type=int, default=10, help='保留的备份数量')
|
||||
|
||||
clean_db_parser = subparsers.add_parser('clean-db', help='清理数据库中未使用的表和字段')
|
||||
clean_db_parser.add_argument('--execute', action='store_true', help='执行实际清理(不使用此选项则只进行模拟运行)')
|
||||
clean_db_parser.add_argument('--orphaned', action='store_true', help='仅清理孤立记录')
|
||||
clean_db_parser.add_argument('--schema', action='store_true', help='仅清理未使用的表和字段')
|
||||
clean_db_parser.add_argument('--backup-first', action='store_true', help='清理前先进行备份')
|
||||
clean_db_parser.add_argument('--drop-tables', type=str, help='指定要删除的表,用逗号分隔')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'backup':
|
||||
await backup_database(args.name, not args.no_compress if hasattr(args, 'no_compress') else True)
|
||||
await cleanup_old_backups(args.keep if hasattr(args, 'keep') else 10)
|
||||
elif args.command == 'restore':
|
||||
if args.file:
|
||||
await restore_database(args.file)
|
||||
else:
|
||||
await interactive_restore()
|
||||
elif args.command == 'list':
|
||||
backups = await list_backups()
|
||||
if backups:
|
||||
print("\n可用的备份文件:")
|
||||
for backup in backups:
|
||||
file_path = os.path.join(BACKUP_DIR, backup)
|
||||
size = os.path.getsize(file_path) / (1024 * 1024) # 转换为MB
|
||||
print(f"{backup} ({size:.2f} MB)")
|
||||
else:
|
||||
print("没有找到可用的备份")
|
||||
elif args.command == 'cleanup':
|
||||
keep = args.keep if hasattr(args, 'keep') else 10
|
||||
await cleanup_old_backups(keep)
|
||||
elif args.command == 'clean-db':
|
||||
dry_run = not args.execute
|
||||
|
||||
if args.backup_first:
|
||||
print("清理前进行数据库备份...")
|
||||
await backup_database(f"pre_cleanup_{int(asyncio.get_event_loop().time())}", True)
|
||||
|
||||
if args.drop_tables:
|
||||
from scripts.cleanup_db import get_mysql_connection, drop_specific_tables
|
||||
conn = await get_mysql_connection()
|
||||
if conn:
|
||||
try:
|
||||
tables_to_drop = [t.strip() for t in args.drop_tables.split(',')]
|
||||
await drop_specific_tables(conn, tables_to_drop, dry_run)
|
||||
finally:
|
||||
conn.close()
|
||||
else:
|
||||
if not args.orphaned:
|
||||
print("\n分析数据库架构...")
|
||||
await cleanup_database(dry_run)
|
||||
|
||||
if not args.schema:
|
||||
print("\n检查孤立记录...")
|
||||
await remove_orphaned_records(dry_run)
|
||||
|
||||
if dry_run:
|
||||
print("\n这是一次模拟运行,没有执行任何实际更改。")
|
||||
print("要执行实际的清理操作,请使用 --execute 选项。")
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
109
talkingq-url/scripts/devices.csv
Normal file
109
talkingq-url/scripts/devices.csv
Normal file
@@ -0,0 +1,109 @@
|
||||
device_id,serial_number
|
||||
talkingQ_B0EC1F2,3b4c5d6e7f
|
||||
talkingQ_94B7164,3b4c5d6e7f
|
||||
talkingQ_4C2B7F9,0a1b2c3d4e
|
||||
talkingQ_f91D6b8,5a9b8d3f7c
|
||||
talkingQ_1a2b7C3,f6a1c5e9d8
|
||||
talkingQ_7D3c1F4,8b9c7a2f1b
|
||||
talkingQ_2C8f3D6,4e9b0d7c1a
|
||||
talkingQ_0b6F4D3,a3b9c5d7e8
|
||||
talkingQ_9A7b4d1,b2f3a9d6c7
|
||||
talkingQ_5B8a1C9,f0d6c3b2a1
|
||||
talkingQ_7F3d2B6,c5d1e8f9b6
|
||||
talkingQ_8D1f5c7,6b9d0a3c5f
|
||||
talkingQ_4F1C3b9,a7e9f2c4d0
|
||||
talkingQ_2d4b6F7,8c5a1b3d9e
|
||||
talkingQ_7A9b4F2,e3c6d9f8b7
|
||||
talkingQ_1f6D4B2,d9b1a7c5e0
|
||||
talkingQ_5b1f7D9,3a4c2e8b5f
|
||||
talkingQ_8B4C3d9,f7a6c1d4e9
|
||||
talkingQ_0F7a6B4,2c9d5f3b7a
|
||||
talkingQ_6c5d2F8,a1b9c7d3f5
|
||||
talkingQ_3B1f7d5,9c2a4f8e6d
|
||||
talkingQ_2A7b9d4,3f6b1e9c8a
|
||||
talkingQ_9c3d8B7,5f1a2d6e4c
|
||||
talkingQ_7f4B3C1,9a5e7b3f0d
|
||||
talkingQ_B4B7164,3b4c5d6e7f
|
||||
talkingQ_E05AF3A,3b4c5d6e7f
|
||||
talkingQ_A8B7164,3b4c5d6e7f
|
||||
talkingQ_4d7f2B9,8c1b9e3f7a
|
||||
talkingQ_C4B7164,3b4c5d6e7f
|
||||
talkingQ_C0B7164,3b4c5d6e7f
|
||||
talkingQ_F4B7164,3b4c5d6e7f
|
||||
talkingQ_D0B7164,3b4c5d6e7f
|
||||
talkingQ_F0B7164,3b4c5d6e7f
|
||||
talkingQ_ACB7164,3b4c5d6e7f
|
||||
talkingQ_E8B7164,3b4c5d6e7f
|
||||
talkingQ_6B1F9a3,5d7c2e0a9f
|
||||
talkingQ_3A7d5F4,c9b0e6d2a4
|
||||
talkingQ_5B9d2A7,1e4f3c6a9b
|
||||
talkingQ_2d3F1B6,c4e9a5b7d2
|
||||
talkingQ_9A6b5d2,7c1a4f9e0b
|
||||
talkingQ_1A2B3C4,5d6e7f8a9b
|
||||
talkingQ_5D6E7F8,1a2b3c4d5e
|
||||
talkingQ_7G8H9I0,6b7c8d9e0f
|
||||
talkingQ_2J3K4L5,8f9g0h1j2k
|
||||
talkingQ_9M0N1O2,3l4m5n6o7p
|
||||
talkingQ_3P4Q5R6,9q0r1s2t3u
|
||||
talkingQ_6S7T8U9,4v5w6x7y8z
|
||||
talkingQ_4V5W6X7,0a1b2c3d4e
|
||||
talkingQ_8Y9Z0A1,5f6g7h8i9j
|
||||
talkingQ_5B6C7D8,0k1l2m3n4o
|
||||
talkingQ_3E4F5G6,7p8q9r0s1t
|
||||
talkingQ_7H8I9J0,2u3v4w5x6y
|
||||
talkingQ_1K2L3M4,8z9a0b1c2d
|
||||
talkingQ_4N5O6P7,3e4f5g6h7i
|
||||
talkingQ_6Q7R8S9,9j0k1l2m3n
|
||||
talkingQ_2T3U4V5,4o5p6q7r8s
|
||||
talkingQ_9W0X1Y2,0t1u2v3w4x
|
||||
talkingQ_5Z6A7B8,5y6z7a8b9c
|
||||
talkingQ_3C4D5E6,0d1e2f3g4h
|
||||
talkingQ_7F8G9H0,6i7j8k9l0m
|
||||
talkingQ_1I2J3K4,1n2o3p4q5r
|
||||
talkingQ_4L5M6N7,7s8t9u0v1w
|
||||
talkingQ_6O7P8Q9,2x3y4z5a6b
|
||||
talkingQ_2R3S4T5,8c9d0e1f2g
|
||||
talkingQ_9U0V1W2,3h4i5j6k7l
|
||||
talkingQ_5X6Y7Z8,9m0n1o2p3q
|
||||
talkingQ_3A4B5C6,4r5s6t7u8v
|
||||
talkingQ_7D8E9F0,0w1x2y3z4a
|
||||
talkingQ_1G2H3I4,5b6c7d8e9f
|
||||
talkingQ_4J5K6L7,0g1h2i3j4k
|
||||
talkingQ_6M7N8O9,6l7m8n9o0p
|
||||
talkingQ_2P3Q4R5,1q2r3s4t5u
|
||||
talkingQ_9S0T1U2,7v8w9x0y1z
|
||||
talkingQ_5V6W7X8,2a3b4c5d6e
|
||||
talkingQ_3Y4Z5A6,8f9g0h1i2j
|
||||
talkingQ_7B8C9D0,3k4l5m6n7o
|
||||
talkingQ_1E2F3G4,9p0q1r2s3t
|
||||
talkingQ_4H5I6J7,4u5v6w7x8y
|
||||
talkingQ_6K7L8M9,0z1a2b3c4d
|
||||
talkingQ_2N3O4P5,5e6f7g8h9i
|
||||
talkingQ_9Q0R1S2,0j1k2l3m4n
|
||||
talkingQ_5T6U7V8,6o7p8q9r0s
|
||||
talkingQ_3W4X5Y6,1t2u3v4w5x
|
||||
talkingQ_7Z8A9B0,7y8z9a0b1c
|
||||
talkingQ_1C2D3E4,2d3e4f5g6h
|
||||
talkingQ_4F5G6H7,8i9j0k1l2m
|
||||
talkingQ_6I7J8K9,3n4o5p6q7r
|
||||
talkingQ_2L3M4N5,9s0t1u2v3w
|
||||
talkingQ_9O0P1Q2,4x5y6z7a8b
|
||||
talkingQ_5R6S7T8,0c1d2e3f4g
|
||||
talkingQ_8B9D3E4,a1b2c3d4e5
|
||||
talkingQ_2A4F7C8,f6e5d4c3b2
|
||||
talkingQ_1E3F5G7,d8c7b9a0f1
|
||||
talkingQ_4G7H9K1,c2b3a4d5e6
|
||||
talkingQ_5F2D4A8,b7c8d9e1f0
|
||||
talkingQ_7J3L5O6,e1f2d3c4b5
|
||||
talkingQ_3C8B7E4,d9a6f5b2c3
|
||||
talkingQ_9A1D3F5,e4b6c7d8a9
|
||||
talkingQ_6B5C2D4,a7e8f9b0c1
|
||||
talkingQ_4G8H3J7,d2f5c9b4a0
|
||||
talkingQ_D485583,3b4c5d6e7f
|
||||
talkingQ_8C85583,3b4c5d6e7f
|
||||
talkingQ_4486583,3b4c5d6e7f
|
||||
talkingQ_68A81F2,3b4c5d6e7f
|
||||
talkingQ_70A81F2,3b4c5d6e7f
|
||||
talkingQ_7885583,3b4c5d6e7f
|
||||
talkingQ_7886583,3b4c5d6e7f
|
||||
talkingQ_C8A71F2,3b4c5d6e7f
|
||||
|
112
talkingq-url/scripts/restore_db.py
Normal file
112
talkingq-url/scripts/restore_db.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import subprocess
|
||||
import gzip
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from config import settings
|
||||
from scripts.backup_db import list_backups, BACKUP_DIR
|
||||
|
||||
async def restore_database(backup_file):
|
||||
"""从备份文件恢复数据库"""
|
||||
file_path = backup_file
|
||||
if not os.path.isabs(backup_file):
|
||||
file_path = os.path.join(BACKUP_DIR, backup_file)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"错误: 备份文件 '{file_path}' 不存在")
|
||||
return False
|
||||
|
||||
confirm = input(f"警告: 这将覆盖当前数据库的所有数据。确定要继续吗? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
print("操作已取消")
|
||||
return False
|
||||
|
||||
temp_file = None
|
||||
sql_file = file_path
|
||||
|
||||
try:
|
||||
if file_path.endswith('.gz'):
|
||||
print("解压备份文件...")
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.sql')
|
||||
temp_file.close()
|
||||
|
||||
with gzip.open(file_path, 'rb') as f_in:
|
||||
with open(temp_file.name, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
sql_file = temp_file.name
|
||||
|
||||
cmd = [
|
||||
"mysql",
|
||||
"-h", settings.db_host,
|
||||
"-P", str(settings.db_port),
|
||||
"-u", settings.db_user,
|
||||
f"-p{settings.db_password}",
|
||||
settings.db_name
|
||||
]
|
||||
|
||||
print(f"开始恢复数据库...")
|
||||
with open(sql_file, 'r') as f:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=f,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
print(f"恢复失败: {stderr.decode()}")
|
||||
return False
|
||||
|
||||
print("数据库恢复成功!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"恢复过程中出错: {e}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
if temp_file and os.path.exists(temp_file.name):
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
async def interactive_restore():
|
||||
"""交互式选择要恢复的备份文件"""
|
||||
backups = await list_backups()
|
||||
|
||||
if not backups:
|
||||
print("没有找到可用的备份文件")
|
||||
return False
|
||||
|
||||
print("\n可用的备份文件:")
|
||||
for i, backup in enumerate(backups):
|
||||
file_path = os.path.join(BACKUP_DIR, backup)
|
||||
size = os.path.getsize(file_path) / (1024 * 1024) # 转换为MB
|
||||
print(f"{i+1}. {backup} ({size:.2f} MB)")
|
||||
|
||||
try:
|
||||
choice = int(input("\n请选择要恢复的备份文件 (输入序号): "))
|
||||
if 1 <= choice <= len(backups):
|
||||
selected_backup = backups[choice-1]
|
||||
return await restore_database(selected_backup)
|
||||
else:
|
||||
print("无效的选择")
|
||||
return False
|
||||
except ValueError:
|
||||
print("无效的输入")
|
||||
return False
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) > 1:
|
||||
await restore_database(sys.argv[1])
|
||||
else:
|
||||
await interactive_restore()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user