#coding: utf-8 #------------------------------------------------------------------- # aaPanel #------------------------------------------------------------------- # Copyright (c) 2015-2017 aaPanel(www.aapanel.com) All rights reserved. #------------------------------------------------------------------- # Author: FAN #------------------------------------------------------------------- #------------------------------ # 引导页一键部署 #------------------------------ import public, os, time, shutil, json, re, sys,math import threading from public.validate import Param class GuidePage: def __init__(self): "php-8.3 mysql-8.0/mariadb-11.4 node-26.1.0/24.15.0/22.22.3" self.status_file = '/tmp/guide_page_install.log' self.lock_file = '/tmp/guide_page_install.lock' self.guide_config = os.path.join(public.get_panel_path(), 'data', 'guide_config.json') self.default_service = {'display_name':'Web Server', 'name':'nginx','version' : '1.30' } self.php_version = ['82', '83', '84'] # 引导页一键部署总入口 def guide_page_install(self, get): """ 引导页一键部署统一入口。 流程:参数校验 -> 异步函数 -> 日志初始化 -> 依赖安装 - > 部署 """ try: get.validate([ Param('install_type').String(), Param('param').Json() ], [ public.validate.trim_filter(), ]) install_type = get.install_type param = json.loads(get.param) # 提前进行参数校验 val_res = self._validate_params(install_type, param) if val_res['status'] is False: return public.return_message(-1, 0, val_res['msg']) clean_param = val_res['data'] if not public.progress_acquire_lock(self.lock_file): return public.return_message(-1, 0, public.lang('Other tasks are being deployed. Please wait!')) task_mapping = { 'wp': self.install_wp_site, 'nodejs': self.install_nodejs_project, 'proxy': self.install_proxy_project, 'php': self.install_php_site } if install_type not in task_mapping: return public.return_message(-1, 0, public.lang('Unsupported installation type.')) from concurrent.futures import ThreadPoolExecutor thread = ThreadPoolExecutor(max_workers=1) task_func = task_mapping[install_type] started = threading.Event() thread.submit(task_func, clean_param, started) started.wait(timeout=5) # 阻塞直到 Worker 写入自己的线程 ID self.save_config(install_type, param) public.set_module_logs(f'GuidePage', f'Installation_{install_type}') public.set_module_logs(f'GuidePage', 'Total_installation_count') return public.return_message(0, 0, 'Successful startup!') except Exception as ex: import traceback traceback.print_exc() return public.return_message(-1, 0, str(ex)) # 安装的依赖列表 def _init_plugin(self, install_type, param): # ===========================此处添加依赖=========================================== # 同步安装的插件 必填 install_tmp_path,用于解压导入安装包 install_list = [self.default_service] if install_type == 'php': install_list.append({'display_name':'PHP','name': 'php', 'version': param['version']}) if param['database'] == 'mysql': install_list.append({'display_name':'MySql','name' : 'mysql', 'version' : '8.0'}) elif param['database'] == 'mariadb': install_list.append({'display_name':'MariaDB','name': 'mysql', 'version': 'mariadb_11.4'}) elif install_type == 'wp': install_list.append({'display_name':'PHP','name': 'php', 'version': '83'}) install_list.append({'display_name':'MySql','name': 'mysql', 'version': '8.0'}) elif install_type == 'proxy': pass elif install_type == 'nodejs': install_list.append({'display_name':'Node.js Manager', 'name': 'nodejs', 'version': '2.3', 'install_tmp_path': '/www/server/panel/temp/nodejs'}) # ===========================此处添加依赖=========================================== return install_list # 参数校验 def _validate_params(self, install_type: str, param: dict) -> dict: """ 独立的参数校验与清洗函数 返回结构: {'status': True/False, 'msg': '错误信息', 'data': 清洗后的参数字典} """ try: clean_data = {} # --- 1. WordPress 参数校验 --- if install_type == 'wp': required_fields = ['domain', 'ssl_auto', 'title', 'language', 'email'] for field in required_fields: if field not in param or not str(param[field]).strip(): return {'status': False, 'msg': f'Missing required parameter: {field}', 'data': {}} clean_data['domain'] = str(param['domain']).strip() clean_data['ssl_auto'] = param['ssl_auto'] clean_data['title'] = str(param['title']).strip() clean_data['language'] = str(param['language']).strip() clean_data['email'] = str(param['email']).strip() # --- 2. 纯 PHP 网站参数校验 --- elif install_type == 'php': required_fields = ['domain', 'ssl_auto' , 'version'] for field in required_fields: if field not in param or not str(param[field]).strip(): return {'status': False, 'msg': f'Missing required parameter: {field}', 'data': {}} clean_data['domain'] = str(param['domain']).strip() clean_data['ssl_auto'] = param['ssl_auto'] clean_data['version'] = str(param['version'].strip()) if clean_data['version'] not in self.php_version: return {'status': False, 'msg': f'Missing required parameter: php version', 'data': {}} # 可选数据库参数校验 db_type = str(param.get('database', '')).strip().lower() clean_data['database'] = db_type if db_type in ['mysql', 'mariadb']: if not param.get('datauser') or not param.get('datapassword'): return {'status': False, 'msg': 'Database user and password are required when database is enabled.', 'data': {}} clean_data['datauser'] = str(param['datauser']).strip() clean_data['datapassword'] = str(param['datapassword']).strip() else: clean_data['database'] = '' clean_data['datauser'] = '' clean_data['datapassword'] = '' # --- 3. nodejs项目参数校验 --- elif install_type == 'nodejs': # 服务校验 if public.get_webserver() in ['apache', 'openlitespeed']: return {'status': False, 'msg': public.lang("The deployment of the introduction page only supports Nginx."), 'data': {}} required_fields = ['domain', 'project_name', 'version', 'package_manager', 'template', 'port'] for field in required_fields: if field not in param or not str(param[field]).strip(): return {'status': False, 'msg': f'Missing required parameter: {field}', 'data': {}} allowed_templates = ['express', 'astro', 'nestjs', 'nextjs', 'fastify'] template = str(param['template']).strip().lower() if template not in allowed_templates: return {'status': False, 'msg': f'Unsupported template: {param["template"]}. Allowed: {", ".join(allowed_templates)}', 'data': {}} allowed_managers = ['npm', 'yarn', 'pnpm'] package_manager = str(param['package_manager']).strip().lower() if package_manager not in allowed_managers: return {'status': False, 'msg': f'Unsupported package manager: {param["package_manager"]}. Allowed: {", ".join(allowed_managers)}', 'data': {}} clean_data['domain'] = str(param['domain']).strip() # clean_data['ssl_auto'] = str(param['ssl_auto']).strip() clean_data['project_name'] = str(param['project_name']).strip() clean_data['version'] = str(param['version']).strip() clean_data['package_manager'] = package_manager clean_data['template'] = template port_str = str(param['port']).strip() if not port_str.isdigit(): return {'status': False, 'msg': 'Port must be a positive integer.', 'data': {}} port_int = int(port_str) if port_int < 1 or port_int > 65535: return {'status': False, 'msg': 'Port must be between 1 and 65535.', 'data': {}} if port_int < 1024: return {'status': False, 'msg': 'Port must be >= 1024 (privileged ports are not allowed).', 'data': {}} import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(1) if sock.connect_ex(('127.0.0.1', port_int)) == 0: return {'status': False, 'msg': f'Port {port_int} is already in use.', 'data': {}} clean_data['port'] = port_str # --- 4. proxy项目参数校验 --- elif install_type == 'proxy': # 服务校验 if public.get_webserver() in ['apache', 'openlitespeed']: return {'status': False, 'msg': public.lang("The deployment of the introduction page only supports Nginx."), 'data': {}} required_fields = ['domain', 'ssl_auto', 'proxy_pass', 'proxy_cache', 'proxy_host'] for field in required_fields: if field not in param or not str(param[field]).strip(): return {'status': False, 'msg': f'Missing required parameter: {field}', 'data': {}} clean_data['domain'] = str(param['domain']).strip() clean_data['ssl_auto'] = str(param['ssl_auto']).strip() clean_data['proxy_cache'] = str(param['proxy_cache']).strip() clean_data['proxy_host'] = str(param['proxy_host']).strip() clean_data['proxy_pass'] = str(param['proxy_pass']).strip() else: return {'status': False, 'msg': f'Unknown installation type: {install_type}', 'data': {}} return {'status': True, 'msg': 'success', 'data': clean_data} except Exception as ex: import traceback traceback.print_exc() return {'status': False, 'msg': str(ex), 'data': {}} # 安装wp网站 def install_wp_site(self, param, started=None): """ 一键部署 WordPress(后台异步运行) """ task_status = os.path.join('/tmp', 'wp_aapanel_deploy.log') step_index = 0 progress = 0 service_count = self._init_log_structure('wp', param) public.writeFile(self.lock_file, str(threading.get_ident())) if started: started.set() try: # 安装依赖服务(每个服务独立步骤,由 install_plugin 内部按 step_index + idx 写入) ok = self.install_plugin('wp', param, step_index) if not ok: return progress = 55 step_index = service_count self.write_deploy_log(step_index=step_index, ps="Creating a WordPress site...", progress=progress, status=0) from panel_site_v2 import panelSite site_obj = panelSite() # 获取wp最新版本 package_version = '7.0' package_version_list = site_obj.get_wp_versions({'php_version_short':'83'}) if package_version_list['status'] == 0: package_version = package_version_list['message'][0].get('version') webname = json.dumps({'domain': param['domain'], 'domainlist': [], 'count': 0}) datapassword = public.GetRandomString(16) password = public.GetRandomString(16) admin = public.GetRandomString(12) datauser = param['domain'].replace('.', '_').replace('-', '_')[:16] site_args = public.to_dict_obj({ 'webname': webname, 'type': 'PHP', 'ps': param['domain'], 'path': '/www/wwwroot/' + param['domain'], 'version': '83', 'sql': 'MySQL', 'datauser': datauser, 'datapassword': datapassword, 'codeing': 'utf8', 'port': '80', 'ssl_auto': str(param['ssl_auto']), 'project_type': 'WP2', 'weblog_title': param['title'], 'language': param['language'], 'user_name':admin, 'password': password, 'pw_weak': 'on', 'email': param['email'], 'prefix': 'wp_', 'enable_cache': '0', 'package_version': package_version }) if os.path.exists(task_status): os.remove(task_status) result = site_obj.AddWPSite(site_args) if result.get('status') != 0: raise ValueError(result.get('message', {}).get('result', result.get('message', result))) # 初始化wp日志监控 wp_step_map = [ ('parameter_verification', service_count, 60), ('create_website', service_count, 72), ('optional_configurations', service_count, 84), ('initialize_wp_website', service_count + 1, 96), ] last_progress = progress reported_steps = set() # 10分钟超时 used_time = 0 while used_time < 600: if not os.path.exists(task_status): time.sleep(1) used_time += 1 continue try: wp_log = json.loads(public.readFile(task_status)) except Exception: time.sleep(1) used_time += 1 continue # 获取相应的日志信息,异步监控状态机 for key, guide_step, base_progress in wp_step_map: step_data = wp_log.get(key, {}) step_status = int(step_data.get('status', 2)) if step_status == 1 and key not in reported_steps: last_progress = max(last_progress, base_progress) # 状态调整,create_website/optional_configurations合并 status = 1 if key in ['create_website','parameter_verification']: status = 0 self.write_deploy_log( step_index=guide_step, ps=step_data.get('ps', ''), progress=last_progress, status=status, error_msg=step_data.get('error', '') ) reported_steps.add(key) continue if step_status == 0: last_progress = max(last_progress, base_progress) self.write_deploy_log( step_index=guide_step, ps=step_data.get('ps', ''), progress=last_progress, status=0, error_msg=step_data.get('error', '') ) break if step_status == -1: self.write_deploy_log( step_index=guide_step, ps=step_data.get('ps', 'An error has occurred.'), progress=last_progress, status=-1, error_msg=step_data.get('error', '') ) return if int(wp_log.get('status', 0)) == 1: failed_step = None for key, guide_step, base_progress in wp_step_map: step_data = wp_log.get(key, {}) if int(step_data.get('status', 2)) == -1: failed_step = (guide_step, step_data) break if failed_step: guide_step, step_data = failed_step self.write_deploy_log( step_index=guide_step, ps=step_data.get('ps', 'An error has occurred.'), progress=last_progress, status=-1, error_msg=step_data.get('error', '') ) return res = { 'domain': param['domain'], 'databaseStatus': True, 'databaseUser': site_args.datauser, 'databasePass': site_args.datapassword, 'wordpressUser': site_args.user_name, 'wordpressPass': site_args.password, 'php': 'PHP-83' } self.write_deploy_log(step_index=service_count + 2, ps="WordPress deployment successful", progress=100, status=1, result=res) return time.sleep(2) used_time += 2 raise TimeoutError('WordPress deployment timed out.') except Exception as ex: self.write_deploy_log(step_index=step_index, ps="An error has occurred.", progress=progress, status=-1,error_msg=str(ex)) finally: public.progress_release_lock(self.lock_file) # 安装php网站 def install_php_site(self, param, started=None): """ 一键部署 纯PHP网站(后台异步运行) """ service_count = self._init_log_structure('php', param) thread_id = threading.get_ident() public.writeFile(self.lock_file, str(thread_id)) if started: started.set() step_index = 0 progress = 0 try: # 安装依赖服务(每个服务独立步骤) ok = self.install_plugin('php', param, step_index) if not ok: return # 创建网站与数据库 progress = 55 step_index = service_count self.write_deploy_log(step_index=step_index, ps="Start creating the website...", progress=progress, status=0) from panel_site_v2 import panelSite site_obj = panelSite() webname = json.dumps({'domain': param['domain'], 'domainlist': [], 'count': 0}) site_args = public.to_dict_obj({ 'webname': webname, 'type': 'PHP', 'ps': param['domain'], 'path': '/www/wwwroot/' + param['domain'], 'version': param['version'], 'sql': 'MySQL' if param['database'] else '', 'datapassword': param['datapassword'], 'datauser' : param['datauser'], 'codeing': 'utf8mb4', 'port': '80', 'type_id': 0, 'force_ssl': 0, 'ftp': False, 'is_create_default_file': True, 'ssl_auto': param['ssl_auto'], 'sub_dir': '', 'project_type': 'PHP', }) result = site_obj.AddSite(site_args) if result.get('status') != 0: raise ValueError(f"Failed to create the website: {result['message'].get('result')}") res = { "siteId": result['message'].get('siteId'), "domain": param['domain'], "databaseStatus": result['message'].get('databaseStatus'), "databaseUser": result['message'].get('databaseUser', ''), "databasePass": result['message'].get('databasePass', ''), 'site_path': '/www/wwwroot/' + param['domain'], 'php' : 'PHP-'+param['version'] } self.write_deploy_log(step_index=step_index, ps="Success in creation", progress=100, status=1, result=res) except Exception as ex: self.write_deploy_log(step_index=step_index, ps="An error has occurred.", progress=progress, status=-1,error_msg=str(ex)) finally: public.progress_release_lock(self.lock_file) # nodejs项目部署 def install_nodejs_project(self, param, started=None): """ 一键部署 Node.js 项目(后台异步运行) 流程:安装插件 -> 安装Node.js版本 -> 部署项目 """ service_count = self._init_log_structure('nodejs', param) public.writeFile(self.lock_file, str(threading.get_ident())) if started: started.set() step_index = 0 progress = 0 try: # 安装依赖服务(每个服务独立步骤) ok = self.install_plugin('nodejs', param, step_index) if not ok: return # 安装Node.js + 部署项目 progress = 55 step_index = service_count self.write_deploy_log(step_index=step_index, ps="Installing Node.js and deploying project...", progress=progress, status=0) version = param['version'] package_manager = param.get('package_manager', 'npm') template = param['template'] project_name = param['project_name'] port = str(param['port']) project_path = os.path.join('/www/wwwroot', project_name) if os.path.exists(project_path): project_name = project_name + '_' + str(time.time())[-4:] project_path = os.path.join('/www/wwwroot', project_name) os.makedirs(project_path, mode=0o755, exist_ok=True) # 1. 安装Node.js版本 + PM2 + 包管理器 + 设为默认 node_res = self._install_nodejs_with_modules(version, package_manager) if not node_res.get('status'): raise ValueError(node_res.get('error_msg', 'Node.js installation failed')) progress = 60 self.write_deploy_log(step_index=step_index, ps="Node.js runtime ready!", progress=progress, status=1) time.sleep(2) step_index = service_count + 1 self.write_deploy_log(step_index=step_index, ps="Scaffolding project files...", progress=progress, status=0) # 2. 按框架执行脚手架命令(Express/Fastify 需预建目录,NestJS/Next.js CLI 自行建目录,Astro 手动创建) if template in ('express', 'fastify'): if os.path.exists(project_path): shutil.rmtree(project_path) os.makedirs(project_path, mode=0o755, exist_ok=True) elif template == 'astro': if os.path.exists(project_path): shutil.rmtree(project_path) self._scaffold_astro(project_path, port) else: # CLI 脚手架会自己创建目录,若已存在则先清理 if os.path.exists(project_path): shutil.rmtree(project_path) if template != 'astro': desc, cmd = self._get_scaffold_tasks(template, package_manager, project_name, project_path) self.write_deploy_log(step_index=step_index, ps=desc, progress=progress, status=0) out, err = public.ExecShell(cmd) if err and 'error' in err: raise ValueError(f"{desc} failed: {err.strip()}") # Next.js: .env 供 next dev 使用,next start 的 PORT 由启动脚本 export 注入 if template == 'nextjs': public.writeFile(os.path.join(project_path, '.env'), f'PORT={port}\n') # NestJS CLI 生成的 main.ts 硬编码 app.listen(3000),需 patch 为读取 PORT 环境变量 if template == 'nestjs': self._patch_nestjs_main(project_path, port) # Express/Fastify 需要手写入口文件 if template in ('express', 'fastify'): self._write_entry_file(project_path, template, port) progress = 75 self.write_deploy_log(step_index=step_index, ps="Installing dependencies...", progress=progress, status=0) # 4. 安装项目依赖 install_cmd = f'cd {project_path} && {package_manager} install' out, err = public.ExecShell(install_cmd) if err and 'ERR!' in err: raise ValueError(f"Dependency installation failed: {err.strip()}") # 5. 构建(仅 SSR/SSG 框架需要,Express/Fastify 跳过) if template not in ('express', 'fastify'): self.write_deploy_log(step_index=step_index, ps="Building project...", progress=80, status=0) build_cmd = f'cd {project_path} && {package_manager} run build' public.ExecShell(build_cmd) progress = 85 self.write_deploy_log(step_index=step_index, ps="Preparing project configuration...", progress=progress, status=0) # 校验脚手架是否成功创建项目目录 if not os.path.isdir(project_path): raise ValueError(f"Scaffolding failed: directory {project_path} does not exist.") if template in ('express', 'fastify'): # Express / Fastify -> PM2 managed self._pm2_deploy(template, project_name, project_path, version, package_manager, port, param, step_index, service_count, progress) else: # NestJS/Next.js/Astro via npm scripts package_file = os.path.join(project_path, 'package.json') scripts = {} if os.path.exists(package_file): scripts = json.loads(public.readFile(package_file)).get('scripts', {}) if 'dev' in scripts: project_script = 'dev' elif 'start' in scripts: project_script = 'start' else: project_script = next(iter(scripts), 'start') # Astro needs XDG_CONFIG_HOME to avoid www user ~/.config permission errors if template == 'astro': xdg_dir = os.path.join(project_path, '.config') os.makedirs(xdg_dir, mode=0o755, exist_ok=True) public.ExecShell('chown -R www:www {}'.format(xdg_dir)) project_script = 'env XDG_CONFIG_HOME={} npm run {}'.format(xdg_dir, project_script) # Build get object get = public.dict_obj() get.project_name = project_name get.project_cwd = project_path get.project_script = project_script get.project_ps = f'{template} project' get.bind_extranet = 1 if param.get('domain') else 0 get.domains = [f"{param['domain']}:80"] if param.get('domain') else [] get.is_power_on = 1 get.run_user = 'www' get.max_memory_limit = 2048 get.nodejs_version = version get.port = port get.project_file = '' # Create via nodejsModel from projectModelV2.nodejsModel import main as nodejsModel project_model = nodejsModel() res = project_model.create_project(get) if res['status'] != 0: raise ValueError(f"Project creation failed: {res['message'].get('error_msg', res['message'])}") access_url = f"http://{param['domain']}" if param.get('domain') else f"http://YOUR_SERVER_IP" res = { "version": version, "package_manager": package_manager, "template": template, "project_name": project_name, "project_path": project_path, "project_script": project_script, "port": port, "access_url": access_url, } self.write_deploy_log(step_index=step_index, ps="Node.js project deployed successfully!", progress=100, status=1, result=res) except Exception as ex: import traceback traceback.print_exc() self.write_deploy_log(step_index=step_index, ps="An error has occurred.", progress=progress, status=-1, error_msg=str(ex)) finally: public.progress_release_lock(self.lock_file) def _pm2_deploy(self, template: str, project_name: str, project_path: str, version: str, package_manager: str, port: str, param: dict, step_index: int, service_count: int, progress: int): """Express / Fastify via PM2 deployed""" from mod.project.nodejs.pm2Mod import main as pm2Mod class _MockWS: """Mock WebSocket: discards PM2 progress updates; raises on error""" def __init__(self): self._error = None def send(self, data: str): try: msg = json.loads(data) if not msg.get('status', True): self._error = RuntimeError( msg.get('msg', 'PM2 creation failed')) except Exception: pass def close(self): if self._error: raise self._error get = public.dict_obj() get._ws = _MockWS() get.def_name = 'guide_page_nodejs' get.project_type = 'pm2' get.project_name = project_name get.project_cwd = project_path get.project_file = os.path.join(project_path, 'app.js') get.nodejs_version = version get.run_user = 'www' get.is_power_on = True get.max_memory_limit = 2048 get.cluster = 1 get.watch = False get.pkg_manager = package_manager get.not_install_pkg = True get.env = 'PORT={}'.format(port) get.project_args = '' get.port = port get.project_ps = '{} project'.format(template) get.bind_extranet = 1 if param.get('domain') else 0 get.domains = [f"{param['domain']}:80"] if param.get('domain') else [] get.release_firewall = False pm2_obj = pm2Mod() pm2_obj.create_project(get) access_url = f"http://{param['domain']}" if param.get('domain') else f"http://YOUR_SERVER_IP" self.write_deploy_log( step_index=step_index, ps="Node.js project deployed successfully!", progress=100, status=1, result={ "version": version, "package_manager": package_manager, "template": template, "project_name": project_name, "project_path": project_path, "port": port, "access_url": access_url, }) def _write_entry_file(self, project_path: str, framework: str, port: str): """为 Express/Fastify 生成入口文件""" if framework == 'express': content = f"""const express = require('express') const app = express() const PORT = process.env.PORT || {port} app.get('/', (req, res) => {{ res.json({{ hello: 'express' }}) }}) app.listen(PORT, '0.0.0.0', () => {{ console.log('server running at http://0.0.0.0:' + PORT) }}) """ elif framework == 'fastify': content = f"""const fastify = require('fastify')() const PORT = process.env.PORT || {port} fastify.get('/', async (request, reply) => {{ return {{ hello: 'fastify' }} }}) fastify.listen({{ port: PORT, host: '0.0.0.0' }}, (err) => {{ if (err) throw err console.log('server running at http://0.0.0.0:' + PORT) }}) """ else: return public.writeFile(os.path.join(project_path, 'app.js'), content) def _patch_nestjs_main(self, project_path: str, port: str): """Patch NestJS main.ts: app.listen(3000) → app.listen(process.env.PORT || port)""" main_ts = os.path.join(project_path, 'src', 'main.ts') if not os.path.exists(main_ts): return content = public.readFile(main_ts) if not content: return if 'process.env.PORT' not in content: content = content.replace('app.listen(3000)', 'app.listen(process.env.PORT || {})'.format(port)) public.writeFile(main_ts, content) def _scaffold_astro(self, project_path: str, port: str): """手动创建 Astro 项目结构(绕过 create-astro 的 GitHub 联网校验)""" os.makedirs(project_path, mode=0o755, exist_ok=True) # package.json public.writeFile(os.path.join(project_path, 'package.json'), json.dumps({ "name": os.path.basename(project_path), "type": "module", "version": "0.0.1", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview" }, "dependencies": { "astro": "^5.0.0" } }, indent=2)) # astro.config.mjs public.writeFile(os.path.join(project_path, 'astro.config.mjs'), 'import { defineConfig } from \'astro/config\';\n' 'export default defineConfig({\n' ' server: { host: "0.0.0.0", port: ' + port + ' }\n' '});\n' ) # tsconfig.json public.writeFile(os.path.join(project_path, 'tsconfig.json'), json.dumps({ "extends": "astro/tsconfigs/strict" }, indent=2)) # src/pages/index.astro pages_dir = os.path.join(project_path, 'src', 'pages') os.makedirs(pages_dir, mode=0o755, exist_ok=True) public.writeFile(os.path.join(pages_dir, 'index.astro'), '---\n' '---\n' '\n' ' Astro\n' '

