Update to v8.16.0

This commit is contained in:
aapanel.com
2026-07-06 09:16:25 +08:00
parent 88012af9cf
commit c529749cf8
2034 changed files with 122712 additions and 38986 deletions
+481
View File
@@ -0,0 +1,481 @@
# 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': {}}
+3
View File
@@ -366,6 +366,9 @@ class NodeJs():
except ValueError:
res_domains.append({"name": domain, "status": False, "msg": 'Incorrect domain name format'})
continue
if int(domain_arr[1]) == 443:
res_domains.append({"name": domain, "status": False, "msg": 'Port 443 is not allowed for Node.js projects'})
continue
if not public.M('domain').where('name=? and port=?', (domain_arr[0], domain_arr[1])).count():
public.M('domain').add('name,pid,port,addtime',
(domain_arr[0], project_id, domain_arr[1], public.getDate()))
+429 -3
View File
@@ -6,12 +6,11 @@
# -------------------------------------------------------------------
# Author: wzz <wzz@aapanel.com>
# -------------------------------------------------------------------
import json
import os
# ------------------------------
# nodejs项目业务接口
# ------------------------------
import sys
import sys, time, shutil, os, json
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
@@ -189,6 +188,8 @@ class main(NodeJs):
for domain, port in domains:
if public.M('domain').where('name=? AND port=?', (domain, port)).count():
self.ws_err_exit(False, 'The specified domain already exists: {}'.format(domain), code=4)
if port == 443:
self.ws_err_exit(False, 'Port 443 is not allowed for Node.js projects', code=5)
get.domains = ["{}:{}".format(d,p) for d, p in domains]
get.nodejs_version = get.get("nodejs_version", None)
@@ -713,3 +714,428 @@ class main(NodeJs):
self.rebuild_project(get.project_name)
return public.return_message(0,0, public.lang( 'Project modified successfully'))
# 日志
def write_backup_log(self, status_file, status, ps, result_list):
data = {
"status": status, "error": "",
"backup_website": {
"status": status, "ps": ps, "title": public.lang("Backup Project"),
"result_list": result_list
}
}
public.writeFile(status_file, json.dumps(data))
# 文件备份
def _do_nodejs_file_backup(self, site_ids: list) -> None:
lock_file = '/tmp/nodejs_backup.lock'
status_file = '/tmp/nodejs_backup.log'
import threading
public.writeFile(lock_file, str(threading.get_ident()))
try:
total = len(site_ids)
result_list = []
self.write_backup_log(status_file, 0, public.lang("Backing up {}/{}").format(0, total), [])
for idx, site_id in enumerate(site_ids, 1):
find = public.M('sites').where("id=? AND project_type=?", (site_id, 'Node')).field('name,path,id').find()
if not find:
result_list.append({
'site_id': site_id, 'site_name': 'Unknown',
'status': -1, 'msg': public.lang('Site not found')
})
self.write_backup_log(status_file, 0, public.lang("Backup site: {}/{}").format(idx, total), result_list)
time.sleep(0.3)
continue
try:
site_name = find['name']
site_path = find['path']
backup_path = public.M('config').where('id=?', (1,)).getField('backup_path') + '/nodejs'
if not os.path.exists(backup_path):
os.makedirs(backup_path)
file_name = '{}_{}.zip'.format(site_name, time.strftime('%Y%m%d_%H%M%S', time.localtime()))
zip_path = os.path.join(backup_path, file_name)
public.ExecShell(
"cd '{}' && zip '{}' -r . -x 'node_modules/*' > /dev/null 2>&1".format(
site_path, zip_path
)
)
file_size = os.path.getsize(zip_path) if os.path.exists(zip_path) else 0
public.M('backup').add('type,name,pid,filename,size,addtime,backup_type',
(0, file_name, find['id'], zip_path, file_size, public.getDate(), 0))
result_list.append({
'site_id': site_id, 'site_name': site_name,
'status': 0, 'msg': public.lang('Backup successful'), 'file': file_name
})
except Exception as e:
result_list.append({
'site_id': site_id, 'site_name': find.get('name', 'Unknown'),
'status': -1, 'msg': str(e)
})
self.write_backup_log(status_file, 0, public.lang("Backup site: {}/{}").format(idx, total), result_list)
time.sleep(0.3)
success_count = sum(1 for r in result_list if r['status'] == 0)
failed_count = sum(1 for r in result_list if r['status'] != 0)
self.write_backup_log(status_file, 1,
public.lang("Backup completed: {} success, {} failed").format(success_count, failed_count),
result_list)
public.write_log_gettext('Node Project', 'Batch backup completed: {} success, {} failed',
(success_count, failed_count))
except Exception as e:
self.write_backup_log(status_file, -1, public.lang("Backup failed"), [])
public.writeFile(status_file, json.dumps({"status": -1, "error": str(e)}))
finally:
public.progress_release_lock(lock_file)
# 全量备份
def _do_nodejs_full_backup(self, site_ids: list) -> None:
lock_file = '/tmp/nodejs_backup.lock'
status_file = '/tmp/nodejs_backup.log'
import threading
public.writeFile(lock_file, str(threading.get_ident()))
try:
total = len(site_ids)
result_list = []
self.write_backup_log(status_file, 0, public.lang("Backing up {}/{}").format(0, total), [])
for idx, site_id in enumerate(site_ids, 1):
find = public.M('sites').where("id=? AND project_type=?", (site_id, 'Node')).field(
'id,name,path,project_config').find()
if not find:
result_list.append({
'site_id': site_id, 'site_name': 'Unknown',
'status': -1, 'msg': public.lang('Site not found')
})
self.write_backup_log(status_file, 0, public.lang("Backup site: {}/{}").format(idx, total), result_list)
time.sleep(0.3)
continue
self.write_backup_log(status_file, 0,
public.lang("Backing up [{}] {}/{}").format(find['name'], idx, total), result_list)
tmp_path = None
try:
site_name = find['name']
site_path = find['path']
project_config = json.loads(find['project_config'])
tmp_path = public.make_panel_tmp_path()
# 1. 备份项目文件
self.write_backup_log(status_file, 0,
public.lang("Backing up [{}] - compressing files").format(site_name), result_list)
files_tar = os.path.join(tmp_path, 'files.tar.gz')
public.ExecShell(
"tar -zcf '{}' -C '{}' --exclude='node_modules' . > /dev/null 2>&1".format(
files_tar, site_path
)
)
# 2. 备份 Web 服务器配置
self.write_backup_log(status_file, 0,
public.lang("Backing up [{}] - configs").format(site_name), result_list)
self._backup_nodejs_configs(site_name, os.path.join(tmp_path, 'configs'))
# 3. 备份启动脚本
script_src = '{}/vhost/scripts/{}.sh'.format(self.nodejs_path, site_name)
if os.path.exists(script_src):
scripts_dir = os.path.join(tmp_path, 'configs', 'scripts')
os.makedirs(scripts_dir, 0o755, exist_ok=True)
shutil.copy2(script_src, scripts_dir)
# 4. 备份 PM2 ecosystem 配置
has_ecosystem = False
config_file = project_config.get('config_file', '')
if project_config.get('project_type') == 'pm2' and config_file:
if os.path.exists(config_file) and os.path.isfile(config_file):
shutil.copy2(config_file, os.path.join(tmp_path, 'ecosystem.config.cjs'))
has_ecosystem = True
# 5. 获取域名列表
domain_list = []
domains = public.M('domain').where('pid=?', (site_id,)).field('name,port').select()
if isinstance(domains, list):
domain_list = domains
# 6. 获取 Web 服务器信息
try:
webservice_status = public.get_multi_webservice_status()
if webservice_status:
site_info = public.M('sites').where('id=?', (site_id,)).field('service_type').find()
webserver = site_info.get('service_type', 'nginx') if isinstance(site_info, dict) else 'nginx'
else:
webserver = public.get_webserver()
except Exception:
webserver = 'nginx'
# 7. 写入 meta.json
meta = {
'backup_version': 1,
'backup_type': 'nodejs_full',
'backup_time': public.getDate(),
'site_id': site_id,
'site_name': site_name,
'site_path': site_path,
'project_type': project_config.get('project_type', 'nodejs'),
'nodejs_version': project_config.get('nodejs_version', ''),
'project_file': project_config.get('project_file', ''),
'project_script': project_config.get('project_script', ''),
'project_args': project_config.get('project_args', ''),
'project_cwd': project_config.get('project_cwd', site_path),
'pm2_name': project_config.get('pm2_name', ''),
'env': project_config.get('env', ''),
'run_user': project_config.get('run_user', 'www'),
'port': project_config.get('port'),
'config_file': config_file,
'config_body': project_config.get('config_body', ''),
'pkg_manager': project_config.get('pkg_manager', 'npm'),
'max_memory_limit': project_config.get('max_memory_limit', 4096),
'is_power_on': project_config.get('is_power_on', True),
'watch': project_config.get('watch', False),
'cluster': project_config.get('cluster', 1),
'bind_extranet': project_config.get('bind_extranet', 0),
'log_path': project_config.get('log_path', self.node_logs_path),
'domain_list': domain_list,
'webserver': webserver,
'has_ecosystem_config': has_ecosystem,
'has_start_script': os.path.exists(script_src),
'addtime': public.M('sites').where('id=?', (site_id,)).getField('addtime'),
}
meta_file = os.path.join(tmp_path, 'meta.json')
with open(meta_file, 'w', encoding='utf-8') as fp:
json.dump(meta, fp, ensure_ascii=False)
# 8. 打包最终备份文件
file_name = 'full_{}_{}.tar.gz'.format(site_name, time.strftime('%Y%m%d_%H%M%S', time.localtime()))
backup_path = public.M('config').where('id=?', (1,)).getField('backup_path') + '/nodejs'
if not os.path.exists(backup_path):
os.makedirs(backup_path)
full_backup_path = os.path.join(backup_path, file_name)
public.ExecShell("tar -zcf '{}' -C '{}' .".format(full_backup_path, tmp_path))
file_size = os.path.getsize(full_backup_path) if os.path.exists(full_backup_path) else 0
public.M('backup').add('type,name,pid,filename,size,addtime,backup_type',
(0, file_name, find['id'], full_backup_path, file_size, public.getDate(), 1))
result_list.append({
'site_id': site_id, 'site_name': site_name,
'status': 0, 'msg': public.lang('Backup successful'), 'file': file_name
})
except Exception as e:
import traceback
traceback.print_exc()
result_list.append({
'site_id': site_id, 'site_name': find.get('name', 'Unknown'),
'status': -1, 'msg': str(e)
})
finally:
if tmp_path and os.path.exists(tmp_path):
shutil.rmtree(tmp_path, ignore_errors=True)
self.write_backup_log(status_file, 0, public.lang("Backup site: {}/{}").format(idx, total), result_list)
time.sleep(0.3)
success_count = sum(1 for r in result_list if r['status'] == 0)
failed_count = sum(1 for r in result_list if r['status'] != 0)
self.write_backup_log(status_file, 1,
public.lang("Backup completed: {} success, {} failed").format(success_count, failed_count),
result_list)
public.write_log_gettext('Node Project', 'Batch full backup completed: {} success, {} failed',
(success_count, failed_count))
except Exception as e:
self.write_backup_log(status_file, -1, public.lang("Backup failed"), [])
public.writeFile(status_file, json.dumps({"status": -1, "error": str(e)}))
finally:
public.progress_release_lock(lock_file)
# 配置备份
def _backup_nodejs_configs(self, site_name: str, conf_dir: str) -> None:
if not os.path.exists(conf_dir):
os.makedirs(conf_dir, 0o755)
vhost = self.vhost_path
# --- Nginx ---
nginx_src = '{}/nginx/node_{}.conf'.format(vhost, site_name)
if os.path.exists(nginx_src):
nginx_dir = os.path.join(conf_dir, 'nginx')
os.makedirs(nginx_dir, 0o755, exist_ok=True)
shutil.copy2(nginx_src, nginx_dir)
well_known_src = '{}/nginx/well-known/{}'.format(vhost, site_name)
if os.path.exists(well_known_src):
well_known_dst = os.path.join(conf_dir, 'nginx', 'well-known')
os.makedirs(well_known_dst, 0o755, exist_ok=True)
shutil.copytree(well_known_src, os.path.join(well_known_dst, site_name), dirs_exist_ok=True)
# --- Apache ---
apache_src = '{}/apache/node_{}.conf'.format(vhost, site_name)
if os.path.exists(apache_src):
apache_dir = os.path.join(conf_dir, 'apache')
os.makedirs(apache_dir, 0o755, exist_ok=True)
shutil.copy2(apache_src, apache_dir)
# --- OpenLiteSpeed ---
ols_src = '{}/openlitespeed/{}.conf'.format(vhost, site_name)
if os.path.exists(ols_src):
ols_dir = os.path.join(conf_dir, 'openlitespeed')
os.makedirs(ols_dir, 0o755, exist_ok=True)
shutil.copy2(ols_src, ols_dir)
ols_detail_src = '{}/openlitespeed/detail/{}.conf'.format(vhost, site_name)
if os.path.exists(ols_detail_src):
ols_detail_dir = os.path.join(conf_dir, 'openlitespeed', 'detail')
os.makedirs(ols_detail_dir, 0o755, exist_ok=True)
shutil.copy2(ols_detail_src, ols_detail_dir)
ols_ssl_src = '{}/openlitespeed/detail/ssl/{}.conf'.format(vhost, site_name)
if os.path.exists(ols_ssl_src):
ols_ssl_dir = os.path.join(conf_dir, 'openlitespeed', 'detail', 'ssl')
os.makedirs(ols_ssl_dir, 0o755, exist_ok=True)
shutil.copy2(ols_ssl_src, ols_ssl_dir)
# --- Proxy / Extension / Dir_auth ---
for server in ['nginx', 'apache']:
proxy_src = '{}/{}/proxy/{}'.format(vhost, server, site_name)
if os.path.exists(proxy_src):
proxy_dst = os.path.join(conf_dir, server, 'proxy')
os.makedirs(proxy_dst, 0o755, exist_ok=True)
shutil.copytree(proxy_src, os.path.join(proxy_dst, site_name), dirs_exist_ok=True)
extension_src = '{}/{}/extension/{}'.format(vhost, server, site_name)
if os.path.exists(extension_src):
ext_dst = os.path.join(conf_dir, server, 'extension')
os.makedirs(ext_dst, 0o755, exist_ok=True)
shutil.copytree(extension_src, os.path.join(ext_dst, site_name), dirs_exist_ok=True)
dir_auth_src = '{}/{}/dir_auth/{}'.format(vhost, server, site_name)
if os.path.exists(dir_auth_src):
dir_auth_dst = os.path.join(conf_dir, server, 'dir_auth')
os.makedirs(dir_auth_dst, 0o755, exist_ok=True)
shutil.copytree(dir_auth_src, os.path.join(dir_auth_dst, site_name), dirs_exist_ok=True)
# --- Rewrite ---
rewrite_src = '{}/rewrite/node_{}.conf'.format(vhost, site_name)
if os.path.exists(rewrite_src):
rewrite_dir = os.path.join(conf_dir, 'rewrite')
os.makedirs(rewrite_dir, 0o755, exist_ok=True)
shutil.copy2(rewrite_src, rewrite_dir)
rewrite_vhost = '{}/rewrite/'.format(vhost)
if os.path.exists(rewrite_vhost):
for f in os.listdir(rewrite_vhost):
if f.endswith('.conf') and site_name in f:
rewrite_dir = os.path.join(conf_dir, 'rewrite')
os.makedirs(rewrite_dir, 0o755, exist_ok=True)
shutil.copy2(os.path.join(rewrite_vhost, f), rewrite_dir)
# --- SSL Cert ---
cert_src = '{}/cert/{}'.format(vhost, site_name)
if os.path.exists(cert_src):
cert_dst = os.path.join(conf_dir, '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)
# 进度获取
def get_general_progress(self, get=None):
from panel_site_v2 import panelSite
return panelSite().get_general_progress(public.to_dict_obj({'type' : 'nodejs'}))
# id校验
def _parse_backup_site_ids(self, get, lock_file: str) -> list:
"""解析并校验备份项目 ID 列表"""
site_ids = get.get("id", [])
if isinstance(site_ids, str):
try:
site_ids = json.loads(site_ids)
except Exception:
site_ids = [site_ids]
if not isinstance(site_ids, list):
site_ids = [site_ids]
try:
site_ids = [int(sid) for sid in site_ids]
except (ValueError, TypeError):
public.progress_release_lock(lock_file)
return []
if not site_ids:
public.progress_release_lock(lock_file)
return site_ids
# 备份入口
def nodejs_backup(self, get):
'''
@name 批量备份 Node.js 项目
@param get.id list 项目 ID 列表 [1,2,3]
@param get.backup_type int 备份类型 0=文件备份, 1=全量备份 默认0
'''
lock_file = '/tmp/nodejs_backup.lock'
if not public.progress_acquire_lock(lock_file):
return public.return_message(-1, 0, public.lang('Other backup task is running. Please wait!'))
site_ids = self._parse_backup_site_ids(get, lock_file)
if not site_ids:
return public.return_message(-1, 0, public.lang('No project IDs provided'))
backup_type = int(get.get('backup_type', 0))
from concurrent.futures import ThreadPoolExecutor
thread = ThreadPoolExecutor(max_workers=1)
if backup_type == 1:
thread.submit(self._do_nodejs_full_backup, site_ids)
else:
thread.submit(self._do_nodejs_file_backup, site_ids)
return public.return_message(0, 0, public.lang('Backup started'))
# 备份还原入口
def nodejs_restore(self, get):
'''
@name 还原 Node.js 项目备份
@param get.file_name string 备份文件名
@param get.site_id int 项目 ID
@param get.restore_type string JSON 数组 '["file","conf"]'
'''
file_name = get.get("file_name", "")
site_id = get.get("site_id", 0)
restore_type = get.get("restore_type", '["file","conf"]')
if not file_name:
return public.return_message(-1, 0, public.lang('The "file_name" parameter cannot be left blank'))
try:
site_id = int(site_id)
except (ValueError, TypeError):
return public.return_message(-1, 0, public.lang('Invalid "site_id" parameter'))
if not site_id:
return public.return_message(-1, 0, public.lang('The "site_id" parameter cannot be left blank'))
try:
restore_type = json.loads(restore_type)
except Exception:
restore_type = ["file", "conf"]
from mod.project.nodejs.backupMod import NodeBackup
nodejs = public.M('backup').where('name=? AND pid=?', (file_name, site_id)).find()
if not nodejs:
return public.return_message(-1, 0, public.lang('Backup package does not exist.'))
backup_type = nodejs.get('backup_type', 0)
if backup_type == 1:
return NodeBackup().do_nodejs_full_restore(file_name, site_id, restore_type)
else:
return NodeBackup().do_nodejs_file_restore(file_name, site_id)