feat: 优化打包产物与历史筛选
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""任务 API"""
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import uuid
|
||||
@@ -164,6 +165,25 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
if task.status in ("running", "pending"):
|
||||
raise HTTPException(status_code=400, detail="任务正在运行中,无法删除")
|
||||
|
||||
# 同一个 App / 版本 / 分支重复打包会覆盖并复用同一个远端文件;
|
||||
# 只要仍有其他历史记录引用该下载地址,就不能删除远端产物。
|
||||
has_shared_artifact = bool(
|
||||
task.oss_url and db.query(Task).filter(
|
||||
Task.id != task.id,
|
||||
Task.oss_url == task.oss_url,
|
||||
).first()
|
||||
)
|
||||
if task.oss_url and not has_shared_artifact:
|
||||
if not task.config_json:
|
||||
raise HTTPException(status_code=400, detail="缺少打包配置快照,无法安全删除远端文件")
|
||||
try:
|
||||
build_config = json.loads(task.config_json)
|
||||
from .config import load_config
|
||||
from ..services.distribution import DistributionError, delete_published_artifacts
|
||||
delete_published_artifacts(build_config, load_config().get("upload", {}))
|
||||
except (json.JSONDecodeError, DistributionError) as exc:
|
||||
raise HTTPException(status_code=502, detail=f"远端产物删除失败,记录未删除:{exc}") from exc
|
||||
|
||||
# 删除日志文件
|
||||
log_path = Path(__file__).parent.parent / "logs" / f"{task_id}.log"
|
||||
if log_path.exists():
|
||||
@@ -178,7 +198,10 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
|
||||
return {"message": "记录已删除"}
|
||||
message = "记录已删除"
|
||||
if task.oss_url:
|
||||
message += "(远端产物已删除)" if not has_shared_artifact else "(远端产物仍被其他记录引用,未删除)"
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@router.get("/{task_id}/log")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import json
|
||||
import plistlib
|
||||
import posixpath
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
@@ -15,7 +16,12 @@ class DistributionError(Exception):
|
||||
|
||||
def _artifact_stem(config: dict) -> str:
|
||||
version = config.get("VERSION", "0").replace(".", "_")
|
||||
return f"{config.get('APPID', 'app')}_{version}"
|
||||
# 历史任务快照没有 SOURCE_BRANCH,继续按旧文件名处理,确保删除旧记录时能清理到原产物。
|
||||
if "SOURCE_BRANCH" not in config:
|
||||
return f"{config.get('APPID', 'app')}_{version}"
|
||||
branch = re.sub(r"[^A-Za-z0-9._-]+", "_", config.get("SOURCE_BRANCH", "main")).strip("._-")
|
||||
build_type = "appstore" if config.get("BUILD_TYPE") == "App_Store" else "adhoc"
|
||||
return f"{config.get('APPID', 'app')}_{version}_{branch or 'main'}_{build_type}"
|
||||
|
||||
|
||||
def _write_distribution_files(config: dict, ipa_path: Path, output_dir: Path) -> tuple[Path, Path, Path]:
|
||||
@@ -70,6 +76,60 @@ def _remote_paths(config: dict) -> tuple[str, str, str, str]:
|
||||
)
|
||||
|
||||
|
||||
def _delete_webdav(config: dict, remote_paths: list[str]):
|
||||
webdav = config.get("webdav", {})
|
||||
server_url = webdav.get("server_url", "").rstrip("/")
|
||||
if not server_url or not webdav.get("username"):
|
||||
raise DistributionError("WebDAV 配置不完整")
|
||||
|
||||
base_path = webdav.get("base_path", "/ios-builds").strip("/")
|
||||
auth = (webdav.get("username", ""), webdav.get("password", ""))
|
||||
with httpx.Client(auth=auth, timeout=120, follow_redirects=True) as client:
|
||||
for relative_path in remote_paths:
|
||||
remote_path = posixpath.join(base_path, relative_path)
|
||||
response = client.delete(f"{server_url}/{remote_path}")
|
||||
# 404 代表文件已不存在,可视为清理完成。
|
||||
if response.status_code not in (200, 202, 204, 404):
|
||||
raise DistributionError(f"WebDAV 删除失败: {relative_path} ({response.status_code})")
|
||||
|
||||
|
||||
def _delete_oss(config: dict, remote_paths: list[str]):
|
||||
oss = config.get("oss", {})
|
||||
required = ["access_key_id", "access_key_secret", "endpoint", "bucket_name"]
|
||||
if any(not oss.get(key) for key in required):
|
||||
raise DistributionError("OSS 配置不完整")
|
||||
try:
|
||||
import oss2
|
||||
except ImportError as exc:
|
||||
raise DistributionError("未安装 oss2,请重新执行 ./deploy.sh build") from exc
|
||||
|
||||
bucket = oss2.Bucket(
|
||||
oss2.Auth(oss["access_key_id"], oss["access_key_secret"]),
|
||||
oss["endpoint"],
|
||||
oss["bucket_name"],
|
||||
connect_timeout=30,
|
||||
)
|
||||
for relative_path in remote_paths:
|
||||
result = bucket.delete_object(relative_path)
|
||||
if result.status // 100 != 2:
|
||||
raise DistributionError(f"OSS 删除失败: {relative_path}")
|
||||
|
||||
|
||||
def delete_published_artifacts(build_config: dict, upload_config: dict):
|
||||
"""删除任务对应的远端分发产物。"""
|
||||
mode = upload_config.get("mode", "")
|
||||
if mode not in {"oss", "webdav"}:
|
||||
raise DistributionError("请选择 OSS 或 WebDAV 上传方式")
|
||||
|
||||
ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config)
|
||||
remote_paths = [ipa_remote]
|
||||
if build_config.get("BUILD_TYPE") != "App_Store":
|
||||
remote_paths.extend([manifest_remote, html_remote, qr_remote])
|
||||
|
||||
deleter = _delete_oss if mode == "oss" else _delete_webdav
|
||||
deleter(upload_config, remote_paths)
|
||||
|
||||
|
||||
def _ensure_webdav_dirs(client: httpx.Client, server_url: str, remote_path: str):
|
||||
path = ""
|
||||
for segment in remote_path.strip("/").split("/")[:-1]:
|
||||
|
||||
Reference in New Issue
Block a user