mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-29 12:54:50 +02:00
Update to v8.21.0
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user