mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-24 00:22:27 +02:00
389 lines
15 KiB
Python
389 lines
15 KiB
Python
# coding: utf-8
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
from .base import BaseCreator
|
|
from ..core.constants import NODE_RUNTIME_RECOMMENDED, NODE_RUNTIME_VERSIONS, locks_dir
|
|
from ..core.exceptions import ProjectImportError
|
|
from ..core.json_store import file_lock
|
|
|
|
|
|
NODEJS_ROOT = "/www/server/nodejs"
|
|
NODEJS_MANAGER_MAIN = "/www/server/panel/plugin/nodejs/nodejs_main.py"
|
|
NODE_PACKAGE_MANAGERS = ("npm", "yarn", "pnpm")
|
|
|
|
|
|
def get_installed_nodejs_versions(nodejs_root=None, manager_main=None, default_node=None):
|
|
"""Return Node.js runtimes offered by the import wizard.
|
|
|
|
The manager's online version list is intentionally not used here because
|
|
reading the import form must not refresh remote metadata or install
|
|
anything. A version is reported as installed only when its bin/node
|
|
exists. Whitelist versions that are not installed yet are still offered
|
|
as options with ``installed=False`` so the wizard can auto-install them
|
|
later. Optional arguments and environment variables make the filesystem
|
|
scan testable without changing production paths.
|
|
"""
|
|
nodejs_root = nodejs_root or os.environ.get("AAPANEL_NODEJS_ROOT", NODEJS_ROOT)
|
|
manager_main = manager_main or os.environ.get(
|
|
"AAPANEL_NODEJS_MANAGER_MAIN",
|
|
NODEJS_MANAGER_MAIN,
|
|
)
|
|
default_node = default_node or os.environ.get("AAPANEL_DEFAULT_NODE", "/usr/bin/node")
|
|
manager_installed = os.path.isfile(manager_main)
|
|
options = []
|
|
|
|
if manager_installed and os.path.isdir(nodejs_root):
|
|
default_realpath = _existing_realpath(default_node)
|
|
for version in os.listdir(nodejs_root):
|
|
version_root = os.path.join(nodejs_root, version)
|
|
node_bin = os.path.join(version_root, "bin", "node")
|
|
if not os.path.isdir(version_root) or not os.path.isfile(node_bin):
|
|
continue
|
|
options.append({
|
|
"label": "Node.js {}".format(version.lstrip("v")),
|
|
"value": version,
|
|
"installed": True,
|
|
})
|
|
|
|
installed_values = [item["value"] for item in options]
|
|
for version in NODE_RUNTIME_VERSIONS:
|
|
if version in installed_values:
|
|
continue
|
|
options.append({
|
|
"label": "Node.js {}".format(version.lstrip("v")),
|
|
"value": version,
|
|
# "is_default": False,
|
|
"installed": False,
|
|
})
|
|
|
|
options.sort(key=lambda item: _node_version_sort_key(item["value"]), reverse=True)
|
|
|
|
return {
|
|
"manager_installed": manager_installed,
|
|
"versions": [item["value"] for item in options if item.get("installed")],
|
|
"options": options,
|
|
"recommended_version": NODE_RUNTIME_RECOMMENDED,
|
|
"auto_install": True,
|
|
}
|
|
|
|
|
|
def _existing_realpath(path):
|
|
if not path or not (os.path.exists(path) or os.path.islink(path)):
|
|
return ""
|
|
return os.path.realpath(path)
|
|
|
|
|
|
def _same_path(left, right):
|
|
return os.path.normcase(os.path.abspath(left)) == os.path.normcase(os.path.abspath(right))
|
|
|
|
|
|
def _node_version_sort_key(version):
|
|
numbers = [int(item) for item in re.findall(r"\d+", str(version))[:4]]
|
|
numbers.extend([0] * (4 - len(numbers)))
|
|
return tuple(numbers) + (str(version),)
|
|
|
|
|
|
def _normalize_node_version(value):
|
|
version = str(value or "").strip()
|
|
if version and not version.startswith("v"):
|
|
version = "v" + version
|
|
return version
|
|
|
|
|
|
def prepare_node_config(config, project_root, install_runtime=True, progress=None):
|
|
"""Validate Node.js settings and optionally verify the selected runtime."""
|
|
requested = _normalize_node_version(
|
|
config.get("runtime_version", config.get("nodejs_version", ""))
|
|
)
|
|
if not requested:
|
|
raise ProjectImportError(
|
|
"Node.js runtime version is required",
|
|
"NODE_RUNTIME_REQUIRED",
|
|
)
|
|
|
|
inventory = get_installed_nodejs_versions()
|
|
if requested not in NODE_RUNTIME_VERSIONS and requested not in inventory["versions"]:
|
|
raise ProjectImportError(
|
|
"Unsupported Node.js runtime '{}'".format(requested),
|
|
"NODE_RUNTIME_UNSUPPORTED",
|
|
)
|
|
|
|
package_manager = str(config.get("package_manager", "npm") or "npm").strip().lower()
|
|
if package_manager not in NODE_PACKAGE_MANAGERS:
|
|
raise ProjectImportError(
|
|
"Unsupported Node.js package manager: {}".format(package_manager),
|
|
"NODE_PACKAGE_MANAGER_UNSUPPORTED",
|
|
)
|
|
|
|
package_file = os.path.join(project_root, "package.json")
|
|
if not os.path.isfile(package_file):
|
|
raise ProjectImportError("package.json was not found", "NODE_PACKAGE_JSON_REQUIRED")
|
|
try:
|
|
with open(package_file, "r", encoding="utf-8-sig") as handle:
|
|
package_info = json.load(handle)
|
|
except (OSError, ValueError) as exc:
|
|
raise ProjectImportError(
|
|
"Invalid package.json: {}".format(exc),
|
|
"NODE_PACKAGE_JSON_INVALID",
|
|
)
|
|
scripts = package_info.get("scripts", {}) if isinstance(package_info, dict) else {}
|
|
project_script = str(config.get("project_script", config.get("start_script", "start"))).strip()
|
|
if not isinstance(scripts, dict) or project_script not in scripts:
|
|
raise ProjectImportError(
|
|
"The startup script does not exist in package.json: {}".format(project_script),
|
|
"NODE_SCRIPT_NOT_FOUND",
|
|
)
|
|
|
|
config["runtime_version"] = requested
|
|
config["nodejs_version"] = requested
|
|
config["package_manager"] = package_manager
|
|
config["project_script"] = project_script
|
|
if install_runtime:
|
|
prepare_node_runtime(config, progress=progress)
|
|
return config
|
|
|
|
|
|
def prepare_node_runtime(config, progress=None):
|
|
"""Install the selected Node.js runtime and package manager when missing."""
|
|
import public
|
|
|
|
version = _normalize_node_version(
|
|
config.get("runtime_version", config.get("nodejs_version", ""))
|
|
)
|
|
package_manager = str(config.get("package_manager", "npm") or "npm").strip().lower()
|
|
if not version:
|
|
raise ProjectImportError(
|
|
"Node.js runtime version is required",
|
|
"NODE_RUNTIME_REQUIRED",
|
|
)
|
|
if package_manager not in NODE_PACKAGE_MANAGERS:
|
|
raise ProjectImportError(
|
|
"Unsupported Node.js package manager: {}".format(package_manager),
|
|
"NODE_PACKAGE_MANAGER_UNSUPPORTED",
|
|
)
|
|
inventory = get_installed_nodejs_versions()
|
|
if version not in NODE_RUNTIME_VERSIONS and version not in inventory["versions"]:
|
|
raise ProjectImportError(
|
|
"Unsupported Node.js runtime '{}'".format(version),
|
|
"NODE_RUNTIME_UNSUPPORTED",
|
|
)
|
|
|
|
# Serialize concurrent imports so the same version is not installed twice.
|
|
with file_lock(os.path.join(locks_dir(), "node_runtime.install.lock")):
|
|
_report_runtime(progress, 0.05, "Checking Node.js Manager")
|
|
manager = _load_node_manager(public, progress)
|
|
from projectModelV2.nodejsModel import main as NodeModel
|
|
|
|
project_model = NodeModel()
|
|
|
|
node_bin = os.path.join(manager._nodejs_path, version, "bin", "node")
|
|
if not os.path.isfile(node_bin):
|
|
_report_runtime(progress, 0.25, "Installing Node.js {}".format(version))
|
|
try:
|
|
result = manager.install_nodejs(public.to_dict_obj({"version": version}))
|
|
except Exception as exc:
|
|
raise ProjectImportError(
|
|
"Failed to install Node.js {}: {}".format(version, exc),
|
|
"NODE_RUNTIME_INSTALL_FAILED",
|
|
)
|
|
if not _manager_result_ok(result, ("already installed",)):
|
|
raise ProjectImportError(
|
|
BaseCreator.error_message(
|
|
result,
|
|
"Failed to install Node.js {}".format(version),
|
|
),
|
|
"NODE_RUNTIME_INSTALL_FAILED",
|
|
)
|
|
if not os.path.isfile(node_bin):
|
|
raise ProjectImportError(
|
|
"Node.js {} installation completed without a usable node binary".format(version),
|
|
"NODE_RUNTIME_INSTALL_INCOMPLETE",
|
|
)
|
|
|
|
getter = {
|
|
"npm": project_model.get_npm_bin,
|
|
"pnpm": project_model.get_pnpm_bin,
|
|
"yarn": project_model.get_yarn_bin,
|
|
}[package_manager]
|
|
if not getter(version):
|
|
if package_manager == "npm":
|
|
raise ProjectImportError(
|
|
"npm is not available for Node.js {}".format(version),
|
|
"NODE_PACKAGE_MANAGER_NOT_FOUND",
|
|
)
|
|
_report_runtime(
|
|
progress,
|
|
0.75,
|
|
"Installing {} for Node.js {}".format(package_manager, version),
|
|
)
|
|
try:
|
|
result = manager.install_module(public.to_dict_obj({
|
|
"version": version,
|
|
"module": package_manager,
|
|
}))
|
|
except Exception as exc:
|
|
raise ProjectImportError(
|
|
"Failed to install {} for Node.js {}: {}".format(
|
|
package_manager,
|
|
version,
|
|
exc,
|
|
),
|
|
"NODE_PACKAGE_MANAGER_INSTALL_FAILED",
|
|
)
|
|
if not _manager_result_ok(result, ("has been installed", "already installed")):
|
|
raise ProjectImportError(
|
|
BaseCreator.error_message(
|
|
result,
|
|
"Failed to install {} for Node.js {}".format(package_manager, version),
|
|
),
|
|
"NODE_PACKAGE_MANAGER_INSTALL_FAILED",
|
|
)
|
|
if not getter(version):
|
|
raise ProjectImportError(
|
|
"Package manager '{}' is not available for Node.js {}".format(
|
|
package_manager,
|
|
version,
|
|
),
|
|
"NODE_PACKAGE_MANAGER_NOT_FOUND",
|
|
)
|
|
|
|
_report_runtime(progress, 1, "Node.js runtime is ready")
|
|
return version
|
|
|
|
|
|
def _load_node_manager(public, progress=None):
|
|
import importlib
|
|
|
|
plugin_path = public.get_plugin_path("nodejs")
|
|
plugin_main = os.path.join(plugin_path, "nodejs_main.py")
|
|
if not os.path.isfile(plugin_main):
|
|
_report_runtime(progress, 0.1, "Installing Node.js Manager")
|
|
_install_node_manager(public)
|
|
importlib.invalidate_caches()
|
|
try:
|
|
from plugin.nodejs.nodejs_main import nodejs_main
|
|
return nodejs_main()
|
|
except Exception as exc:
|
|
raise ProjectImportError(
|
|
"Failed to load Node.js Manager: {}".format(exc),
|
|
"NODE_MANAGER_LOAD_FAILED",
|
|
)
|
|
|
|
|
|
def _install_node_manager(public):
|
|
"""Install the Node.js Manager package using the setup-wizard package flow."""
|
|
try:
|
|
from panel_guide_page import GuidePage
|
|
from panel_plugin_v2 import panelPlugin
|
|
|
|
guide = GuidePage()
|
|
plugin_name = "nodejs"
|
|
plugin_version = _node_manager_install_version(public)
|
|
temporary_path = os.path.join(public.get_panel_path(), "temp", "nodejs")
|
|
os.makedirs(temporary_path, mode=0o700, exist_ok=True)
|
|
archive_path = guide._download_plugin_zip(
|
|
plugin_name,
|
|
plugin_version,
|
|
temporary_path,
|
|
)
|
|
if not archive_path or not os.path.isfile(archive_path):
|
|
raise ProjectImportError(
|
|
str(archive_path or "Failed to download Node.js Manager"),
|
|
"NODE_MANAGER_DOWNLOAD_FAILED",
|
|
)
|
|
public.extract_archive_to_target(archive_path, temporary_path)
|
|
nested_archive = os.path.join(temporary_path, plugin_name + ".zip")
|
|
if os.path.isfile(nested_archive):
|
|
os.remove(nested_archive)
|
|
result = panelPlugin().input_zip(public.to_dict_obj({
|
|
"plugin_name": plugin_name,
|
|
"tmp_path": temporary_path,
|
|
}))
|
|
if not isinstance(result, dict) or result.get("status") != 0:
|
|
raise ProjectImportError(
|
|
BaseCreator.error_message(result, "Failed to install Node.js Manager"),
|
|
"NODE_MANAGER_INSTALL_FAILED",
|
|
)
|
|
except ProjectImportError:
|
|
raise
|
|
except Exception as exc:
|
|
raise ProjectImportError(
|
|
"Failed to install Node.js Manager: {}".format(exc),
|
|
"NODE_MANAGER_INSTALL_FAILED",
|
|
)
|
|
|
|
|
|
def _node_manager_install_version(public, default="2.3"):
|
|
"""Return the plugin version to download, preferring a deployed info.json.
|
|
"""
|
|
try:
|
|
info_file = os.path.join(public.get_plugin_path("nodejs"), "info.json")
|
|
if os.path.isfile(info_file):
|
|
with open(info_file, "r", encoding="utf-8") as handle:
|
|
info = json.load(handle)
|
|
version = str(info.get("versions", "")).strip()
|
|
if version:
|
|
return version
|
|
except (OSError, ValueError):
|
|
pass
|
|
return default
|
|
|
|
|
|
def _manager_result_ok(result, allowed_messages=()):
|
|
if isinstance(result, dict) and result.get("status") is True:
|
|
return True
|
|
message = BaseCreator.error_message(result, "").lower()
|
|
return any(item in message for item in allowed_messages)
|
|
|
|
|
|
def _report_runtime(progress, ratio, message):
|
|
if progress:
|
|
progress(ratio, message)
|
|
|
|
|
|
|
|
|
|
class NodeCreator(BaseCreator):
|
|
def create(self):
|
|
import public
|
|
from projectModelV2.nodejsModel import main as NodeModel
|
|
|
|
domains = self.domains()
|
|
config = self.config
|
|
args = public.to_dict_obj({
|
|
"project_cwd": self.project_path,
|
|
"project_name": str(config.get("project_name", "imported_node")),
|
|
"project_script": str(config.get("project_script", config.get("start_script", "start"))),
|
|
"port": str(config.get("port", 3000)),
|
|
"run_user": str(config.get("run_user", "www")),
|
|
"nodejs_version": str(config.get("runtime_version", config.get("nodejs_version", ""))),
|
|
"project_ps": str(config.get("project_note", "Imported project")),
|
|
"project_env": str(config.get("project_env", "")),
|
|
"bind_extranet": 1 if domains else 0,
|
|
"domains": [item if ":" in item else item + ":80" for item in domains],
|
|
"is_power_on": int(config.get("is_power_on", 1)),
|
|
"max_memory_limit": int(config.get("max_memory_limit", 0)),
|
|
"pkg_manager": str(config.get("package_manager", "npm")),
|
|
"package_manager": str(config.get("package_manager", "npm")),
|
|
})
|
|
model = NodeModel()
|
|
result = model.create_project(args)
|
|
payload = self.ensure_success(result)
|
|
project_id = public.M("sites").where("name=?", (args.project_name,)).getField("id") or 0
|
|
# 依赖安装与启动由 nodejsModel.create_project 内部完成
|
|
# (见 nodejsModel.py 的 install_packages/start_project 调用),这里不再重复执行
|
|
return {
|
|
"site_id": int(project_id),
|
|
"project_name": args.project_name,
|
|
"domain": domains[0] if domains else "",
|
|
"project_type": "node",
|
|
"path": self.project_path,
|
|
"port": int(args.port),
|
|
"runtime_version": args.nodejs_version,
|
|
"warnings": [],
|
|
"raw": payload if isinstance(payload, dict) else {},
|
|
}
|