fix(addon): 复审 r2 —— 下载阶段抽出可测边界并补守门
- 阻塞项:下载循环从 `install::run` 抽到新文件 `addon/downloads.rs`
(`download_failure()` + `download_stage()`),假下载器就能驱动它;三条用例分别钉
①失败→Unreachable+叶子成因、②第 2 个失败不中断且已装不回滚、③解析完就取消一份都不下。
实测删掉 `record_failure` 那行会红(failures 0 vs 1),把 `cause` 丢掉也会红;
- nit① 补 core 用例:夹具里 `{"version_id":"vid-only","dependency_type":"required"}` ⇒
`plan.unresolved_required == [UnresolvedRequired{parent:"MIXED001",…}]`,同版本的
`project_id` required 仍被跟;删掉收集那段即红(left: [] vs 一条);
- nit② 取消检查加回解析之后(`download_stage` 入口),并用例钉「解析后取消 ⇒ 不下任何文件」;
- nit③ `fail()` 文档写明去重按**请求键**而非规范 id 的边界(先 slug 后 project_id 仍会两条),
不消掉但不再让人误以为全覆盖;
- `install.rs` 只留解析 + 收口(275 行),模块表登记 `downloads`。
This commit is contained in:
1 parent
aa33210bb6
commit
ff2f229b80
5 files changed
+383
-56
No files matched your search
@@ -44,8 +44,7 @@ use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
download_verified, resolve, DependencyFailure, EngineArtifactFetcher, EngineTransport,
|
||||
FailureKind, Modrinth, PlannedNode,
|
||||
resolve, EngineArtifactFetcher, EngineTransport, Modrinth, PlannedNode,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::game::install::instance::read_instance_config;
|
||||
@@ -58,7 +57,7 @@ use crate::dto::{
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::super::job::{HostTask, JobSender};
|
||||
use super::plan;
|
||||
use super::{downloads, plan};
|
||||
|
||||
/// 跑一次装入(同步;由 [`super::spawn_install`] 放进后台线程)。
|
||||
pub(super) fn install(
|
||||
@@ -194,56 +193,16 @@ fn run(
|
||||
});
|
||||
|
||||
let fetcher = EngineArtifactFetcher::new(&core_engine);
|
||||
let mut installed: Vec<PlannedNode> = Vec::new();
|
||||
for index in 0..dependency_plan.nodes.len() {
|
||||
if cancel.load(Ordering::SeqCst) {
|
||||
return Err(local("已取消(下载中途)"));
|
||||
}
|
||||
let node = dependency_plan.nodes[index].clone();
|
||||
progress.message = node_label(&node);
|
||||
let _ = tx.send(HostTask::Install {
|
||||
progress: Box::new(progress.clone()),
|
||||
epoch,
|
||||
});
|
||||
// M162:走 `online::download_verified`——sha1 交给引擎流式校验、sha512 落盘后复核,
|
||||
// 失败重试且不留半截文件(内容库安装与整合包导入用**同一段**校验,不各写一份)。
|
||||
let dest = target_dir.join(safe_file_name(&node.file.filename));
|
||||
let outcome = block_on(download_verified(
|
||||
&fetcher,
|
||||
&node.file.url,
|
||||
node.file.sha1(),
|
||||
node.file.sha512(),
|
||||
(node.file.size > 0).then_some(node.file.size),
|
||||
&dest,
|
||||
));
|
||||
match outcome {
|
||||
Ok(_) => installed.push(node),
|
||||
Err(error) => {
|
||||
// 下载阶段的失败**也记进同一套两档结构**(Unreachable + 叶子成因):
|
||||
// 已下好的不回滚、失败项进「可只重试」,整份计划的上下文(缺的是谁的前置)不丢。
|
||||
// 以前这里 `.map_err(...)?` 直接中断,用户看不到「主文件已装、缺 fabric-api」。
|
||||
dependency_plan.record_failure(DependencyFailure {
|
||||
requested_as: node.requested_as.clone(),
|
||||
required_by: node.required_by.clone(),
|
||||
depth: node.depth,
|
||||
kind: FailureKind::Unreachable,
|
||||
cause: Some(error.kind()),
|
||||
message: format!(
|
||||
"下载 {} 失败({}):{}",
|
||||
node.file.filename,
|
||||
error.kind().as_str(),
|
||||
error.message()
|
||||
),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
progress.files_done += 1;
|
||||
let _ = tx.send(HostTask::Install {
|
||||
progress: Box::new(progress.clone()),
|
||||
epoch,
|
||||
});
|
||||
}
|
||||
// 下载阶段在 `downloads.rs`:可测(假下载器能驱动),失败进名单而不中断。
|
||||
let installed = downloads::download_stage(
|
||||
&mut dependency_plan,
|
||||
&target_dir,
|
||||
&fetcher,
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
epoch,
|
||||
)?;
|
||||
|
||||
// 解析与下载两段的失败在这里一起收口:两档分成两句话(不合并成「下载失败」),
|
||||
// 并列出「可只重试这 N 个」。
|
||||
@@ -263,7 +222,7 @@ pub(super) fn finish(progress: &mut InstallProgressView) {
|
||||
}
|
||||
|
||||
/// 进度面板上的那一行文件名(前置标出来,用户才知道多出来的文件是什么)。
|
||||
fn node_label(node: &PlannedNode) -> String {
|
||||
pub(super) fn node_label(node: &PlannedNode) -> String {
|
||||
if node.is_root() {
|
||||
node.file.filename.clone()
|
||||
} else {
|
||||
@@ -272,7 +231,7 @@ fn node_label(node: &PlannedNode) -> String {
|
||||
}
|
||||
|
||||
/// 本地失败(找实例 / 建目录 / 取消)——不是网络问题,归到 `Other`。
|
||||
fn local(reason: impl Into<String>) -> ContentFailure {
|
||||
pub(super) fn local(reason: impl Into<String>) -> ContentFailure {
|
||||
ContentFailure::new(ContentFailureKind::Other, reason)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user