sets default admin creds in docker compose. first pass remote builder feature
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import get_current_admin_user
|
||||
from app.config import settings
|
||||
from app.database import async_session, get_session
|
||||
from app.models import MobileBuildArtifact, MobileBuildRequest, MobileBuildUploadedFile, StationConfig
|
||||
from app.schemas import (
|
||||
MobileBuildArtifactResponse,
|
||||
MobileBuildCreate,
|
||||
MobileBuildDefaultsResponse,
|
||||
MobileBuildPreflightResponse,
|
||||
MobileBuildRequestResponse,
|
||||
MobileBuildUploadedFileResponse,
|
||||
)
|
||||
from app.user_models import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_ALLOWED_KINDS = {
|
||||
"android": {"keystore", "descriptor"},
|
||||
"ios": {"certificate", "profile"},
|
||||
}
|
||||
|
||||
_RUNNING_TASKS: set[int] = set()
|
||||
|
||||
|
||||
def _utc_iso(value: datetime | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.replace(microsecond=0).isoformat() + "Z"
|
||||
|
||||
|
||||
def _hash_file(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _ensure_dirs() -> tuple[Path, Path]:
|
||||
upload_dir = Path(settings.MOBILE_BUILD_UPLOAD_DIR)
|
||||
artifact_dir = Path(settings.MOBILE_BUILD_ARTIFACT_DIR)
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
artifact_dir.mkdir(parents=True, exist_ok=True)
|
||||
return upload_dir, artifact_dir
|
||||
|
||||
|
||||
def _check_mobileprovision_for_carplay(path: Path) -> bool:
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
except Exception:
|
||||
return False
|
||||
text = raw.decode("latin-1", errors="ignore").lower()
|
||||
return "carplay" in text
|
||||
|
||||
|
||||
def _build_ssh_prefix(host: str) -> list[str]:
|
||||
if not host:
|
||||
raise RuntimeError("Build host is not configured")
|
||||
return [
|
||||
"sshpass",
|
||||
"-p",
|
||||
settings.MOBILE_BUILD_SSH_PASSWORD,
|
||||
"ssh",
|
||||
"-p",
|
||||
str(settings.MOBILE_BUILD_SSH_PORT),
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
f"{settings.MOBILE_BUILD_SSH_USER}@{host}",
|
||||
]
|
||||
|
||||
|
||||
def _build_scp_prefix(host: str) -> list[str]:
|
||||
if not host:
|
||||
raise RuntimeError("Build host is not configured")
|
||||
return [
|
||||
"sshpass",
|
||||
"-p",
|
||||
settings.MOBILE_BUILD_SSH_PASSWORD,
|
||||
"scp",
|
||||
"-P",
|
||||
str(settings.MOBILE_BUILD_SSH_PORT),
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
]
|
||||
|
||||
|
||||
def _run_sync_command(cmd: list[str]) -> tuple[int, str]:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
output = (result.stdout or "") + ("\n" + result.stderr if result.stderr else "")
|
||||
return result.returncode, output.strip()
|
||||
|
||||
|
||||
async def _run_command(cmd: list[str]) -> tuple[int, str]:
|
||||
return await asyncio.to_thread(_run_sync_command, cmd)
|
||||
|
||||
|
||||
async def _append_log(session: AsyncSession, request: MobileBuildRequest, line: str) -> None:
|
||||
if request.build_log:
|
||||
request.build_log += "\n"
|
||||
request.build_log += line
|
||||
request.updated_at = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _load_files(session: AsyncSession, request_id: int) -> list[MobileBuildUploadedFile]:
|
||||
result = await session.execute(
|
||||
select(MobileBuildUploadedFile).where(MobileBuildUploadedFile.request_id == request_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
def _preflight_issues(request: MobileBuildRequest, files: list[MobileBuildUploadedFile]) -> list[str]:
|
||||
issues: list[str] = []
|
||||
|
||||
if not request.platform_android and not request.platform_ios:
|
||||
issues.append("Select at least one platform.")
|
||||
|
||||
if request.platform_android and not request.android_application_id:
|
||||
issues.append("Android application ID is required when Android is selected.")
|
||||
|
||||
if request.platform_ios and not request.ios_bundle_id:
|
||||
issues.append("iOS bundle ID is required when iOS is selected.")
|
||||
|
||||
if not request.version_name.strip():
|
||||
issues.append("Version name is required.")
|
||||
|
||||
if not request.build_number.strip():
|
||||
issues.append("Build number is required.")
|
||||
|
||||
if request.platform_android and not settings.MOBILE_BUILD_ANDROID_HOST:
|
||||
issues.append("Android build host is not configured.")
|
||||
|
||||
if request.platform_ios and not settings.MOBILE_BUILD_IOS_HOST:
|
||||
issues.append("iOS build host is not configured.")
|
||||
|
||||
grouped: dict[str, dict[str, MobileBuildUploadedFile]] = {"android": {}, "ios": {}}
|
||||
for f in files:
|
||||
grouped.setdefault(f.platform, {})[f.file_kind] = f
|
||||
|
||||
if request.platform_android:
|
||||
present = set(grouped.get("android", {}).keys())
|
||||
expected = _ALLOWED_KINDS["android"]
|
||||
if present != expected:
|
||||
missing = sorted(expected - present)
|
||||
extra = sorted(present - expected)
|
||||
if missing:
|
||||
issues.append(f"Android requires files for kinds: {', '.join(missing)}")
|
||||
if extra:
|
||||
issues.append(f"Android has unsupported file kinds: {', '.join(extra)}")
|
||||
|
||||
if request.platform_ios:
|
||||
present = set(grouped.get("ios", {}).keys())
|
||||
expected = _ALLOWED_KINDS["ios"]
|
||||
if present != expected:
|
||||
missing = sorted(expected - present)
|
||||
extra = sorted(present - expected)
|
||||
if missing:
|
||||
issues.append(f"iOS requires files for kinds: {', '.join(missing)}")
|
||||
if extra:
|
||||
issues.append(f"iOS has unsupported file kinds: {', '.join(extra)}")
|
||||
|
||||
profile = grouped.get("ios", {}).get("profile")
|
||||
if profile:
|
||||
profile_path = Path(profile.stored_path)
|
||||
if not _check_mobileprovision_for_carplay(profile_path):
|
||||
issues.append("iOS provisioning profile does not appear to include CarPlay entitlement text.")
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def _to_file_response(row: MobileBuildUploadedFile) -> MobileBuildUploadedFileResponse:
|
||||
return MobileBuildUploadedFileResponse(
|
||||
id=row.id,
|
||||
request_id=row.request_id,
|
||||
platform=row.platform,
|
||||
file_kind=row.file_kind,
|
||||
original_name=row.original_name,
|
||||
size=row.size,
|
||||
uploaded_at=_utc_iso(row.uploaded_at) or "",
|
||||
)
|
||||
|
||||
|
||||
def _to_artifact_response(row: MobileBuildArtifact) -> MobileBuildArtifactResponse:
|
||||
return MobileBuildArtifactResponse(
|
||||
id=row.id,
|
||||
request_id=row.request_id,
|
||||
platform=row.platform,
|
||||
artifact_type=row.artifact_type,
|
||||
file_name=row.file_name,
|
||||
sha256=row.sha256,
|
||||
size=row.size,
|
||||
created_at=_utc_iso(row.created_at) or "",
|
||||
download_url=f"/api/mobile-builds/artifacts/{row.id}/download",
|
||||
)
|
||||
|
||||
|
||||
async def _hydrate_request(session: AsyncSession, request: MobileBuildRequest) -> MobileBuildRequestResponse:
|
||||
files_result = await session.execute(
|
||||
select(MobileBuildUploadedFile).where(MobileBuildUploadedFile.request_id == request.id)
|
||||
)
|
||||
artifact_result = await session.execute(
|
||||
select(MobileBuildArtifact).where(MobileBuildArtifact.request_id == request.id)
|
||||
)
|
||||
|
||||
files = list(files_result.scalars().all())
|
||||
artifacts = list(artifact_result.scalars().all())
|
||||
|
||||
return MobileBuildRequestResponse(
|
||||
id=request.id,
|
||||
tenant_key=request.tenant_key,
|
||||
platform_android=request.platform_android,
|
||||
platform_ios=request.platform_ios,
|
||||
android_application_id=request.android_application_id,
|
||||
ios_bundle_id=request.ios_bundle_id,
|
||||
version_name=request.version_name,
|
||||
build_number=request.build_number,
|
||||
release_profile=request.release_profile,
|
||||
status=request.status,
|
||||
preflight_passed=request.preflight_passed,
|
||||
preflight_report=request.preflight_report,
|
||||
build_log=request.build_log,
|
||||
error_message=request.error_message,
|
||||
created_at=_utc_iso(request.created_at) or "",
|
||||
updated_at=_utc_iso(request.updated_at) or "",
|
||||
started_at=_utc_iso(request.started_at),
|
||||
completed_at=_utc_iso(request.completed_at),
|
||||
files=[_to_file_response(f) for f in files],
|
||||
artifacts=[_to_artifact_response(a) for a in artifacts],
|
||||
)
|
||||
|
||||
|
||||
async def _copy_worker_artifact(
|
||||
session: AsyncSession,
|
||||
request: MobileBuildRequest,
|
||||
platform: Literal["android", "ios"],
|
||||
host: str,
|
||||
artifact_glob: str,
|
||||
) -> None:
|
||||
_, artifact_dir = _ensure_dirs()
|
||||
local_dir = artifact_dir / f"request_{request.id}" / platform
|
||||
local_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ssh_prefix = _build_ssh_prefix(host)
|
||||
list_cmd = ssh_prefix + [f"ls -1t {shlex.quote(artifact_glob)} 2>/dev/null | head -n 1"]
|
||||
code, out = await _run_command(list_cmd)
|
||||
if code != 0 or not out:
|
||||
raise RuntimeError(f"No {platform} artifact found using glob {artifact_glob}")
|
||||
|
||||
remote_path = out.splitlines()[-1].strip()
|
||||
if not remote_path:
|
||||
raise RuntimeError(f"No {platform} artifact path returned by worker")
|
||||
|
||||
file_name = Path(remote_path).name
|
||||
local_path = local_dir / file_name
|
||||
|
||||
scp_cmd = _build_scp_prefix(host) + [
|
||||
f"{settings.MOBILE_BUILD_SSH_USER}@{host}:{remote_path}",
|
||||
str(local_path),
|
||||
]
|
||||
code, scp_out = await _run_command(scp_cmd)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"Failed to copy {platform} artifact: {scp_out}")
|
||||
|
||||
artifact = MobileBuildArtifact(
|
||||
request_id=request.id,
|
||||
platform=platform,
|
||||
artifact_type="aab" if platform == "android" else "ipa",
|
||||
file_name=file_name,
|
||||
file_path=str(local_path),
|
||||
sha256=_hash_file(local_path),
|
||||
size=local_path.stat().st_size,
|
||||
)
|
||||
session.add(artifact)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _upload_files_to_worker(
|
||||
files: list[MobileBuildUploadedFile], host: str
|
||||
) -> None:
|
||||
scp_prefix = _build_scp_prefix(host)
|
||||
remote_target = f"{settings.MOBILE_BUILD_SSH_USER}@{host}:{settings.MOBILE_BUILD_REMOTE_APP_DIR}/"
|
||||
|
||||
for row in files:
|
||||
local_path = Path(row.stored_path)
|
||||
if not local_path.exists():
|
||||
raise RuntimeError(f"Uploaded file missing: {row.original_name}")
|
||||
cmd = scp_prefix + [str(local_path), remote_target]
|
||||
code, out = await _run_command(cmd)
|
||||
if code != 0:
|
||||
raise RuntimeError(f"Failed to upload file {row.original_name}: {out}")
|
||||
|
||||
|
||||
async def _run_platform_build(
|
||||
session: AsyncSession,
|
||||
request: MobileBuildRequest,
|
||||
platform: Literal["android", "ios"],
|
||||
host: str,
|
||||
script_name: str,
|
||||
) -> None:
|
||||
files = await _load_files(session, request.id)
|
||||
platform_files = [f for f in files if f.platform == platform]
|
||||
await _append_log(session, request, f"[{platform}] Uploading {len(platform_files)} file(s) to worker")
|
||||
await _upload_files_to_worker(platform_files, host)
|
||||
|
||||
ssh_prefix = _build_ssh_prefix(host)
|
||||
cmd = ssh_prefix + [
|
||||
(
|
||||
f"cd {settings.MOBILE_BUILD_REMOTE_APP_DIR} && "
|
||||
f"chmod +x ./{script_name} && "
|
||||
f"./{script_name}"
|
||||
)
|
||||
]
|
||||
await _append_log(session, request, f"[{platform}] Executing {script_name} on {host}")
|
||||
code, out = await _run_command(cmd)
|
||||
await _append_log(session, request, f"[{platform}] Exit code {code}\n{out}")
|
||||
if code != 0:
|
||||
raise RuntimeError(f"{platform} build failed with exit code {code}")
|
||||
|
||||
artifact_glob = (
|
||||
settings.MOBILE_BUILD_ANDROID_ARTIFACT_GLOB
|
||||
if platform == "android"
|
||||
else settings.MOBILE_BUILD_IOS_ARTIFACT_GLOB
|
||||
)
|
||||
await _copy_worker_artifact(session, request, platform, host, artifact_glob)
|
||||
await _append_log(session, request, f"[{platform}] Artifact copied successfully")
|
||||
|
||||
|
||||
async def _execute_request(request_id: int) -> None:
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
select(MobileBuildRequest).where(MobileBuildRequest.id == request_id)
|
||||
)
|
||||
request = result.scalar_one_or_none()
|
||||
if request is None:
|
||||
return
|
||||
|
||||
request.status = "building"
|
||||
request.error_message = None
|
||||
request.started_at = datetime.utcnow()
|
||||
request.updated_at = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
try:
|
||||
if request.platform_android:
|
||||
await _run_platform_build(
|
||||
session,
|
||||
request,
|
||||
"android",
|
||||
settings.MOBILE_BUILD_ANDROID_HOST,
|
||||
settings.MOBILE_BUILD_ANDROID_SCRIPT,
|
||||
)
|
||||
|
||||
if request.platform_ios:
|
||||
await _run_platform_build(
|
||||
session,
|
||||
request,
|
||||
"ios",
|
||||
settings.MOBILE_BUILD_IOS_HOST,
|
||||
settings.MOBILE_BUILD_IOS_SCRIPT,
|
||||
)
|
||||
|
||||
request.status = "completed"
|
||||
request.completed_at = datetime.utcnow()
|
||||
request.updated_at = datetime.utcnow()
|
||||
await session.commit()
|
||||
except Exception as exc:
|
||||
request.status = "failed"
|
||||
request.error_message = str(exc)
|
||||
request.completed_at = datetime.utcnow()
|
||||
request.updated_at = datetime.utcnow()
|
||||
await _append_log(session, request, f"Build failed: {exc}")
|
||||
await session.commit()
|
||||
finally:
|
||||
_RUNNING_TASKS.discard(request_id)
|
||||
|
||||
|
||||
@router.get("/defaults", response_model=MobileBuildDefaultsResponse)
|
||||
async def get_mobile_build_defaults(session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(
|
||||
select(StationConfig).where(StationConfig.key == "default")
|
||||
)
|
||||
cfg = result.scalar_one_or_none()
|
||||
if cfg is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Station config not found")
|
||||
|
||||
app_name = f"{cfg.name_primary} {cfg.name_secondary}".strip()
|
||||
return MobileBuildDefaultsResponse(
|
||||
tenant_key=cfg.key,
|
||||
app_name=app_name,
|
||||
tagline=cfg.tagline,
|
||||
support_email=cfg.email,
|
||||
website=cfg.website,
|
||||
stream_url=cfg.stream_url,
|
||||
stream_metadata_url=cfg.stream_metadata_url,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/requests", response_model=list[MobileBuildRequestResponse])
|
||||
async def list_mobile_build_requests(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(MobileBuildRequest).order_by(MobileBuildRequest.created_at.desc())
|
||||
)
|
||||
rows = list(result.scalars().all())
|
||||
return [await _hydrate_request(session, row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/requests", response_model=MobileBuildRequestResponse, status_code=201)
|
||||
async def create_mobile_build_request(
|
||||
payload: MobileBuildCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
user: User = Depends(get_current_admin_user),
|
||||
):
|
||||
request = MobileBuildRequest(
|
||||
tenant_key="default",
|
||||
platform_android=payload.platform_android,
|
||||
platform_ios=payload.platform_ios,
|
||||
android_application_id=payload.android_application_id,
|
||||
ios_bundle_id=payload.ios_bundle_id,
|
||||
version_name=payload.version_name,
|
||||
build_number=payload.build_number,
|
||||
release_profile=payload.release_profile,
|
||||
status="draft",
|
||||
created_by_id=user.id,
|
||||
updated_at=datetime.utcnow(),
|
||||
)
|
||||
session.add(request)
|
||||
await session.commit()
|
||||
await session.refresh(request)
|
||||
return await _hydrate_request(session, request)
|
||||
|
||||
|
||||
@router.get("/requests/{request_id}", response_model=MobileBuildRequestResponse)
|
||||
async def get_mobile_build_request(
|
||||
request_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(MobileBuildRequest).where(MobileBuildRequest.id == request_id)
|
||||
)
|
||||
request = result.scalar_one_or_none()
|
||||
if request is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Build request not found")
|
||||
return await _hydrate_request(session, request)
|
||||
|
||||
|
||||
@router.post("/requests/{request_id}/files", response_model=MobileBuildUploadedFileResponse)
|
||||
async def upload_mobile_build_file(
|
||||
request_id: int,
|
||||
platform: Literal["android", "ios"] = Query(...),
|
||||
file_kind: str = Query(...),
|
||||
file: UploadFile = File(...),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
if file_kind not in _ALLOWED_KINDS[platform]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Unsupported file_kind '{file_kind}' for platform '{platform}'",
|
||||
)
|
||||
|
||||
request_result = await session.execute(
|
||||
select(MobileBuildRequest).where(MobileBuildRequest.id == request_id)
|
||||
)
|
||||
request = request_result.scalar_one_or_none()
|
||||
if request is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Build request not found")
|
||||
|
||||
upload_dir, _ = _ensure_dirs()
|
||||
req_dir = upload_dir / f"request_{request_id}"
|
||||
req_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
suffix = Path(file.filename or "upload.bin").suffix
|
||||
local_name = f"{platform}_{file_kind}_{uuid.uuid4().hex}{suffix}"
|
||||
local_path = req_dir / local_name
|
||||
|
||||
payload = await file.read()
|
||||
local_path.write_bytes(payload)
|
||||
|
||||
sha = hashlib.sha256(payload).hexdigest()
|
||||
size = len(payload)
|
||||
|
||||
await session.execute(
|
||||
delete(MobileBuildUploadedFile).where(
|
||||
MobileBuildUploadedFile.request_id == request_id,
|
||||
MobileBuildUploadedFile.platform == platform,
|
||||
MobileBuildUploadedFile.file_kind == file_kind,
|
||||
)
|
||||
)
|
||||
|
||||
row = MobileBuildUploadedFile(
|
||||
request_id=request_id,
|
||||
platform=platform,
|
||||
file_kind=file_kind,
|
||||
original_name=file.filename or local_name,
|
||||
stored_path=str(local_path),
|
||||
sha256=sha,
|
||||
size=size,
|
||||
)
|
||||
session.add(row)
|
||||
|
||||
request.preflight_passed = False
|
||||
request.preflight_report = ""
|
||||
request.updated_at = datetime.utcnow()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
|
||||
return _to_file_response(row)
|
||||
|
||||
|
||||
@router.post("/requests/{request_id}/preflight", response_model=MobileBuildPreflightResponse)
|
||||
async def preflight_mobile_build(
|
||||
request_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(MobileBuildRequest).where(MobileBuildRequest.id == request_id)
|
||||
)
|
||||
request = result.scalar_one_or_none()
|
||||
if request is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Build request not found")
|
||||
|
||||
files = await _load_files(session, request_id)
|
||||
issues = _preflight_issues(request, files)
|
||||
|
||||
request.preflight_passed = len(issues) == 0
|
||||
request.preflight_report = "\n".join(issues)
|
||||
request.updated_at = datetime.utcnow()
|
||||
await session.commit()
|
||||
|
||||
return MobileBuildPreflightResponse(passed=request.preflight_passed, issues=issues)
|
||||
|
||||
|
||||
@router.post("/requests/{request_id}/trigger", response_model=MobileBuildRequestResponse)
|
||||
async def trigger_mobile_build(
|
||||
request_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(MobileBuildRequest).where(MobileBuildRequest.id == request_id)
|
||||
)
|
||||
request = result.scalar_one_or_none()
|
||||
if request is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Build request not found")
|
||||
|
||||
files = await _load_files(session, request_id)
|
||||
issues = _preflight_issues(request, files)
|
||||
if issues:
|
||||
request.preflight_passed = False
|
||||
request.preflight_report = "\n".join(issues)
|
||||
request.updated_at = datetime.utcnow()
|
||||
await session.commit()
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=issues)
|
||||
|
||||
if request.status in {"queued", "building"}:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Build is already in progress")
|
||||
|
||||
request.status = "queued"
|
||||
request.preflight_passed = True
|
||||
request.preflight_report = ""
|
||||
request.error_message = None
|
||||
request.build_log = ""
|
||||
request.started_at = None
|
||||
request.completed_at = None
|
||||
request.updated_at = datetime.utcnow()
|
||||
|
||||
await session.execute(
|
||||
delete(MobileBuildArtifact).where(MobileBuildArtifact.request_id == request_id)
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
if request_id not in _RUNNING_TASKS:
|
||||
_RUNNING_TASKS.add(request_id)
|
||||
asyncio.create_task(_execute_request(request_id))
|
||||
|
||||
return await _hydrate_request(session, request)
|
||||
|
||||
|
||||
@router.get("/artifacts/{artifact_id}/download")
|
||||
async def download_mobile_build_artifact(
|
||||
artifact_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(
|
||||
select(MobileBuildArtifact).where(MobileBuildArtifact.id == artifact_id)
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Artifact not found")
|
||||
|
||||
path = Path(row.file_path)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Artifact file no longer exists")
|
||||
|
||||
return FileResponse(path=str(path), filename=row.file_name, media_type="application/octet-stream")
|
||||
Reference in New Issue
Block a user