feat(updater): add GitCode fallback for GUI and CLIProxyAPI core releases
- split GitCode repositories into GUI and core build variables - query GitCode latest release when GitHub version discovery fails - construct GitCode attachment URLs from repository, tag, and asset filename - retry core downloads through GitCode when GitHub downloads fail
This commit is contained in:
1 parent
dd86eabefa
commit
e8b2ced3f8
7 files changed
+200
-51
No files matched your search
+165
-35
@@ -1563,6 +1563,8 @@ struct GithubRelease {
|
||||
struct GithubAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
#[serde(default)]
|
||||
fallback_download_urls: Vec<String>,
|
||||
size: Option<u64>,
|
||||
digest: Option<String>,
|
||||
}
|
||||
@@ -10784,7 +10786,7 @@ async fn fetch_portable_update_manifest(
|
||||
match fetch_portable_update_manifest_url(client, APP_UPDATE_MANIFEST_URL).await {
|
||||
Ok(manifest) => Ok(manifest),
|
||||
Err(github_error) => {
|
||||
let Some(repository) = configured_gitcode_repository() else {
|
||||
let Some(repository) = configured_gitcode_gui_repository() else {
|
||||
return Err(github_error);
|
||||
};
|
||||
let fallback = async {
|
||||
@@ -10840,8 +10842,16 @@ async fn fetch_portable_update_manifest_url(
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
fn configured_gitcode_repository() -> Option<&'static str> {
|
||||
let repository = option_env!("GITCODE_REPOSITORY")?.trim();
|
||||
fn configured_gitcode_gui_repository() -> Option<&'static str> {
|
||||
configured_gitcode_repository(option_env!("GITCODE_GUI_REPOSITORY"))
|
||||
}
|
||||
|
||||
fn configured_gitcode_core_repository() -> Option<&'static str> {
|
||||
configured_gitcode_repository(option_env!("GITCODE_CORE_REPOSITORY"))
|
||||
}
|
||||
|
||||
fn configured_gitcode_repository(repository: Option<&'static str>) -> Option<&'static str> {
|
||||
let repository = repository?.trim();
|
||||
let mut parts = repository.split('/');
|
||||
let valid_part = |part: &str| {
|
||||
!part.is_empty()
|
||||
@@ -11048,7 +11058,7 @@ fn validate_portable_update_asset_fallbacks(
|
||||
asset,
|
||||
tag,
|
||||
expected_filenames,
|
||||
configured_gitcode_repository(),
|
||||
configured_gitcode_gui_repository(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11807,17 +11817,7 @@ async fn install_core_version_inner(
|
||||
.ok_or_else(|| format!("非法 asset 文件名: {}", asset.name))?;
|
||||
let archive_path = download_dir.join(archive_file_name);
|
||||
|
||||
let downloaded = download_asset(
|
||||
&client,
|
||||
&asset.browser_download_url,
|
||||
&archive_path,
|
||||
asset.size,
|
||||
asset.digest.as_deref(),
|
||||
window,
|
||||
state,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
let downloaded = download_asset(&client, asset, &archive_path, window, state, &token).await?;
|
||||
validate_downloaded_asset(asset, &downloaded)?;
|
||||
|
||||
ensure_not_cancelled(&token, Some(&archive_path))?;
|
||||
@@ -11933,14 +11933,50 @@ async fn fetch_release(
|
||||
return Ok(release_from_tag(version));
|
||||
}
|
||||
let atom_result = fetch_release_from_atom(client).await;
|
||||
match atom_result {
|
||||
let github_result = match atom_result {
|
||||
Ok(release) => Ok(release),
|
||||
Err(atom_error) => fetch_release_from_page(client).await.map_err(|page_error| {
|
||||
format!("GitHub 发布源请求失败: {atom_error};release 页面请求失败: {page_error}")
|
||||
}),
|
||||
};
|
||||
match github_result {
|
||||
Ok(release) => Ok(release),
|
||||
Err(github_error) => {
|
||||
let Some(repository) = configured_gitcode_core_repository() else {
|
||||
return Err(github_error);
|
||||
};
|
||||
fetch_release_from_gitcode(client, repository)
|
||||
.await
|
||||
.map_err(|gitcode_error| {
|
||||
format!(
|
||||
"GitHub 内核发布源失败: {github_error};GitCode 回退源失败: {gitcode_error}"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_release_from_gitcode(
|
||||
client: &reqwest::Client,
|
||||
repository: &str,
|
||||
) -> Result<GithubRelease, String> {
|
||||
let release_url = format!("https://api.gitcode.com/api/v5/repos/{repository}/releases/latest");
|
||||
let release = client
|
||||
.get(release_url)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.header(reqwest::header::USER_AGENT, USER_AGENT)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("查询 GitCode 最新内核发行版失败: {error}"))?
|
||||
.error_for_status()
|
||||
.map_err(|error| format!("读取 GitCode 最新内核发行版失败: {error}"))?
|
||||
.json::<GitcodeRelease>()
|
||||
.await
|
||||
.map_err(|error| format!("解析 GitCode 最新内核发行版失败: {error}"))?;
|
||||
validate_release_tag(&release.tag_name)?;
|
||||
Ok(release_from_gitcode_tag(&release.tag_name, repository))
|
||||
}
|
||||
|
||||
async fn fetch_release_from_page(client: &reqwest::Client) -> Result<GithubRelease, String> {
|
||||
let response = client
|
||||
.get(RELEASE_PAGE_URL)
|
||||
@@ -12010,6 +12046,18 @@ fn release_tag_from_atom(xml: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn release_from_tag(tag: &str) -> GithubRelease {
|
||||
release_from_tag_for_repositories(tag, configured_gitcode_core_repository(), false)
|
||||
}
|
||||
|
||||
fn release_from_gitcode_tag(tag: &str, repository: &str) -> GithubRelease {
|
||||
release_from_tag_for_repositories(tag, Some(repository), true)
|
||||
}
|
||||
|
||||
fn release_from_tag_for_repositories(
|
||||
tag: &str,
|
||||
gitcode_repository: Option<&str>,
|
||||
prefer_gitcode: bool,
|
||||
) -> GithubRelease {
|
||||
let tag = normalize_version(tag);
|
||||
let version = tag.trim_start_matches('v');
|
||||
let assets = [
|
||||
@@ -12023,8 +12071,20 @@ fn release_from_tag(tag: &str) -> GithubRelease {
|
||||
.into_iter()
|
||||
.map(|(os, arch, extension)| {
|
||||
let name = format!("CLIProxyAPI_{version}_{os}_{arch}.{extension}");
|
||||
let github_url = format!("{RELEASE_DOWNLOAD_PREFIX}{tag}/{name}");
|
||||
let gitcode_url = gitcode_repository
|
||||
.map(|repository| gitcode_release_attachment_url(repository, &tag, &name));
|
||||
let (browser_download_url, fallback_download_urls) = if prefer_gitcode {
|
||||
(
|
||||
gitcode_url.unwrap_or_else(|| github_url.clone()),
|
||||
Vec::new(),
|
||||
)
|
||||
} else {
|
||||
(github_url, gitcode_url.into_iter().collect())
|
||||
};
|
||||
GithubAsset {
|
||||
browser_download_url: format!("{RELEASE_DOWNLOAD_PREFIX}{tag}/{name}"),
|
||||
browser_download_url,
|
||||
fallback_download_urls,
|
||||
name,
|
||||
size: None,
|
||||
digest: None,
|
||||
@@ -12099,6 +12159,7 @@ fn parse_release_assets(html: &str) -> Vec<GithubAsset> {
|
||||
assets.push(GithubAsset {
|
||||
name: name.to_string(),
|
||||
browser_download_url,
|
||||
fallback_download_urls: Vec::new(),
|
||||
size: None,
|
||||
digest,
|
||||
});
|
||||
@@ -12136,6 +12197,7 @@ async fn fetch_release_cancelable(
|
||||
|
||||
fn http_client() -> Result<reqwest::Client, String> {
|
||||
reqwest::Client::builder()
|
||||
.redirect(release_https_redirect_policy())
|
||||
.connect_timeout(Duration::from_secs(15))
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(600))
|
||||
@@ -12176,30 +12238,53 @@ fn core_release_asset_name(version: &str, platform: &CorePlatform) -> String {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn download_asset(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
asset: &GithubAsset,
|
||||
archive_path: &Path,
|
||||
expected_total: Option<u64>,
|
||||
expected_digest: Option<&str>,
|
||||
window: &tauri::Window,
|
||||
state: &CoreDownloadState,
|
||||
token: &CancellationToken,
|
||||
) -> Result<DownloadedArchive, String> {
|
||||
let result = download_asset_inner(
|
||||
client,
|
||||
url,
|
||||
archive_path,
|
||||
expected_total,
|
||||
expected_digest,
|
||||
window,
|
||||
state,
|
||||
token,
|
||||
)
|
||||
.await;
|
||||
if result.is_err() {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
let urls =
|
||||
std::iter::once(&asset.browser_download_url).chain(asset.fallback_download_urls.iter());
|
||||
let mut failures = Vec::new();
|
||||
for (index, url) in urls.enumerate() {
|
||||
if index > 0 {
|
||||
state.progress(
|
||||
window,
|
||||
"GitHub 下载失败,正在切换到 GitCode",
|
||||
0,
|
||||
asset.size,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let result = download_asset_inner(
|
||||
client,
|
||||
url,
|
||||
archive_path,
|
||||
asset.size,
|
||||
asset.digest.as_deref(),
|
||||
window,
|
||||
state,
|
||||
token,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(downloaded) => return Ok(downloaded),
|
||||
Err(error) if token.is_cancelled() => {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
return Err(error);
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
if failures.is_empty() {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
return Err("内核发行版没有可用的下载地址".to_string());
|
||||
}
|
||||
Err(format!("所有内核下载源均失败: {}", failures.join(";")))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -24027,6 +24112,51 @@ custom_option = "keep-original"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_core_release_uses_gitcode_as_download_fallback() {
|
||||
let release = release_from_tag_for_repositories(
|
||||
"7.2.80",
|
||||
Some("lzt404/CLIProxyAPI"),
|
||||
false,
|
||||
);
|
||||
let platform = CorePlatform {
|
||||
os: "windows".to_string(),
|
||||
arch: "x86_64".to_string(),
|
||||
asset_os: "windows".to_string(),
|
||||
asset_arch: "amd64".to_string(),
|
||||
archive_kind: "zip".to_string(),
|
||||
};
|
||||
let asset = select_release_asset(&release, &platform).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
asset.browser_download_url,
|
||||
"https://github.com/router-for-me/CLIProxyAPI/releases/download/v7.2.80/CLIProxyAPI_7.2.80_windows_amd64.zip"
|
||||
);
|
||||
assert_eq!(
|
||||
asset.fallback_download_urls,
|
||||
["https://api.gitcode.com/api/v5/repos/lzt404/CLIProxyAPI/releases/v7.2.80/attach_files/CLIProxyAPI_7.2.80_windows_amd64.zip/download"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitcode_discovered_core_release_downloads_from_gitcode_first() {
|
||||
let release = release_from_gitcode_tag("v7.2.80", "lzt404/CLIProxyAPI");
|
||||
let platform = CorePlatform {
|
||||
os: "linux".to_string(),
|
||||
arch: "aarch64".to_string(),
|
||||
asset_os: "linux".to_string(),
|
||||
asset_arch: "aarch64".to_string(),
|
||||
archive_kind: "tar.gz".to_string(),
|
||||
};
|
||||
let asset = select_release_asset(&release, &platform).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
asset.browser_download_url,
|
||||
"https://api.gitcode.com/api/v5/repos/lzt404/CLIProxyAPI/releases/v7.2.80/attach_files/CLIProxyAPI_7.2.80_linux_aarch64.tar.gz/download"
|
||||
);
|
||||
assert!(asset.fallback_download_urls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_asset_names_cover_the_six_supported_gui_targets() {
|
||||
let targets = [
|
||||
|
||||
Reference in new issue
Block a user