From 72185e1c9cb702ac909ba4a342c57f380f67695b Mon Sep 17 00:00:00 2001 From: jeeinn Date: Wed, 23 Sep 2026 20:43:38 +0800 Subject: [PATCH] refactor(config): store workspace user files under .user-config Keep mirror, version override, and site settings in one directory so later user-side configs do not accumulate at the workspace root. Co-authored-by: Cursor --- .gitignore | 5 +- AGENTS.md | 2 +- docs/IMPROVEMENT_REPORT.md | 6 +- docs/architecture/ARCHITECTURE.md | 12 ++- docs/architecture/DECISIONS.md | 6 +- docs/architecture/EXTENSION_GUIDE.md | 4 +- docs/architecture/WORKFLOWS.md | 11 +-- docs/guides/DYNAMIC_BASE_IMAGE.md | 10 +-- src-tauri/src/commands/env_config.rs | 3 +- src-tauri/src/commands/paths.rs | 2 +- src-tauri/src/engine/backup_engine.rs | 84 +++++++++---------- src-tauri/src/engine/config_generator.rs | 29 ++++--- src-tauri/src/engine/mirror_config_manager.rs | 7 +- src-tauri/src/engine/mod.rs | 1 + src-tauri/src/engine/restore_engine.rs | 79 +++++++---------- src-tauri/src/engine/site_manager.rs | 36 ++++---- src-tauri/src/engine/user_config.rs | 67 +++++++++++++++ src-tauri/src/engine/user_override_manager.rs | 18 ++-- src/i18n/locales/en.json | 2 +- src/i18n/locales/zh-CN.json | 2 +- 20 files changed, 221 insertions(+), 165 deletions(-) create mode 100644 src-tauri/src/engine/user_config.rs diff --git a/.gitignore b/.gitignore index b29017b..f15c193 100644 --- a/.gitignore +++ b/.gitignore @@ -36,9 +36,8 @@ docker-compose.yml* data/ logs/ -# User custom version overrides (machine-specific, same level as .env) -.user_version_overrides.json -.user_mirror_config.json +# Workspace user config (machine-specific) +.user-config/ # Ignore runtime services directory (but keep src-tauri/services templates) services*/ diff --git a/AGENTS.md b/AGENTS.md index 4cfeea5..b6768df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -207,7 +207,7 @@ - 功能:持久化用户自定义镜像覆盖配置 - 特性: - 按服务类型 + 版本 ID 保存/查询覆盖 - - 配置持久化到 `.user_version_overrides.json` + - 配置持久化到 `.user-config/version_overrides.json` ### 10. 工作目录管理器(v0.3.0 新增) - 位置:`src-tauri/src/engine/workspace_manager.rs` diff --git a/docs/IMPROVEMENT_REPORT.md b/docs/IMPROVEMENT_REPORT.md index 156e4d1..b2d1e8f 100644 --- a/docs/IMPROVEMENT_REPORT.md +++ b/docs/IMPROVEMENT_REPORT.md @@ -106,7 +106,7 @@ 2. 用户级配置(workspace.json、.user_*.json、日志)迁移到 Tauri 官方 `app.path().app_data_dir()`(通过 `AppHandle` 传入,不再自己爬目录);工作区内的文件(.env、services/、备份)保持原地不动——它们本来就是"工作区"的一部分。 3. 首次启动时检测旧位置文件并自动迁移 + 提示。 -**实现偏差(2026-09-21)**:本轮只迁移了 `workspace.json` 与日志文件,`.user_mirror_config.json` / `.user_version_overrides.json` **保留在项目根**。原因:这两个文件被 `backup_engine` 按项目根打包、`restore_engine` 按项目根还原,且语义上随工作区走(换工作区就该换一套覆盖)——迁到 app_data_dir 会直接打断备份/恢复闭环,收益不抵风险。若后续要做,需连同备份/恢复管道一起改,届时应作为一个独立改动统一处理。 +**实现偏差(2026-09-21)**:本轮只迁移了 `workspace.json` 与日志文件,用户覆盖文件当时留在项目根。原因:它们被备份/恢复按工作区打包,且语义上随工作区走——迁到 app_data_dir 会打断备份/恢复闭环。后续已收进工作区 `.user-config/`(`mirror_config.json`、`version_overrides.json`、`sites.json`),仍然不进 app_data_dir。 **实现方式**:`app_data_dir` 由 setup 注入一次后存入 `OnceLock`,而不是把 `AppHandle` 透传进 20 余处命令——后者会让所有命令签名膨胀,与"简单"原则冲突。未注入时回退旧逻辑,保证单元测试可用。 @@ -128,7 +128,7 @@ ### A5.【P2】应用数据文件清单没有单一事实来源 `[已完成]` -当前磁盘写入物散布:项目根(.env、docker-compose.yml、services/、data/、logs/、.user_mirror_config.json、.user_version_overrides.json)、exe 目录(workspace.json、php-stack.log)。建议在 `doc/architecture/ARCHITECTURE.md` 增加一张"应用写入了什么、在哪、谁负责"的表,并作为 A1 重构的验收依据。 +当前磁盘写入物散布:项目根(.env、docker-compose.yml、services/、data/、logs/、.user-config/)、exe 目录已不再作为配置落点。`workspace.json` 与 `php-stack.log` 在 app_data_dir。写入清单以 `docs/architecture/ARCHITECTURE.md` 为准。 ### A6.【P2】死代码与占位实现 `[已完成]` @@ -198,7 +198,7 @@ ### E1.【P1】版本清单嵌入二进制,新增版本必须重新发版 `[已完成]` -`version_manifest.rs:44` 用 `include_str!` 把 `services/version_manifest.json` 编译进二进制。PHP 8.5 / MySQL 9.x 发布时,用户必须升级整个应用才能选新版本。而项目已有的 `.user_version_overrides.json` 机制只覆盖"改镜像 tag",覆盖不了"新增条目"。 +`version_manifest.rs:44` 用 `include_str!` 把 `services/version_manifest.json` 编译进二进制。PHP 8.5 / MySQL 9.x 发布时,用户必须升级整个应用才能选新版本。而项目已有的 `.user-config/version_overrides.json` 机制只覆盖"改镜像 tag",覆盖不了"新增条目"。 **建议**:启动时按 `app_data_dir/services/version_manifest.json`(若存在)→ 内置 fallback 的顺序加载,用户可下载新清单文件覆盖。**不**要做远程自动拉取(保持简单,避免新增网络依赖面)。约 30 行 + 文档。 diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 8e657b5..78f7dae 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -103,7 +103,7 @@ PHP-Stack 是一个基于 **Tauri v2 + Docker** 的跨平台 PHP 开发环境可 | 模块 | 文件 | 职责 | |------|------|------| | **版本清单** | `version_manifest.rs` | 管理 `VersionEntry` 数据,提供 `get_entry()`、`get_available_entries()`、`find_entry_by_env_prefix()` 等查询 API | -| **用户覆盖** | `user_override_manager.rs` | 管理 `.user_version_overrides.json`,通过 `get_merged_entry()` 合并用户自定义 `image_tag` | +| **用户覆盖** | `user_override_manager.rs` | 管理 `.user-config/version_overrides.json`,通过 `get_merged_entry()` 合并用户自定义 `image_tag` | | **配置生成** | `config_generator.rs` | 根据 GUI 输入生成 `.env`、`docker-compose.yml`、`services/` 目录 | | **Env 解析** | `env_parser.rs` | `.env` 文件可靠读写,保留注释和空行 | | **镜像源管理** | `mirror_manager.rs` | 统一管理 Docker/APT/Composer/NPM 镜像源 | @@ -263,7 +263,10 @@ php-stack/ │ └── nginx124/ ~ nginx128/ # Nginx 服务模板(5 个版本) ├── .env # 生成的环境变量 ├── docker-compose.yml # 生成的 Compose 文件 -├── .user_version_overrides.json # 用户版本覆盖配置 +├── .user-config/ # 用户侧配置 +│ ├── mirror_config.json +│ ├── version_overrides.json +│ └── sites.json └── workspace.json # 工作目录配置(现位于 app_data_dir) ``` @@ -277,8 +280,9 @@ php-stack/ |--------|------|----------|------| | `.env` / `docker-compose.yml` | **工作区**(`workspace.json` 配置路径) | `config_generator` / `env_config` | 可视化配置生成 | | `services/` / `data/` / `logs/` | 工作区 | `config_generator` / Docker 挂载 | 模板与运行时数据 | -| `.user_mirror_config.json` | 工作区 | `mirror_config_manager` | 用户镜像源覆盖 | -| `.user_version_overrides.json` | 工作区 | `user_override_manager` | 用户镜像 tag 覆盖 | +| `.user-config/mirror_config.json` | 工作区 | `mirror_config_manager` | 用户镜像源覆盖 | +| `.user-config/version_overrides.json` | 工作区 | `user_override_manager` | 用户镜像 tag 覆盖 | +| `.user-config/sites.json` | 工作区 | `site_manager` | 站点定义(不含宿主机绝对路径) | | 备份 ZIP / `.restore_rollback_*.zip` | 用户选择路径 / 工作区 | `backup_engine` / `commands::backup` | 备份与恢复前回滚包 | | `workspace.json` | **app_data_dir** | `workspace_manager` | 工作区路径持久化 | | `php-stack.log` | **app_data_dir** | `logging` | 文件日志(轮转) | diff --git a/docs/architecture/DECISIONS.md b/docs/architecture/DECISIONS.md index a1b5d3a..7fb2047 100644 --- a/docs/architecture/DECISIONS.md +++ b/docs/architecture/DECISIONS.md @@ -40,7 +40,7 @@ **设计理念**: 默认配置由开发者维护(安全性),高级用户可自定义(灵活性)。 **实现策略**: -- `.user_version_overrides.json` 中按 manifest ID 覆盖 `image_tag` +- `.user-config/version_overrides.json` 中按 manifest ID 覆盖 `image_tag` - `get_merged_entry()` 合并逻辑:用户覆盖仅替换 `image_tag`(和可选 `description`),其他字段保持 manifest 默认值 - 支持保存/删除/重置操作 @@ -57,7 +57,7 @@ ``` **优先级顺序**: -1. `.user_version_overrides.json` 用户覆盖配置(最高) +1. `.user-config/version_overrides.json` 用户覆盖配置(最高) 2. `version_manifest.json` 默认清单 3. Dockerfile 中的硬编码默认值(兜底) @@ -164,7 +164,7 @@ for attempt in 1..=10 { **数据流**: ``` -用户配置 (.user_version_overrides.json) +用户配置 (.user-config/version_overrides.json) → config_generator.rs 获取 entry.image_tag → 写入 .env: PHP82_VERSION=php:8.2-fpm-alpine → docker-compose.yml: PHP_BASE_IMAGE="${PHP82_VERSION}" diff --git a/docs/architecture/EXTENSION_GUIDE.md b/docs/architecture/EXTENSION_GUIDE.md index 87e990b..222d358 100644 --- a/docs/architecture/EXTENSION_GUIDE.md +++ b/docs/architecture/EXTENSION_GUIDE.md @@ -107,9 +107,9 @@ cd src-tauri && cargo build 5. 可选填写备注说明 6. 点击"保存" -### 方法 2: 手动编辑 `.user_version_overrides.json` +### 方法 2: 手动编辑 `.user-config/version_overrides.json` -在项目根目录(与 `.env` 同级)创建或编辑 `.user_version_overrides.json`: +在工作区 `.user-config/` 目录创建或编辑 `version_overrides.json`: ```json { diff --git a/docs/architecture/WORKFLOWS.md b/docs/architecture/WORKFLOWS.md index a211fd5..1c14999 100644 --- a/docs/architecture/WORKFLOWS.md +++ b/docs/architecture/WORKFLOWS.md @@ -109,7 +109,7 @@ sequenceDiagram **备份文件格式**: - **命名规则**: `config_backup_YYYYMMDD_HHMMSS.zip` -- **包含内容**: `.env`、`docker-compose.yml`、`services/`、`.user_mirror_config.json`、`.user_version_overrides.json` +- **包含内容**: `.env`、`docker-compose.yml`、`services/`、`.user-config/mirror_config.json`、`.user-config/version_overrides.json` **v0.2.0 变更**: - `apply()` 始终生成全新 `.env`,不再读取现有 `.env` 进行合并 @@ -134,7 +134,7 @@ sequenceDiagram CG->>VM: new() VM->>VM: 加载 version_manifest.json CG->>UOM: new(project_root) - UOM->>UOM: 加载 .user_version_overrides.json + UOM->>UOM: 加载 .user-config/version_overrides.json CG->>UOM: get_merged_entry(&ServiceType::Mysql, "mysql84") UOM-->>CG: VersionEntry { image_tag: "mysql:8.4", service_dir: "mysql84", ... } CG->>CG: env_prefix = "MYSQL84" (service_dir.to_uppercase()) @@ -215,7 +215,7 @@ sequenceDiagram CMD->>VM: new() VM->>VM: 加载 version_manifest.json CMD->>UOM: new(project_root) - UOM->>UOM: 加载 .user_version_overrides.json + UOM->>UOM: 加载 .user-config/version_overrides.json CMD->>VM: get_available_entries(service_type) VM-->>CMD: Vec<(&String, &VersionEntry)>(按版本号降序) CMD->>CMD: 合并 has_user_override 标记 @@ -273,8 +273,9 @@ graph LR - `.env` — 环境变量配置 - `docker-compose.yml` — Compose 配置 - `services/` — 服务配置目录 -- `.user_mirror_config.json` — 用户镜像源配置(如存在) -- `.user_version_overrides.json` — 用户版本覆盖配置(如存在) +- `.user-config/mirror_config.json` — 用户镜像源配置(如存在) +- `.user-config/version_overrides.json` — 用户版本覆盖配置(如存在) +- `.user-config/sites.json` — 站点定义(如存在,不含宿主机绝对路径) - `database/` — 数据库导出(可选) - `projects/` — 项目文件(可选,glob 模式匹配) diff --git a/docs/guides/DYNAMIC_BASE_IMAGE.md b/docs/guides/DYNAMIC_BASE_IMAGE.md index c1f7b2c..12cc977 100644 --- a/docs/guides/DYNAMIC_BASE_IMAGE.md +++ b/docs/guides/DYNAMIC_BASE_IMAGE.md @@ -2,14 +2,14 @@ ## 概述 -PHP-Stack v0.1.0 引入了**动态基础镜像切换**功能,允许用户通过修改 `.env` 文件或 `.user_version_overrides.json` 来自定义 PHP 容器的基础镜像标签。 +PHP-Stack v0.1.0 引入了**动态基础镜像切换**功能,允许用户通过修改 `.env` 文件或 `.user-config/version_overrides.json` 来自定义 PHP 容器的基础镜像标签。 ## 工作原理 ### 1. 数据流向 ``` -用户配置 → .env / .user_version_overrides.json +用户配置 → .env / .user-config/version_overrides.json ↓ config_generator.rs 读取完整镜像标签(如 php:8.2-fpm-alpine) ↓ @@ -138,7 +138,7 @@ WORKDIR /www 5. 点击 **应用配置** 系统会自动: -- 更新 `.user_version_overrides.json` +- 更新 `.user-config/version_overrides.json` - 重新生成 `.env` 和 `docker-compose.yml` - 下次启动时使用新的基础镜像 @@ -160,9 +160,9 @@ docker compose down docker compose up -d --build ``` -### 方法 3:使用 `.user_version_overrides.json` +### 方法 3:使用 `.user-config/version_overrides.json` -创建或编辑 `.user_version_overrides.json`: +创建或编辑 `.user-config/version_overrides.json`: ```json { diff --git a/src-tauri/src/commands/env_config.rs b/src-tauri/src/commands/env_config.rs index 4ebb468..682c4ac 100644 --- a/src-tauri/src/commands/env_config.rs +++ b/src-tauri/src/commands/env_config.rs @@ -3,6 +3,7 @@ use crate::docker::manager::DockerManager; use crate::engine::config_extractor::{ConfigExtractor, ExtractOutcome, ImageStatus}; use crate::engine::config_generator::{ConfigGenerator, EnvConfig}; use crate::engine::site_manager; +use crate::engine::user_config; use crate::engine::version_manifest::{ServiceType as VmServiceType, VersionManifest}; /// 单个镜像拉取结果(前端"待拉取"确认弹窗使用) @@ -372,7 +373,7 @@ pub async fn apply_env_config( ); // 检查用户覆盖配置 - let overrides_path = project_root.join(".user_version_overrides.json"); + let overrides_path = user_config::path(&project_root, user_config::VERSION_OVERRIDES); if overrides_path.exists() { ui_log!( app_handle, diff --git a/src-tauri/src/commands/paths.rs b/src-tauri/src/commands/paths.rs index dccfd4f..e9daa92 100644 --- a/src-tauri/src/commands/paths.rs +++ b/src-tauri/src/commands/paths.rs @@ -11,7 +11,7 @@ //! - `project_root()`:工作区目录(.env / services / 备份所在),语义不同,保持独立; //! - `log_file()`:日志文件路径,随 app_data_dir 走。 //! -//! 约定:**工作区内的文件(.env、docker-compose.yml、services/、备份包、.user_*.json) +//! 约定:**工作区内的文件(.env、docker-compose.yml、services/、备份包、`.user-config/`) //! 一律原地不动**——它们本来就是"工作区"的一部分,且备份/恢复管道按项目根读写。 //! 只有用户级配置与日志迁入 app_data_dir。 diff --git a/src-tauri/src/engine/backup_engine.rs b/src-tauri/src/engine/backup_engine.rs index 104bd95..3f68d00 100644 --- a/src-tauri/src/engine/backup_engine.rs +++ b/src-tauri/src/engine/backup_engine.rs @@ -6,6 +6,7 @@ use std::path::Path; use zip::write::FileOptions; use super::backup_manifest::{BackupManifest, BackupOptions}; +use super::user_config; use crate::app_log; /// 备份进度事件 @@ -56,37 +57,20 @@ impl BackupEngine { // Step 3.5: Pack user custom configuration files (35%) Self::emit_progress(app_handle, "backup.progress.steps.userConfig", 35); - // .user_mirror_config.json - User mirror source configuration - let user_mirror_config_path = project_root.join(".user_mirror_config.json"); - if user_mirror_config_path.exists() { - Self::add_file_to_zip( - &mut zip, - ".user_mirror_config.json", - &user_mirror_config_path, - &mut manifest, - )?; - } - - // .user_version_overrides.json - User version override configuration - let user_version_overrides_path = project_root.join(".user_version_overrides.json"); - if user_version_overrides_path.exists() { - Self::add_file_to_zip( - &mut zip, - ".user_version_overrides.json", - &user_version_overrides_path, - &mut manifest, - )?; - } - - // .user_sites.json - 站点定义(不含宿主机绝对路径) - let user_sites_path = project_root.join(crate::engine::site_manager::SITES_FILE_NAME); - if user_sites_path.exists() { - Self::add_file_to_zip( - &mut zip, - crate::engine::site_manager::SITES_FILE_NAME, - &user_sites_path, - &mut manifest, - )?; + for file_name in [ + user_config::MIRROR_CONFIG, + user_config::VERSION_OVERRIDES, + user_config::SITES, + ] { + let disk_path = user_config::path(project_root, file_name); + if disk_path.exists() { + Self::add_file_to_zip( + &mut zip, + &user_config::relative(file_name), + &disk_path, + &mut manifest, + )?; + } } manifest.sites = crate::engine::site_manager::collect_manifest_sites(project_root); @@ -330,6 +314,7 @@ impl BackupEngine { #[cfg(test)] mod tests { use super::*; + use crate::engine::user_config; /// 流式写入必须跨分块边界算出与全量读取一致的 SHA256。 /// @@ -449,17 +434,17 @@ mod tests { fs::create_dir_all(&services_dir).expect("创建 services 目录失败"); fs::write(services_dir.join("php.ini"), "memory_limit=256M\n").expect("写入 php.ini 失败"); - // Create user custom configuration files + user_config::ensure_dir(project_root).expect("创建 .user-config 失败"); fs::write( - project_root.join(".user_mirror_config.json"), + user_config::path(project_root, user_config::MIRROR_CONFIG), "{\"apt\":{\"source\":\"http://mirrors.aliyun.com/debian/\",\"enabled\":true}}", ) - .expect("写入 .user_mirror_config.json 失败"); + .expect("写入 mirror_config.json 失败"); fs::write( - project_root.join(".user_version_overrides.json"), + user_config::path(project_root, user_config::VERSION_OVERRIDES), "{\"php\":{\"8.2\":{\"tag\":\"8.2-custom\"}}}", ) - .expect("写入 .user_version_overrides.json 失败"); + .expect("写入 version_overrides.json 失败"); let backup_path = project_root.join("backup.zip"); let options = BackupOptions { @@ -501,12 +486,12 @@ mod tests { "ZIP 应包含 services/php82/php.ini,实际: {file_names:?}" ); assert!( - file_names.contains(&".user_mirror_config.json".to_string()), - "ZIP 应包含 .user_mirror_config.json,实际: {file_names:?}" + file_names.contains(&user_config::relative(user_config::MIRROR_CONFIG)), + "ZIP 应包含 .user-config/mirror_config.json,实际: {file_names:?}" ); assert!( - file_names.contains(&".user_version_overrides.json".to_string()), - "ZIP 应包含 .user_version_overrides.json,实际: {file_names:?}" + file_names.contains(&user_config::relative(user_config::VERSION_OVERRIDES)), + "ZIP 应包含 .user-config/version_overrides.json,实际: {file_names:?}" ); assert!( file_names.contains(&"manifest.json".to_string()), @@ -536,12 +521,16 @@ mod tests { "manifest 应包含 services/php82/php.ini 的 SHA256" ); assert!( - manifest.files.contains_key(".user_mirror_config.json"), - "manifest 应包含 .user_mirror_config.json 的 SHA256" + manifest + .files + .contains_key(&user_config::relative(user_config::MIRROR_CONFIG)), + "manifest 应包含 .user-config/mirror_config.json 的 SHA256" ); assert!( - manifest.files.contains_key(".user_version_overrides.json"), - "manifest 应包含 .user_version_overrides.json 的 SHA256" + manifest + .files + .contains_key(&user_config::relative(user_config::VERSION_OVERRIDES)), + "manifest 应包含 .user-config/version_overrides.json 的 SHA256" ); } @@ -552,8 +541,9 @@ mod tests { fs::write(external.path().join("index.php"), b", @@ -339,7 +340,7 @@ impl ConfigGenerator { } } - // Merge mirror configuration from .user_mirror_config.json + // Merge mirror configuration from .user-config/mirror_config.json if let Ok(user_mirror_config) = UserMirrorConfig::load(project_root) { // APT Mirror if let Some(apt_cat) = user_mirror_config.get_category("apt") { @@ -1105,15 +1106,19 @@ impl ConfigGenerator { } // Add user custom configuration files (same as backup_engine.rs) - let user_config_files = - vec![".user_mirror_config.json", ".user_version_overrides.json"]; - - for config_file in &user_config_files { - let config_path = project_root.join(config_file); + let user_config_files = [ + user_config::MIRROR_CONFIG, + user_config::VERSION_OVERRIDES, + user_config::SITES, + ]; + + for file_name in user_config_files { + let config_path = user_config::path(project_root, file_name); + let zip_name = user_config::relative(file_name); if config_path.exists() { match std::fs::read(&config_path) { Ok(content) => { - zip.start_file(config_file, zip_options).map_err(|e| { + zip.start_file(&zip_name, zip_options).map_err(|e| { format!("failed to add user config to zip: {e}") })?; zip.write_all(&content) @@ -1122,7 +1127,7 @@ impl ConfigGenerator { info, "engine::config_generator", "Added user config: {}", - config_file + zip_name ); backed_up_count += 1; } @@ -1131,7 +1136,7 @@ impl ConfigGenerator { warn, "engine::config_generator", "Failed to read user config {}: {}", - config_file, + zip_name, e ); // Continue with other config files @@ -1217,7 +1222,7 @@ impl ConfigGenerator { /// Backup existing configuration files by creating a ZIP archive. /// Format: config_backup_YYYYMMDD_HHMMSS.zip - /// Contains: .env, docker-compose.yml, services/, .user_mirror_config.json, .user_version_overrides.json + /// Contains: .env, docker-compose.yml, services/, .user-config/ pub fn backup_existing_config(project_root: &Path) -> Result, String> { app_log!(info, "engine::config_generator", "Prechecking backup..."); diff --git a/src-tauri/src/engine/mirror_config_manager.rs b/src-tauri/src/engine/mirror_config_manager.rs index 75fd10c..0094855 100644 --- a/src-tauri/src/engine/mirror_config_manager.rs +++ b/src-tauri/src/engine/mirror_config_manager.rs @@ -1,7 +1,7 @@ //! 用户自定义镜像源配置管理器 //! //! 类似于 UserOverrideManager,管理用户对镜像源的自定义配置。 -//! 配置文件:.user_mirror_config.json +//! 配置文件:`.user-config/mirror_config.json` use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -35,7 +35,7 @@ pub struct UserMirrorConfig { impl UserMirrorConfig { /// 从文件加载用户配置 pub fn load(project_root: &Path) -> Result { - let config_path = project_root.join(".user_mirror_config.json"); + let config_path = super::user_config::path(project_root, super::user_config::MIRROR_CONFIG); if !config_path.exists() { return Ok(Self::default()); @@ -50,7 +50,8 @@ impl UserMirrorConfig { /// 保存用户配置到文件 pub fn save(&self, project_root: &Path) -> Result<(), String> { - let config_path = project_root.join(".user_mirror_config.json"); + super::user_config::ensure_dir(project_root)?; + let config_path = super::user_config::path(project_root, super::user_config::MIRROR_CONFIG); let content = serde_json::to_string_pretty(self) .map_err(|e| format!("failed to serialize user mirror config: {e}"))?; diff --git a/src-tauri/src/engine/mod.rs b/src-tauri/src/engine/mod.rs index ae29d58..1d99856 100644 --- a/src-tauri/src/engine/mod.rs +++ b/src-tauri/src/engine/mod.rs @@ -8,6 +8,7 @@ pub mod mirror_config_manager; // 用户镜像源配置管理器 pub mod mirror_manager; // 统一镜像源管理器 pub mod restore_engine; // 恢复引擎 pub mod site_manager; // 站点清单(宿主机路径与容器路径分离) +pub mod user_config; // 工作区用户配置路径 pub mod user_override_manager; // 用户版本覆盖管理器 pub mod version_manifest; // 版本清单管理器 pub mod workspace_manager; // 工作目录管理器 diff --git a/src-tauri/src/engine/restore_engine.rs b/src-tauri/src/engine/restore_engine.rs index 4616348..3b0ca4a 100644 --- a/src-tauri/src/engine/restore_engine.rs +++ b/src-tauri/src/engine/restore_engine.rs @@ -8,6 +8,7 @@ use super::backup_engine::BackupEngine; use super::backup_manifest::{check_manifest_version, BackupManifest}; use super::env_parser::EnvFile; use super::site_manager::{self, ManifestSite}; +use super::user_config; /// 恢复预览信息 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -401,39 +402,21 @@ impl RestoreEngine { // Step 4.5: Restore user custom configuration files Self::emit_progress(app_handle, "restore.progress.steps.userConfig", 45); - // .user_mirror_config.json - User mirror source configuration - match Self::extract_file_to_path( - &mut archive, - ".user_mirror_config.json", - &project_root.join(".user_mirror_config.json"), - ) { - Ok(()) => restored_files.push(".user_mirror_config.json".to_string()), - Err(_) => { - // Not a critical error, file may not exist in backup - } - } - - // .user_version_overrides.json - User version override configuration - match Self::extract_file_to_path( - &mut archive, - ".user_version_overrides.json", - &project_root.join(".user_version_overrides.json"), - ) { - Ok(()) => restored_files.push(".user_version_overrides.json".to_string()), - Err(_) => { - // Not a critical error, file may not exist in backup + for file_name in [ + user_config::MIRROR_CONFIG, + user_config::VERSION_OVERRIDES, + user_config::SITES, + ] { + let zip_name = user_config::relative(file_name); + if let Ok(()) = Self::extract_file_to_path( + &mut archive, + &zip_name, + &user_config::path(project_root, file_name), + ) { + restored_files.push(zip_name); } } - // .user_sites.json - 站点定义(不含宿主机路径) - if let Ok(()) = Self::extract_file_to_path( - &mut archive, - site_manager::SITES_FILE_NAME, - &project_root.join(site_manager::SITES_FILE_NAME), - ) { - restored_files.push(site_manager::SITES_FILE_NAME.to_string()); - } - // Step 5: Extract vhosts/ to services/nginx/conf.d/ Self::emit_progress(app_handle, "restore.progress.steps.vhost", 55); match Self::extract_prefix( @@ -691,6 +674,7 @@ mod tests { use super::*; use crate::engine::backup_engine::BackupEngine; use crate::engine::backup_manifest::{BackupManifest, BackupOptions, ManifestService}; + use crate::engine::user_config; use std::collections::HashMap; use std::fs; use std::io::Write; @@ -886,14 +870,14 @@ mod tests { // Add user custom configuration files let user_mirror_config_content = b"{\"apt\":{\"source\":\"http://mirrors.aliyun.com/debian/\",\"enabled\":true}}"; - zip.start_file(".user_mirror_config.json", zip_options) - .unwrap(); + let mirror_entry = user_config::relative(user_config::MIRROR_CONFIG); + zip.start_file(&mirror_entry, zip_options).unwrap(); zip.write_all(user_mirror_config_content).unwrap(); let user_mirror_hash = BackupEngine::compute_sha256(user_mirror_config_content); + let overrides_entry = user_config::relative(user_config::VERSION_OVERRIDES); let user_version_overrides_content = b"{\"php\":{\"8.2\":{\"tag\":\"8.2-custom\"}}}"; - zip.start_file(".user_version_overrides.json", zip_options) - .unwrap(); + zip.start_file(&overrides_entry, zip_options).unwrap(); zip.write_all(user_version_overrides_content).unwrap(); let user_version_hash = BackupEngine::compute_sha256(user_version_overrides_content); @@ -902,11 +886,8 @@ mod tests { files.insert(".env".to_string(), env_hash); files.insert("docker-compose.yml".to_string(), compose_hash); files.insert("services/php82/php.ini".to_string(), php_ini_hash); - files.insert(".user_mirror_config.json".to_string(), user_mirror_hash); - files.insert( - ".user_version_overrides.json".to_string(), - user_version_hash, - ); + files.insert(mirror_entry, user_mirror_hash); + files.insert(overrides_entry, user_version_hash); let mut ports = HashMap::new(); ports.insert(3306, 3306); @@ -1018,7 +999,7 @@ mod tests { assert_eq!(preview.manifest.services.len(), 1); assert_eq!(preview.manifest.services[0].name, "mysql"); - // Verify file count (5 files: .env, docker-compose.yml, services/php82/php.ini, .user_mirror_config.json, .user_version_overrides.json) + // Verify file count (5 files: .env, docker-compose.yml, services/php82/php.ini, mirror_config.json, version_overrides.json) assert_eq!( preview.file_count, 5, "Should have 5 files (excluding manifest.json), got {}", @@ -1189,30 +1170,28 @@ mod tests { "php.ini should contain memory_limit" ); - // Verify .user_mirror_config.json was restored - let user_mirror_path = restore_dir.join(".user_mirror_config.json"); + let user_mirror_path = user_config::path(&restore_dir, user_config::MIRROR_CONFIG); assert!( user_mirror_path.exists(), - ".user_mirror_config.json should be restored" + ".user-config/mirror_config.json should be restored" ); let user_mirror_content = - fs::read_to_string(&user_mirror_path).expect("读取 .user_mirror_config.json 失败"); + fs::read_to_string(&user_mirror_path).expect("读取 mirror_config.json 失败"); assert!( user_mirror_content.contains("mirrors.aliyun.com"), - ".user_mirror_config.json should contain mirror source" + "mirror_config.json should contain mirror source" ); - // Verify .user_version_overrides.json was restored - let user_version_path = restore_dir.join(".user_version_overrides.json"); + let user_version_path = user_config::path(&restore_dir, user_config::VERSION_OVERRIDES); assert!( user_version_path.exists(), - ".user_version_overrides.json should be restored" + ".user-config/version_overrides.json should be restored" ); let user_version_content = - fs::read_to_string(&user_version_path).expect("读取 .user_version_overrides.json 失败"); + fs::read_to_string(&user_version_path).expect("读取 version_overrides.json 失败"); assert!( user_version_content.contains("8.2-custom"), - ".user_version_overrides.json should contain custom version tag" + "version_overrides.json should contain custom version tag" ); // Verify restored_files list diff --git a/src-tauri/src/engine/site_manager.rs b/src-tauri/src/engine/site_manager.rs index ecd02fc..cfbd9af 100644 --- a/src-tauri/src/engine/site_manager.rs +++ b/src-tauri/src/engine/site_manager.rs @@ -1,6 +1,6 @@ //! 精简站点清单。 //! -//! 宿主机路径只写入 `.env`。`.user_sites.json` 与生成的 Nginx conf 只保留容器路径, +//! 宿主机路径只写入 `.env`。`.user-config/sites.json` 与生成的 Nginx conf 只保留容器路径, //! 这样备份换机器时只需重写 `.env` 里的盘符。 use std::fs; @@ -9,9 +9,8 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use super::env_parser::EnvFile; +use super::user_config; use crate::app_log; - -pub const SITES_FILE_NAME: &str = ".user_sites.json"; pub const MANAGED_MARKER_PREFIX: &str = "# php-stack:managed site="; pub const PRIMARY_ENV_KEY: &str = "SOURCE_DIR"; pub const PRIMARY_CONTAINER_PATH: &str = "/www"; @@ -34,7 +33,7 @@ pub struct SiteEntry { pub public_dir: String, } -/// 写入 `.user_sites.json` 的站点,不含宿主机绝对路径。 +/// 写入 `.user-config/sites.json` 的站点,不含宿主机绝对路径。 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] struct SiteRecord { id: String, @@ -387,21 +386,26 @@ pub fn load_sites_with_hosts(project_root: &Path, env: &EnvFile) -> Vec PathBuf { + user_config::path(project_root, user_config::SITES) +} + pub fn save_sites(project_root: &Path, sites: &[SiteEntry]) -> Result<(), String> { - let path = project_root.join(SITES_FILE_NAME); + let path = sites_path(project_root); + let label = user_config::relative(user_config::SITES); if sites.is_empty() { if path.exists() { - fs::remove_file(&path) - .map_err(|e| format!("failed to remove {SITES_FILE_NAME}: {e}"))?; + fs::remove_file(&path).map_err(|e| format!("failed to remove {label}: {e}"))?; } return Ok(()); } + user_config::ensure_dir(project_root)?; let file = UserSitesFile { sites: sites.iter().map(SiteEntry::to_record).collect(), }; let json = serde_json::to_string_pretty(&file) - .map_err(|e| format!("failed to serialize {SITES_FILE_NAME}: {e}"))?; - fs::write(&path, json).map_err(|e| format!("failed to write {SITES_FILE_NAME}: {e}")) + .map_err(|e| format!("failed to serialize {label}: {e}"))?; + fs::write(&path, json).map_err(|e| format!("failed to write {label}: {e}")) } impl SiteEntry { @@ -419,14 +423,14 @@ impl SiteEntry { } fn read_records(project_root: &Path) -> Result, String> { - let path = project_root.join(SITES_FILE_NAME); + let path = sites_path(project_root); if !path.exists() { return Ok(Vec::new()); } - let text = - fs::read_to_string(&path).map_err(|e| format!("failed to read {SITES_FILE_NAME}: {e}"))?; - let file: UserSitesFile = serde_json::from_str(&text) - .map_err(|e| format!("failed to parse {SITES_FILE_NAME}: {e}"))?; + let label = user_config::relative(user_config::SITES); + let text = fs::read_to_string(&path).map_err(|e| format!("failed to read {label}: {e}"))?; + let file: UserSitesFile = + serde_json::from_str(&text).map_err(|e| format!("failed to parse {label}: {e}"))?; Ok(file.sites) } @@ -766,7 +770,7 @@ mod tests { let mut sites = vec![sample_site("main", "D:/code/app")]; normalize_sites(&mut sites); save_sites(root, &sites).unwrap(); - let raw = fs::read_to_string(root.join(SITES_FILE_NAME)).unwrap(); + let raw = fs::read_to_string(super::sites_path(root)).unwrap(); assert!(!raw.contains("D:/code")); assert!(raw.contains("SOURCE_DIR")); @@ -796,7 +800,7 @@ mod tests { root.join("services/nginx125/conf.d/custom.conf").exists(), "无标记的用户 conf 不应删除" ); - assert!(!root.join(SITES_FILE_NAME).exists()); + assert!(!super::sites_path(root).exists()); } #[test] diff --git a/src-tauri/src/engine/user_config.rs b/src-tauri/src/engine/user_config.rs new file mode 100644 index 0000000..916aaed --- /dev/null +++ b/src-tauri/src/engine/user_config.rs @@ -0,0 +1,67 @@ +//! 工作区内用户侧配置的唯一路径约定。 +//! +//! 全部落在工作区 `.user-config/` 下,文件名不再带前导点。 + +use std::fs; +use std::path::{Path, PathBuf}; + +pub const DIR_NAME: &str = ".user-config"; +pub const MIRROR_CONFIG: &str = "mirror_config.json"; +pub const VERSION_OVERRIDES: &str = "version_overrides.json"; +pub const SITES: &str = "sites.json"; + +pub fn dir(project_root: &Path) -> PathBuf { + project_root.join(DIR_NAME) +} + +pub fn path(project_root: &Path, file_name: &str) -> PathBuf { + dir(project_root).join(file_name) +} + +/// ZIP 条目与清单键。始终使用 `/`,与操作系统无关。 +pub fn relative(file_name: &str) -> String { + format!("{DIR_NAME}/{file_name}") +} + +pub fn ensure_dir(project_root: &Path) -> Result<(), String> { + fs::create_dir_all(dir(project_root)).map_err(|e| format!("failed to create {DIR_NAME}: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn path_and_relative_stay_under_user_config_dir() { + let root = Path::new("workspace"); + let mirror = path(root, MIRROR_CONFIG); + assert_eq!( + mirror.file_name().and_then(|n| n.to_str()), + Some(MIRROR_CONFIG) + ); + assert_eq!( + mirror + .parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()), + Some(DIR_NAME) + ); + assert_eq!(mirror.parent().and_then(|p| p.parent()), Some(root)); + assert_eq!( + relative(VERSION_OVERRIDES), + ".user-config/version_overrides.json" + ); + assert_eq!(relative(SITES), ".user-config/sites.json"); + assert!(!relative(MIRROR_CONFIG).contains('\\')); + } + + #[test] + fn ensure_dir_creates_the_folder() { + let tmp = tempfile::tempdir().expect("创建临时目录失败"); + let folder = dir(tmp.path()); + assert!(!folder.exists()); + ensure_dir(tmp.path()).expect("创建 .user-config 失败"); + assert!(folder.is_dir()); + ensure_dir(tmp.path()).expect("重复创建应成功"); + } +} diff --git a/src-tauri/src/engine/user_override_manager.rs b/src-tauri/src/engine/user_override_manager.rs index 6cc1eba..89c9950 100644 --- a/src-tauri/src/engine/user_override_manager.rs +++ b/src-tauri/src/engine/user_override_manager.rs @@ -39,8 +39,8 @@ impl UserOverrideManager { fn load_user_overrides( project_root: &Path, ) -> HashMap> { - // 使用 project_root 作为配置文件存放位置(与 .env 同级) - let overrides_path = project_root.join(".user_version_overrides.json"); + let overrides_path = + super::user_config::path(project_root, super::user_config::VERSION_OVERRIDES); if !overrides_path.exists() { app_log!( @@ -168,8 +168,9 @@ impl UserOverrideManager { .or_default() .insert(id, override_config); - // 序列化并保存到文件(与 .env 同级目录) - let overrides_path = project_root.join(".user_version_overrides.json"); + super::user_config::ensure_dir(project_root)?; + let overrides_path = + super::user_config::path(project_root, super::user_config::VERSION_OVERRIDES); let json = serde_json::to_string_pretty(&self.user_overrides) .map_err(|e| format!("serialize failed: {e}"))?; @@ -189,8 +190,9 @@ impl UserOverrideManager { entries.remove(id); } - // 重新保存(与 .env 同级目录) - let overrides_path = project_root.join(".user_version_overrides.json"); + super::user_config::ensure_dir(project_root)?; + let overrides_path = + super::user_config::path(project_root, super::user_config::VERSION_OVERRIDES); let json = serde_json::to_string_pretty(&self.user_overrides) .map_err(|e| format!("serialize failed: {e}"))?; @@ -203,8 +205,8 @@ impl UserOverrideManager { pub fn reset_all_overrides(&mut self, project_root: &Path) -> Result<(), String> { self.user_overrides.clear(); - // 删除配置文件(与 .env 同级目录) - let overrides_path = project_root.join(".user_version_overrides.json"); + let overrides_path = + super::user_config::path(project_root, super::user_config::VERSION_OVERRIDES); if overrides_path.exists() { std::fs::remove_file(&overrides_path) .map_err(|e| format!("failed to delete file: {e}"))?; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e5ee1e5..4b3bff6 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -415,7 +415,7 @@ "hints": { "title": "Tips:", "autoApply": "Clicking “{action}” will automatically apply the config to .env file", - "customSaved": "Custom mirrors are saved to .user_mirror_config.json", + "customSaved": "Custom mirrors are saved to .user-config/mirror_config.json", "testConnection": "Click “{action}” to verify mirror availability" }, "confirm": { diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index efe4234..42e26ec 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -415,7 +415,7 @@ "hints": { "title": "提示:", "autoApply": "点击“{action}”按钮后会自动应用配置到 .env 文件", - "customSaved": "自定义镜像源会被保存到 .user_mirror_config.json", + "customSaved": "自定义镜像源会被保存到 .user-config/mirror_config.json", "testConnection": "点击“{action}”验证镜像源是否可用" }, "confirm": {