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
@@ -6,6 +6,21 @@
|
||||
//! `DevDocs/SubAgentsTalk/addon-整合包导入导出对接要点.md`(施工口径)。
|
||||
//! 格式模型与安全闸是 M110 的 [`crate::addon::pack`]:本模块**只消费,不重写**。
|
||||
//!
|
||||
//! # 逐文件冲突决策(M160):预检 → 用户点头 → 导入
|
||||
//!
|
||||
//! | 步骤 | 入口 | 写盘吗 |
|
||||
//! |------|------|--------|
|
||||
//! | 预检:算出"哪几个文件会盖掉用户的" | [`scan_pack`] | **不写**(只读盘算摘要) |
|
||||
//! | 用户点「全部保留 / 全部覆盖 / 逐项」 | [`ConflictDecisions`] | — |
|
||||
//! | 导入 | [`import_pack`] | 写(按决策处置冲突) |
|
||||
//!
|
||||
//! 三条硬口径(改它们之前先看 [`scan`] 与 [`decision`] 的模块文档):
|
||||
//!
|
||||
//! 1. 进冲突清单的**只有**"目标里已存在同名普通文件且内容(SHA-1)不同"的路径;内容相同、
|
||||
//! 目标不存在、以及用户删过的文件都不打扰用户;
|
||||
//! 2. 决策按**路径**给,逐项优先于整批档,整批档缺省 = 保留;
|
||||
//! 3. 缺省**绝不覆盖**:没收到决策的冲突一律保留用户那一侧([`ConflictChoice::default`])。
|
||||
//!
|
||||
//! # 三条硬要求落在哪
|
||||
//!
|
||||
//! | 要求 | 落点 |
|
||||
@@ -63,6 +78,7 @@
|
||||
//! 磁盘满、真实 CurseForge `files[]` 补全(缺 CF 客户端,见 [`fetch`] 的说明)。
|
||||
|
||||
pub mod baseline;
|
||||
pub mod decision;
|
||||
pub mod error;
|
||||
pub mod fetch;
|
||||
pub mod merge;
|
||||
@@ -70,9 +86,11 @@ pub mod phase;
|
||||
pub mod pipeline;
|
||||
pub mod plan;
|
||||
pub mod report;
|
||||
pub mod scan;
|
||||
pub mod stage;
|
||||
|
||||
pub use baseline::{PackBaseline, BASELINE_FILE};
|
||||
pub use decision::ConflictDecisions;
|
||||
pub use error::{ImportError, ImportErrorKind};
|
||||
pub use fetch::{
|
||||
CurseFileMeta, DownloadEngineFetcher, FetchError, FetchErrorKind, FetchFuture, FetchRequest,
|
||||
@@ -86,6 +104,7 @@ pub use report::{
|
||||
ImportProgress, ImportProgressSink, ImportReport, ImportStage, ImportStatus, ImportedFile,
|
||||
LoaderTarget, MissingFile, MissingReason, RegisteredInstance, RejectedComponent,
|
||||
};
|
||||
pub use scan::{scan_pack, ImportConflict, ImportScan};
|
||||
pub use stage::{RollbackOutcome, Staging};
|
||||
|
||||
use std::path::Path;
|
||||
@@ -93,7 +112,15 @@ use std::path::Path;
|
||||
use crate::addon::pack::PackLimits;
|
||||
use crate::platform::layout::LauncherLayout;
|
||||
|
||||
/// 冲突处置策略(默认保留用户文件)。
|
||||
/// 整次导入的**缺省**冲突档(逐项决策见 [`ConflictDecisions`])。
|
||||
///
|
||||
/// 保留这个名字是因为 CLI 已经按它调用(`pack import --overwrite`);语义上它就是
|
||||
/// [`ConflictDecisions::default_choice`] 的构造器:
|
||||
///
|
||||
/// - [`ConflictPolicy::KeepUser`](缺省)= 逐项决策的缺省档是"保留";
|
||||
/// - [`ConflictPolicy::Overwrite`] = 逐项决策的缺省档是"覆盖"。
|
||||
///
|
||||
/// **逐项决策优先于它**,所以"整批覆盖 + 某几条保留"是可表达的。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ConflictPolicy {
|
||||
/// 保留用户那一侧(不覆盖、不补回被删的文件),冲突逐条进报告。
|
||||
@@ -103,6 +130,17 @@ pub enum ConflictPolicy {
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
impl ConflictPolicy {
|
||||
/// 对应的逐项决策缺省档。
|
||||
#[must_use]
|
||||
const fn choice(self) -> ConflictChoice {
|
||||
match self {
|
||||
Self::KeepUser => ConflictChoice::KeepUser,
|
||||
Self::Overwrite => ConflictChoice::Overwrite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一次导入请求。
|
||||
pub struct ImportRequest<'a> {
|
||||
/// 包路径:zip(`.zip` / `.mrpack`)或 MultiMC 实例目录。
|
||||
@@ -115,8 +153,8 @@ pub struct ImportRequest<'a> {
|
||||
pub register: Option<&'a LauncherLayout>,
|
||||
/// 实例展示名(缺省用包名)。
|
||||
pub instance_name: Option<&'a str>,
|
||||
/// 冲突策略。
|
||||
pub conflict_policy: ConflictPolicy,
|
||||
/// 逐文件冲突决策(含整批缺省档,**缺省 = 全部保留**)。
|
||||
pub decisions: ConflictDecisions,
|
||||
/// 包读取限额(条数 / 单条体积 / 解压总量)。
|
||||
pub limits: PackLimits,
|
||||
/// 进度回调。
|
||||
@@ -133,7 +171,7 @@ impl<'a> ImportRequest<'a> {
|
||||
fetcher: None,
|
||||
register: None,
|
||||
instance_name: None,
|
||||
conflict_policy: ConflictPolicy::KeepUser,
|
||||
decisions: ConflictDecisions::default(),
|
||||
limits: PackLimits::default(),
|
||||
progress: None,
|
||||
}
|
||||
@@ -154,10 +192,24 @@ impl<'a> ImportRequest<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// 改冲突策略。
|
||||
/// 整批缺省档(逐项决策仍然优先)。
|
||||
#[must_use]
|
||||
pub fn with_conflict_policy(mut self, policy: ConflictPolicy) -> Self {
|
||||
self.conflict_policy = policy;
|
||||
self.decisions = std::mem::take(&mut self.decisions).with_default(policy.choice());
|
||||
self
|
||||
}
|
||||
|
||||
/// 整表逐文件决策(「全部保留 / 全部覆盖 / 逐项」三条入口的产物)。
|
||||
#[must_use]
|
||||
pub fn with_decisions(mut self, decisions: ConflictDecisions) -> Self {
|
||||
self.decisions = decisions;
|
||||
self
|
||||
}
|
||||
|
||||
/// 挂一条逐项决策(链式,便于测试与命令行逐条给)。
|
||||
#[must_use]
|
||||
pub fn with_decision(mut self, path: impl Into<String>, choice: ConflictChoice) -> Self {
|
||||
self.decisions.set(path, choice);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user