mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-28 20:34:52 +02:00
Update to v8.21.0
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
# 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 {},
|
||||
}
|
||||
Reference in New Issue
Block a user