fix(update): retry core downloads across available sources

This commit is contained in:
lzt404 committed 2026-09-04 01:50:46 +08:00
1 parent 594ab43b6a
commit ccbe3a5625
4 files changed
+163 -11

No files matched your search

+74 -8
View File
@@ -117,8 +117,10 @@ pub(crate) async fn install_core_version(
Ok(was_running) => (
was_running,
install_core_version_inner(
&app,
&window,
state.inner(),
gui_config_state.inner(),
token,
version,
&proxy_url,
@@ -280,8 +282,10 @@ pub(crate) fn restart_core_process_with_state(
}
pub(crate) async fn install_core_version_inner(
app: &tauri::AppHandle,
window: &tauri::Window,
state: &CoreDownloadState,
gui_config_state: &GuiConfigState,
token: CancellationToken,
version: Option<String>,
proxy_url: &str,
@@ -291,7 +295,8 @@ pub(crate) async fn install_core_version_inner(
let platform = current_core_platform()?;
let client = http_client(proxy_url, &custom_mirrors)?;
state.progress(window, "检查版本", 0, None, true);
let (release, _) = fetch_release_cancelable(
let requested_source = download_source.clone();
let (release, resolved_source) = fetch_release_cancelable(
&client,
version.as_deref(),
&token,
@@ -300,6 +305,13 @@ pub(crate) async fn install_core_version_inner(
)
.await?;
let asset = select_release_asset(&release, &platform)?;
let download_candidates = core_download_candidates(
&release.tag_name,
asset,
resolved_source,
configured_gitcode_core_repository(),
&custom_mirrors,
);
let install_dir = core_install_dir()?;
let base_dir = core_base_dir()?;
@@ -319,8 +331,23 @@ pub(crate) 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, &archive_path, window, state, &token).await?;
let (downloaded, successful_source) = download_asset(
&client,
asset,
&download_candidates,
&archive_path,
window,
state,
&token,
)
.await?;
validate_downloaded_asset(asset, &downloaded)?;
persist_automatic_download_source_switch(
app,
gui_config_state,
&requested_source,
&successful_source,
)?;
ensure_not_cancelled(&token, Some(&archive_path))?;
state.progress(
@@ -660,6 +687,46 @@ pub(crate) fn release_from_tag_for_repositories(
}
}
pub(crate) fn core_download_candidates(
tag: &str,
asset: &GithubAsset,
preferred: VersionDownloadCandidate,
gitcode_repository: Option<&str>,
custom_mirrors: &[String],
) -> Vec<(VersionDownloadCandidate, String)> {
let tag = normalize_version(tag);
let generated_github_url = format!("{RELEASE_DOWNLOAD_PREFIX}{tag}/{}", asset.name);
let provided_urls = std::iter::once(&asset.browser_download_url)
.chain(asset.fallback_download_urls.iter())
.collect::<Vec<_>>();
let github_url = provided_urls
.iter()
.find(|url| core_download_source_name(url) == "GitHub")
.map(|url| (*url).clone())
.unwrap_or(generated_github_url);
let gitcode_url = provided_urls
.iter()
.find(|url| core_download_source_name(url) == "GitCode")
.map(|url| (*url).clone())
.or_else(|| {
gitcode_repository
.map(|repository| gitcode_release_attachment_url(repository, &tag, &asset.name))
});
version_download_source_candidates(preferred, gitcode_url.is_some(), custom_mirrors)
.into_iter()
.filter_map(|candidate| {
let url = match candidate.source {
VersionDownloadSource::Github => github_url.clone(),
VersionDownloadSource::Gitcode => gitcode_url.clone()?,
VersionDownloadSource::GhProxy
| VersionDownloadSource::GhFast
| VersionDownloadSource::Custom => version_source_url(&candidate, &github_url),
};
Some((candidate, url))
})
.collect()
}
pub(crate) fn release_tag_from_url(url: &reqwest::Url) -> Option<String> {
let mut segments = url.path_segments()?;
let tag = segments.next_back()?.trim();
@@ -833,19 +900,18 @@ pub(crate) fn core_release_asset_name(version: &str, platform: &CorePlatform) ->
pub(crate) async fn download_asset(
client: &reqwest::Client,
asset: &GithubAsset,
download_candidates: &[(VersionDownloadCandidate, String)],
archive_path: &Path,
window: &tauri::Window,
state: &CoreDownloadState,
token: &CancellationToken,
) -> Result<DownloadedArchive, String> {
let urls =
std::iter::once(&asset.browser_download_url).chain(asset.fallback_download_urls.iter());
) -> Result<(DownloadedArchive, VersionDownloadCandidate), String> {
let mut failures = Vec::new();
for (index, url) in urls.enumerate() {
for (index, (candidate, url)) in download_candidates.iter().enumerate() {
if index > 0 {
state.progress(
window,
&format!("下载失败,正在切换到 {}", core_download_source_name(url)),
&format!("下载失败,正在切换到 {}", candidate.display_name()),
0,
asset.size,
true,
@@ -863,7 +929,7 @@ pub(crate) async fn download_asset(
)
.await;
match result {
Ok(downloaded) => return Ok(downloaded),
Ok(downloaded) => return Ok((downloaded, candidate.clone())),
Err(error) if token.is_cancelled() => {
let _ = fs::remove_file(archive_path);
return Err(error);
+52
View File
@@ -756,6 +756,58 @@ fn github_proxy_core_release_uses_proxy_then_official_and_gitcode() {
);
}
#[test]
fn core_download_candidates_follow_the_complete_fallback_chain() {
let release = release_from_tag_for_repositories(
"v7.2.80",
Some("lzt404/CLIProxyAPI"),
&VersionDownloadCandidate::builtin(VersionDownloadSource::Github),
);
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();
let custom_mirrors = vec!["https://mirror.example.com/".to_string()];
let candidates = core_download_candidates(
&release.tag_name,
asset,
VersionDownloadCandidate::builtin(VersionDownloadSource::Github),
Some("lzt404/CLIProxyAPI"),
&custom_mirrors,
);
assert_eq!(
candidates
.iter()
.map(|(candidate, _)| candidate.key())
.collect::<Vec<_>>(),
[
"github",
"gitcode",
"gh-proxy",
"gh-fast",
"custom:https://mirror.example.com/",
]
);
assert_eq!(
candidates
.iter()
.map(|(_, url)| core_download_source_name(url))
.collect::<Vec<_>>(),
[
"GitHub",
"GitCode",
"gh-proxy.com",
"ghfast.top",
"mirror.example.com",
]
);
}
#[test]
fn release_asset_names_cover_the_six_supported_gui_targets() {
let targets = [
+29 -2
View File
@@ -63,6 +63,7 @@ function downloadSourceLabel(source: VersionDownloadSource, t: ReturnType<typeof
export type MessageType = 'info' | 'success' | 'error';
const APP_RELEASE_URL = 'https://github.com/router-for-me/EasyCLIProxyAPI/releases/latest';
export const DEFAULT_VERSION_DOWNLOAD_SOURCE = 'github';
export function displayAppVersion(version: string) {
const resolvedVersion = version.trim();
@@ -391,7 +392,33 @@ export function VersionManagementPage() {
});
loadInstallTask();
loadVersionSourceSettings();
const initializeDefaultVersionSource = async () => {
setVersionSourceSaving(true);
setVersionSourceError('');
try {
const settings = await invoke<VersionSourceSettings>('set_download_source', {
source: DEFAULT_VERSION_DOWNLOAD_SOURCE,
});
if (disposed) return;
setVersionSource(settings);
resetLatest();
await Promise.all([
checkAppUpdate(),
checkLatest(true),
]);
} catch (error) {
if (!disposed) {
await loadVersionSourceSettings();
setVersionSourceError(String(error));
}
} finally {
if (!disposed) {
setVersionSourceSaving(false);
}
}
};
void initializeDefaultVersionSource();
void getVersion()
.then((version) => {
@@ -559,7 +586,7 @@ export function VersionManagementPage() {
<label>
<span className="sr-only">{t('kernel.versions.downloadSource')}</span>
<select
value={versionSource?.source ?? 'github'}
value={versionSource?.source ?? DEFAULT_VERSION_DOWNLOAD_SOURCE}
disabled={
!versionSource
|| versionSourceSaving
+8 -1
View File
@@ -1,9 +1,16 @@
import { describe, expect, it } from 'bun:test';
import { displayAppVersion } from '../src/pages/VersionManagementPage';
import {
DEFAULT_VERSION_DOWNLOAD_SOURCE,
displayAppVersion,
} from '../src/pages/VersionManagementPage';
import { coreUpdateAvailable } from '../src/coreUpdate';
import { appUpdateIndicatorState } from '../src/appUpdateModel';
describe('VersionManagement helper functions', () => {
it('defaults every version management session to GitHub', () => {
expect(DEFAULT_VERSION_DOWNLOAD_SOURCE).toBe('github');
});
it('formats app versions with v prefix properly', () => {
expect(displayAppVersion('1.0.0')).toBe('v1.0.0');
expect(displayAppVersion('v1.2.3')).toBe('v1.2.3');