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:
worker-modpackpick authored and AskaEth committed 2026-11-07 13:52:43 +08:00
1 parent a4f096eea3
commit 4defc41e8d
64 files changed
+3819 -442

No files matched your search

+32 -2
View File
@@ -149,8 +149,8 @@ macro_rules! content_entries {
),
PackOverwriteAction => ("Re-import, the pack wins", "以包为准重新导入"),
PackOverwriteNote => (
"Keeping both (renamed) is not supported yet: the import pipeline has one global conflict policy and no per-file decision channel.",
"「两者都留(改名)」暂不支持:导入管线只有一套全局冲突策略,没有逐文件决策通道。"
"Keeping both (renamed) is not supported yet: for each existing file you pick either “keep” or “overwrite”.",
"「两者都留(改名)」暂不支持:每个已存在的文件只能选「保留」或「覆盖」。"
),
PackMissingOffline => ("offline import", "离线导入"),
PackMissingNoSource => (
@@ -173,6 +173,36 @@ macro_rules! content_entries {
"包内 {pack} · 基线 {base} · 磁盘 {disk}"
),
PackConflictDetail => ("Hashes", "三方哈希"),
PackPickTitle => (
"Before importing: {count} files already exist with different content",
"导入前:有 {count} 个文件已存在且内容不同"
),
PackPickSummary => (
"Keep {keep} · overwrite {overwrite} · overwriting discards {bytes}",
"保留 {keep} · 覆盖 {overwrite} · 覆盖将丢弃 {bytes}"
),
PackPickKeepAll => ("Keep all mine", "全部保留我的"),
PackPickOverwriteAll => ("Overwrite all", "全部覆盖"),
PackPickRowKeep => ("Keep", "保留"),
PackPickRowOverwrite => ("Overwrite", "覆盖"),
PackPickPage => ("Items {from}-{to} of {total}", "第 {from}-{to} 项,共 {total} 项"),
PackPickPrev => ("Previous", "上一页"),
PackPickNext => ("Next", "下一页"),
PackPickConfirm => ("Import with these choices", "按这些选择导入"),
PackPickCancel => ("Cancel import", "取消导入"),
PackPickDefaultNote => (
"Anything you do not change stays as you have it: file content is only replaced where you pick “Overwrite”.",
"没有改动的一律按「保留」:只有你点了「覆盖」的文件才会被包内内容替换。"
),
PackPickUnknownRemote => (
"{count} remote entries only learn their path after download; they follow “keep”.",
"还有 {count} 条远端条目要下载后才知道路径,一律按「保留」处置。"
),
PackPickSourceOverrides => ("in the pack (overrides/)", "包内 overrides/"),
PackPickSourceClientOverrides => ("in the pack (client-overrides/)", "包内 client-overrides/"),
PackPickSourceManifest => ("manifest files[]", "清单 files[]"),
PackPickDisk => ("yours: {size}", "你的:{size}"),
PackPickIncoming => ("pack: {size}", "包内:{size}"),
PackFormatModrinth => ("Modrinth .mrpack", "Modrinth 的 .mrpack"),
PackFormatCurseforge => ("CurseForge manifest", "CurseForge 清单"),
PackFormatMultimc => ("MultiMC / Prism instance", "MultiMC·Prism 实例"),