Harden backup paths and backup handling

This commit is contained in:
chenxiangtong
2026-06-05 20:26:24 +08:00
parent d0103519d4
commit 9cc35b9aac
4 changed files with 437 additions and 36 deletions

147
AGENTS.md Normal file
View File

@@ -0,0 +1,147 @@
# AMT - ARinera Minecraft Tool
Minecraft 实用 TUI 工具。Go + bubbletea前后端分离。
## 文件结构
```
main.go 入口,解析 exe 目录tea.NewProgram(WithAltScreen)
model.go 顶层 model 定义page 枚举Init/Update/View 路由
pages.go 4 个页面的 update/view + resetExecState + describeAction
scan.go 扫描 <exeDir>/*/.minecraft/versions/* 目录
api.go HTTP 客户端 + Action/StepResponse 类型定义
actions.go action 执行引擎download/unzip/delete/copy/move/new/backup
items.go list.Item 实现versionItem, menuItem, mirrorItem
styles.go lipgloss 样式常量
backend/ Python FastAPI 后端(独立进程,端口 3131
```
全部 Go 文件在项目根目录package main。`go build -o dist/amt.exe .`
## 依赖
- `bubbletea` v1.3.10 / `bubbles` v1.0.0 / `lipgloss` v1.1.0
- 标准库:`net/http`, `archive/zip`, `encoding/json`, `io`, `os`, `path/filepath`
## 架构:页面状态机
```
pageVersionSelect ──Enter──▶ pageMainMenu ──"输入数字码"──▶ pageCodeInput ──Enter(4位)──▶ pageExecuting
▲ │ │ │ │
│ Esc/q=退出 │ Esc=返回 任意键=返回
└──"切换版本"─────────────┘ └──────────────────────────┘◀──────────────────────────┘
```
### model 核心字段
| 字段 | 类型 | 用途 |
|------|------|------|
| `currentPage` | `page` (iota 枚举) | 当前页面 |
| `exeDir` | string | 可执行文件所在目录 |
| `versionDir` | string | 选中的版本完整路径 |
| `versionName` | string | 版本目录名 |
| `versionList` | list.Model | 版本选择列表Init 时预初始化,避免零值 panic |
| `menuList` | list.Model | 主菜单列表(同上) |
| `codeInput` | textinput.Model | 4 位数字码输入框 |
| `actions` | []Action | 当前执行的 action 列表 |
| `actionIdx` | int | 当前执行到第几个 |
| `logLines` | []string | 执行日志(也被 viewVersionSelect 用于显示扫描错误) |
| `execErr` / `execDone` | error / bool | 执行状态 |
| `choosingMirror` | bool | 是否在 mirror 选择子页面 |
| `pendingAction` | *Action | mirror 选择时暂存的 action |
| `backupDir` | string | 当次执行的备份相对路径 `amt/backup/<timestamp>` |
| `progressCh` | chan float64 | 下载进度 channel预留 |
### 消息类型
| 消息 | 来源 | 处理页面 |
|------|------|----------|
| `versionsFoundMsg` | `scanVersions()` | pageVersionSelect |
| `scanErrorMsg` | `scanVersions()` | pageVersionSelect |
| `actionsReceivedMsg` | `fetchActions()` | pageExecuting |
| `apiErrorMsg` | `fetchActions()` | pageExecuting |
| `actionCompleteMsg` | `executeAction()` | pageExecuting |
| `actionErrorMsg` | `executeAction()` | pageExecuting |
| `actionProgressMsg` | `waitForProgress()` | pageExecuting |
| `mirrorChoiceMsg` | `executeAdd()` | pageExecuting |
### 状态清理规则
**关键约束**`logLines``viewVersionSelect` 用于判断是否显示错误,必须在离开相关上下文时清空。
- **进入 pageExecuting (T4)**:调用 `resetExecState(m)` 清空所有执行状态
- **离开 pageExecuting (T6)**:同上
- **进入 pageVersionSelect (T3)**:清空 `logLines` + 清空列表项
- **`versionsFoundMsg` 到达时**:清空 `logLines`(覆盖旧扫描错误)
- **离开 pageVersionSelect (T1)**:清空 `logLines`
- **mirror Esc 取消**:清空 `pendingAction`
`resetExecState` 清空字段:`actions`, `actionIdx`, `logLines`, `execErr`, `execDone`, `choosingMirror`, `pendingAction`, `backupDir`, `progressCh`
## 后端 API
地址:`http://localhost:3131`
### GET /tools?code=XXXX
返回 step 结构:
```json
{
"actions": [
{"type": "add", "path": "相对路径", "unzip": false, "url": "https://...", "mirrors": ["https://..."]},
{"type": "delete", "path": "相对路径"},
{"type": "copy", "path": "源相对路径", "new_path": "目标相对路径"},
{"type": "move", "path": "源相对路径", "new_path": "目标相对路径"},
{"type": "new", "path": "相对路径", "is_dir": false}
]
}
```
所有 `path` 相对于 `versionDir`
## Action 执行逻辑 (actions.go)
顺序执行,任一失败立即中断。
| type | 行为 | 备份 |
|------|------|------|
| `add` | 下载 url→pathunzip=true 时解压后删 zip | 目标已存在则备份 |
| `delete` | 删除 path | 删前备份 |
| `copy` | 复制 path→new_path | new_path 已存在则备份 |
| `move` | os.Rename失败则 copy+delete | new_path 已存在则备份 |
| `new` | is_dir=true 创建目录,否则创建空文件(父目录自动创建)。目标已存在则跳过 | 无 |
### 备份
目录:`<versionDir>/amt/backup/<YYYYMMDD_HHMMSS>/<原相对路径>`
每次执行创建一个时间戳目录,同次执行的所有备份共享。
### 下载失败 Mirror 处理
主 URL 失败 → 发送 `mirrorChoiceMsg` → 设 `choosingMirror=true` → 显示 mirror 列表 → 用户选择后用新 URL 重试。用户 Esc 取消则中断执行。
### 关键函数
```
executeAction(versionDir, action, index, backupDir) → tea.Cmd → actionCompleteMsg | actionErrorMsg | mirrorChoiceMsg
executeAdd(...) add 专用,处理下载/解压/mirror
downloadFile(url, dest) 标准 http.Get + 临时文件 + rename
unzipFile(zip, dir) archive/zip 解压,含 zip slip 防护
backupPath(versionDir, relPath, backupDir) 备份单个文件/目录
copyPath(src, dst) 递归复制文件/目录
copyFile(src, dst) 单文件复制
copyDir(src, dst) filepath.Walk 递归复制
```
## 版本扫描 (scan.go)
`filepath.Glob(exeDir/*/.minecraft/versions/*)` → 过滤目录 → `versionsFoundMsg`
## 构建
```bash
go build -o dist/amt.exe . # 或 make
make backend # 启动后端 (uvicorn, port 3131)
```