feat(m160): 整合包逐文件覆盖/保留决策 + 导入前冲突清单 + 限额口径统一
主任务(逐文件覆盖/保留)
- core: 新增 import/scan.rs —— 只读预检产出「冲突清单」(目标已存在且 SHA-1 不同才进),
逐条给路径/来源/两侧体积与哈希;用户删过的、内容相同的、被叠层遮蔽的一律不打扰。
- core: 新增 import/decision.rs —— ConflictDecisions(整批档 + 逐项表,逐项优先),
缺省档恒为 KeepUser;ImportRequest.conflict_policy 改由它承载,CLI 调用点不变。
- core: phase.rs 的 settle 按 path 取决策;非冲突路径不受决策影响。
- presenter: ImportScanView / ImportConflictView / ConflictDecisionsView(全部保留 /
全部覆盖 / 逐项{default,items})+ PackCommand::Scan / PackEvent::Scan;host 新增只读
spawn_scan,且新实例路径不再提前建目录(target_of 只算路径,prepare_target 才建)。
- app-iced: 导入前冲突清单页(分页 40 条/页、三入口、计数摘要、确认/取消、两行式行布局);
预检干净时直接往下导,不打扰用户。出图见 C:/Temp/m160-shots/{pick-light,pick-dark}.png。
- i18n: 新增 18 条 en/zh 文案 + 更新「两者都留」那条口径说明。
限额口径统一
- pack/archive/limits.rs 成为唯一来源:PACK_MAX_ENTRIES/UNCOMPRESSED_BYTES/ENTRY_BYTES
(20k / 512 MiB / 64 MiB);读侧 PackLimits::default 与写侧 WriteLimits::production 同源。
- 写侧不再直接用 zip::ZIP64_BYTES_THR;新增单条目闸,于是「能导出的包一定能导入」。
不等式在 pack/tests/limits.rs 里用 const 断言钉死。
测试临时物泄漏(塔台追加项)
- addon/temp.rs:TempRoot —— Drop 即删(panic 展开同样删),Deref/AsRef 到落点路径,
under()/at() 支持"子树落点、删父根"。形状照 core::update::tests::support::TempDir。
- addon::pack 与 addon::tests 两族全部改用 TempRoot(含 fixtures::Fixture 持守卫、
multimc/fabric_instance 等返回守卫);4 GiB 稀疏文件的语义一字未改,只改"谁负责删"。
- 自证:pack/tests/mod.rs 三条用例(drop 删 / panic 也删 / 子树删父根)。
测试
- import_decisions.rs(11 例):预检清单、不打扰、逐项决策后的最终文件集、基线记包内哈希。
- roundtrip_three.rs(2 例):叠层 + 冲突夹具 import→export→import 三轮文件集逐字节一致,
以及「选保留就按设计分叉」的反向证据。
- pack/tests/limits.rs(5 例):单条/总量边界、读侧用同一批常量、写出即可读回。
证据:cargo fmt --check 干净;cargo clippy --workspace --all-targets -D warnings 干净;
cargo test --workspace --all-targets -j 4 → 2507 passed / 0 failed(51 个 test binary)。
This commit is contained in:
1 parent
a4f096eea3
commit
4defc41e8d
64 files changed
+3819
-442
No files matched your search
@@ -8,7 +8,7 @@
|
||||
//!
|
||||
//! | 变量 | 取值 | 作用 |
|
||||
//! |---|---|---|
|
||||
//! | `ULL_CONTENT_PACK` | `importing` / `partial` / `conflict` / `failed` / `export` | 选导入导出区的哪一态(不设 = 只有入口) |
|
||||
//! | `ULL_CONTENT_PACK` | `importing` / `pick` / `partial` / `conflict` / `failed` / `export` | 选导入导出区的哪一态(不设 = 只有入口) |
|
||||
//!
|
||||
//! 夹具**不直接造视图模型**,而是造 presenter 侧的快照(`ImportState` / `ExportState`),
|
||||
//! 再走产品路径同一个 [`Pack::of`]。这样截图里看到的每一行字都是产品路径会画的那一行,
|
||||
@@ -68,6 +68,21 @@ pub(super) fn pack_in(state: &str, lang: Lang) -> (Pack, PackPageState) {
|
||||
|
||||
match state {
|
||||
"importing" => import.progress = Some(progress()),
|
||||
// M160:导入前的冲突清单(分页 + 三个入口 + 计数摘要)
|
||||
"pick" => {
|
||||
import.scan = Some(scan(312));
|
||||
// 停在第 2 页,且已经逐项点过两条「覆盖」——图上能同时看见分页与逐项状态
|
||||
page.pick_page = 1;
|
||||
// 两条落到**第 2 页**的路径上:图上能同时看见「保留」与「覆盖」两种行状态
|
||||
page.pick_choices.insert(
|
||||
"config/generated/module-041.toml".to_string(),
|
||||
unlucky_presenter::dto::ConflictChoiceView::Overwrite,
|
||||
);
|
||||
page.pick_choices.insert(
|
||||
"config/generated/module-044.toml".to_string(),
|
||||
unlucky_presenter::dto::ConflictChoiceView::Overwrite,
|
||||
);
|
||||
}
|
||||
"partial" => import.report = Some(report(ImportStatusView::Partial, false)),
|
||||
"conflict" => {
|
||||
import.report = Some(report(ImportStatusView::Partial, true));
|
||||
@@ -194,6 +209,59 @@ fn conflict(path: &str, reason: ConflictReasonView) -> ConflictFileView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一条待决冲突(M160 预览清单的一行)。
|
||||
fn pending(index: usize) -> unlucky_presenter::dto::ImportConflictView {
|
||||
// 前几条用真实形态的路径,其余按序号生成:截图里既要有可读的真实感,也要有"很多条"的证据。
|
||||
let path = match index {
|
||||
0 => "config/sodium-options.json".to_string(),
|
||||
1 => "mods/create-1.2.3.jar".to_string(),
|
||||
2 => "config/jei/jei-client.ini".to_string(),
|
||||
3 => "options.txt".to_string(),
|
||||
_ => format!("config/generated/module-{index:03}.toml"),
|
||||
};
|
||||
unlucky_presenter::dto::ImportConflictView {
|
||||
path,
|
||||
source: if index.is_multiple_of(3) {
|
||||
unlucky_presenter::dto::ConflictSourceView::ClientOverrides
|
||||
} else {
|
||||
unlucky_presenter::dto::ConflictSourceView::Overrides
|
||||
},
|
||||
reason: if index.is_multiple_of(2) {
|
||||
unlucky_presenter::dto::ConflictReasonView::PreExisting
|
||||
} else {
|
||||
unlucky_presenter::dto::ConflictReasonView::UserModified
|
||||
},
|
||||
disabled: false,
|
||||
pack_sha1: "9f2c41ab77de3350aa1b8c0d5e6f708192a3b4c5".to_string(),
|
||||
pack_size: 3_800 + (index as u64 * 37) % 900,
|
||||
disk_sha1: "c1d2e3f4050617283940a5b6c7d8e9f0a1b2c3d4".to_string(),
|
||||
disk_size: 4_100 + (index as u64 * 53) % 1_200,
|
||||
base_sha1: (!index.is_multiple_of(2))
|
||||
.then(|| "3a7d90ff11bc22445566ef7788990011aabbccdd".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 预检结果(`count` 条冲突;图上靠分页只画当前这一页)。
|
||||
fn scan(count: usize) -> unlucky_presenter::dto::ImportScanView {
|
||||
let conflicts: Vec<unlucky_presenter::dto::ImportConflictView> =
|
||||
(0..count).map(pending).collect();
|
||||
let lost_bytes = conflicts.iter().map(|each| each.disk_size).sum();
|
||||
let incoming_bytes = conflicts.iter().map(|each| each.pack_size).sum();
|
||||
unlucky_presenter::dto::ImportScanView {
|
||||
target: PathBuf::from("C:/ULL/instances/survival/.minecraft"),
|
||||
format: Some(PackFormatView::Modrinth),
|
||||
pack_name: Some("Better MC".to_string()),
|
||||
pack_version: Some("1.2.0".to_string()),
|
||||
game_version: Some("1.20.1".to_string()),
|
||||
conflicts,
|
||||
unknown_remote: 4,
|
||||
lost_bytes,
|
||||
incoming_bytes,
|
||||
notes: Vec::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 导出预检:12 内联 / 1 跳过(包不完整)。
|
||||
fn preview() -> ExportPreviewView {
|
||||
let mut files = vec![ExportFileView {
|
||||
|
||||
Reference in new issue
Block a user