Files
aaPanel/class_v2/panel_restore_v2.py
2026-07-06 09:16:25 +08:00

866 lines
41 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding: utf-8
# -------------------------------------------------------------------
# aaPanel
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 aaPanel(www.aapanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: zhwwen <zhw@aapanel.com>
# -------------------------------------------------------------------
#
# ------------------------------
# 网站恢复
# ------------------------------
import public,os,files,sys, json, shlex, shutil, time, ftplib
from time import sleep
from public.validate import Param
class panel_restore:
_local_file = '/tmp/{}'
_progress_file = '/tmp/restore_site.log'
_setup_path = '/www/server'
# def __init__(self):
# # 清空日志文件
def _progress_rewrite(self,content,mothed='a+'):
sleep(2)
public.writeFile(self._progress_file,content+'\n',mothed)
def _get_local_backup_path(self):
local_backdir = public.M('config').field('backup_path').find()['backup_path']
return local_backdir
def _build_aws_backup_path(self,btype,file_name,domain):
config_file = "/www/server/panel/plugin/aws_s3/config.conf"
conf = public.readFile(config_file)
backup_path = conf.split('|')[-2].strip()+'/'+btype+'/'+ domain + '/' + file_name
return backup_path
def _build_google_backup_path(self,btype,file_name,domain):
object_name = 'bt_backup/{}/{}/{}'.format(btype,domain,file_name)
return object_name
def _get_backfile_method(self,filename):
backup_info = public.M('backup').where("name=?", (filename,)).getField('filename')
backup_info = backup_info.split('|')
if len(backup_info) >= 3:
method = backup_info[1]
else:
method = 'local'
return method
def _remove_old_website_file_to_trush(self,args):
# 将原来目录移至回收站
files.files().DeleteDir(args)
def _get_website_info(self,site_id):
site_name = public.M('sites').where("id=?",(site_id,)).getField('name')
site_path = public.M('sites').where("id=?",(site_id,)).getField('path')
return {'site_name':site_name,'site_path':site_path}
def _restore_backup(self, local_backup_file_path, site_info, args):
# 判断备份文件是否存在,如果不存在继续检查是否远程备份
if not os.path.exists(local_backup_file_path):
self._progress_rewrite('No backup file found: {}'.format(str(local_backup_file_path)))
return public.return_message(-1, 0, public.lang("Panel does not find the backup file: {}",local_backup_file_path))
# 区分全量备份与文件备份
bak_type = public.M('backup').where("name=? and pid=?", (args.file_name,args.site_id)).getField('backup_type')
if bak_type and bak_type== 1:
self._progress_rewrite('Detected full backup archive, switching to full restore mode...')
return self._restore_full_backup(local_backup_file_path, site_info, args)
# 将网站目录移至回收站
self._progress_rewrite('Move the current website directory to the recycle bin: {}'.format(str(args.path)))
self._remove_old_website_file_to_trush(args)
if not os.path.exists(args.path):
self._progress_rewrite('Create an empty directory for the site: {}'.format(str(args.path)))
os.makedirs(site_info['site_path'])
if 'zip' in args.file_name:
uncompress_comand = 'unzip'
else:
uncompress_comand = 'tar -zxvf'
self._progress_rewrite('The decompression command is: {}'.format(str(uncompress_comand)))
self._progress_rewrite('Start to restore data......')
public.ExecShell('cd {} && {} {} >> /tmp/restore_site.log'.format(site_info['site_path'], uncompress_comand, local_backup_file_path))
if len(os.listdir(site_info['site_path'])) == 2:
public.ExecShell('cd {s} && mv {s}/{d}/{{*,.*}} .'.format(s=site_info['site_path'],d=site_info['site_name']))
public.ExecShell('cd {s} && rmdir {d}'.format(s=site_info['site_path'],d=site_info['site_name']))
# 将文件全新设置为644,文件夹设置为755
self._progress_rewrite('Setting site permissions......')
files.files().fix_permissions(args)
public.write_log_gettext('Site manager', f'Successfully restored the website [{site_info['site_name']}], Restored file: [{local_backup_file_path}]')
def _download_aws_file(self,args,btype='site'):
sys.path.append('/www/server/panel/plugin/aws_s3')
import aws_s3_main
aws3 = aws_s3_main.aws_s3_main()
self._progress_rewrite('Building S3 download path...')
download_file = self._build_aws_backup_path(btype,args.file_name,args.obj_name)
self._progress_rewrite('The download path is:{}'.format(download_file))
self._local_file = self._local_file.format(args.file_name)
self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file))
self._progress_rewrite('Starting to download file:{}'.format(self._local_file))
if download_file[0] != '/':
download_file = '/' + download_file
args.object_name = download_file
args.local_file = self._local_file
aws3.download_file(args)
self._progress_rewrite('Download completed:{}'.format(self._local_file))
return self._local_file
def _download_google_cloud_file(self,args,btype='site'):
sys.path.append('/www/server/panel/plugin/gcloud_storage')
import gcloud_storage_main
gs = gcloud_storage_main.gcloud_storage_main()
self._progress_rewrite('Building Google Store download path...')
download_file = self._build_google_backup_path(btype,args.file_name,args.obj_name)
self._progress_rewrite('The download path is:{}'.format(download_file))
self._local_file = self._local_file.format(args.file_name)
self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file))
self._progress_rewrite('Starting to download file:{}'.format(self._local_file))
args.source_blob_name = download_file
args.destination_file_name = self._local_file
gs.download_blob(args)
self._progress_rewrite('Download completed:{}'.format(self._local_file))
return self._local_file
def _download_google_drive_file(self,args):
sys.path.append('/www/server/panel/plugin/gdrive')
import gdrive_main
gd = gdrive_main.gdrive_main()
self._local_file = self._local_file.format(args.file_name)
self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file))
self._progress_rewrite('Starting to download file:{}'.format(self._local_file))
gd.download_file(args.file_name)
self._progress_rewrite('Download completed:{}'.format(self._local_file))
return self._local_file
def _download_ftp_file(self, args, btype='site'):
sys.path.append('/www/server/panel/plugin/ftp')
import ftp_main
ftp = ftp_main.ftp_main()
self._progress_rewrite('Building FTP download path...')
# 从备份表获取上传时存储的真实对象路径
file_path = ''
if btype == 'site':
file_path = public.M('backup').where("name=?",(args.file_name,)).getField('filename')
else:
file_path = public.M('backup').where("name=?",(args.file_name,)).getField('filename')
if not file_path:
return public.return_message(-1,0,public.lang('No file found!'))
file_list = file_path.split('|')
object_name = file_list[-1]
self._progress_rewrite('The download path is:{}'.format(object_name))
self._local_file = self._local_file.format(args.file_name)
self._progress_rewrite('Backup file will be downloaded to:{}'.format(self._local_file))
self._progress_rewrite('Starting to download file:{}'.format(self._local_file))
config = ftp.client.get_config()
host_port = config.get('ftp_host', '')
if ':' in host_port:
host, port = host_port.split(':', 1)
else:
host, port = host_port, '21'
user = config.get('ftp_user', '')
passwd = config.get('ftp_pass', '')
ftp_conn = ftplib.FTP()
try:
ftp_conn.connect(host, int(port))
ftp_conn.login(user, passwd)
ftp_conn.voidcmd('TYPE I')
remote_dir = '/'.join(object_name.split('/')[:-1])
remote_file = object_name.split('/')[-1]
if remote_dir:
ftp_conn.cwd(remote_dir)
with open(self._local_file, 'wb') as f:
ftp_conn.retrbinary('RETR ' + remote_file, f.write)
except:
return public.return_message(-1,0,public.lang('There was an error during the download of the backup package. Please check if the backup exists!'))
finally:
try:
ftp_conn.quit()
except Exception:
pass
self._progress_rewrite('Download completed:{}'.format(self._local_file))
return self._local_file
def restore_website_backup(self,args):
"""
@name 恢复站点文件
@author zhwen<zhw@aapanel.com>
@parma file_name 备份得文件名
@parma site_id 网站id
"""
# 校验参数
try:
args.validate([
Param('file_name').String(),
Param('site_id').Integer(),
], [
public.validate.trim_filter(),
])
except Exception as ex:
public.print_log("error info: {}".format(ex))
return public.return_message(-1, 0, str(ex))
self._progress_rewrite('','w')
site_info = self._get_website_info(args.site_id)
self._progress_rewrite('Get site information:{}'.format(str(site_info)))
args.path = site_info['site_path']
args.obj_name = site_info['site_name']
self._progress_rewrite('Get the site path:{}'.format(str(site_info['site_path'])))
local_backup_path = self._get_local_backup_path()
local_backup_file_path = local_backup_path +'/site/'+ args.file_name
if not os.path.exists(local_backup_file_path):
local_backup_file_path = local_backup_path +'/site/'+ site_info['site_name']+'/'+args.file_name
self._progress_rewrite('Get the local backup file path: {}'.format(str(local_backup_path)))
backup_method = self._get_backfile_method(args.file_name)
self._progress_rewrite('Get the backup method: {}'.format(str(backup_method)))
if backup_method == 'local':
self._progress_rewrite('Start to restore local backup files: {}'.format(str(local_backup_file_path)))
result = self._restore_backup(local_backup_file_path,site_info,args)
if result:
self._progress_rewrite('Recovery failed: {}'.format(str(site_info['site_path'])))
return result
elif backup_method == 'aws_s3':
self._download_aws_file(args)
result = self._restore_backup(self._local_file, site_info, args)
elif backup_method == 'Google Cloud' or backup_method == 'gcloud_storage':
self._download_google_cloud_file(args)
result = self._restore_backup(self._local_file, site_info, args)
elif backup_method == 'Google Drive' or backup_method == 'gdrive':
self._download_google_drive_file(args)
result = self._restore_backup(self._local_file, site_info, args)
elif backup_method == 'ftp':
self._download_ftp_file(args)
result = self._restore_backup(self._local_file, site_info, args)
else:
return public.return_msg_gettext(False,'Currently only supports restoring local, Google storage, AWS S3 and FTP backups')
if os.path.exists(self._local_file):
os.remove(self._local_file)
if result:
self._progress_rewrite('Recovery failed: {}'.format(str(site_info['site_path'])))
return result
self._progress_rewrite('Successful recovery: {}'.format(str(site_info['site_path'])))
return public.return_message(0, 0, public.lang("Restore Successful"))
# 取任务进度
def get_progress(self, get):
"""
@name 获取进度日志
@author zhwen<zhw@aapanel.com>
"""
# result = public.GetNumLines(self._progress_file, 20)
result = public.ExecShell('tail -n 20 {}'.format(self._progress_file))[0]
if len(result) < 1:
return public.return_message(0, 0, public.lang("Wait for the restore to start"))
return public.return_message(0,0,result)
# 恢复数据库
def restore_db_backup(self,args):
"""
@name 恢复站点文件
@author zhwen<zhw@aapanel.com>
@parma file_name 备份得文件名 /www/backup/database/db_test_com_20200817_112722.sql.gz|Google Drive|db_test_com_20200817_112722.sql.gz
@parma obj_name 数据库名
"""
if "|" not in args.file:
return public.returnMsg(True,'success')
try:
backup_info = args.file.split('|')
args.file_name = backup_info[-1]
args.obj_name = args.name
backup_method = backup_info[1]
self._progress_rewrite('','w')
self._progress_rewrite('Restoring database...')
self._progress_rewrite('Get the backup method: {}'.format(str(backup_method)))
if backup_method == 'aws_s3':
self._download_aws_file(args,'database')
elif backup_method == 'Google Cloud':
self._download_google_cloud_file(args,'database')
elif backup_method == 'Google Drive':
self._download_google_drive_file(args)
elif backup_method == 'ftp':
self._download_ftp_file(args,'database')
else:
return public.returnMsg(False,'Currently only supports restoring local, Google storage, AWS S3 and FTP backups')
public.ExecShell('mv {} {}/database'.format(self._local_file, self._get_local_backup_path()))
return public.returnMsg(True,'success')
except:
return public.returnMsg(False,"Download error!")
# ====================== PHP站点全量还原 ====================== #
def _restore_full_backup(self, local_backup_file_path: str, site_info: dict,
args, tmp_path: str = None) -> dict:
"""PHP全量备份还原主逻辑
解压 → meta.json → 文件 → 配置 → 数据库 → PHP版本 → 域名
@return: dict {site_name, site_id, site_path}
"""
tmp_path = f"/tmp/{str(time.time()).replace('.', '_')}"
os.makedirs(tmp_path)
try:
# 0. 初始化
try:
restore_type = json.loads(args.get('restore_type', []))
except:
restore_type = []
if not restore_type:
raise FileNotFoundError(public.lang('Error in the "restore_type" parameter transmission.'))
# 1. 解压备份文件到临时目录
self._progress_rewrite('Extracting full backup archive to temp directory...')
extract_cmd = 'tar -zxf "{}" -C "{}"'.format(local_backup_file_path, tmp_path)
public.ExecShell(extract_cmd)
public.ExecShell('chown -R www:www "{}"'.format(tmp_path))
public.ExecShell('chmod -R 755 "{}"'.format(tmp_path))
# 2. 读取 meta.json
meta_file = os.path.join(tmp_path, 'meta.json')
if not os.path.exists(meta_file):
raise FileNotFoundError(public.lang('meta.json not found in backup archive'))
with open(meta_file, 'r', encoding='utf-8') as fp:
meta = json.load(fp)
site_name = meta.get('site_name', '')
site_path = meta.get('site_path', '')
php_version = meta.get('php_version', '')
has_database = meta.get('has_database', False)
db_name = meta.get('db_name', '')
domain_list = meta.get('domain_list', [])
webserver = meta.get('webserver', '')
if not site_name:
raise ValueError(public.lang('site_name is empty in meta.json'))
# 查找目标站点
if site_info and site_info.get('site_name'):
target_name = site_info['site_name']
else:
target_name = site_name
find = public.M('sites').where("name=?", (target_name,)).field('id,name,path').find()
if not isinstance(find, dict) or not find.get('id'):
raise ValueError(public.lang('Site [{}] does not exist, please create it first').format(target_name))
site_id = find['id']
current_path = find.get('path', site_path)
# 3. 还原网站文件
if 'file' in restore_type:
self._progress_rewrite('Restoring site files to: {}'.format(current_path))
files_zip = os.path.join(tmp_path, 'files.tar.gz')
if os.path.exists(files_zip):
self._restore_site_files(current_path, files_zip)
# 4. 还原数据库
if has_database and db_name and 'db' in restore_type:
self._progress_rewrite('Restoring database: {}'.format(db_name))
db_file = os.path.join(tmp_path, 'database.sql.gz')
if os.path.exists(db_file):
self._restore_site_database(db_name, db_file, site_id, meta)
# 5. 还原配置文件
if 'conf' in restore_type:
self._progress_rewrite('Restoring site configs...')
configs_dir = os.path.join(tmp_path, 'configs')
if os.path.exists(configs_dir):
self._restore_site_configs(target_name, configs_dir, meta)
# 6. 恢复PHP版本
if php_version:
self._progress_rewrite('Restoring PHP version: {}'.format(php_version))
self._restore_php_version(target_name, php_version)
# 7. 恢复域名
if domain_list:
self._progress_rewrite('Restoring domains...')
self._restore_site_domains(site_id, target_name, domain_list)
# 8. 恢复Web服务状态
if webserver:
self._progress_rewrite('Restoring webservice status...')
self._restore_site_webserver(site_id, meta)
# 修复权限
self._progress_rewrite('Setting site permissions...')
if args and hasattr(args, 'path'):
files.files().fix_permissions(args)
self._progress_rewrite('Full backup restore completed for: {}'.format(target_name))
public.write_log_gettext('Site manager',
f'Successfully restored the website [{site_info['site_name']}], Restored file: [{local_backup_file_path}]')
except Exception as e:
self._progress_rewrite('Errors occurred during the restoration process.: {}'.format(str(e)))
return public.return_message(-1, 0, 'Errors occurred during the restoration process.: {}'.format(str(e)))
finally:
if os.path.exists(tmp_path):
shutil.rmtree(tmp_path, ignore_errors=True)
self._progress_rewrite('Successful recovery: {}'.format(str(site_info['site_path'])))
return public.return_message(0, 0, public.lang("Restore Successful"))
# 网站文件还原
def _restore_site_files(self, site_path: str, files_zip: str) -> None:
"""还原网站文件:备份当前 .user.ini 后解压覆盖"""
if not os.path.exists(site_path):
os.makedirs(site_path, 0o755)
# 备份当前 .user.ini(先解锁再操作)
user_ini = os.path.join(site_path, '.user.ini')
user_ini_bak = None
if os.path.exists(user_ini):
public.ExecShell('chattr -i ' + user_ini)
user_ini_bak = user_ini + '.restore_bak'
shutil.copy2(user_ini, user_ini_bak)
try:
if '.zip' in files_zip:
public.ExecShell(f'unzip -oq {files_zip} -d {site_path}')
else:
public.ExecShell(f'tar -zxf {files_zip} -C {site_path}')
finally:
# 还原user_ini
if user_ini_bak and os.path.exists(user_ini_bak):
if not os.path.exists(user_ini):
shutil.move(user_ini_bak, user_ini)
else:
if os.path.exists(user_ini_bak):
os.remove(user_ini_bak)
if os.path.exists(user_ini):
public.ExecShell('chattr +i ' + user_ini)
# 网站服务配置文件还原
def _restore_site_configs(self, site_name: str, configs_dir: str, mate: dict = None) -> None:
"""还原站点配置文件(与 _backup_site_configs 对称)"""
vhost = self._setup_path + '/panel/vhost'
# --- Nginx ---
nginx_src = os.path.join(configs_dir, 'nginx', '{}.conf'.format(site_name))
if os.path.exists(nginx_src):
nginx_dst = '{}/nginx/{}.conf'.format(vhost, site_name)
shutil.copy2(nginx_src, nginx_dst)
well_known_src = os.path.join(configs_dir, 'nginx', 'well-known', site_name)
if os.path.exists(well_known_src):
well_known_dst = '{}/nginx/well-known/{}'.format(vhost, site_name)
if os.path.exists(well_known_dst):
shutil.rmtree(well_known_dst, ignore_errors=True)
shutil.copytree(well_known_src, well_known_dst, dirs_exist_ok=True)
# --- Apache ---
apache_src = os.path.join(configs_dir, 'apache', '{}.conf'.format(site_name))
if os.path.exists(apache_src):
apache_dst = '{}/apache/{}.conf'.format(vhost, site_name)
shutil.copy2(apache_src, apache_dst)
# --- OpenLiteSpeed ---
ols_src = os.path.join(configs_dir, 'openlitespeed', '{}.conf'.format(site_name))
if os.path.exists(ols_src):
ols_dst = '{}/openlitespeed/{}.conf'.format(vhost, site_name)
shutil.copy2(ols_src, ols_dst)
ols_detail_src = os.path.join(configs_dir, 'openlitespeed', 'detail', '{}.conf'.format(site_name))
if os.path.exists(ols_detail_src):
ols_detail_dst = '{}/openlitespeed/detail/{}.conf'.format(vhost, site_name)
shutil.copy2(ols_detail_src, ols_detail_dst)
ols_ssl_src = os.path.join(configs_dir, 'openlitespeed', 'detail', 'ssl', '{}.conf'.format(site_name))
if os.path.exists(ols_ssl_src):
ols_ssl_dst = '{}/openlitespeed/detail/ssl/{}.conf'.format(vhost, site_name)
shutil.copy2(ols_ssl_src, ols_ssl_dst)
# --- Extension proxy dir_auth---
for server in ['nginx', 'apache']:
ext_src = os.path.join(configs_dir, server, 'extension', site_name)
if os.path.exists(ext_src):
ext_dst = '{}/{}/extension/{}'.format(vhost, server, site_name)
if os.path.exists(ext_dst):
shutil.rmtree(ext_dst, ignore_errors=True)
shutil.copytree(ext_src, ext_dst, dirs_exist_ok=True)
proxy_src = os.path.join(configs_dir, server, 'proxy', site_name)
if os.path.exists(proxy_src):
proxy_dst = '{}/{}/proxy/{}'.format(vhost, server, site_name)
if os.path.exists(proxy_dst):
shutil.rmtree(proxy_dst, ignore_errors=True)
shutil.copytree(proxy_src, proxy_dst, dirs_exist_ok=True)
dir_auth_src = os.path.join(configs_dir, server, 'dir_auth', site_name)
if os.path.exists(dir_auth_src):
dir_auth_dst = '{}/{}/dir_auth/{}'.format(vhost, server, site_name)
if os.path.exists(dir_auth_dst):
shutil.rmtree(dir_auth_dst, ignore_errors=True)
shutil.copytree(dir_auth_src, dir_auth_dst, dirs_exist_ok=True)
# --- Rewrite ---
rewrite_src = os.path.join(configs_dir, 'rewrite', '{}.conf'.format(site_name))
if os.path.exists(rewrite_src):
rewrite_dst = '{}/rewrite/{}.conf'.format(vhost, site_name)
shutil.copy2(rewrite_src, rewrite_dst)
rewrite_dir = os.path.join(configs_dir, 'rewrite')
if os.path.exists(rewrite_dir):
for f in os.listdir(rewrite_dir):
if f.endswith('.conf') and site_name in f:
shutil.copy2(os.path.join(rewrite_dir, f),
os.path.join(vhost, 'rewrite', f))
if mate.get('is_proxy'):
proxy = "/www/server/panel/data/proxyfile.json"
if os.path.exists(proxy):
try:
is_ = False
proxy_json = json.loads(public.readFile(proxy))
for idx, i in enumerate(proxy_json):
if i.get('sitename') == site_name:
proxy_json[idx] = mate.get('is_proxy')
is_ = True
break
if not is_:
proxy_json.append(mate.get('is_proxy'))
public.writeFile(proxy, json.dumps(proxy_json))
except:
pass
if mate.get('is_redirect'):
redirect = "/www/server/panel/data/redirect.conf"
if os.path.exists(redirect):
try:
is_ = False
redirect_json = json.loads(public.readFile(redirect))
for idx, i in enumerate(redirect_json):
if i.get('sitename') == site_name:
redirect_json[idx] = mate.get('is_redirect')
is_ = True
break
if not is_:
redirect_json.append(mate.get('is_redirect'))
public.writeFile(redirect, json.dumps(redirect_json))
except:
pass
# --- SSL/Cert ---
cert_src = os.path.join(configs_dir, 'cert', site_name)
if os.path.exists(cert_src):
cert_dst = os.path.join(vhost, 'cert', site_name)
if os.path.exists(cert_dst):
shutil.rmtree(cert_dst, ignore_errors=True)
shutil.copytree(cert_src, cert_dst, dirs_exist_ok=True)
# --- pass ---
pass_src = '/www/server/pass/{}'.format(site_name)
if os.path.exists(pass_src):
pass_dst = os.path.join(vhost, 'pass', site_name)
if os.path.exists(pass_dst):
shutil.rmtree(pass_dst, ignore_errors=True)
shutil.copytree(pass_src, pass_dst, dirs_exist_ok=True)
if mate.get('is_dir_auth'):
dir_auth = "/www/server/panel/data/site_dir_auth.json"
if os.path.exists(dir_auth):
try:
redirect_json = json.loads(public.readFile(dir_auth))
redirect_json[site_name] = mate.get('is_dir_auth')
public.writeFile(dir_auth, json.dumps(redirect_json))
except:
pass
# 数据库还原
def _restore_site_database(self, db_name: str, db_file: str, site_id: int = 0, meta: dict = None) -> None:
"""还原数据库:若不存在则创建本地数据库并绑定网站,通过 gunzip + mysql 导入"""
db_find = public.M('databases').where("name=?", (db_name,)).field('id,name,db_type,conn_config').find()
if not isinstance(db_find, dict) or not db_find.get('id'):
db_username = meta.get('db_username', '')
db_password = meta.get('db_password', '')
# 数据库不存在,创建本地数据库
self._progress_rewrite('Database [{}] not found, creating local database...'.format(db_name))
from database_v2 import database
db_obj = database()
get = public.to_dict_obj({
'name': db_name,
'db_user': db_username,
'codeing': 'utf8mb4',
'password': db_password,
'sid': 0,
'active': True,
'address': '%',
'ps': '',
'dtype': 'MySQL',
'pid': site_id,
})
result = db_obj.AddDatabase(get)
if result.get('status') != 0:
raise ValueError(public.lang('Failed to create database [{}]: {}').format(
db_name, result.get('message', '')))
# 确保绑定网站ID
new_db = public.M('databases').where("name=?", (db_name,)).field('id,pid').find()
if isinstance(new_db, dict) and new_db.get('id') and new_db.get('pid', 0) != site_id:
public.M('databases').where('id=?', (new_db['id'],)).update({'pid': site_id})
self._progress_rewrite('Database [{}] created and bound to site [{}]'.format(db_name, site_id))
# 重新查询
db_find = public.M('databases').where("name=?", (db_name,)).field('id,name,db_type,conn_config').find()
if not isinstance(db_find, dict) or not db_find.get('id'):
raise ValueError(public.lang('Database [{}] creation failed').format(db_name))
# 解压 .sql.gz → .sql
sql_file = db_file.replace('.sql.gz', '.sql')
public.ExecShell(f'gunzip -f {db_file}')
if not os.path.exists(sql_file):
raise FileNotFoundError(public.lang('Decompressed SQL file not found: {}').format(sql_file))
mysql_bin = public.get_mysql_bin()
if db_find['db_type'] in ['0', 0]:
# 本地数据库
mysql_root = public.M('config').where('id=?', (1,)).getField('mysql_root')
if not mysql_root:
raise ValueError(public.lang('MySQL root password is empty'))
from database_v2 import database
db_obj = database()
if not db_obj.mypass(True, mysql_root):
raise ValueError(public.lang('Failed to get MySQL root password'))
try:
os.environ["MYSQL_PWD"] = str(mysql_root)
public.ExecShell('{} -u root --default-character-set=utf8mb4 {} < {}'.format(
shlex.quote(mysql_bin),
shlex.quote(db_name),
shlex.quote(sql_file)
))
finally:
os.environ["MYSQL_PWD"] = ""
db_obj.mypass(False, mysql_root)
else:
# 远程数据库
conn_config = json.loads(db_find.get('conn_config', '{}'))
if not conn_config.get('db_host'):
raise ValueError(public.lang('Remote database connection config missing'))
try:
os.environ["MYSQL_PWD"] = str(conn_config.get('db_password', ''))
public.ExecShell('{} -h {} -P {} -u {} --default-character-set=utf8mb4 {} < {}'.format(
shlex.quote(mysql_bin),
shlex.quote(str(conn_config.get('db_host', ''))),
shlex.quote(str(conn_config.get('db_port', 3306))),
shlex.quote(str(conn_config.get('db_user', ''))),
shlex.quote(db_name),
shlex.quote(sql_file)
))
finally:
os.environ["MYSQL_PWD"] = ""
# PHP版本还原
def _restore_php_version(self, site_name: str, php_version: str) -> None:
"""恢复站点PHP版本(通过 panelSite.SetPHPVersion"""
try:
current_ver = public.get_site_php_version(site_name)
if current_ver != php_version and php_version not in ('', 'other', '00'):
from panel_site_v2 import panelSite
site_obj = panelSite()
get = public.to_dict_obj({'siteName': site_name, 'version': php_version, 'other': ''})
site_obj.SetPHPVersion(get)
except Exception as e:
self._progress_rewrite('Failed to switch PHP version: {}'.format(e))
# web服务状态还原
def _restore_site_webserver(self, site_id: int, meta: dict) -> None:
"""恢复站点Web服务状态(多Web服务场景下切换service_type"""
backup_webserver = meta.get('webserver', '')
backup_multi = meta.get('multi_webservice_status', False)
if not backup_webserver or backup_webserver not in ('nginx', 'apache', 'openlitespeed'):
return
current_multi = public.get_multi_webservice_status()
# 多服务已启用,但备份文件中为单服务配置
if current_multi and not backup_multi:
site = public.M('sites').where('id=?', (site_id,)).field('service_type').find()
current_type = (site.get('service_type') or 'nginx') if isinstance(site, dict) else 'nginx'
if current_type == backup_webserver:
return
self._progress_rewrite('Switching webservice from [{}] to [{}]...'.format(current_type, backup_webserver))
from panel_site_v2 import panelSite
site_obj = panelSite()
args = public.to_dict_obj({
'site_id': str(site_id),
'service_type': backup_webserver,
'is_reload': True,
})
try:
result = site_obj.switch_webservice(args)
if isinstance(result, dict) and result.get('status', 0) < 0:
self._progress_rewrite('Webservice switch warning: {}'.format(
result.get('message', 'Unknown error')))
except Exception as e:
self._progress_rewrite('Webservice restore failed: {}'.format(str(e)))
elif not current_multi and backup_multi:
# 当前多服务未开启,但备份是多服务下配置文件
site_name = public.M('sites').where('id=?', (site_id,)).getField('name')
site_path = public.M('sites').where('id=?', (site_id,)).getField('path')
current_global = public.get_webserver()
vhost = os.path.join(public.get_panel_path(), 'vhost')
from panel_site_v2 import panelSite
site_obj = panelSite()
# 切换nginx配置为当前全局webserver对应的格式
nginx_conf = os.path.join(vhost, 'nginx', site_name + '.conf')
if os.path.exists(nginx_conf):
self._progress_rewrite('Regenerating nginx config for webservice [{}]...'.format(current_global))
site_obj.nginx_update_config(current_global, nginx_conf, backup_webserver, site_name, site_path, site_id)
# Apache配置端口替换与反向代理IP
apache_conf = os.path.join(vhost, 'apache', site_name + '.conf')
if os.path.exists(apache_conf):
self._progress_rewrite('Fixing Apache config ports...')
self._fix_apache_config_ports(apache_conf)
# if current_global == 'apache':
# site_obj.set_apache_logs_ip(apache_conf)
# OLS配置端口替换
self._progress_rewrite('Fixing OLS config ports...')
self._fix_ols_config_ports(vhost, site_name)
backup_webserver = 'nginx'
public.M('sites').where('id=?', (site_id,)).update({'service_type': backup_webserver})
def _fix_apache_config_ports(self, conf_path: str) -> None:
"""修复Apache配置端口(代理模式:80→8288, 443→8290"""
import re
try:
content = public.readFile(conf_path)
if not content:
return
content = re.sub(r'(<VirtualHost\s+\*:)80\b', r'\g<1>8288', content)
content = re.sub(r'(<VirtualHost\s+\*:)443\b', r'\g<1>8290', content)
public.writeFile(conf_path, content)
except Exception as e:
self._progress_rewrite('Apache port fix failed: {}'.format(str(e)))
def _fix_ols_config_ports(self, vhost: str, site_name: str) -> None:
"""修复OLS配置端口(代理模式:80→8188, 443→8190"""
import re
ols_paths = [
os.path.join(vhost, 'openlitespeed', '{}.conf'.format(site_name)),
os.path.join(vhost, 'openlitespeed', 'detail', '{}.conf'.format(site_name)),
os.path.join(vhost, 'openlitespeed', 'detail', 'ssl', '{}.conf'.format(site_name)),
]
for conf_path in ols_paths:
if not os.path.exists(conf_path):
continue
try:
content = public.readFile(conf_path)
if not content:
continue
content = re.sub(r'(\*:)(80)\b', r'\g<1>8188', content)
content = re.sub(r'(\*:)(443)\b', r'\g<1>8190', content)
public.writeFile(conf_path, content)
except Exception as e:
self._progress_rewrite('OLS port fix failed for {}: {}'.format(conf_path, str(e)))
# 域名还原
def _restore_site_domains(self, site_id: int, site_name: str,
domain_list: list) -> None:
"""恢复域名:先删除现有域名(保留主域名),再添加备份域名"""
# 预检:过滤可用域名(未被其他站点占用的)
usable_domains = []
for info in domain_list:
name = info.get('name', '')
if not name:
continue
owner = public.M('domain').where('name=?', (name,)).field('pid').find()
if owner and site_id != owner['pid']:
continue
usable_domains.append(info)
if not usable_domains:
return self._progress_rewrite(public.lang('No usable domains in backup, all domains are occupied by other sites'))
from panel_site_v2 import panelSite
site_obj = panelSite()
# 构建过滤站点
to_add_set = {
(info.get('name'), int(info.get('port', 80)))
for info in domain_list if info.get('name')
}
# 1. 删除现有域名(保留主域名即站点名)
existing = public.M('domain').where('pid=?', (site_id,)).field('name,port').select()
del_error = []
if isinstance(existing, list):
for d in existing:
try:
# 判断域名是否需要删除
if (d['name'], int(d.get('port', 80))) in to_add_set:
continue
get = public.to_dict_obj({
'id': site_id,
'webname': site_name,
'domain': d['name'],
'port': int(d.get('port', 80))
})
res = site_obj.DelDomain(get)
if res['status'] == -1:
del_error.append({'domain': d['name'],'port': int(d.get('port', 80))})
except Exception as e:
self._progress_rewrite("Domain deletion failed: " + str(e))
# 2. 添加可用域名
for domain_info in usable_domains:
domain_name = domain_info.get('name', '')
if not domain_name:
continue
try:
get = public.to_dict_obj({
'id': site_id,
'webname': site_name,
'domain': domain_name + ':' + str(domain_info.get('port', '80'))
})
site_obj.AddDomain(get)
except Exception as e:
self._progress_rewrite("Domain restoration failed: " + str(e))
# 3. 检查删除失败的域名,若不存在于待添加的域名列表中,则再次尝试删除
if del_error:
for error_item in del_error:
# 判断当前失败的域名和端口,是否在准备添加的列表中
if (error_item['domain'], error_item['port']) not in to_add_set:
try:
get = public.to_dict_obj({
'id': site_id,
'webname': site_name,
'domain': error_item['domain'],
'port': error_item['port']
})
# 再次尝试删除
site_obj.DelDomain(get)
except Exception as e:
self._progress_rewrite(f"Retry deleting domain {error_item['domain']} failed: {str(e)}")
return
def restore_site_database(self, db_name: str, db_file: str, site_id: int = 0, meta: dict = None) -> None:
return self._restore_site_database(db_name,db_file,site_id,meta)
# ====================== PHP站点全量还原 end ====================== #