feat: 优化发布清单和安装结构自动识别

This commit is contained in:
2026-07-15 06:33:33 +00:00
parent 0548692db0
commit 4df439b5ce
28 changed files with 1702 additions and 198 deletions
+51 -11
View File
@@ -37,6 +37,41 @@ PUBLISH_MAX_FILES = int(os.getenv("PUBLISH_MAX_FILES", "20000"))
PUBLISH_MAX_FIELDS = int(os.getenv("PUBLISH_MAX_FIELDS", str(PUBLISH_MAX_FILES + 100)))
def normalize_install_root(raw_value: str) -> str:
value = str(raw_value or os.getenv("CLIENT_INSTALL_ROOT", "..")).strip().replace(chr(92), "/")
if value in ("", "."):
return "."
if value == "..":
return ".."
raise HTTPException(status_code=400, detail="install_root 目前仅支持 . 或 ..")
def normalize_publish_executable(raw_value: str) -> str:
value = str(raw_value or "").strip().replace(chr(92), "/").strip("/")
if not value:
value = RELEASE_MAIN_EXECUTABLE.rsplit("/", 1)[-1]
if TARGET_PLATFORM == "linux" and value.casefold().endswith(".exe"):
value = value[:-4]
elif TARGET_PLATFORM == "windows":
parts = value.split("/")
leaf = parts[-1]
if "." not in leaf:
leaf += ".exe"
parts[-1] = leaf
value = "/".join(parts)
return normalize_relative_path(value)
def release_main_executable_for_install(install_root: str, main_executable: str) -> str:
normalized_install_root = normalize_install_root(install_root)
normalized_main = normalize_publish_executable(main_executable)
if normalized_install_root == ".":
return normalized_main
if normalized_main.casefold().startswith("bin/"):
return normalized_main
return normalize_relative_path(f"bin/{normalized_main}")
def normalize_relative_path(raw_path: str) -> str:
if not isinstance(raw_path, str):
raise HTTPException(status_code=400, detail="文件相对路径无效")
@@ -256,7 +291,7 @@ def extract_release_archive(archive_path: Path, filename: str, extract_dir: Path
raise HTTPException(status_code=400, detail="压缩发布包仅支持 zip、tar.gz、tgz、tar.bz2、tbz2、rar")
def release_items_from_extracted_dir(extract_dir: Path):
def release_items_from_extracted_dir(extract_dir: Path, required_main_executable: str):
items = []
for path in extract_dir.rglob("*"):
if path.is_symlink() or not path.is_file():
@@ -265,13 +300,13 @@ def release_items_from_extracted_dir(extract_dir: Path):
if should_skip_release_path(rel_path):
continue
items.append({"path": rel_path, "local_path": path})
return normalize_release_item_roots(items)
return normalize_release_item_roots(items, required_main_executable)
def normalize_release_item_roots(items: list[dict]):
def normalize_release_item_roots(items: list[dict], required_main_executable: str):
if not items:
return items
required_main_key = RELEASE_MAIN_EXECUTABLE.casefold()
required_main_key = required_main_executable.casefold()
lower_paths = [item["path"].casefold() for item in items]
if required_main_key in lower_paths:
return items
@@ -298,7 +333,7 @@ def normalize_release_item_roots(items: list[dict]):
return normalized
def validate_release_items(items: list[dict]):
def validate_release_items(items: list[dict], required_main_executable: str):
if not items:
raise HTTPException(status_code=400, detail="未选择任何文件,请选择程序文件夹或压缩发布包")
if len(items) > PUBLISH_MAX_FILES:
@@ -322,15 +357,15 @@ def validate_release_items(items: list[dict]):
if not filtered:
raise HTTPException(status_code=400, detail="发布内容为空;请确认发布目录或压缩包中包含程序文件")
required_main_key = RELEASE_MAIN_EXECUTABLE.casefold()
required_main_key = required_main_executable.casefold()
if required_main_key not in seen_paths:
nested_main = next((path for path in seen_paths if path.endswith("/" + required_main_key)), None)
if nested_main:
raise HTTPException(
status_code=400,
detail=f"{RELEASE_MAIN_EXECUTABLE} 位于更深层目录,请选择包含该相对路径的正确发布根目录。当前发现: {nested_main}",
detail=f"{required_main_executable} 位于更深层目录,请选择包含该相对路径的正确发布根目录。当前发现: {nested_main}",
)
raise HTTPException(status_code=400, detail=f"发布目录中缺少 {RELEASE_MAIN_EXECUTABLE}")
raise HTTPException(status_code=400, detail=f"发布目录中缺少 {required_main_executable}")
nested_main = next((path for path in seen_paths if path.endswith("/" + required_main_key)), None)
if nested_main:
raise HTTPException(
@@ -461,6 +496,9 @@ async def collect_publish_items(request: Request):
app_id = form.get("app_id")
channel = form.get("channel") or ""
version = form.get("version")
install_root = normalize_install_root(str(form.get("install_root") or ""))
main_executable = str(form.get("main_executable") or "")
required_main_executable = release_main_executable_for_install(install_root, main_executable)
try:
client_protocol = int(form.get("client_protocol") or 2)
except (TypeError, ValueError):
@@ -526,8 +564,8 @@ async def collect_publish_items(request: Request):
extract_dir = archive_temp_dir / "extracted"
extract_dir.mkdir(parents=True, exist_ok=True)
extract_release_archive(archive_path, archive_name, extract_dir)
raw_items = release_items_from_extracted_dir(extract_dir)
upload_items = validate_release_items(raw_items)
raw_items = release_items_from_extracted_dir(extract_dir, required_main_executable)
upload_items = validate_release_items(raw_items, required_main_executable)
extracted_total = sum(item["local_path"].stat().st_size for item in upload_items)
check_extracted_publish_limits(extracted_total, len(upload_items))
else:
@@ -540,7 +578,7 @@ async def collect_publish_items(request: Request):
for index, file in enumerate(files):
raw_path = relative_paths[index] if relative_paths else file.filename
raw_items.append({"upload": file, "path": str(raw_path)})
upload_items = validate_release_items(raw_items)
upload_items = validate_release_items(raw_items, required_main_executable)
except Exception:
if archive_upload is not None:
await close_upload_safely(archive_upload)
@@ -552,6 +590,8 @@ async def collect_publish_items(request: Request):
"channel": str(channel),
"version": str(version),
"client_protocol": client_protocol,
"install_root": install_root,
"required_main_executable": required_main_executable,
"upload_items": upload_items,
"archive_upload": archive_upload,
"archive_temp_dir": archive_temp_dir,