fix: improve parallel pack downloads and suppress stale notifications
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled

Download hosted pack files concurrently, recover file transfers from proxy and content-encoding failures, and prevent old installation failures from reappearing in notifications.
This commit is contained in:
2026-09-15 21:14:18 +08:00
parent bc904065c3
commit bd09fed0ed
5 changed files with 362 additions and 93 deletions

View File

@ -12,14 +12,17 @@ use crate::{
download_to_path,
},
};
use futures::FutureExt;
use futures::{FutureExt, StreamExt, stream};
use progress::PackProgress;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, HashSet},
path::{Path, PathBuf},
sync::{Arc, LazyLock},
sync::{
Arc, LazyLock,
atomic::{AtomicUsize, Ordering},
},
};
use tokio::{
io::AsyncReadExt,
@ -968,71 +971,117 @@ async fn synchronize_with_progress(
.map(|(file, _, _)| file.size)
.sum::<u64>();
let total_files = pending_downloads.len();
let mut completed_bytes = 0;
for (index, (file, url, object)) in
pending_downloads.into_values().enumerate()
{
ensure_session(&auth).await?;
let url = if let Some(external) = &file.external {
match crate::api::curseforge::get_file(
external.project_id,
external.file_id,
)
if total_files > 0 {
let concurrency = crate::api::settings::get()
.await?
.download_url
{
Some(url) => url,
None => crate::api::curseforge::get_download_url(
external.project_id,
external.file_id,
)
.await?
.ok_or_else(|| {
invalid("This CurseForge file requires a manual download")
})?,
}
} else {
url
};
let label =
format!("文件 {}/{} · {}", index + 1, total_files, file.path);
progress.download(completed_bytes, total_bytes, &label, index == 0);
let mut transferred = 0;
let mut on_progress = |received: u64, _total: u64| {
transferred = received.min(file.size);
progress.download(
completed_bytes + transferred,
total_bytes,
&label,
false,
);
futures::future::ready(Ok(())).boxed()
};
let mut request = DownloadRequest::new(&url, ResourceClass::Modpack)
.with_integrity(Integrity {
size: Some(file.size),
sha256: Some(file.sha256),
..Default::default()
});
if url.starts_with(&format!("{API}/files/")) {
request = request.with_header("Authorization", auth.clone());
}
download_to_path(
request,
&object,
&state.fetch_semaphore,
&state.pool,
Some(&mut on_progress),
)
.await?;
completed_bytes += file.size;
downloaded += file.size;
.effective_max_concurrent_downloads()
.clamp(1, 16);
let current = std::sync::Mutex::new(BTreeMap::<String, u64>::new());
let completed = AtomicUsize::new(0);
progress.download(
completed_bytes,
0,
total_bytes,
&label,
index + 1 == total_files,
&format!("文件 0/{total_files}"),
true,
);
let results = stream::iter(pending_downloads.into_values().map(
|(file, url, object)| {
let current = &current;
let completed = &completed;
let state = &state;
let auth = &auth;
async move {
ensure_session(auth).await?;
let url = if let Some(external) = &file.external {
match crate::api::curseforge::get_file(
external.project_id,
external.file_id,
)
.await?
.download_url
{
Some(url) => url,
None => crate::api::curseforge::get_download_url(
external.project_id,
external.file_id,
)
.await?
.ok_or_else(|| {
invalid(
"This CurseForge file requires a manual download",
)
})?,
}
} else {
url
};
let file_size = file.size;
let file_path = file.path.clone();
let progress_key = file.path.clone();
let mut on_progress = |received: u64, _total: u64| {
let mut bytes = current
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
bytes.insert(progress_key.clone(), received.min(file_size));
progress.download(
bytes.values().sum(),
total_bytes,
&format!(
"文件 {}/{} · {}",
completed.load(Ordering::Relaxed),
total_files,
file_path
),
false,
);
futures::future::ready(Ok(())).boxed()
};
let mut request =
DownloadRequest::new(&url, ResourceClass::Modpack)
.with_integrity(Integrity {
size: Some(file_size),
sha256: Some(file.sha256),
..Default::default()
});
if url.starts_with(&format!("{API}/files/")) {
request = request
.with_header("Authorization", auth.clone());
}
download_to_path(
request,
&object,
&state.fetch_semaphore,
&state.pool,
Some(&mut on_progress),
)
.await?;
let completed_files =
completed.fetch_add(1, Ordering::Relaxed) + 1;
let completed_bytes = {
let mut bytes = current
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
bytes.insert(progress_key, file_size);
bytes.values().sum()
};
progress.download(
completed_bytes,
total_bytes,
&format!(
"文件 {completed_files}/{total_files} · {file_path}"
),
completed_files == total_files,
);
Ok::<_, crate::Error>(file_size)
}
},
))
.buffer_unordered(concurrency)
.collect::<Vec<_>>()
.await;
downloaded += results.into_iter().try_fold(0, |sum, result| {
Ok::<u64, crate::Error>(sum + result?)
})?;
}
ensure_session(&auth).await?;
downloaded += tagged::download(