mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-07 01:57:38 +02:00
866 lines
41 KiB
Python
866 lines
41 KiB
Python
# 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 ====================== #
|