Update to v8.21.0

This commit is contained in:
aapanel.com
2026-08-13 18:24:50 +08:00
parent 403317be61
commit 07c7e75ec6
285 changed files with 18063 additions and 5697 deletions
@@ -0,0 +1,6 @@
# coding: utf-8
from .importer import DatabaseImporter
__all__ = ["DatabaseImporter"]
@@ -0,0 +1,97 @@
# coding: utf-8
import os
import re
import shutil
import tempfile
def update_database_config(project_root, project_type, database):
updated = []
if project_type == "php" and os.path.isfile(os.path.join(project_root, "wp-config.php")):
path = os.path.join(project_root, "wp-config.php")
replacements = {
"DB_NAME": database["name"],
"DB_USER": database["user"],
"DB_PASSWORD": database["password"],
"DB_HOST": database.get("host", "127.0.0.1"),
}
_replace_wp_config(path, replacements)
updated.append(path)
elif project_type == "php" and os.path.isfile(os.path.join(project_root, "artisan")):
path = os.path.join(project_root, ".env")
if os.path.isfile(path):
replacements = {
"DB_HOST": database.get("host", "127.0.0.1"),
"DB_DATABASE": database["name"],
"DB_USERNAME": database["user"],
"DB_PASSWORD": database["password"],
}
_replace_env(path, replacements)
updated.append(path)
return updated
def _backup(path):
backup = path + ".aapanel-import.bak"
if not os.path.exists(backup):
shutil.copy2(path, backup)
return backup
def _atomic_text_write(path, content):
fd, tmp_path = tempfile.mkstemp(prefix=".project_import_", dir=os.path.dirname(path))
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(content)
handle.flush()
os.fsync(handle.fileno())
_set_web_owner(tmp_path)
os.replace(tmp_path, path)
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def _set_web_owner(path):
"""让写入的配置文件归面板 web 用户(www)所有,并保证其可读。
mkstemp 生成的文件属主为 root 且权限为 0600,PHP-FPM 以 www 用户运行,
不修正会导致读取配置时 Permission denied。
"""
import public
try:
public.set_own(path, "www", "www")
os.chmod(path, 0o640)
except Exception:
pass
def _replace_wp_config(path, replacements):
_backup(path)
with open(path, "r", encoding="utf-8", errors="ignore") as handle:
content = handle.read()
for key, value in replacements.items():
pattern = re.compile(r"(define\(\s*['\"]{}['\"]\s*,\s*)['\"][^'\"]*['\"](\s*\)\s*;)".format(re.escape(key)))
content = pattern.sub(lambda match: "{}'{}'{}".format(match.group(1), str(value).replace("'", "\\'"), match.group(2)), content)
_atomic_text_write(path, content)
def _replace_env(path, replacements):
_backup(path)
with open(path, "r", encoding="utf-8", errors="ignore") as handle:
lines = handle.read().splitlines()
output = []
found = set()
for line in lines:
key = line.split("=", 1)[0].strip() if "=" in line else ""
if key in replacements:
output.append("{}={}".format(key, replacements[key]))
found.add(key)
else:
output.append(line)
for key, value in replacements.items():
if key not in found:
output.append("{}={}".format(key, value))
_atomic_text_write(path, "\n".join(output) + "\n")
@@ -0,0 +1,140 @@
# coding: utf-8
import gzip
import os
import shutil
import subprocess
import tempfile
from ..core.exceptions import ProjectImportError
from ..core.security import ensure_child_path
from .config_updater import update_database_config
class DatabaseImporter:
def __init__(self, config, session, project_result, project_path, progress, cancelled):
self.config = config or {}
self.session = session
self.project_result = project_result or {}
self.project_path = project_path
self.progress = progress
self.cancelled = cancelled
def run(self):
if not self.config.get("enabled"):
return {"enabled": False}
import public
from database_v2 import database as DatabaseModel
name = str(self.config.get("database_name", "")).strip().lower()
user = str(self.config.get("database_user", name)).strip().lower()
password = str(self.config.get("database_password", ""))
charset = str(self.config.get("charset", "utf8mb4"))
if not name or not user or not password:
raise ProjectImportError("Database name, user and password are required", "DATABASE_CONFIG_REQUIRED")
# 在建库前先定位SQL文件 文件缺失直接失败
sql_path = self._resolve_sql_file()
args = public.to_dict_obj({
"name": name,
"db_user": user,
"codeing": charset,
"password": password,
"sid": 0,
"active": True,
"address": "127.0.0.1",
"ps": str(self.project_result.get("project_name", "Imported project")),
"dtype": "MySQL",
"pid": int(self.project_result.get("site_id", 0) or 0),
})
result = DatabaseModel().AddDatabase(args)
status = result.get("status") if isinstance(result, dict) else None
# 注意:success 时 status 为 0(return_message(0, ...)),不能写成
# `status in (-1, False)`——Python 中 0 == False,会把成功误判为失败。
if not isinstance(result, dict) or status is False or status == -1:
message = result.get("message", result) if isinstance(result, dict) else result
if isinstance(message, dict) and "result" in message:
message = message["result"]
raise ProjectImportError("Failed to create database: {}".format(message), "DATABASE_CREATE_FAILED")
if sql_path:
self._stream_sql(sql_path, name)
database_id = public.M("databases").where("name=?", (name,)).getField("id") or 0
updated = []
if self.config.get("update_project_config"):
updated = update_database_config(
self.project_path,
str(self.project_result.get("project_type", "")),
{"name": name, "user": user, "password": password, "host": "127.0.0.1"},
)
return {
"enabled": True,
"database_id": int(database_id),
"database_name": name,
"database_user": user,
"config_updated": [os.path.relpath(item, self.project_path).replace("\\", "/") for item in updated],
}
def _resolve_sql_file(self):
"""根据 sql_file_path 定位要导入的 SQL/SQL.GZ 文件。
扫描到的文件(analysis.sql_files[].path)和用户上传到服务器指定目录的
文件都以实际服务端路径传入;相对路径则相对项目根目录解析(限制在根目录内)。
"""
configured = str(self.config.get("sql_file_path", "")).strip()
if not configured:
return ""
if not os.path.isabs(configured):
root = self.session.get("internal", {}).get("project_root", "")
if not root:
return ""
configured = ensure_child_path(root, os.path.join(root, configured))
path = os.path.realpath(configured)
if os.path.isfile(path):
return path
raise ProjectImportError(
"The selected SQL file does not exist on the server: {}".format(configured),
"SQL_FILE_NOT_FOUND",
)
def _stream_sql(self, sql_path, database_name):
import public
mysql_bin = public.get_mysql_bin()
if not mysql_bin or not os.path.isfile(mysql_bin):
mysql_bin = shutil.which("mysql")
if not mysql_bin:
raise ProjectImportError("MySQL client was not found", "MYSQL_CLIENT_NOT_FOUND")
root_password = public.M("config").where("id=?", (1,)).getField("mysql_root") or ""
fd, defaults_file = tempfile.mkstemp(prefix="project_import_mysql_", suffix=".cnf")
os.close(fd)
try:
with open(defaults_file, "w", encoding="utf-8") as handle:
handle.write("[client]\nuser=root\npassword={}\ndefault-character-set=utf8mb4\n".format(root_password))
os.chmod(defaults_file, 0o600)
command = [mysql_bin, "--defaults-extra-file={}".format(defaults_file), database_name]
process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
total = max(1, os.path.getsize(sql_path))
source = gzip.open(sql_path, "rb") if sql_path.lower().endswith(".gz") else open(sql_path, "rb")
try:
while True:
self.cancelled()
chunk = source.read(1024 * 1024)
if not chunk:
break
process.stdin.write(chunk)
position = source.fileobj.tell() if hasattr(source, "fileobj") else source.tell()
self.progress(min(0.99, position / total), "Importing database")
process.stdin.close()
stderr = process.stderr.read().decode("utf-8", errors="ignore") if process.stderr else ""
code = process.wait()
if code != 0:
raise ProjectImportError("Database import failed: {}".format(stderr[-2000:]), "DATABASE_IMPORT_FAILED")
self.progress(1, "Database import completed")
finally:
source.close()
if process.poll() is None:
process.terminate()
finally:
try:
os.remove(defaults_file)
except OSError:
pass