Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
6 changes: 3 additions & 3 deletions docs/IMPROVEMENT_REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 余处命令——后者会让所有命令签名膨胀,与"简单"原则冲突。未注入时回退旧逻辑,保证单元测试可用。

Expand All @@ -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】死代码与占位实现 `[已完成]`

Expand Down Expand Up @@ -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 行 + 文档。

Expand Down
12 changes: 8 additions & 4 deletions docs/architecture/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 镜像源 |
Expand Down Expand Up @@ -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)
```

Expand All @@ -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` | 文件日志(轮转) |
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 默认值
- 支持保存/删除/重置操作

Expand All @@ -57,7 +57,7 @@
```

**优先级顺序**:
1. `.user_version_overrides.json` 用户覆盖配置(最高)
1. `.user-config/version_overrides.json` 用户覆盖配置(最高)
2. `version_manifest.json` 默认清单
3. Dockerfile 中的硬编码默认值(兜底)

Expand Down Expand Up @@ -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}"
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/EXTENSION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
11 changes: 6 additions & 5 deletions docs/architecture/WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 进行合并
Expand All @@ -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())
Expand Down Expand Up @@ -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 标记
Expand Down Expand Up @@ -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 模式匹配)

Expand Down
10 changes: 5 additions & 5 deletions docs/guides/DYNAMIC_BASE_IMAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
↓
Expand Down Expand Up @@ -138,7 +138,7 @@ WORKDIR /www
5. 点击 **应用配置**

系统会自动:
- 更新 `.user_version_overrides.json`
- 更新 `.user-config/version_overrides.json`
- 重新生成 `.env` 和 `docker-compose.yml`
- 下次启动时使用新的基础镜像

Expand All @@ -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
{
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/commands/env_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

/// 单个镜像拉取结果(前端"待拉取"确认弹窗使用)
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/commands/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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。

Expand Down
84 changes: 38 additions & 46 deletions src-tauri/src/engine/backup_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// 备份进度事件
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -330,6 +314,7 @@ impl BackupEngine {
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::user_config;

/// 流式写入必须跨分块边界算出与全量读取一致的 SHA256。
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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"
);
}

Expand All @@ -552,8 +541,9 @@ mod tests {
fs::write(external.path().join("index.php"), b"<?php echo 1;\n").unwrap();
let host = external.path().to_string_lossy().replace('\\', "/");
fs::write(workspace.path().join(".env"), format!("SITE_SHOP={host}\n")).unwrap();
user_config::ensure_dir(workspace.path()).unwrap();
fs::write(
workspace.path().join(".user_sites.json"),
user_config::path(workspace.path(), user_config::SITES),
r#"{"sites":[{"id":"shop","server_name":"shop.test","env_key":"SITE_SHOP","container_path":"/sites/shop","nginx_service":"nginx125","php_service":"php82"}]}"#,
)
.unwrap();
Expand Down Expand Up @@ -592,7 +582,9 @@ mod tests {
names.iter().all(|name| !name.contains(':')),
"ZIP 条目名不应包含盘符: {names:?}"
);
assert!(names.iter().any(|name| name == ".user_sites.json"));
assert!(names
.iter()
.any(|name| name == &user_config::relative(user_config::SITES)));

let mut manifest_file = archive.by_name("manifest.json").unwrap();
let mut json = String::new();
Expand Down
Loading