Files
2026-07-24 16:25:43 +08:00

1062 lines
47 KiB
Python

# coding: utf-8
# -------------------------------------------------------------------
# aapanel
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 aapanel(http://www.aapanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: miku <miku@bt.cn>
# -------------------------------------------------------------------
import json
import os
import random
import re
import shutil
import string
import sys
import time
import warnings
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
if "/www/server/panel/class_v2" not in sys.path:
sys.path.insert(0, "/www/server/panel/class_v2")
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
import public
from BTPanel import app
import panelMysql
import database_v2 as database
import databaseModelV2.pgsqlModel as panelPgsql
import databaseModelV2.mongodbModel as panelMongoDB
import databaseModelV2.redisModel as panelRedis
from mod.project.backup_restore.data_manager import DataManager
warnings.filterwarnings("ignore", category=SyntaxWarning)
class DatabaseModule(DataManager):
_MYSQLDUMP_BIN = public.get_mysqldump_bin()
_MYSQL_BIN = public.get_mysql_bin()
_MONGODBDUMP_BIN = "/www/server/mongodb/bin/mongodump"
_MONGOEXPORT_BIN = "/www/server/mongodb/bin/mongoexport"
_PGDUMP_BIN = "/www/server/pgsql/bin/pg_dump"
def __init__(self):
super().__init__()
self.base_path = '/www/backup/backup_restore'
self.bakcup_task_json = self.base_path + '/backup_task.json'
self.mysql_table_data_exist = True
self.mg_table_data_exist = True
self.pgsql_table_data_exist = True
def get_database_backup_conf(self, timestamp=None):
database_ids = ["ALL"]
if timestamp is not None:
backup_conf = self.get_backup_conf(str(timestamp))
if backup_conf:
database_ids = self.normalize_backup_id_list(backup_conf.get("database_id"))
mysql_data = public.M('databases').select()
db_list = []
database_id_set = set(str(database_id) for database_id in database_ids)
with app.app_context():
for data in mysql_data:
if "ALL" not in database_ids and str(data.get('id')) not in database_id_set:
continue
try:
real_access = database.database().GetDatabaseAccess(
public.to_dict_obj({'name': data['name']})
)['message'].get("permission", "127.0.0.1")
except:
real_access = "127.0.0.1"
related_site = self._get_current_site_name_by_pid(data.get('pid', 0))
if related_site == "" and data.get('ps'):
# try ps
p_site = public.M('sites').where('name=?', (data['ps'],)).field('id,name').find()
if p_site and not isinstance(p_site, str):
related_site = p_site.get('name', '')
db_info = {
'name': data['name'],
'type': data['type'],
'id': data['id'],
'sid': data['sid'],
'ps': data['ps'],
'username': data['username'],
'password': data['password'],
"data_type": "backup",
'accept': data['accept'],
'database_record': data,
'real_access': real_access,
'related_site': related_site,
'status': 0,
'msg': None,
}
db_list.append(db_info)
include_redis = "ALL" in database_ids or "0" in database_id_set or "redis" in database_id_set
if include_redis and os.path.exists("/www/server/redis/src/redis-server") and os.path.exists("/www/server/redis/version.pl"):
db_info = self.get_redis_info()
db_list.append(db_info)
return db_list
def get_redis_info(self):
return {
'name': "redis",
'type': "redis",
'id': 0,
'sid': 0,
'ps': "redis",
'username': "redis",
'password': "redis",
'status': 0,
'msg': None,
}
def get_remote_db_list(self, timestamp=None):
import db
sql = db.Sql()
sql.table('database_servers')
result = sql.select()
if result:
return {
'status': True,
'msg': result
}
else:
return {
'status': False,
'msg': public.lang('The list of remote databases is empty'),
}
def resotre_remote_db_server(self, remote_db_list):
try:
for remote_db in remote_db_list:
local_remote_db_info = public.M('database_servers').where(
'db_host=? AND db_port=?',
(remote_db['db_host'], remote_db['db_port'])
).select()
if not local_remote_db_info:
pdata = {
'id': remote_db['id'],
'db_host': remote_db['db_host'],
'db_port': remote_db['db_port'],
'db_user': remote_db['db_user'],
'db_password': remote_db['db_password'],
'ps': remote_db['ps'],
'type': remote_db['type'],
'db_type': remote_db['db_type']
}
result = public.M("database_servers").insert(pdata)
except:
pass
def restore_remote_database(self, db_data):
database_record = db_data['database_record']
local_db_info = public.M('databases').where('name=?', (db_data["name"],)).select()
if not local_db_info:
pdata = {
'pid': database_record['pid'],
'name': database_record['name'],
'username': database_record['username'],
'password': database_record['password'],
'accept': database_record['accept'],
'ps': database_record['ps'],
'addtime': database_record['addtime'],
'db_type': database_record['db_type'],
'conn_config': database_record['conn_config'],
'sid': database_record['sid'],
'type': database_record['type'],
'type_id': database_record['type_id']
}
public.M("databases").insert(pdata)
def backup_redis_data(self, timestamp: int):
"""
备份数据库
"""
db_fname = "all_db"
redis_obj = panelRedis.panelRedisDB()
if redis_obj.redis_conn(0) is False:
return public.returnMsg(False, public.lang("Redis connection exception!"))
_db_num = 16
_REDIS_CONF = os.path.join(public.get_setup_path(), "redis/redis.conf")
if os.path.exists(_REDIS_CONF):
redis_conf = public.readFile(_REDIS_CONF)
db_obj = re.search("\ndatabases\s+(\d+)", redis_conf)
if db_obj:
_db_num = int(db_obj.group(1))
for db_idx in range(0, _db_num):
try:
redis_obj.redis_conn(db_idx).save()
except:
continue
redis_obj = redis_obj.redis_conn(0)
src_path = os.path.join(redis_obj.config_get().get("dir", ""), "dump.rdb")
if not os.path.exists(src_path):
return public.returnMsg(False, public.lang('BACKUP_ERROR'))
backup_path = f"/www/backup/backup_restore/{timestamp}_backup/database/redis".format(timestamp=timestamp)
if not os.path.exists(backup_path):
public.ExecShell("mkdir -p {}".format(backup_path))
file_name = "{db_fname}_{backup_time}_redis_data.rdb".format(
db_fname=db_fname,
backup_time=time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())
)
file_path = os.path.join(backup_path, file_name)
shutil.copyfile(src_path, file_path)
if not os.path.exists(file_path):
return public.returnMsg(False, public.lang('BACKUP_ERROR'))
return public.returnMsg(True, file_path)
def backup_database_data(self, timestamp):
data_list = self.get_backup_data_list(timestamp)
if not data_list:
return None
self.print_log("====================================================", "backup")
self.print_log(public.lang("Starting a backup of database data"), 'backup')
for db in data_list['data_list']['database']:
db['status'] = 1
self.update_backup_data_list(timestamp, data_list)
try:
if db['sid'] == 0:
log_str = f"Backup {db['type']} database: {db['name']}"
self.print_log(public.lang(log_str), "backup")
backup_result = None
db['sql_file_name'] = None
db['size'] = None
db['sql_sha256'] = None
if db.get('type', '').lower() == 'mysql':
backup_result = self.backup_mysql_data(db['name'], timestamp)
elif db.get('type', '').lower() == 'mongodb':
backup_result = self.backup_mongodb_data(db['name'], timestamp)
elif db.get('type', '').lower() == 'pgsql':
backup_result = self.backup_pgsql_data(db['name'], timestamp)
elif db.get('type', '').lower() == 'redis':
backup_result = self.backup_redis_data(timestamp)
if backup_result:
if backup_result['status'] is True:
backup_file = backup_result['msg']
db['sql_file_name'] = backup_file
db['size'] = self.get_file_size(backup_file)
db['sql_sha256'] = self.get_file_sha256(backup_file)
db['status'] = 2
db['msg'] = None
format_backup_file_size = self.format_size(int(db['size']))
new_log_str = f"{db['type']} database {db['name']} ✓ ({format_backup_file_size})"
self.replace_log(log_str, new_log_str, 'backup')
elif backup_result['status'] is False:
db['status'] = 3
db['msg'] = backup_result['msg']
new_log_str = f"{db['type']} database {db['name']} ✗ ({backup_result['msg']})"
self.replace_log(log_str, new_log_str, 'backup')
else:
db['status'] = 2
db['msg'] = None
else:
log_str = public.lang("Backup {}remote {}database".format(db['name'], db['type']))
self.print_log(log_str, "backup")
db['status'] = 2
db['msg'] = None
new_log_str = public.lang("{}Remote {} database information ✓".format(db['name'], db['type']))
self.replace_log(log_str, new_log_str, 'backup')
except Exception as e:
self.print_log(public.lang(f"Backup {db['type']} database {db['name']} failed Cause:{str(e)}"),
"backup")
db['status'] = 3
db['msg'] = str(e)
continue
self.update_backup_data_list(timestamp, data_list)
get_remote_list_result = self.get_remote_db_list()
if get_remote_list_result['status'] is True:
data_list['data_list']['remote_db_list'] = get_remote_list_result['msg']
self.update_backup_data_list(timestamp, data_list)
self.print_log(public.lang("Database data backup completed"), 'backup')
# ======================== sqlite ==========================================
def backup_sqlite_data(self, timestamp):
db_model_path = "/www/server/panel/data/db_model.json"
if os.path.exists(db_model_path):
if not os.path.exists(
"/www/backup/backup_restore/{timestamp}_backup/database/db_model.json".format(timestamp=timestamp)):
public.ExecShell(
"mkdir -p /www/backup/backup_restore/{timestamp}_backup/database".format(timestamp=timestamp))
public.ExecShell(
"\cp -rpa {db_model_path} /www/backup/backup_restore/{timestamp}_backup/database/db_model.json".format(
db_model_path=db_model_path, timestamp=timestamp))
db_model_info = json.loads(public.readFile(db_model_path))
db_list = []
for db_path, db_info in db_model_info.items():
if os.path.exists(db_path):
backup_dir = "/www/backup/backup_restore/{timestamp}_backup/database/sqlite".format(
timestamp=timestamp)
if not os.path.exists(backup_dir):
public.ExecShell("mkdir -p {backup_dir}".format(backup_dir=backup_dir))
random_suffix = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
new_db_name = db_info['name'] + '_' + random_suffix
db_list.append({
"db_path": db_path,
"db_name": db_info['name'],
"new_db_name": new_db_name
})
backup_file_path = "{backup_dir}/{db_name}".format(backup_dir=backup_dir, db_name=new_db_name)
public.ExecShell("\cp -rpa {db_path} {backup_file_path}".format(db_path=db_path,
backup_file_path=backup_file_path))
public.WriteFile("/www/backup/backup_restore/{timestamp}_backup/database/sqlite/db_list.json".format(
timestamp=timestamp), json.dumps(db_list))
def restore_sqlite_data(self, timestamp):
self.print_log("==================================", "restore")
self.print_log(public.lang("Start restoring SQLite database list"), "restore")
db_model_path = "/www/server/panel/data/db_model.json"
backup_db_model_path = "/www/backup/backup_restore/{timestamp}_backup/database/db_model.json".format(
timestamp=timestamp)
backup_sqlite_dir = "/www/backup/backup_restore/{timestamp}_backup/database/sqlite".format(timestamp=timestamp)
backup_sqlite_info_path = "/www/backup/backup_restore/{timestamp}_backup/database/sqlite/db_list.json".format(
timestamp=timestamp)
if not os.path.exists(backup_db_model_path) or not os.path.exists(backup_sqlite_dir):
self.print_log(public.lang("SQLite database list backup file does not exist, skipping restoration"),
"restore")
return True
db_model_info = json.loads(public.readFile(backup_sqlite_info_path))
for db_info in db_model_info:
db_name = db_info['db_name']
new_db_name = db_info['new_db_name']
db_path = db_info['db_path']
if os.path.exists(db_path):
continue
db_dir = os.path.dirname(db_path)
if not os.path.exists(db_dir):
public.ExecShell("mkdir -p {}".format(db_dir))
public.ExecShell(
"\cp -rpa {backup_sqlite_dir}/{new_db_name} {db_path}".format(backup_sqlite_dir=backup_sqlite_dir,
new_db_name=new_db_name, db_path=db_path))
cp_cmd = "\cp -rpa {backup_db_model_path} {db_model_path}".format(backup_db_model_path=backup_db_model_path,
db_model_path=db_model_path)
public.ExecShell(cp_cmd)
self.print_log(public.lang("SQLite database list restoration completed"), "restore")
return True
# ======================== sqlite ==========================================
def backup_mysql_data(self, db_name: str, timestamp: int):
try:
db_port = int(panelMysql.panelMysql().query("show global variables like 'port'")[0][1])
except:
db_port = 3306
db_charset = public.get_database_character(db_name)
set_gtid_purged = ""
resp = public.ExecShell("{} --help | grep set-gtid-purged".format(self._MYSQLDUMP_BIN))[0]
if resp.find("--set-gtid-purged") != -1:
set_gtid_purged = "--set-gtid-purged=OFF"
db_user = "root"
db_password = public.M("config").where("id=?", (1,)).getField("mysql_root")
db_host = "localhost"
backup_path = "/www/backup/backup_restore/{timestamp}_backup/database/mysql".format(timestamp=timestamp)
if not os.path.exists(backup_path):
os.makedirs(backup_path, exist_ok=True)
sql_file = backup_path + "/{}.sql".format(db_name)
# 导出结构和数据
shell = "'{mysqldump_bin}' {set_gtid_purged} --opt --skip-lock-tables --single-transaction --routines --events --skip-triggers --default-character-set='{db_charset}' --force " \
"--host='{db_host}' --port={db_port} --user='{db_user}' --password='{db_password}' '{db_name}'".format(
mysqldump_bin=self._MYSQLDUMP_BIN,
set_gtid_purged=set_gtid_purged,
db_charset=db_charset,
db_host=db_host,
db_port=db_port,
db_user=db_user,
db_password=db_password,
db_name=db_name,
)
shell += " > '{export_sql_file}' ".format(export_sql_file=sql_file)
public.ExecShell(shell, env={"MYSQL_PWD": db_password})
return {"status": True, "msg": sql_file}
def backup_mongodb_data(self, db_name, timestamp: int):
"""
备份 MongoDB 数据库(仅本地)
Args:
db_name (str): 数据库名称
timestamp (int): 时间戳
Returns:
dict: 备份结果状态
"""
# 检查备份工具是否存在
if not os.path.exists(self._MONGODBDUMP_BIN):
return {"status": False,
"msg": public.lang("Lack of backup tools, please install MongoDB via Software Manager first!")}
if not os.path.exists(self._MONGOEXPORT_BIN):
return {"status": False,
"msg": public.lang("Lack of backup tools, please install MongoDB via Software Manager first!")}
# 查询数据库信息
db_find = public.M("databases").where("name=? AND LOWER(type)=LOWER('mongodb')", (db_name,)).find()
if not db_find:
return {"status": False, "msg": public.lang(f"Database not found! {db_name}")}
if not public.process_exists("mongod"):
return {"status": False, "msg": public.lang("Mongodb service is not running!")}
# 设置基本备份参数
db_user = db_find.get("username", "")
db_password = db_find.get("password", "")
file_type = "bson" # 默认使用bson格式备份
db_host = "127.0.0.1"
db_port = panelMongoDB.panelMongoDB().get_config_options("net", "port", 27017)
# 设置备份路径
backup_path = f"/www/backup/backup_restore/{timestamp}_backup/database/mongodb"
if not os.path.exists(backup_path):
os.makedirs(backup_path, exist_ok=True)
file_name = f"{db_name}_{file_type}_{time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime())}_mongodb_data"
export_dir = os.path.join(backup_path, file_name)
# 构建备份命令
mongodump_shell = f"'{self._MONGODBDUMP_BIN}' --host='{db_host}' --port={int(db_port)} --db='{db_name}' --out='{export_dir}'"
auth_status = panelMongoDB.panelMongoDB().get_config_options("security", "authorization",
"disabled") == "enabled"
if auth_status and not os.path.exists(f"{export_dir}/auth.pl"):
public.ExecShell(f"echo True > {export_dir}/auth.pl")
# 如果需要认证,添加用户名和密码
if auth_status and db_password:
mongodump_shell += f" --username='{db_user}' --password='{db_password}'"
# 执行备份命令
public.ExecShell(mongodump_shell)
# 检查备份是否成功
if not os.path.exists(export_dir):
return {"status": False, "msg": public.lang("Database backup failed, export directory does not exist!")}
# 压缩备份文件
backup_file = f"{export_dir}.zip"
public.ExecShell(f"cd {backup_path} && zip -m {backup_file} -r {file_name}")
if not os.path.exists(backup_file):
public.ExecShell(f"rm -rf {export_dir}")
return {"status": False, "msg": public.lang("Backup compression failed!")}
# 记录备份信息
return {"status": True, "msg": backup_file}
def backup_pgsql_data(self, db_name: str, timestamp: int):
"""
备份PostgreSQL数据库(仅本地)
Args:
db_name (str): 数据库名称
timestamp (int): 时间戳,用于创建备份目录
Returns:
dict: 备份结果状态
"""
# 检查备份工具是否存在
if not os.path.exists(self._PGDUMP_BIN):
return {"status": False,
"msg": public.lang("Lack of backup tool, please install pgsql manager via software store first!")}
# 查询数据库信息
db_find = public.M("databases").where("name=? AND LOWER(type)=LOWER('pgsql')", (db_name,)).find()
if not db_find:
return {"status": False, "msg": public.lang(f"Database does not exist! {db_name}")}
# Set basic backup parameters
db_user = "postgres"
db_host = "127.0.0.1"
db_port = 5432
# Get PostgreSQL password
try:
t_path = os.path.join(public.get_panel_path(), "data/postgresAS.json")
if not os.path.isfile(t_path):
return {"status": False, "msg": public.lang("Please set administrator password first!")}
admin_info = json.loads(public.readFile(t_path))
db_password = admin_info.get("password", "")
if not db_password:
return {"status": False,
"msg": public.lang("Database password is empty! Please set database password first!")}
except Exception as e:
return {"status": False, "msg": public.lang(f"Failed to get PostgreSQL password: {str(e)}")}
# 设置备份路径
backup_path = f"/www/backup/backup_restore/{timestamp}_backup/database/pgsql"
if not os.path.exists(backup_path):
os.makedirs(backup_path, exist_ok=True)
# 构建备份文件名
file_name = f"{db_name}_{time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime())}_pgsql_data.sql.gz"
backup_file = os.path.join(backup_path, file_name)
# 构建备份命令
shell = f"'{self._PGDUMP_BIN}' --host='{db_host}' --port={int(db_port)} --username='{db_user}' --dbname='{db_name}' --clean | gzip > '{backup_file}'"
# 执行备份命令
public.ExecShell(shell, env={"PGPASSWORD": db_password})
# 检查备份是否成功
if not os.path.exists(backup_file):
return {"status": False, "msg": public.lang("Database backup failed, export file does not exist!")}
# 写入日志
public.WriteLog("TYPE_DATABASE", "DATABASE_BACKUP_SUCCESS", (db_name,))
return {"status": True, "msg": backup_file}
def _sync_database(self, db_client: object):
bin_path = None
name = "unknown"
try:
if hasattr(db_client, "_MYSQL_BIN"):
bin_path = "/www/server/mysql/bin/mysqld"
name = "MySQL"
elif hasattr(db_client, "_MONGODBDUMP_BIN"):
bin_path = "/www/server/mongodb/bin/mongod"
name = "MongoDB"
elif hasattr(db_client, "_PSQL_BIN"):
bin_path = "/www/server/pgsql/bin/pg_config"
name = "PostgreSQL"
if not bin_path or not os.path.exists(bin_path) or not name:
return
log = public.lang(f"{name} synchronization...")
self.print_log(log, "restore")
# noinspection PyUnresolvedReferences
db_client.SyncToDatabases(public.to_dict_obj({"type": 0, "ids": "[]"}))
self.replace_log(log, public.lang(f"{name} synchronization completed!"), "restore")
except Exception as e:
public.print_log("{} sync database func error: {}".format(name, str(e)))
def _init_mysql_root(self):
try:
root_pwd = None
if self.overwrite: # table config has overwrited this time
root_pwd = public.M("config").where("id=?", (1,)).getField("mysql_root")
if not os.path.exists("/www/server/panel/data/remysql_root.pl"):
root_pwd = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
if root_pwd:
init_root_cmd = "btpython /www/server/panel/tools.py root {}".format(root_pwd)
public.ExecShell(init_root_cmd)
public.ExecShell("echo 'True' > /www/server/panel/data/remysql_root.pl")
self.print_log(public.lang("MySQL initialization successful!"), "restore")
self.print_log(public.lang("MySQL will restart, please wait..."), "restore")
time.sleep(5)
public.ExecShell("/etc/init.d/mysqld restart")
time.sleep(7)
except:
pass
def _init_mg_root(self, timestamp: int):
if not self.overwrite:
return
try:
auth = os.path.join(self.base_path, f"{timestamp}_backup/database/mongodb/auth.pl")
body = {"status": 1} if auth else {"status": 0}
set_root = panelMongoDB.main().set_auth_status(public.to_dict_obj(body))
time.sleep(1)
if set_root.get("status") == 0:
self.print_log(public.lang("MongoDB initialization successful!"), "restore")
else:
self.print_log(public.lang(f"MongoDB set auth failed!"), "restore")
except Exception as e:
self.print_log(public.lang("MongoDB set auth failed! error: {}".format(e)), "restore")
def _init_pg_root(self):
try:
current_root = panelPgsql.main().get_root_pwd(public.dict_obj())["message"].get("result")
except:
current_root = None
pwd_root = current_root if current_root else public.GetRandomString(16)
set_root = panelPgsql.main().set_root_pwd(
public.to_dict_obj({"password": pwd_root})
)
time.sleep(1)
if set_root.get("status") == 0:
self.print_log(public.lang("PostgreSQL initialization successful!"), "restore")
else:
self.print_log(public.lang(f"PostgreSQL set root failed!"), "restore")
def _init_redis(self):
config = public.readFile("/www/server/redis/redis.conf")
if config and "protected-mode yes" in config and not re.search(
r"# Redis configuration file example.\nrequirepass (\S+)", config
):
config = config.replace(
"# Redis configuration file example.\n",
f"# Redis configuration file example.\nrequirepass {public.GetRandomString(16)}\n",
)
public.writeFile("/www/server/redis/redis.conf", config)
self.print_log(public.lang("Redis initialization successful!"), "restore")
public.ExecShell("/etc/init.d/redis restart")
time.sleep(1)
def _before_restore(self, timestamp: int):
# init root
self._init_mysql_root()
self._init_mg_root(timestamp)
self._init_pg_root()
self._init_redis()
# sync to database
self._sync_database(database.database())
self._sync_database(panelMongoDB.main())
self._sync_database(panelPgsql.main())
def restore_database_data(self, timestamp):
self.print_log("==================================", "restore")
self.print_log(public.lang("Start restoring database"), "restore")
restore_data = self.get_restore_data_list(timestamp)
database_data = restore_data['data_list']['database']
with app.app_context():
# === before restore ===
self._before_restore(timestamp)
self.print_log(public.lang("Start restoring database's data..."), "restore")
for db_data in database_data:
log_str = public.lang("Restoring {} database {}").format(db_data['type'], db_data['name'])
result = None
self.print_log(log_str, "restore")
db_data['restore_status'] = 1
self.update_restore_data_list(timestamp, restore_data)
try:
# server local db
if db_data['sid'] == 0:
# ==================== MySQL ====================
if db_data['type'] == 'MySQL':
self.restore_mysql_info(db_data)
result = self.input_mysql_sql(db_data)
# ==================== MongoDB ====================
elif db_data['type'] == 'MongoDB':
self.restore_mongodb_info(db_data)
result = self.input_mongodb_data(db_data)
# ==================== PostgreSQL ====================
elif db_data['type'] == 'pgsql':
self.restore_pgsql_info(db_data)
result = self.input_pgsql_data(db_data)
# ==================== Redis ====================
elif db_data['type'] == 'redis':
result = self.restore_redis_data(db_data)
else: # remote db
self.restore_remote_database(db_data)
result = {'status': True, 'msg': ''} if not result else result
if result['status'] is True:
db_data['restore_status'] = 2
self.update_restore_data_list(timestamp, restore_data)
new_log_str = public.lang("{} database {} ✓").format(db_data['type'], db_data['name'])
self.replace_log(log_str, new_log_str, "restore")
else:
db_data['restore_status'] = 3
db_data['msg'] = result['msg']
new_log_str = public.lang("{} database {} ✗ ({})").format(db_data['type'], db_data['name'],
result['msg'])
self.replace_log(log_str, new_log_str, 'restore')
except Exception as e:
err_msg = public.lang("Failed to restore {} database {} Reason: {}").format(db_data['type'],
db_data['name'], str(e))
self.replace_log(log_str, err_msg, 'restore')
db_data['restore_status'] = 3
db_data['msg'] = err_msg
self.update_restore_data_list(timestamp, restore_data)
continue
self.print_log(public.lang("Database data restoration completed"), "restore")
# ==================== remote db =================
remote_db_list = restore_data['data_list'].get('remote_db_list')
if remote_db_list is not None:
self.print_log("==================================", "restore")
self.print_log(public.lang("Start restoring remote database"), "restore")
self.resotre_remote_db_server(restore_data['data_list']['remote_db_list'])
self.print_log(public.lang("Remote database restoration completed"), "restore")
def _get_mysql_db_access(self, db_data) -> tuple[str, str]:
real_access = db_data.get("real_access", "127.0.0.1")
if real_access == "%":
dataAccess = "%"
elif real_access not in ["%", "127.0.0.1"]:
dataAccess = "ip"
else:
dataAccess = "127.0.0.1"
return dataAccess, real_access
def _reset_password(self, db_client, id: int, name: str, password: str) -> dict:
res = db_client.ResDatabasePassword(public.to_dict_obj({
"id": id,
"name": name,
"password": password,
}))
return res
def _before_input_sql(self, db_client, db_data: dict, db_type: str) -> dict:
# 插入前备份
db_id = public.M('databases').where(
"name=? AND LOWER(type)=?", (db_data['name'], db_type)
).getField('id')
if not db_id:
self.print_log(public.lang("{} backup id does not exist, import terminated").format(db_data['name']),
"restore")
return {"status": False,
"msg": public.lang("{} database id does not exist, data recovery terminated").format(db_type)}
back_up = db_client.ToBackup(public.to_dict_obj({"id": db_id}))
if back_up.get('status') != 0:
return {"status": False, "msg": back_up.get('message', '')}
return {"status": True, "msg": "success"}
def restore_mysql_info(self, db_data) -> dict:
dataAccess, real_access = self._get_mysql_db_access(db_data)
if_exist = public.M('databases').where(
"name=? AND LOWER(type)=LOWER('mysql')", (db_data["name"],)
).find()
if if_exist:
self.mysql_table_data_exist = True
# always fix pid
public.M("databases").where("id=?", (if_exist['id'],)).update({
"pid": self._get_current_pid_by_site_name(
db_data.get("related_site", "")
)
})
if not self.overwrite:
return {"status": True, "msg": public.lang("MySQL database {} already exists").format(db_data['name'])}
# default.db is overwrited
# mysql real access
set_res = database.database().SetDatabaseAccess(public.to_dict_obj({
"dataAccess": dataAccess,
"access": real_access,
"name": db_data["username"],
"ssl": "", # todo ssl 计划移除
}))
if set_res.get("status") != 0:
self.print_log(
public.lang(
"Failed to restore {} database access permissions. Reason: {}"
).format(db_data['name'], set_res['message']),
"restore"
)
return set_res
else: # no exitst
self.mysql_table_data_exist = False
args = public.dict_obj()
args.pid = self._get_current_pid_by_site_name(db_data.get("related_site", ""))
args.name = db_data["name"]
args.db_user = db_data["username"]
args.password = db_data["password"]
args.dataAccess = dataAccess # restore access
args.address = real_access # restore access
args.codeing = "utf8mb4"
args.dtype = "MySQL"
args.ps = db_data["ps"]
args.sid = "0"
args.listen_ip = "0.0.0.0/0"
args.active = False
res = database.database().AddDatabase(args)
if res['status'] != 0:
self.print_log(
public.lang("Failed to create {} database. Reason: {}").format(db_data['name'], res['message']),
"restore")
return res
def input_mysql_sql(self, db_data):
db_host = "localhost"
db_user = "root"
try:
db_port = int(panelMysql.panelMysql().query("show global variables like 'port'")[0][1])
except:
db_port = 3306
if self.mysql_table_data_exist and not self.overwrite:
return {"status": True, "msg": public.lang("Database already exists, skipping SQL restoration")}
# 备份数据库
back_up = self._before_input_sql(
db_client=database.database(),
db_data=db_data,
db_type='mysql'
)
if not back_up.get('status'):
return back_up
# force dump
db_password = public.M("config").where("id=?", (1,)).getField("mysql_root")
db_name = db_data['name']
db_charset = public.get_database_character(db_name)
shell = "'{mysql_bin}' --force --default-character-set='{db_charset}' --host='{db_host}' --port={db_port} --user='{db_user}' --password='{password}' '{db_name}'".format(
mysql_bin=self._MYSQL_BIN,
db_charset=db_charset,
db_host=db_host,
db_port=db_port,
db_user=db_user,
password=db_password,
db_name=db_name,
)
sql_path = db_data['sql_file_name']
output, error = public.ExecShell(
"{shell} < '{path}'".format(shell=shell, path=sql_path), env={"MYSQL_PWD": db_password}
)
if "error:" in output.lower() or "error:" in error.lower():
return {"status": False, "msg": output + error}
else:
return {"status": True, "msg": public.lang("Import successful")}
def restore_mongodb_info(self, db_data):
if_exist = public.M('databases').where(
"name=? AND LOWER(type)=LOWER('mongodb')", (db_data["name"],)
).find()
if if_exist:
self.mg_table_data_exist = True
if not self.overwrite:
return {
"status": True, "msg": public.lang("MongoDB database {} already exists").format(db_data['name'])
}
return {"status": True, "msg": public.lang(f"MongoDB database {db_data['name']} successfully")}
else:
self.mg_table_data_exist = False
args = public.dict_obj()
args.name = db_data['name']
args.db_user = db_data['username']
args.password = db_data['password']
args.ps = db_data['ps']
args.sid = 0
# args.dataAccess = db_data['accept']
# args.address = ""
# args.codeing = "utf8mb4"
# args.dtype = "MongoDb"
# args.listen_ip = "0.0.0.0/0"
# args.host = ""
res = panelMongoDB.main().AddDatabase(args)
if res['status'] is False:
return {"status": False, "msg": res['msg']}
else:
return {"status": True, "msg": public.lang("Creation successful")}
def input_mongodb_data(self, db_data):
if self.mg_table_data_exist and not self.overwrite:
return {"status": True, "msg": public.lang("Database already exists, skipping data restoration")}
back_up = self._before_input_sql(
db_client=panelMongoDB.main(),
db_data=db_data,
db_type='mongodb'
)
if not back_up.get('status'):
return back_up
db_name = db_data['name']
sql_path = db_data['sql_file_name']
args = public.dict_obj()
args.file = sql_path
args.name = db_name
res = panelMongoDB.main().InputSql(args)
if res['status'] is False:
return {"status": False, "msg": res['msg']}
else:
return {"status": True, "msg": public.lang("Import successful")}
def restore_pgsql_info(self, db_data):
if_exist = public.M('databases').where(
"name=? AND LOWER(type)=LOWER('pgsql')", (db_data["name"],)
).find()
if if_exist:
self.pgsql_table_data_exist = True
if not self.overwrite:
return {"status": True,
"msg": public.lang("PostgreSQL database {} already exists").format(db_data['name'])}
return {
"status": True, "msg": public.lang(f"PostgreSQL database {db_data['name']} successfully")
}
else:
self.pgsql_table_data_exist = False
args = public.dict_obj()
args.name = db_data['name']
args.db_user = db_data['username']
args.password = db_data['password']
args.ps = db_data['ps']
args.sid = 0
# todo pgsql listener ip 依赖前端传递, 默认本地
# args.listen_ip = "0.0.0.0/0"
args.host = ""
res = panelPgsql.main().AddDatabase(args)
if res['status'] is False:
return {"status": False, "msg": res['msg']}
else:
return {"status": True, "msg": public.lang("Creation successful")}
def restore_pgsql_root_pwd(self, pgsql_root_pwd):
args = public.dict_obj()
args.password = pgsql_root_pwd
panelPgsql.main().set_root_pwd(args)
def input_pgsql_data(self, db_data):
"""还原PostgreSQL数据库
@param db_data: dict 数据库信息
"""
try:
if self.pgsql_table_data_exist and not self.overwrite:
return {"status": True, "msg": public.lang("Database already exists, skipping data restoration")}
# 备份数据库
back_up = self._before_input_sql(
db_client=panelPgsql.main(),
db_data=db_data,
db_type='pgsql'
)
if not back_up.get('status'):
return back_up
if not os.path.exists('/www/server/pgsql/bin/psql'):
return {"status": False, "msg": public.lang(
"Lack of restoration tools, please install pgsql via software manager first!")}
db_name = db_data['name']
sql_gz_file = db_data['sql_file_name']
if os.path.exists(sql_gz_file):
public.ExecShell("gunzip {sql_file}".format(sql_file=sql_gz_file))
sql_file = sql_gz_file.replace(".gz", "")
if not os.path.exists(sql_file):
return {"status": False, "msg": public.lang("Backup file does not exist!")}
# 获取本地PostgreSQL的配置信息
t_path = os.path.join('/www/server/panel/data/postgresAS.json')
if not os.path.isfile(t_path):
characters = string.ascii_lowercase + string.digits
pgsql_root_pwd = ''.join(random.choice(characters) for _ in range(16))
self.restore_pgsql_root_pwd(pgsql_root_pwd)
db_port = panelPgsql.main().get_port(None)["data"]
db_password = json.loads(public.readFile(t_path)).get("password", "")
# 构建psql命令
shell = "'/www/server/pgsql/bin/psql' --host='127.0.0.1' --port={} --username='postgres' --dbname='{}'".format(
int(db_port),
db_name
)
# 执行还原命令
result = public.ExecShell("{} < '{}'".format(shell, sql_file), env={"PGPASSWORD": db_password})
# if "error:" in result[0].lower() or "error:" in result[1].lower():
# return {"status": False, "msg": result[0] + result[1]}
return {"status": True, "msg": public.lang("Restoration successful")}
except Exception as e:
return {"status": False, "msg": str(e)}
def restore_redis_data(self, db_data):
try:
rdb_file = db_data['sql_file_name']
if os.path.exists(rdb_file):
if os.path.exists("/www/server/redis/dump.rdb"):
public.ExecShell("/etc/init.d/redis stop")
time.sleep(1)
public.ExecShell("rm -f /www/server/redis/dump.rdb.bak")
public.ExecShell("mv /www/server/redis/dump.rdb /www/server/redis/dump.rdb.bak")
public.ExecShell("\cp -pra {rdb_file} /www/server/redis/dump.rdb".format(rdb_file=rdb_file))
public.ExecShell("chown -R redis:redis /www/server/redis")
public.ExecShell("chmod 644 /www/server/redis/dump.rdb")
time.sleep(1)
public.ExecShell("/etc/init.d/redis start")
time.sleep(1)
public.ExecShell("/etc/init.d/redis stop")
public.ExecShell("rm -f /www/server/redis/dump.rdb.bak")
public.ExecShell("mv /www/server/redis/dump.rdb /www/server/redis/dump.rdb.bak")
public.ExecShell("\cp -pra {rdb_file} /www/server/redis/dump.rdb".format(rdb_file=rdb_file))
public.ExecShell("chown -R redis:redis /www/server/redis")
public.ExecShell("chmod 644 /www/server/redis/dump.rdb")
public.ExecShell("/etc/init.d/redis start")
return {"status": True, "msg": public.lang("Restoration successful")}
except Exception as e:
return {"status": False, "msg": str(e)}
def fix_wp_onekey(self, timestamp: int):
restore_data = self.get_restore_data_list(timestamp)
site_backup_path = self.base_path + f"/{timestamp}_backup/site/"
if not os.path.exists(site_backup_path):
return
for site in restore_data['data_list'].get('site', []):
if site.get('project_type').lower() not in ['wp2', 'wp']:
continue
if not site.get('wp_onekey'):
continue
# wp_onekey = {"prefix": "wp_", "user", "wp_user", "pass": "wp_password"}
wp_onekey: dict = site['wp_onekey']
s_id = self._get_current_pid_by_site_name(site['name'])
if not s_id:
continue
d_id = public.M('databases').where(
'(pid=? AND LOWER(type)=LOWER("mysql")) OR ps=?', (s_id, site['name'])
).getField('id')
if not d_id:
continue
try:
if_exist = public.M('wordpress_onekey').where('prefix=?', (wp_onekey['prefix'],)).find()
if if_exist:
public.M('wordpress_onekey').where('prefix=?', (wp_onekey['prefix'],)).update({
's_id': int(s_id),
'd_id': int(d_id),
})
else:
public.M('wordpress_onekey').insert({
**wp_onekey,
's_id': int(s_id),
'd_id': int(d_id),
})
except Exception as e:
public.print_log("fix forign key error: {}".format(str(e)))
continue
if __name__ == '__main__':
# 获取命令行参数
if len(sys.argv) < 3:
print("Usage: btpython backup_manager.py <method> <timestamp>")
sys.exit(1)
method_name = sys.argv[1] # 方法名
timestamp = sys.argv[2] # IP地址
database_module = DatabaseModule() # 实例化对象
if hasattr(database_module, method_name): # 检查方法是否存在
method = getattr(database_module, method_name) # 获取方法
method(timestamp) # 调用方法
else:
print(f"Error: method '{method_name}' not found")