fix: dSYM/混淆映射下载缺少认证头导致下载失败,dSYM 远端下载改为签名链接

历史记录点击 dSYM 下载后停留在接口地址本身:window.open 无法带
Authorization 头,接口未认证直接 401,重定向逻辑根本没机会执行。
改为前端先用 authFetch 认证请求,再按返回类型跳转远端地址或保存二
进制文件;远端地址同时改造成与 IPA 一致的 OSS 签名短时链接,避免
未签名地址在私有 bucket 下被拒绝。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-08-26 16:36:36 +09:00
co-authored by Claude Sonnet 5
parent 1466e24c83
commit 082c487cf8
4 changed files with 82 additions and 19 deletions
+14 -2
View File
@@ -8,7 +8,7 @@ import uuid
from datetime import datetime
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from sqlalchemy.orm import Session
@@ -344,7 +344,19 @@ async def download_dsym(task_id: str, request: Request, user: dict = Depends(get
db.commit()
if not local_exists:
return RedirectResponse(task.dsym_url)
# dSYM 需要携带认证信息才能访问该接口,浏览器无法直接跳转到本接口的
# 302 目标,因此改为返回 JSON 让前端自行发起下载。
try:
from .config import load_config
from ..services.distribution import DistributionError, get_published_dsym_url
download_url = get_published_dsym_url(
json.loads(task.config_json or "{}"),
load_config().get("upload", {}),
task.dsym_url,
)
except (json.JSONDecodeError, DistributionError) as exc:
raise HTTPException(status_code=502, detail=f"生成下载链接失败:{exc}") from exc
return {"url": download_url}
# .dSYM 是目录(macOS bundle),需要压缩为 zip 再下载
if os.path.isdir(dsym_path):
+24 -9
View File
@@ -315,14 +315,7 @@ def delete_published_dsym(build_config: dict, upload_config: dict):
deleter(upload_config, [_dsym_remote_path(build_config)])
def get_published_download_url(build_config: dict, upload_config: dict, published_url: str) -> str:
"""返回用于受控下载的短时链接;WebDAV 保持原公开链接。"""
if upload_config.get("mode") != "oss":
return published_url
remote_paths = _remote_paths_from_published_url(build_config, upload_config, published_url)
if not remote_paths:
raise DistributionError("无法识别 OSS 文件路径,无法生成受控下载链接")
def _sign_oss_url(upload_config: dict, remote_path: str) -> str:
oss = upload_config.get("oss", {})
required = ["access_key_id", "access_key_secret", "endpoint", "bucket_name"]
if any(not oss.get(key) for key in required):
@@ -336,7 +329,29 @@ def get_published_download_url(build_config: dict, upload_config: dict, publishe
oss2.Auth(oss["access_key_id"], oss["access_key_secret"]),
oss["endpoint"], oss["bucket_name"], connect_timeout=30,
)
return bucket.sign_url("GET", remote_paths[0], DOWNLOAD_URL_EXPIRE_SECONDS, slash_safe=True)
return bucket.sign_url("GET", remote_path, DOWNLOAD_URL_EXPIRE_SECONDS, slash_safe=True)
def get_published_download_url(build_config: dict, upload_config: dict, published_url: str) -> str:
"""返回用于受控下载的短时链接;WebDAV 保持原公开链接。"""
if upload_config.get("mode") != "oss":
return published_url
remote_paths = _remote_paths_from_published_url(build_config, upload_config, published_url)
if not remote_paths:
raise DistributionError("无法识别 OSS 文件路径,无法生成受控下载链接")
return _sign_oss_url(upload_config, remote_paths[0])
def get_published_dsym_url(build_config: dict, upload_config: dict, dsym_url: str) -> str:
"""返回 dSYM 受控下载的短时链接;WebDAV 保持原公开链接。
dSYM 的远端对象键完全由构建配置决定,直接按命名规则重新计算即可,
不需要像 IPA 那样从已保存的下载链接反推。
"""
if upload_config.get("mode") != "oss":
return dsym_url
return _sign_oss_url(upload_config, _dsym_remote_path(build_config))
def _ensure_webdav_dirs(client: httpx.Client, server_url: str, remote_path: str):