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:
AskaEth committed 2026-11-11 13:29:56 +08:00
1 parent 3e852331f4
commit c921b9c185
28 files changed
+3016 -216

No files matched your search

+25 -1
View File
@@ -106,7 +106,6 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("InstancesModsReasonCollision", "a file with that name already exists", "同名文件已存在"),
("InstancesModsReasonRename", "the rename failed", "改名失败"),
("InstancesSavesTitle", "SAVES", "存档"),
("InstancesSavesReadOnly", "Read-only in this version: nothing here is moved or deleted.", "这一版只读:这里不移动、也不删除任何东西。"),
("InstancesSavesCount", "{worlds} worlds · {shots} screenshots", "{worlds} 个世界 · {shots} 张截图"),
("InstancesSavesLoading", "Reading saves/…", "正在读取 saves/…"),
("InstancesSavesEmpty", "No worlds or screenshots yet — play once and they show up here.", "还没有世界或截图——玩一次就会出现在这里。"),
@@ -125,6 +124,31 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("InstancesSavesReasonDir", "the directory is not usable", "目录不可用"),
("InstancesSavesReasonInstance", "the instance is gone", "实例已不存在"),
("InstancesSavesReasonOpen", "the file manager did not start", "文件管理器没起来"),
("InstancesSavesRename", "Rename", "重命名"),
("InstancesSavesBackup", "Back up", "备份"),
("InstancesSavesDelete", "Delete", "删除"),
("InstancesSavesRenamePlaceholder", "New name for this world", "这个世界的新名字"),
("InstancesSavesRenameConfirm", "Rename", "改名"),
("InstancesSavesCancel", "Cancel", "取消"),
("InstancesSavesBusy", "Working…", "正在处理…"),
("InstancesSavesDeleteTitle", "Delete this world?", "删除这个世界?"),
("InstancesSavesDeleteBody", "{dir} and everything inside it will be deleted. This cannot be undone.", "{dir} 和它里面的全部内容都会被删除,且不能撤销。"),
("InstancesSavesDeleteConfirm", "Delete world", "删除世界"),
("InstancesSavesRenamed", "Renamed {from} → {to}.", "已把 {from} 改名为 {to}。"),
("InstancesSavesDeleted", "Deleted {dir}.", "已删除 {dir}。"),
("InstancesSavesBackedUp", "Backed up {dir} to {path} ({size}).", "已把 {dir} 备份到 {path}({size})。"),
("InstancesSavesBackupSkipped", "{count} symlinks skipped", "跳过 {count} 个符号链接"),
("InstancesSavesRenameFailed", "Could not rename {file}: {reason}", "重命名 {file} 失败:{reason}"),
("InstancesSavesDeleteFailed", "Could not delete {file}: {reason}", "删除 {file} 失败:{reason}"),
("InstancesSavesBackupFailed", "Could not back up {file}: {reason}", "备份 {file} 失败:{reason}"),
("InstancesSavesReasonUnsafeName", "the name is not usable", "名字不可用"),
("InstancesSavesReasonSymlink", "that path is a symbolic link (not followed)", "那个路径是符号链接(不跟随)"),
("InstancesSavesReasonMissing", "the world is gone", "那个世界已不在"),
("InstancesSavesReasonNotDir", "that is not a directory", "那不是目录"),
("InstancesSavesReasonConflict", "a world with that name already exists", "同名世界已存在"),
("InstancesSavesReasonBoundary", "that path is outside saves/", "那个路径落在 saves/ 之外"),
("InstancesSavesReasonTooLarge", "the world is too large for one zip", "这个世界太大,一个 zip 装不下"),
("InstancesSavesReasonWrite", "the write failed", "写盘失败"),
];
/// 金表的**全量**行(本文件 → [`tail`] → [`tail_ai`] → [`tail2`],顺序即 `Key::ALL`)。
+28 -1
View File
@@ -159,7 +159,6 @@ macro_rules! instances_entries {
// 「这一版只读」,不会以为漏了删除按钮。`saves/` / `screenshots/` 是**目录名**,
// 两套语言都原样保留(与 MODS 区的 `mods` 同一条口径)。
InstancesSavesTitle => ("SAVES", "存档"),
InstancesSavesReadOnly => ("Read-only in this version: nothing here is moved or deleted.", "这一版只读:这里不移动、也不删除任何东西。"),
InstancesSavesCount => ("{worlds} worlds · {shots} screenshots", "{worlds} 个世界 · {shots} 张截图"),
InstancesSavesLoading => ("Reading saves/…", "正在读取 saves/…"),
InstancesSavesEmpty => ("No worlds or screenshots yet — play once and they show up here.", "还没有世界或截图——玩一次就会出现在这里。"),
@@ -178,6 +177,34 @@ macro_rules! instances_entries {
InstancesSavesReasonDir => ("the directory is not usable", "目录不可用"),
InstancesSavesReasonInstance => ("the instance is gone", "实例已不存在"),
InstancesSavesReasonOpen => ("the file manager did not start", "文件管理器没起来"),
// ── 存档**写操作**(M247 · SAVESOPS):重命名 / 删除(二次确认)/ 备份导出。
// M235 那条「这一版只读」的 `InstancesSavesReadOnly` **已删**:写操作接上之后它就是假的
// (金表同步少了那一行,不是重排)。删除确认框的三句是**不可逆动作**的兜底,不许省。
InstancesSavesRename => ("Rename", "重命名"),
InstancesSavesBackup => ("Back up", "备份"),
InstancesSavesDelete => ("Delete", "删除"),
InstancesSavesRenamePlaceholder => ("New name for this world", "这个世界的新名字"),
InstancesSavesRenameConfirm => ("Rename", "改名"),
InstancesSavesCancel => ("Cancel", "取消"),
InstancesSavesBusy => ("Working…", "正在处理…"),
InstancesSavesDeleteTitle => ("Delete this world?", "删除这个世界?"),
InstancesSavesDeleteBody => ("{dir} and everything inside it will be deleted. This cannot be undone.", "{dir} 和它里面的全部内容都会被删除,且不能撤销。"),
InstancesSavesDeleteConfirm => ("Delete world", "删除世界"),
InstancesSavesRenamed => ("Renamed {from} → {to}.", "已把 {from} 改名为 {to}。"),
InstancesSavesDeleted => ("Deleted {dir}.", "已删除 {dir}。"),
InstancesSavesBackedUp => ("Backed up {dir} to {path} ({size}).", "已把 {dir} 备份到 {path}({size})。"),
InstancesSavesBackupSkipped => ("{count} symlinks skipped", "跳过 {count} 个符号链接"),
InstancesSavesRenameFailed => ("Could not rename {file}: {reason}", "重命名 {file} 失败:{reason}"),
InstancesSavesDeleteFailed => ("Could not delete {file}: {reason}", "删除 {file} 失败:{reason}"),
InstancesSavesBackupFailed => ("Could not back up {file}: {reason}", "备份 {file} 失败:{reason}"),
InstancesSavesReasonUnsafeName => ("the name is not usable", "名字不可用"),
InstancesSavesReasonSymlink => ("that path is a symbolic link (not followed)", "那个路径是符号链接(不跟随)"),
InstancesSavesReasonMissing => ("the world is gone", "那个世界已不在"),
InstancesSavesReasonNotDir => ("that is not a directory", "那不是目录"),
InstancesSavesReasonConflict => ("a world with that name already exists", "同名世界已存在"),
InstancesSavesReasonBoundary => ("that path is outside saves/", "那个路径落在 saves/ 之外"),
InstancesSavesReasonTooLarge => ("the world is too large for one zip", "这个世界太大,一个 zip 装不下"),
InstancesSavesReasonWrite => ("the write failed", "写盘失败"),
} }
};
}