Hello, Astro!

\n' '\n' ) def _get_scaffold_tasks(self, template: str, package_manager: str, project_name: str, project_path: str) -> tuple: """ 根据框架类型返回脚手架命令列表 @return: (description, shell_command) """ if template == 'express': return ( "Initializing Express project...", f"cd {project_path} && npm init -y && npm pkg set scripts.start=\"node app.js\" && npm install express" ) elif template == 'fastify': return ( "Initializing Fastify project...", f"cd {project_path} && npm init -y && npm pkg set scripts.start=\"node app.js\" && npm install fastify" ) elif template == 'nestjs': return ( "Scaffolding NestJS project...", f"cd /www/wwwroot && npx -y @nestjs/cli new {project_name} --package-manager {package_manager} --skip-git" ) elif template == 'nextjs': return ( "Scaffolding Next.js project...", f"cd /www/wwwroot && npx -y create-next-app@latest {project_name} --yes" ) elif template == 'astro': return ( "Scaffolding Astro project...", f"cd /www/wwwroot && npx -y create-astro@latest {project_name} -- --template basics --install --no-git --skip-houston" ) else: raise ValueError(f"Unsupported template: {template}") # proxy项目部署 def install_proxy_project(self, param, started=None): service_count = self._init_log_structure('proxy', param) thread_id = threading.get_ident() public.writeFile(self.lock_file, str(thread_id)) if started: started.set() step_index = 0 progress = 0 try: # 安装依赖服务 ok = self.install_plugin('proxy', param, step_index) if not ok: return step_index = service_count progress = 55 self.write_deploy_log(step_index=step_index, ps="Start creating the reverse proxy project...", progress=progress, status=0) from mod.project.proxy.comMod import main obj = main() dict_obj = { 'domains' : param['domain'], 'proxy_pass' : param['proxy_pass'], 'proxy_type': 'true', 'proxy_host': param['proxy_host'], 'keepuri' : 1, 'remark' : param['domain'], } result = obj.create(public.to_dict_obj(dict_obj)) if result.get('status') != 0: raise ValueError(f"Failed to create the website: {result['message'].get('result')}") s_id = public.M('sites').where('name=?', (param['domain'],)).field('id,name').find() if not s_id: raise ValueError(f"Failed to create the website: The website is gone.") progress = 85 self.write_deploy_log(step_index=step_index, ps="Start creating the reverse proxy project...", progress=progress, status=0) # 统一阻塞2秒 time.sleep(2) # SSL申请失败,不影响项目创建 if param['ssl_auto'] == '1': try: from ssl_domainModelV2.service import smart_ssl smart_ssl(int(s_id['id'])) except: pass # 设置缓存 proxy_cache = False if param['proxy_cache'] == '1': dict_obj = { 'site_name': s_id['name'], 'cache_status': 1, 'expires': '1d', } res = obj.set_global_cache(public.to_dict_obj(dict_obj)) if res['status'] == 0: proxy_cache = True result = { 'domains' : param['domain'], 'proxy_pass' : param['proxy_pass'], 'proxy_cache' : proxy_cache, 'proxy_host': param['proxy_host'] } self.write_deploy_log(step_index=step_index, ps="Start creating the reverse proxy project...", progress=progress, status=1,result=result) except Exception as ex: import traceback traceback.print_exc() self.write_deploy_log(step_index=step_index, ps="An error has occurred.", progress=progress, status=-1,error_msg=str(ex)) finally: public.progress_release_lock(self.lock_file) # 获取进度日志 def get_general_progress(self, get = None): from panel_site_v2 import panelSite return panelSite().get_general_progress(public.to_dict_obj({'type':'guide_page'})) # 写入日志 def write_deploy_log(self, step_index: int, ps: str, progress: int = 0, status: int = 0, error_msg: str = "", result = None): """ 按步骤更新部署日志(与 _init_log_structure 结构对齐) @param step_index: 步骤索引(0-based) @param ps: 当前步骤的描述信息 @param progress: 整体进度百分比字符串(例如 '10', '50', '100') @param status: 步骤状态码(0进行中,1完成,-1错误) @param error_msg: 错误信息(仅在 status=-1 时传入) """ try: # 读取现有日志,不存在则初始化 try: log_data = json.loads(public.readFile(self.status_file)) except: log_data = {'status': 0, 'progress': 0, 'steps': []} # 校验步骤索引有效性 if 0 <= step_index < len(log_data.get('steps', [])): log_data['steps'][step_index]['status'] = status log_data['steps'][step_index]['ps'] = str(ps) log_data['steps'][step_index]['error'] = str(error_msg) # 当前步骤进行中时,将下一个未开始的步骤标记为进行中(避免覆盖已有内容的步骤) if status == 1 and step_index + 1 < len(log_data['steps']): if log_data['steps'][step_index + 1].get('status') == 2: log_data['steps'][step_index + 1]['status'] = 0 # 更新整体进度 log_data['progress'] = progress # 如果某个步骤失败,标记整体状态为失败 if status == -1: log_data['status'] = -1 if result: log_data['result'] = result # 所有步骤完成时,标记整体状态为成功 if all(s.get('status') == 1 for s in log_data.get('steps', [])) and result: log_data['status'] = 1 log_data['progress'] = 100 public.writeFile(self.status_file, json.dumps(log_data, ensure_ascii=False)) if result or status == -1: time.sleep(3) except: pass # 初始化日志结构 def _init_log_structure(self, install_type, param=None): structure_list = { 'status': 0, 'progress': 0, 'result': {}, 'type': install_type } # 1. 按实际依赖生成服务安装步骤 install_list = self._init_plugin(install_type, param or {}) steps = [] for soft in install_list: title = 'Install {}-{}'.format(soft['display_name'], soft['version']) if soft['name'] == 'nginx': title = 'Install {}'.format(soft['display_name']) steps.append({ 'status': 2, 'error': '', 'ps': '', 'title': title }) service_count = len(steps) # 2. 追加业务步骤 if install_type == 'wp': # steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Parameter environment verification"}) steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Creating the website and database"}) steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Initialize WordPress"}) elif install_type == 'php': db_title = "Creating the website and database" if param and param.get('database') in ['mysql', 'mariadb'] else "Creating the website" steps.append({'status': 2, 'error': '', 'ps': '', 'title': db_title}) elif install_type == 'proxy': steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Create a reverse proxy project"}) elif install_type == 'nodejs': steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Installing Node.js and modules"}) steps.append({'status': 2, 'error': '', 'ps': '', 'title': "Create Nodejs project"}) structure_list['steps'] = steps public.writeFile(self.status_file, json.dumps(structure_list)) return service_count # 简易版插件安装状态判断 def get_plugin_status(self, plugin_name: str, version: str = '') -> bool: try: plugin_name = str(plugin_name).strip().lower() version = str(version).strip().lower() if not plugin_name: return False if plugin_name == 'mariadb': plugin_name = 'mysql' soft_list = json.loads(public.readFile('data/softList.conf')) root_path = public.GetConfigValue('root_path') for soft in soft_list: soft_name = str(soft.get('name', '')).strip().lower() if soft_name != plugin_name: continue if soft_name == 'php': check_file = root_path + '/' + soft.get('check', '').replace('VERSION', version.replace('.', '')) else: check_file = root_path + '/' + soft.get('check', '') if os.path.exists(check_file): return True return False except Exception as ex: print(ex) return False # 服务依赖安装 def install_plugin(self, install_type, param, step_index=0) -> bool: install_list = self._init_plugin(install_type, param) progress = 5 progress_step = math.ceil(50 / len(install_list)) cur_step = step_index try: for idx, soft in enumerate(install_list): cur_step = step_index + idx s_name = soft['name'] version = soft['version'] display_name = soft['display_name'] log_name = display_name if s_name == 'nginx' else f"{display_name}-{version}" self.write_deploy_log(step_index=cur_step, ps=f"Installation of {log_name} is in progress.", progress=progress, status=0) if self.get_plugin_status(s_name, version): self.write_deploy_log(step_index=cur_step, ps=f"{log_name} installation successful!", progress=progress, status=1) progress += progress_step continue if public.get_webserver() in ['apache', 'openlitespeed'] and s_name == 'nginx': progress += progress_step self.write_deploy_log(step_index=cur_step, ps=f"{log_name} installation successful!", progress=progress, status=1) continue # 同步安装 if soft.get('install_tmp_path', ''): tmp_path = soft['install_tmp_path'] self.write_deploy_log(step_index=cur_step, ps=f"Downloading {log_name} package...", progress=progress, status=0) zip_file = self._download_plugin_zip(s_name, version, tmp_path) if not zip_file: raise Exception(f"Failed to download {log_name} installation package.") public.extract_archive_to_target(zip_file, tmp_path) if os.path.exists(os.path.join(tmp_path, s_name+'.zip')): public.ExecShell(f"rm -rf {os.path.join(tmp_path, s_name+'.zip')}") from panel_plugin_v2 import panelPlugin obj = panelPlugin() ok = obj.input_zip(public.to_dict_obj({'plugin_name': s_name, 'tmp_path': tmp_path})) if ok['status'] != 0: raise Exception(f"The installation of {log_name} failed.") else: ok = self.add_soft_install_task(s_name,version) if not ok: raise Exception(f"The installation task for {log_name} failed. Please try again!") time.sleep(3) time_out = 0 install = False while time_out < 900: res = public.M('tasks').where("status!=? and name LIKE ?", ('1', '%' + s_name + '%')).count() if not res and self.get_plugin_status(s_name, version): install = True break elif not res: break time.sleep(2) time_out += 2 if not install: if time_out >= 900: raise Exception(f"{log_name} installation has timed out. Please try again later or consider skipping the boot process.") raise Exception(f"The installation of {log_name} failed.") progress += progress_step self.write_deploy_log(step_index=cur_step, ps=f"{log_name} installation successful!", progress=progress, status=1) return True except Exception as e: self._clean_tasks() self.write_deploy_log(step_index=cur_step, ps=f"Error occurred while installing the dependent service.", progress=progress, status=-1, error_msg=str(e)) return False # 仅清理tasks表和残留安装进程,不重启面板(用于异步线程内) def _clean_tasks(self) -> bool: try: tasks = public.M('tasks').where('status!=?', ('1',)).field('id,status,name').select() if not isinstance(tasks, list): return True has_running = any(str(task.get('status')) == '-1' for task in tasks) for task in tasks: public.M('tasks').delete(task['id']) if has_running: public.ExecShell("kill `ps -ef |grep 'install_soft.sh'|grep -v grep|grep -v panelExec|awk '{print $2}'`") public.ExecShell(''' pids=`ps aux | grep 'sh'|grep -v grep|grep install|awk '{print $2}'` arr=($pids) for p in ${arr[@]} do kill -9 $p done ''') public.writeFile('/tmp/panelTask.pl', 'True') return True except Exception as ex: return False # 简易版插件安装,直接写入task,不通过云端 def add_soft_install_task(self, s_name: str, version: str) -> bool: try: s_name = str(s_name).strip().lower() version = str(version).strip() if not s_name or not version: raise ValueError('Missing software name or version') if s_name == 'mariadb': s_name = 'mysql' if s_name == 'php': version = version.replace('.', '') if public.get_webserver() == 'openlitespeed' and s_name == 'php': s_name = 'php-ols' install_type = '4' if os.path.exists('/usr/bin/apt-get') else '1' public.writeFile('/var/bt_setupPath.conf', '/www') if not public.M('tasks').where("status!=? and name LIKE ?", ('1', '%' + s_name + '%')).count(): panel_install_path = public.GetConfigValue('setup_path') + '/panel/install' execstr = "cd {} && /bin/bash install_soft.sh {} install {} {}".format( panel_install_path, install_type, s_name, version ) public.M('tasks').add( 'id,name,type,status,addtime,execstr', ( None, 'Install[{}-{}]'.format(s_name, version), 'execshell', '-1', time.strftime('%Y-%m-%d %H:%M:%S'), execstr ) ) public.writeFile('/tmp/panelTask.pl', 'True') return True except: return False # 从官方API同步下载插件安装包 def _download_plugin_zip(self, s_name: str, version: str, tmp_path: str) -> str: """ 参照 panel_plugin_v2.__download_plugin 流程,从同步插件API下载zip包 @return: zip文件路径,失败返回空字符串 """ try: import requests download_url = public.SyncPluginOfficialApiBase() + '/api/panel/download_plugin' pdata = public.get_user_info() pdata['name'] = s_name pdata['version'] = version pdata['os'] = 'Linux' pdata['environment_info'] = json.dumps(public.fetch_env_info(), ensure_ascii=False) if not os.path.exists(tmp_path): os.makedirs(tmp_path, mode=0o755, exist_ok=True) zip_file = os.path.join(tmp_path, '{}.zip'.format(s_name)) download_res = requests.post( download_url, pdata, headers=public.get_requests_headers(), timeout=(60, 1800), stream=True ) download_res.raise_for_status() total_size = int(download_res.headers.get('File-size', 0)) chunk_size = 1024 * 1024 if total_size > 1024 * 1024 * 5 else 8192 with open(zip_file, 'wb') as f: for chunk in download_res.iter_content(chunk_size=chunk_size): if chunk: f.write(chunk) # MD5 校验 content_md5 = download_res.headers.get('Content-md5', '') if content_md5 and public.FileMd5(zip_file) != content_md5: os.remove(zip_file) raise Exception(f"{s_name}-{version} package hash verification failed") return zip_file except Exception as ex: import traceback traceback.print_exc() return str(ex) # 安装nodejs版本并附加PM2、指定包管理器模块 def _install_nodejs_with_modules(self, version: str, package_manager: str = 'npm') -> dict: """ 辅助函数:安装指定Node.js版本,随后安装PM2和指定包管理器并设为默认 @param version: Node.js版本号,如 'v22.22.3' @param package_manager: 包管理器名称,支持 npm/yarn/pnpm @return: {'status': bool, 'error_msg': str, 'data': mixed} """ try: # 动态加载nodejs插件 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': {}} # 跳过引导 def close_guide(self, get=None): path = os.path.join(public.get_panel_path(), 'data', 'guide_marking.pl') public.writeFile(path, 'close') return public.return_message(0, 0, public.lang("Operation successful.")) # 保存配置 def save_config(self,install_type, param): save = { 'install_type':install_type, 'param': param } public.writeFile(self.guide_config, json.dumps(save, ensure_ascii=False)) return def get_save_config(self, get=None): param = {} if os.path.exists(self.guide_config): try: param = json.loads(public.readFile(self.guide_config)) except: pass param['status'] = 1 progress = self.get_general_progress() if progress['status'] == 0: param['status'] = progress['message']['status'] return public.return_message(0, 0, param)