- tasks 路由级认证保护,所有 API 需登录 - 未登录时显示登录页面,不暴露导航和功能 - ConfigView 仅管理员可访问,普通用户显示权限提示 - BuildView/HistoryView 所有 API 请求携带 token
276 lines
9.1 KiB
Python
276 lines
9.1 KiB
Python
"""任务 API"""
|
||
import os
|
||
import shutil
|
||
import tempfile
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import List
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from fastapi.responses import FileResponse
|
||
from starlette.background import BackgroundTask
|
||
from sqlalchemy.orm import Session
|
||
|
||
from ..database import get_db
|
||
from ..models import Task
|
||
from ..schemas import TaskCreate, TaskResponse
|
||
from ..deps import get_current_user
|
||
|
||
router = APIRouter(prefix="/api/tasks", tags=["tasks"], dependencies=[Depends(get_current_user)])
|
||
|
||
|
||
@router.post("", response_model=TaskResponse)
|
||
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||
"""创建打包任务"""
|
||
from .config import load_config
|
||
|
||
config = load_config()
|
||
apps = config.get("apps", {})
|
||
schemes = config.get("schemes", {})
|
||
|
||
if task.app_id not in apps:
|
||
raise HTTPException(status_code=400, detail="App 不存在")
|
||
if task.scheme_id not in schemes:
|
||
raise HTTPException(status_code=400, detail="Scheme 不存在")
|
||
|
||
app = apps[task.app_id]
|
||
scheme = schemes[task.scheme_id]
|
||
|
||
task_id = str(uuid.uuid4())
|
||
db_task = Task(
|
||
id=task_id,
|
||
app_id=task.app_id,
|
||
app_name=app.get("name", ""),
|
||
build_type=task.build_type,
|
||
scheme_id=task.scheme_id,
|
||
scheme_name=scheme.get("displayName") or scheme.get("name", ""),
|
||
obfuscation=task.obfuscation,
|
||
branch=task.branch,
|
||
status="pending",
|
||
)
|
||
db.add(db_task)
|
||
db.commit()
|
||
db.refresh(db_task)
|
||
|
||
# 启动打包任务
|
||
from ..services.build_queue import build_queue
|
||
await build_queue.submit(task_id)
|
||
|
||
return db_task
|
||
|
||
|
||
def _enrich_tasks(tasks):
|
||
"""为任务列表补充 has_log 字段"""
|
||
from pathlib import Path
|
||
logs_dir = Path(__file__).parent.parent / "logs"
|
||
result = []
|
||
for task in tasks:
|
||
resp = TaskResponse.from_orm(task)
|
||
# 检查独立日志目录
|
||
if logs_dir.exists() and (logs_dir / f"{task.id}.log").exists():
|
||
resp.has_log = True
|
||
# 检查构建目录
|
||
elif task.build_dir:
|
||
log_path = Path(task.build_dir) / "build.log"
|
||
if log_path.exists():
|
||
resp.has_log = True
|
||
# 运行中的任务,内存中有日志
|
||
elif task.status == "running":
|
||
from ..services.log_streamer import log_streamer
|
||
if task.id in log_streamer._log_lines:
|
||
resp.has_log = True
|
||
result.append(resp)
|
||
return result
|
||
|
||
|
||
@router.get("", response_model=List[TaskResponse])
|
||
async def list_tasks(
|
||
status: str = None,
|
||
limit: int = 20,
|
||
offset: int = 0,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取任务列表"""
|
||
query = db.query(Task)
|
||
if status:
|
||
query = query.filter(Task.status == status)
|
||
tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(limit).all()
|
||
return _enrich_tasks(tasks)
|
||
|
||
|
||
@router.get("/{task_id}", response_model=TaskResponse)
|
||
async def get_task(task_id: str, db: Session = Depends(get_db)):
|
||
"""获取任务详情"""
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
return _enrich_tasks([task])[0]
|
||
|
||
|
||
@router.delete("/{task_id}")
|
||
async def cancel_task(task_id: str, db: Session = Depends(get_db)):
|
||
"""取消任务"""
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
|
||
if task.status in ("completed", "failed", "cancelled"):
|
||
raise HTTPException(status_code=400, detail="任务已完成或已取消")
|
||
|
||
task.status = "cancelled"
|
||
db.commit()
|
||
|
||
# 尝试取消运行中的任务
|
||
from ..services.build_queue import build_queue
|
||
build_queue.cancel(task_id)
|
||
|
||
# 清理残留文件
|
||
import shutil
|
||
from pathlib import Path
|
||
log_path = Path(__file__).parent.parent / "logs" / f"{task_id}.log"
|
||
if log_path.exists():
|
||
log_path.unlink()
|
||
if task.build_dir:
|
||
build_path = Path(task.build_dir)
|
||
if build_path.exists():
|
||
shutil.rmtree(build_path, ignore_errors=True)
|
||
|
||
return {"message": "任务已取消"}
|
||
|
||
|
||
@router.delete("/{task_id}/delete")
|
||
async def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||
"""删除打包记录"""
|
||
import shutil
|
||
from pathlib import Path
|
||
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
|
||
if task.status in ("running", "pending"):
|
||
raise HTTPException(status_code=400, detail="任务正在运行中,无法删除")
|
||
|
||
# 删除日志文件
|
||
log_path = Path(__file__).parent.parent / "logs" / f"{task_id}.log"
|
||
if log_path.exists():
|
||
log_path.unlink()
|
||
|
||
# 删除构建目录
|
||
if task.build_dir:
|
||
build_path = Path(task.build_dir)
|
||
if build_path.exists():
|
||
shutil.rmtree(build_path, ignore_errors=True)
|
||
|
||
db.delete(task)
|
||
db.commit()
|
||
|
||
return {"message": "记录已删除"}
|
||
|
||
|
||
@router.get("/{task_id}/log")
|
||
async def get_build_log(task_id: str, db: Session = Depends(get_db)):
|
||
"""获取打包日志"""
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
|
||
from ..services.log_streamer import log_streamer
|
||
|
||
# 先从内存中获取(任务运行中)
|
||
lines = log_streamer._log_lines.get(task_id)
|
||
if lines:
|
||
content = ""
|
||
for entry in lines:
|
||
ts = entry["timestamp"]
|
||
level = entry["level"].upper()
|
||
msg = entry["message"]
|
||
content += f"[{ts}] [{level}] {msg}\n"
|
||
return {"log": content}
|
||
|
||
# 从独立日志目录获取
|
||
from pathlib import Path
|
||
log_path = Path(__file__).parent.parent / "logs" / f"{task_id}.log"
|
||
if log_path.exists():
|
||
return {"log": log_path.read_text(encoding="utf-8")}
|
||
|
||
# 从构建目录获取(兼容旧数据)
|
||
if task.build_dir:
|
||
log_path = Path(task.build_dir) / "build.log"
|
||
if log_path.exists():
|
||
return {"log": log_path.read_text(encoding="utf-8")}
|
||
|
||
return {"log": ""}
|
||
|
||
|
||
@router.get("/{task_id}/dsym")
|
||
async def download_dsym(task_id: str, db: Session = Depends(get_db)):
|
||
"""下载 dSYM 文件"""
|
||
import shutil
|
||
import zipfile
|
||
import tempfile
|
||
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
if not task.dsym_path:
|
||
raise HTTPException(status_code=404, detail="dSYM 文件不存在")
|
||
|
||
dsym_path = task.dsym_path
|
||
if not os.path.exists(dsym_path):
|
||
raise HTTPException(status_code=404, detail="dSYM 文件已被清理")
|
||
|
||
# .dSYM 是目录(macOS bundle),需要压缩为 zip 再下载
|
||
if os.path.isdir(dsym_path):
|
||
zip_path = os.path.join(tempfile.gettempdir(), os.path.basename(dsym_path) + ".zip")
|
||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||
for root, dirs, files in os.walk(dsym_path):
|
||
for f in files:
|
||
full = os.path.join(root, f)
|
||
arcname = os.path.relpath(full, os.path.dirname(dsym_path))
|
||
zf.write(full, arcname)
|
||
return FileResponse(zip_path, filename=os.path.basename(zip_path), media_type="application/zip")
|
||
|
||
return FileResponse(dsym_path, filename=os.path.basename(dsym_path))
|
||
|
||
|
||
@router.get("/{task_id}/obfuscation-maps")
|
||
async def download_obfuscation_maps(task_id: str, db: Session = Depends(get_db)):
|
||
"""下载混淆映射表"""
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
if not task.obfuscation_maps_path:
|
||
raise HTTPException(status_code=404, detail="混淆映射表不存在")
|
||
|
||
maps_path = task.obfuscation_maps_path
|
||
if not os.path.exists(maps_path):
|
||
raise HTTPException(status_code=404, detail="混淆映射表已被清理")
|
||
|
||
# 如果是目录,打包成 zip
|
||
if os.path.isdir(maps_path):
|
||
fd, zip_path = tempfile.mkstemp(suffix=".zip")
|
||
os.close(fd)
|
||
shutil.make_archive(zip_path.replace(".zip", ""), "zip", maps_path)
|
||
return FileResponse(
|
||
zip_path,
|
||
filename=f"{task.app_name}_混淆映射.zip",
|
||
background=BackgroundTask(os.remove, zip_path),
|
||
)
|
||
|
||
return FileResponse(maps_path, filename=os.path.basename(maps_path))
|
||
|
||
|
||
@router.get("/{task_id}/qrcode")
|
||
async def download_qrcode(task_id: str, db: Session = Depends(get_db)):
|
||
"""下载二维码图片"""
|
||
task = db.query(Task).filter(Task.id == task_id).first()
|
||
if not task:
|
||
raise HTTPException(status_code=404, detail="任务不存在")
|
||
if not task.qr_code_path:
|
||
raise HTTPException(status_code=404, detail="二维码不存在")
|
||
|
||
if not os.path.exists(task.qr_code_path):
|
||
raise HTTPException(status_code=404, detail="二维码文件已被清理")
|
||
|
||
return FileResponse(task.qr_code_path, media_type="image/png")
|