Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 100 additions & 15 deletions src-tauri/src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ static DOWNLOAD_CANCELLED: AtomicBool = AtomicBool::new(false);
/// 当前下载的 session ID,用于区分不同的下载任务
static CURRENT_DOWNLOAD_SESSION: AtomicU64 = AtomicU64::new(0);

fn apply_proxy(
mut client_builder: reqwest::ClientBuilder,
proxy_url: Option<&str>,
log_prefix: &str,
target_url: &str,
) -> Result<reqwest::ClientBuilder, String> {
if let Some(proxy) = proxy_url.filter(|proxy| !proxy.is_empty()) {
info!("[{}] 使用代理: {}", log_prefix, proxy);
info!("[{}] 目标: {}", log_prefix, target_url);
let reqwest_proxy = reqwest::Proxy::all(proxy).map_err(|e| {
error!("代理配置失败: {} (代理地址: {})", e, proxy);
format!(
"代理配置失败: {}。请检查代理格式是否正确(支持 http:// 或 socks5://)",
e
)
})?;
client_builder = client_builder.proxy(reqwest_proxy);
}

Ok(client_builder)
}

/// 根据版本号获取 GitHub Release URL
///
/// 使用 GitHub API 获取指定版本的 Release 信息,支持使用 GitHub PAT 和代理
Expand All @@ -79,21 +101,7 @@ pub async fn get_github_release_by_version(
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(3));

// 添加代理配置(如果提供)
if let Some(ref proxy) = proxy_url {
if !proxy.is_empty() {
info!("[检查更新] 使用代理: {}", proxy);
info!("[检查更新] 目标: {}", url);
let reqwest_proxy = reqwest::Proxy::all(proxy).map_err(|e| {
error!("代理配置失败: {} (代理地址: {})", e, proxy);
format!(
"代理配置失败: {}。请检查代理格式是否正确(支持 http:// 或 socks5://)",
e
)
})?;
client_builder = client_builder.proxy(reqwest_proxy);
}
}
client_builder = apply_proxy(client_builder, proxy_url.as_deref(), "检查更新", &url)?;

let client = client_builder
.build()
Expand Down Expand Up @@ -144,6 +152,32 @@ pub async fn get_github_release_by_version(
Ok(None)
}

/// 检查下载链接的 HTTP 状态码,支持使用代理
#[tauri::command]
pub async fn probe_download_url(url: String, proxy_url: Option<String>) -> Result<u16, String> {
let client_builder = reqwest::Client::builder()
.user_agent(build_user_agent())
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(3));
let client_builder = apply_proxy(
client_builder,
proxy_url.as_deref(),
"检查直接下载链接",
&url,
)?;
let client = client_builder
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;

let response = client
.head(&url)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;

Ok(response.status().as_u16())
}

/// 流式下载文件,支持进度回调和取消
///
/// 使用 reqwest 进行流式下载,直接写入文件而不经过内存缓冲,
Expand Down Expand Up @@ -579,3 +613,54 @@ fn parse_content_disposition(header: &str) -> Option<String> {

None
}

#[cfg(test)]
mod tests {
use super::probe_download_url;
use std::io::{Read, Write};
use std::net::TcpListener;

#[test]
fn probe_download_url_uses_explicit_proxy() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test proxy");
let proxy_address = listener.local_addr().expect("get test proxy address");
let proxy = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept proxied request");
let mut request = Vec::new();
loop {
let mut chunk = [0_u8; 2048];
let bytes_read = stream.read(&mut chunk).expect("read proxied request");
assert!(bytes_read > 0, "proxy closed before request headers");
request.extend_from_slice(&chunk[..bytes_read]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
assert!(
request.len() <= 64 * 1024,
"proxy request headers too large"
);
}
let request = String::from_utf8_lossy(&request);
assert!(request.starts_with("HEAD http://example.invalid/update.zip HTTP/1.1"));
stream
.write_all(
b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
)
.expect("write proxy response");
});

let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build Tokio runtime");
let status = runtime
.block_on(probe_download_url(
"http://example.invalid/update.zip".to_string(),
Some(format!("http://{}", proxy_address)),
))
.expect("probe through proxy");

assert_eq!(status, 204);
proxy.join().expect("join test proxy");
}
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ pub fn run() {
commands::update::cleanup_update_artifacts,
// 下载命令
commands::download::get_github_release_by_version,
commands::download::probe_download_url,
commands::download::download_file,
commands::download::cancel_download,
// 系统相关命令
Expand Down
22 changes: 12 additions & 10 deletions src/services/updateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ async function tryDirectDownloadUrls(
repo: string,
projectName: string,
version: string,
proxyUrl?: string,
): Promise<{ url: string; filename: string } | null> {
const extensions = getDownloadExtensions();
const arch = await getArch();
Expand All @@ -590,18 +591,13 @@ async function tryDirectDownloadUrls(

try {
log.info(`尝试直接下载链接: ${url}`);
const response = await tauriFetch(url, {
method: 'HEAD',
headers: {
'User-Agent': await buildUserAgent(),
},
});
const status = await invoke<number>('probe_download_url', { url, proxyUrl });

if (response.ok) {
if (status >= 200 && status < 300) {
log.info(`直接下载链接可用: ${filename}`);
return { url, filename };
}
log.info(`直接下载链接不存在 (${response.status}): ${filename}`);
log.info(`直接下载链接不存在 (${status}): ${filename}`);
} catch (error) {
log.warn(`检查直接下载链接失败: ${filename}`, error);
}
Expand Down Expand Up @@ -663,7 +659,7 @@ export interface GetGitHubDownloadUrlOptions {
targetVersion: string; // Mirror酱返回的目标版本号
githubPat?: string; // GitHub Personal Access Token (支持 classic 和 fine-grained)
projectName?: string; // 项目名称,用于拼接直接下载链接(来自 interface.name)
proxyUrl?: string; // 代理 URL,用于 GitHub API 请求
proxyUrl?: string; // 代理 URL,用于 GitHub API 请求和直接下载链接探测
}

/**
Expand Down Expand Up @@ -705,7 +701,13 @@ export async function getGitHubDownloadUrl(

// API 失败或未匹配到 asset,尝试直接拼接下载链接
if (projectName) {
const directResult = await tryDirectDownloadUrls(owner, repo, projectName, targetVersion);
const directResult = await tryDirectDownloadUrls(
owner,
repo,
projectName,
targetVersion,
proxyUrl,
);
if (directResult) {
log.info(`使用直接下载链接: ${directResult.filename}`);
return {
Expand Down