# coding: utf-8 # ------------------------------------------------------------------- # aapanel # ------------------------------------------------------------------- # Copyright (c) 2015-2099 aapanel(http://www.aapanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: Fan # ------------------------------------------------------------------- # ------------------------------ # Node.js 项目备份还原模块 # ------------------------------ import json import os import shutil import sys if "/www/server/panel/class" not in sys.path: sys.path.insert(0, "/www/server/panel/class") if "/www/server/panel" not in sys.path: sys.path.insert(0, "/www/server/panel") import public from mod.project.nodejs.base import NodeJs class NodeBackup(NodeJs): def __init__(self): super(NodeBackup, self).__init__() self.status_file = '/tmp/nodejs_backup.log' self.setup_path = '/www/server' # 日志 def write_backup_log(self, status_file: str, status: int, ps: str, result_list: list) -> None: data = { "status": status, "error": "", "backup_website": { "status": status, "ps": ps, "title": public.lang("Backup_website"), "result_list": result_list } } public.writeFile(status_file, json.dumps(data)) # 文件还原 def do_nodejs_file_restore(self, file_name: str, site_id: int): # 定位备份文件 backup_path = public.M('config').where('id=?', (1,)).getField('backup_path') local_file = os.path.join(backup_path, 'nodejs', file_name) if not os.path.exists(local_file): db_filename = public.M('backup').where('name=? AND pid=?', (file_name, site_id)).getField('filename') if db_filename and os.path.exists(db_filename): local_file = db_filename if not os.path.exists(local_file): return public.return_message(-1, 0, public.lang("Backup file not found: {}").format(file_name)) # 查找目标站点 find = public.M('sites').where("id=? AND project_type=?", (site_id, 'Node')).field('id,name,path,project_config').find() if not isinstance(find, dict) or not find.get('id'): return public.return_message(-1, 0, public.lang("Site not found, please create it first")) site_path = find['path'] site_name = find['name'] if not os.path.exists(site_path): os.makedirs(site_path, 0o755) # 解压 if file_name.endswith('.zip'): public.ExecShell("cd '{}' && unzip -o '{}' > /dev/null 2>&1".format(site_path, local_file)) else: public.ExecShell("tar -zxf '{}' -C '{}'".format(local_file, site_path)) # 修复权限 public.ExecShell('chown -R www:www "{}"'.format(site_path)) # 检查依赖,缺失则构建 node_modules_path = os.path.join(site_path, 'node_modules') package_json_path = os.path.join(site_path, 'package.json') if not os.path.exists(node_modules_path) and os.path.exists(package_json_path): try: from projectModelV2.nodejsModel import main as nodejsModel install_get = public.to_dict_obj({'project_name': site_name}) nodejsModel().install_packages(install_get) except Exception as e: return public.return_message(-1, 0, public.lang("Dependency install failed: {}").format(str(e))) # 重启项目 try: project_config = json.loads(find.get('project_config', '{}')) project_type = project_config.get('project_type', 'nodejs') get = public.to_dict_obj({'project_name': site_name, 'project_type': project_type}) if project_type == 'pm2': from mod.project.nodejs.pm2Mod import main as pm2Mod pm2Mod().start_project(get) else: from projectModel.nodejsModel import main as nodeModel nodeModel().start_project(get) except Exception as e: return public.return_message(-1, 0, public.lang("Project restart failed: {}").format(str(e))) public.write_log_gettext('Node Project', 'Successfully restored Node.js project files [{}]', (site_name,)) return public.return_message(0, 0, public.lang("File restore completed for: {}").format(site_name)) # 全量还原 def do_nodejs_full_restore(self, file_name: str, site_id: int, restore_type: list): tmp_path = public.make_panel_tmp_path() try: return self._do_nodejs_full_restore(file_name, site_id, restore_type, tmp_path) finally: if tmp_path and os.path.exists(tmp_path): shutil.rmtree(tmp_path, ignore_errors=True) def _do_nodejs_full_restore(self, file_name: str, site_id: int, restore_type: list, tmp_path: str): # 1. 定位备份文件 backup_path = public.M('config').where('id=?', (1,)).getField('backup_path') local_file = os.path.join(backup_path, 'nodejs', file_name) if not os.path.exists(local_file): local_file = os.path.join(backup_path, 'nodejs', str(site_id), file_name) if not os.path.exists(local_file): db_filename = public.M('backup').where('name=? AND pid=?', (file_name, site_id)).getField('filename') if db_filename and os.path.exists(db_filename): local_file = db_filename if not os.path.exists(local_file): return public.return_message(-1, 0, public.lang("Backup file not found: {}").format(file_name)) # 2. 解压到临时目录 os.makedirs(tmp_path, exist_ok=True) public.ExecShell('tar -zxf "{}" -C "{}"'.format(local_file, tmp_path)) # 3. 读取 meta.json meta_file = os.path.join(tmp_path, 'meta.json') if not os.path.exists(meta_file): return public.return_message(-1, 0, public.lang("meta.json not found in backup")) with open(meta_file, 'r', encoding='utf-8') as fp: meta = json.load(fp) site_name = meta.get('site_name', '') if not site_name: return public.return_message(-1, 0, public.lang("site_name is empty in meta.json")) # 4. 查找目标站点 find = public.M('sites').where("id=? AND project_type=?", (site_id, 'Node')).field('id,name,path,project_config').find() if not isinstance(find, dict) or not find.get('id'): return public.return_message(-1, 0, public.lang("Site [{}] does not exist, please create it first").format(site_name)) # 4.1 校验项目类型一致性 current_config = json.loads(find.get('project_config', '{}')) current_type = current_config.get('project_type', 'nodejs') backup_type = meta.get('project_type', 'nodejs') if current_type != backup_type: type_map = {'pm2': 'PM2', 'nodejs': public.lang('Node.js'), 'general': 'General'} return public.return_message(-1, 0, public.lang("Project type mismatch: current is [{}], backup is [{}], restore not allowed").format( type_map.get(current_type, current_type), type_map.get(backup_type, backup_type))) current_path = find.get('path', meta.get('site_path', '')) backup_site_path = meta.get('site_path', '') # 4.2 目录冲突校验 if backup_site_path and current_path != backup_site_path: if os.path.exists(backup_site_path): return public.return_message(-1, 0, public.lang("Backup source directory [{}] already exists on disk, restore aborted to prevent data conflict").format(backup_site_path)) current_path = backup_site_path if not os.path.exists(current_path): os.makedirs(current_path, 0o755) public.M('sites').where('id=?', (site_id,)).update({'path': current_path}) # 5. 还原文件 if 'file' in restore_type: files_tar = os.path.join(tmp_path, 'files.tar.gz') if os.path.exists(files_tar): if not os.path.exists(current_path): os.makedirs(current_path, 0o755) public.ExecShell('tar -zxf "{}" -C "{}"'.format(files_tar, current_path)) # 6. 还原配置 if 'conf' in restore_type: configs_dir = os.path.join(tmp_path, 'configs') if os.path.exists(configs_dir): self._restore_nodejs_configs(site_name, configs_dir, meta) # 还原 PM2 ecosystem 配置 if meta.get('has_ecosystem_config') and meta.get('project_type') == 'pm2': eco_src = os.path.join(tmp_path, 'ecosystem.config.cjs') eco_dst = meta.get('config_file', '') if os.path.exists(eco_src) and eco_dst: eco_dir = os.path.dirname(eco_dst) if eco_dir and not os.path.exists(eco_dir): os.makedirs(eco_dir, 0o755, exist_ok=True) shutil.copy2(eco_src, eco_dst) # 还原域名(仅 bind_extranet=1 时触发 web 服务配置生成) domain_list = meta.get('domain_list', []) if domain_list: self._restore_nodejs_domains(site_id, site_name, domain_list) # 更新 project_config new_config = { 'project_name': meta.get('project_name', site_name), 'pm2_name': meta.get('pm2_name', ''), 'project_type': meta.get('project_type', ''), 'project_cwd': meta.get('project_cwd', current_path), 'project_file': meta.get('project_file', ''), 'project_script': meta.get('project_script', ''), 'project_args': meta.get('project_args', ''), 'config_file': meta.get('config_file', ''), 'config_body': meta.get('config_body', ''), 'env': meta.get('env', ''), 'run_user': meta.get('run_user', 'www'), 'port': meta.get('port'), 'nodejs_version': meta.get('nodejs_version', ''), 'pkg_manager': meta.get('pkg_manager', 'npm'), 'max_memory_limit': meta.get('max_memory_limit', 4096), 'is_power_on': meta.get('is_power_on', True), 'watch': meta.get('watch', False), 'cluster': meta.get('cluster', 1), 'bind_extranet': meta.get('bind_extranet', 0), 'domains': [d.get('name', '') + ':' + str(d.get('port', 80)) for d in domain_list], 'log_path': meta.get('log_path', self.node_logs_path), } public.M('sites').where('id=?', (site_id,)).update({ 'path': current_path, 'project_config': json.dumps(new_config) }) self.set_config(site_name) # 修复权限 public.ExecShell('chown -R www:www "{}"'.format(current_path)) # 检查 Node.js 版本是否存在,不存在则安装 backup_nodejs_version = meta.get('nodejs_version', '') if backup_nodejs_version: installed_versions = self.get_nodejs_version(None) version_installed = any(v == backup_nodejs_version for v in installed_versions) if not version_installed: pkg_manager = meta.get('pkg_manager', 'npm') node_res = self._install_nodejs_with_modules(backup_nodejs_version, pkg_manager) if not node_res.get('status'): return public.return_message(-1, 0, public.lang("Node.js version [{}] installation failed: {}").format( backup_nodejs_version, node_res.get('error_msg', ''))) # 检查依赖,缺失则构建 node_modules_path = os.path.join(current_path, 'node_modules') package_json_path = os.path.join(current_path, 'package.json') if not os.path.exists(node_modules_path) and os.path.exists(package_json_path): try: from projectModelV2.nodejsModel import main as nodejsModel install_get = public.to_dict_obj({'project_name': site_name}) nodejsModel().install_packages(install_get) except Exception as e: return public.return_message(-1, 0, public.lang("Dependency install failed: {}").format(str(e))) # 重启项目 try: get = public.to_dict_obj({'project_name': site_name, 'project_type': meta.get('project_type', 'nodejs')}) if meta.get('project_type') == 'pm2': from mod.project.nodejs.pm2Mod import main as pm2Mod pm2Mod().start_project(get) else: from projectModel.nodejsModel import main as nodeModel nodeModel().start_project(get) except Exception as e: return public.return_message(-1, 0, public.lang("Project restart failed: {}").format(str(e))) public.write_log_gettext('Node Project', 'Successfully restored Node.js project [{}]', (site_name,)) return public.return_message(0, 0, public.lang("Restore completed for: {}").format(site_name)) # 配置还原 def _restore_nodejs_configs(self, site_name: str, configs_dir: str, meta: dict = None) -> None: vhost = self.vhost_path # --- Nginx --- nginx_src = os.path.join(configs_dir, 'nginx', 'node_{}.conf'.format(site_name)) if os.path.exists(nginx_src): nginx_dst = '{}/nginx/node_{}.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', 'node_{}.conf'.format(site_name)) if os.path.exists(apache_src): apache_dst = '{}/apache/node_{}.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) # --- Proxy / Extension / Dir_auth --- for server in ['nginx', 'apache']: 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) extension_src = os.path.join(configs_dir, server, 'extension', site_name) if os.path.exists(extension_src): ext_dst = '{}/{}/extension/{}'.format(vhost, server, site_name) if os.path.exists(ext_dst): shutil.rmtree(ext_dst, ignore_errors=True) shutil.copytree(extension_src, ext_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', 'node_{}.conf'.format(site_name)) if os.path.exists(rewrite_src): rewrite_dst = '{}/rewrite/node_{}.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: src = os.path.join(rewrite_dir, f) dst = os.path.join(vhost, 'rewrite', f) shutil.copy2(src, dst) # --- 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) # --- Start script --- script_src = os.path.join(configs_dir, 'scripts', '{}.sh'.format(site_name)) if os.path.exists(script_src): script_dst = '{}/vhost/scripts/{}.sh'.format(self.nodejs_path, site_name) script_dir = os.path.dirname(script_dst) if not os.path.exists(script_dir): os.makedirs(script_dir, 0o755, exist_ok=True) shutil.copy2(script_src, script_dst) public.set_own(script_dst, 'www', 'www') public.set_mode(script_dst, 755) public.serviceReload() # 域名还原 def _restore_nodejs_domains(self, site_id: int, site_name: str, domain_list: list) -> None: from projectModelV2.nodejsModel import main as nodejsModel model = nodejsModel() # 预检:过滤可用域名(未被其他站点占用) 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 isinstance(owner, dict) and site_id != owner.get('pid'): continue usable_domains.append(info) if not usable_domains: return # 待添加域名集合 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: if (d['name'], int(d.get('port', 80))) in to_add_set: continue try: domain_str = '{}:{}'.format(d['name'], d.get('port', 80)) get = public.to_dict_obj({ 'project_name': site_name, 'domain': domain_str }) res = model.project_remove_domain(get) if isinstance(res, dict) and res.get('status') == -1: del_error.append({'domain': d['name'], 'port': int(d.get('port', 80))}) except Exception: del_error.append({'domain': d['name'], 'port': int(d.get('port', 80))}) # 2. 逐个添加可用域名 for info in usable_domains: name = info.get('name', '') if not name: continue try: get = public.to_dict_obj({ 'project_name': site_name, 'domains': ['{}:{}'.format(name, info.get('port', 80))] }) model.project_add_domain(get) except Exception: pass # 3. 重试删除失败的域名 if del_error: for error_item in del_error: if (error_item['domain'], error_item['port']) not in to_add_set: try: domain_str = '{}:{}'.format(error_item['domain'], error_item['port']) get = public.to_dict_obj({ 'project_name': site_name, 'domain': domain_str }) model.project_remove_domain(get) except Exception: pass # 安装nodejs版本并附加PM2、指定包管理器模块 def _install_nodejs_with_modules(self, version: str, package_manager: str = 'npm') -> dict: """ 安装指定Node.js版本,随后安装PM2和指定包管理器并设为默认 """ try: plugin_path = '/www/server/panel/plugin/nodejs' if plugin_path not in sys.path: sys.path.insert(0, plugin_path) from plugin.nodejs.nodejs_main import nodejs_main node_obj = nodejs_main() get = public.dict_obj() get.version = version # 1. 安装Node.js install_res = node_obj.install_nodejs(get) if not install_res.get('status') and 'already installed' not in install_res.get('error_msg'): return install_res # 2. 安装PM2模块 pm2_get = public.dict_obj() pm2_get.version = version pm2_get.module = 'pm2' pm2_res = node_obj.install_module(pm2_get) if not pm2_res.get('status') and 'has been installed!' not in pm2_res.get('error_msg'): return pm2_res # 3. 安装指定包管理器(npm内置,无需额外安装) if package_manager in ('yarn', 'pnpm'): mgr_get = public.dict_obj() mgr_get.version = version mgr_get.module = package_manager mgr_res = node_obj.install_module(mgr_get) if not mgr_res.get('status') and 'has been installed!' not in mgr_res.get('error_msg'): return mgr_res # 4. 设置为默认版本 default_get = public.dict_obj() default_get.version = version default_res = node_obj.set_default_env(default_get) return default_res except Exception as ex: return {'status': False, 'error_msg': str(ex), 'data': {}}