feat(core,presenter,app-iced): 存档三个写操作(重命名 / 删除 / 备份)+ 行操作区(M247 SAVESOPS)
core::game::saves 新增 write 模块,三个入口的第一句都是 boundary::guard: - rename_world:saves/<old> → saves/<new>,撞名/同名/非法名字各自具名拒绝 - delete_world:递归删(不跟随符号链接、层级上限 64),先过边界校验 - backup_world:流式 zip(限额预检 → 临时文件 → 原子改名), <世界名>-YYYYMMDD-HHMMSS.zip,撞名顺延,绝不覆盖上一份 guard 三道判据(每次写前重做,不信任调用方):名字必须是单段裸名字; saves/ 必须是运行目录的真实直属子目录;世界目录必须是 saves/ 下的真目录 (解析后比父级,不比字符串前缀)。落点目录要求绝对路径(相对路径会落到进程 CWD, 实测过一次)。 presenter:SavesCommand 增 RenameWorld/DeleteWorld/BackupWorld,host 三个后台入口 写成功后重新列一遍目录并把「回执」挂在快照上(SavesOped:改名两端 / 被删的名字 / 备份落点+字节数+跳过的链接数)。SavesFailureView 增 file 与 op,写失败八类一一对应。 SavesState 增 draft / pending / confirm 三样交互态(草稿是行内输入缓冲, pending 与 ModsState::pending 同一条先例,confirm 是删除确认框在等谁)。 app-iced:世界行右侧重命名/备份/删除;行内输入框改名(现成模态队列没有「输入变了」 这一条消息,用输入模态等于重写一套);删除走现成 modal 的 Confirm,主按钮落点接在 app/update/dialog.rs。回执行画在卡片里(备份落点看不见就是静默失败)。 i18n:新增 25 条中英成对 key + 金表同步;删掉 M235 的「这一版只读」那条(写操作接上 之后它是假的)。出图夹具 ULL_SAVES_FIXTURE=rename|confirm|backed-up 三种形态。
This commit is contained in:
1 parent
3e852331f4
commit
c921b9c185
28 files changed
+3016
-216
No files matched your search
@@ -1,17 +1,31 @@
|
||||
//! 存档 / 截图的**只读**列举与「打开文件夹」(M235 · STOREMVP)。
|
||||
//! 存档 / 截图的列举与「打开文件夹」(M235 · STOREMVP)+ 三个写操作(M247 · SAVESOPS)。
|
||||
//!
|
||||
//! # 这一版做到哪、不做什么(塔裁决 U3)
|
||||
//! # 读的那一半(M235)
|
||||
//!
|
||||
//! 用户原话是「存档截图 / 导入」,M230 审计确认三层全缺(core 没有读路径,`saves/` 只在
|
||||
//! `isolation.rs` 的切换探测里被计数;presenter 没有 DTO;i18n 没有键;没有页面)。
|
||||
//! U3 把本 mission 的边界钉成:**只读列表 + 打开文件夹,不做缩略图墙**。理由有两条:
|
||||
//! U3 把 M235 的边界钉成:**只读列表 + 打开文件夹,不做缩略图墙**。理由有两条:
|
||||
//!
|
||||
//! 1. 缩略图要引图片解码依赖,而 `AGENTS.md` §2 只允许 `palette-extract` 之外不引 image 类依赖;
|
||||
//! 2. 截图/导入的产物形态还没定型,先不进产品。
|
||||
//!
|
||||
//! 所以这里**只读**:只调 `read_dir` / `metadata` / `symlink_metadata`,不建目录、不写文件、
|
||||
//! 所以 [`list`] **只读**:只调 `read_dir` / `metadata` / `symlink_metadata`,不建目录、不写文件、
|
||||
//! 不改名字、不读文件内容(`icon.png` 只判存在——那是将来缩略图墙的输入,不是这一版的解码对象)。
|
||||
//!
|
||||
//! # 写的那一半(M247)
|
||||
//!
|
||||
//! 用户接着拍板:「存档按照主流的方式来」——主流第三方启动器(HMCL / PCL2)的存档区都有
|
||||
//! **重命名世界 / 删除世界(二次确认)/ 备份导出**。这三个落在 [`write`]:
|
||||
//!
|
||||
//! | 操作 | 入口 | 边界 |
|
||||
//! |---|---|---|
|
||||
//! | 重命名 | [`write::rename_world`] | 只动 `saves/<old>` → `saves/<new>`;撞名拒 |
|
||||
//! | 删除 | [`write::delete_world`] | 先过边界校验,再递归删;不跟随符号链接 |
|
||||
//! | 备份 | [`write::backup_world`] | 流式 zip 到调用方给的目录,时戳命名,不覆盖 |
|
||||
//!
|
||||
//! **写操作一律自己重新校验 `run_dir` 与目标路径**(不靠调用方传对)——判据在
|
||||
//! [`write`] 的模块文档里一条条列着。
|
||||
//!
|
||||
//! # 单根语义(D20)
|
||||
//!
|
||||
//! 入口是**已经派生好的运行目录**([`crate::game::isolation::derive_game_dir`] 的结果),
|
||||
@@ -23,6 +37,7 @@
|
||||
//!
|
||||
//! 新实例两个目录都不存在,这**不是**异常(`exists = false` + 空列表)。只有「存在但不可用」
|
||||
//! (符号链接 / 不是目录 / 读不了)才是 [`SavesError`];**并且照样不建目录**。
|
||||
//! 写操作那边不同:目标不存在就是 [`write::SavesWriteKind::NotFound`](写不下去,得说清楚)。
|
||||
//!
|
||||
//! # 收纳口径
|
||||
//!
|
||||
@@ -40,18 +55,24 @@
|
||||
//! |---|---|
|
||||
//! | [`error`] | 「目录存在但不可用」的失败形状(带 OS 原文) |
|
||||
//! | [`list`] | 列 `<run_dir>/saves` 与 `<run_dir>/screenshots`(纯只读) |
|
||||
//! | [`write`] | 重命名 / 删除 / 备份导出(三个写操作 + 边界校验) |
|
||||
//!
|
||||
//! 「打开文件夹」不在本模块:平台差异收敛在 [`crate::platform::opener::open_dir`]
|
||||
//! (`AGENTS.md` §6),本模块一个 `cfg` 都不写。
|
||||
|
||||
mod error;
|
||||
mod list;
|
||||
mod write;
|
||||
|
||||
pub use error::SavesError;
|
||||
pub use list::{
|
||||
saves_dir, scan, screenshots_dir, Area, SavesScan, SavesSkip, ScreenshotEntry, SkipKind,
|
||||
WorldEntry, IMAGE_EXTENSIONS, SAVES_DIR, SCREENSHOTS_DIR, WORLD_ICON,
|
||||
};
|
||||
pub use write::{
|
||||
backup_world, delete_world, rename_world, Backup, SavesWriteError, SavesWriteKind, BACKUP_EXT,
|
||||
MAX_BACKUP_ENTRIES, MAX_BACKUP_TOTAL_BYTES,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user