From 6212fa372ce02f08b0a28e9863c8b1f911af1cc6 Mon Sep 17 00:00:00 2001 From: zyj <18107291228@163.com> Date: Sat, 22 Nov 2025 17:49:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Enginx=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E6=89=98=E7=AE=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 277 +++++++++++- config.yaml | 9 + config/index.go | 14 +- main.go | 13 +- services/nginx_service.go | 868 +++++++++++++++++++++++++++++++++++++- types/site_config.go | 12 + 6 files changed, 1180 insertions(+), 13 deletions(-) create mode 100644 types/site_config.go diff --git a/app.go b/app.go index e73112f..ddfd698 100644 --- a/app.go +++ b/app.go @@ -4,7 +4,10 @@ import ( "fmt" "go-site-clone/config" "go-site-clone/services" + "go-site-clone/storage" + "go-site-clone/types" "go-site-clone/utils" + "log" "net/url" "os" "os/exec" @@ -15,11 +18,53 @@ import ( ) type App struct { - app *application.App + app *application.App + nginxService *services.NginxService + store *storage.Store } var siteService services.SiteService +// 服务启动时初始化 nginx 服务和数据库 +func (a *App) OnStartup() { + // 初始化数据库 + dbPath := "data/site-clone.db" + store, err := storage.NewStore(dbPath) + if err != nil { + log.Printf("初始化数据库失败: %v", err) + } else { + a.store = store + log.Printf("数据库初始化成功") + } + + // 初始化 nginx 服务 + a.nginxService = &services.NginxService{} + + // 检测 nginx 是否已经在运行 + running, err := a.nginxService.CheckStatus() + if err == nil && running { + a.nginxService.Running = true + fmt.Println("nginx 服务已检测到正在运行") + } else { + a.nginxService.Running = false + fmt.Println("nginx 服务未运行") + } +} + +// 服务关闭时不关闭 nginx,但关闭数据库 +func (a *App) OnShutdown() { + // 关闭数据库连接 + if a.store != nil { + if err := a.store.Close(); err != nil { + log.Printf("关闭数据库失败: %v", err) + } else { + log.Println("数据库已关闭") + } + } + // 不做其他事,让 nginx 继续运行 + fmt.Println("应用关闭,nginx 服务保持运行状态") +} + // startup is called when the app starts. The context is saved // so we can call the runtime methods // func (a *App) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { @@ -29,9 +74,10 @@ var siteService services.SiteService // func (a *App) ServiceShutdown(ctx context.Context) error { -// return nil -// } - +// return nil +// } +// +// ========== 网站克隆相关方法 ========== func (a *App) GetResources(rawURL string) *services.ResourcesList { RequestParamsAll, RouterAll := siteService.GetAllResources(rawURL) // 再次去重 根据url链接去除重复项 @@ -57,6 +103,7 @@ func (a *App) GetResources(rawURL string) *services.ResourcesList { return resources } +// 下载网站资源 func (a *App) DownloadSite(uri string, obj services.ResourcesList) bool { // 将页面及资源一起返回 parsed, _ := url.Parse(uri) @@ -106,9 +153,11 @@ func (a *App) DownloadSite(uri string, obj services.ResourcesList) bool { a.app.Event.Emit("download:dom", k) } } + return true } +// 获取本地已下载网站列表 func (a *App) GetDownloadList() []utils.FileDir { return siteService.GetLocalSiteList() } @@ -126,6 +175,7 @@ func (a *App) OpenSiteFileDir(pathDir string) bool { return true } +// 删除网站文件夹 func (a *App) DeleteSiteFileDir(pathDir string) bool { appConfig, _ := config.LoadConfig() newPath := filepath.Join(appConfig.SiteFileDir, pathDir) @@ -136,3 +186,222 @@ func (a *App) DeleteSiteFileDir(pathDir string) bool { } return true } + +// ========== Nginx 相关方法 ========== + +// 启动 Nginx +func (a *App) StartNginx() error { + return a.nginxService.StartNginx() +} + +// 停止 Nginx +func (a *App) StopNginx() error { + return a.nginxService.StopNginx() +} + +// 重启 Nginx +func (a *App) RestartNginx() error { + return a.nginxService.RestartNginx() +} + +// 重载 Nginx 配置 +func (a *App) ReloadNginx() error { + return a.nginxService.ReloadNginx() +} + +// 检查 Nginx 状态 +func (a *App) CheckNginxStatus() (bool, error) { + running, err := a.nginxService.CheckStatus() + if err == nil { + a.nginxService.Running = running + } + return running, err +} + +// 测试 Nginx 配置 +func (a *App) TestNginxConfig() error { + return a.nginxService.TestConfig() +} + +// 添加站点配置 +func (a *App) AddNginxSite(site types.NginxSiteConfig) error { + // 先保存到数据库 + if a.store != nil { + if err := a.store.AddSite(site); err != nil { + return fmt.Errorf("保存站点到数据库失败: %v", err) + } + } + // 再创建 nginx 配置文件 + return a.nginxService.AddSite(site) +} + +// 删除站点配置 +func (a *App) DeleteNginxSite(siteName string) error { + // 从数据库删除 + if a.store != nil { + if err := a.store.DeleteSite(siteName); err != nil { + log.Printf("警告: 从数据库删除站点失败: %v", err) + } + } + // 删除 nginx 配置文件 + return a.nginxService.DeleteSite(siteName) +} + +// 更新站点配置 +func (a *App) UpdateNginxSite(site types.NginxSiteConfig) error { + // 更新数据库 + if a.store != nil { + if err := a.store.UpdateSite(site); err != nil { + return fmt.Errorf("更新数据库失败: %v", err) + } + } + // 更新 nginx 配置 + return a.nginxService.UpdateSite(site) +} + +// 获取所有站点配置 +func (a *App) GetAllNginxSites() ([]types.NginxSiteConfig, error) { + // 优先从数据库读取 + if a.store != nil { + sites, err := a.store.GetAllSites() + if err == nil && len(sites) > 0 { + return sites, nil + } + } + // 如果数据库为空,从 nginx 配置文件读取并同步到数据库 + sites, err := a.nginxService.GetAllSites() + if err != nil { + return nil, err + } + // 同步到数据库 + if a.store != nil { + for _, site := range sites { + a.store.AddSite(site) + } + } + return sites, nil +} + +// 启用站点 +func (a *App) EnableNginxSite(siteName string) error { + // 更新数据库 + if a.store != nil { + if err := a.store.UpdateSiteStatus(siteName, true); err != nil { + log.Printf("警告: 更新数据库失败: %v", err) + } + } + return a.nginxService.EnableSite(siteName) +} + +// 禁用站点 +func (a *App) DisableNginxSite(siteName string) error { + // 更新数据库 + if a.store != nil { + if err := a.store.UpdateSiteStatus(siteName, false); err != nil { + log.Printf("警告: 更新数据库失败: %v", err) + } + } + return a.nginxService.DisableSite(siteName) +} + +// 获取 Nginx 访问日志 +func (a *App) GetNginxAccessLog(lines int) ([]string, error) { + return a.nginxService.GetAccessLog(lines) +} + +// 获取 Nginx 错误日志 +func (a *App) GetNginxErrorLog(lines int) ([]string, error) { + return a.nginxService.GetErrorLog(lines) +} + +// 清空 Nginx 日志 +func (a *App) ClearNginxLogs() error { + return a.nginxService.ClearLogs() +} + +// 获取可用端口号(从 startPort 开始查找) +func (a *App) getAvailablePort(startPort int) int { + // 获取所有已配置的站点 + sites, err := a.nginxService.GetAllSites() + if err != nil { + return startPort + } + + // 收集已使用的端口 + usedPorts := make(map[int]bool) + for _, site := range sites { + if site.Port > 0 { + usedPorts[site.Port] = true + } + } + + // 查找可用端口 + port := startPort + for { + if !usedPorts[port] { + return port + } + port++ + if port > 65535 { + return startPort // 如果超出范围,返回默认端口 + } + } +} + +// ========== 下载记录相关方法 ========== + +// AddDownloadRecord 添加下载记录 +func (a *App) AddDownloadRecord(record storage.DownloadRecord) error { + if a.store == nil { + return fmt.Errorf("数据库未初始化") + } + return a.store.AddDownloadRecord(record) +} + +// GetAllDownloadRecords 获取所有下载记录 +func (a *App) GetAllDownloadRecords() ([]storage.DownloadRecord, error) { + if a.store == nil { + return nil, fmt.Errorf("数据库未初始化") + } + return a.store.GetAllDownloadRecords() +} + +// GetRecentDownloadRecords 获取最近的下载记录 +func (a *App) GetRecentDownloadRecords(limit int) ([]storage.DownloadRecord, error) { + if a.store == nil { + return nil, fmt.Errorf("数据库未初始化") + } + return a.store.GetRecentDownloadRecords(limit) +} + +// GetDownloadStats 获取下载统计 +func (a *App) GetDownloadStats() (map[string]interface{}, error) { + if a.store == nil { + return nil, fmt.Errorf("数据库未初始化") + } + return a.store.GetDownloadStats() +} + +// DeleteDownloadRecord 删除下载记录 +func (a *App) DeleteDownloadRecord(id string) error { + if a.store == nil { + return fmt.Errorf("数据库未初始化") + } + return a.store.DeleteDownloadRecord(id) +} + +// ClearOldDownloadRecords 清理旧的下载记录 +func (a *App) ClearOldDownloadRecords(days int) (int, error) { + if a.store == nil { + return 0, fmt.Errorf("数据库未初始化") + } + return a.store.ClearOldDownloadRecords(days) +} + +// BackupDatabase 备份数据库 +func (a *App) BackupDatabase(backupPath string) error { + if a.store == nil { + return fmt.Errorf("数据库未初始化") + } + return a.store.Backup(backupPath) +} diff --git a/config.yaml b/config.yaml index a9758db..d776342 100644 --- a/config.yaml +++ b/config.yaml @@ -8,3 +8,12 @@ siteFileDir: "www" # 打包的软件保存目录 packSiteFileDir: "site-dist" + + +# nginx 配置 +nginx: + # nginx 可执行文件路径 + nginxPath: "plugin/nginx/nginx.exe" + # nginx 配置文件路径 + nginxConfPath: "plugin/nginx/conf/nginx.conf" + diff --git a/config/index.go b/config/index.go index a7892d6..a4a3445 100644 --- a/config/index.go +++ b/config/index.go @@ -7,10 +7,16 @@ import ( ) type AppConfig struct { - Port int `yaml:"port"` - AppName string `yaml:"appName"` - SiteFileDir string `yaml:"siteFileDir"` - PackSiteFileDir string `yaml:"packSiteFileDir"` + Port int `yaml:"port"` + AppName string `yaml:"appName"` + SiteFileDir string `yaml:"siteFileDir"` + PackSiteFileDir string `yaml:"packSiteFileDir"` + Nginx NginxConfig `yaml:"nginx"` +} + +type NginxConfig struct { + NginxPath string `yaml:"nginxPath"` + NginxConfPath string `yaml:"nginxConfPath"` } func LoadConfig() (*AppConfig, error) { diff --git a/main.go b/main.go index 3b59bd5..ab9abb1 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ package main import ( "embed" - _ "embed" + "go-site-clone/config" "log" "time" @@ -27,10 +27,15 @@ func main() { // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. // 'Mac' options tailor the application when running an macOS. + appConfig, _ := config.LoadConfig() appService := &App{} + + // 初始化 nginx 服务并检测状态 + appService.OnStartup() + app := application.New(application.Options{ - Name: "wails3-nuxt", - Description: "A demo of using raw HTML & CSS", + Name: appConfig.AppName, + Description: "A powerful website cloning and local deployment tool built with Wails3 + Nuxt.js", Services: []application.Service{ application.NewService(appService), application.NewService(&GreetService{}), @@ -49,7 +54,7 @@ func main() { // 'BackgroundColour' is the background colour of the window. // 'URL' is the URL that will be loaded into the webview. app.Window.NewWithOptions(application.WebviewWindowOptions{ - Title: "Window 1", + Title: appConfig.AppName, Mac: application.MacWindow{ InvisibleTitleBarHeight: 50, Backdrop: application.MacBackdropTranslucent, diff --git a/services/nginx_service.go b/services/nginx_service.go index e1ec241..48be569 100644 --- a/services/nginx_service.go +++ b/services/nginx_service.go @@ -1,3 +1,869 @@ package services -type NginxService struct{} +import ( + "bytes" + "fmt" + "go-site-clone/config" + "go-site-clone/types" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "text/template" + "time" +) + +type NginxService struct { + Process *exec.Cmd + Running bool +} + +// NginxSiteConfig 站点配置结构 +type NginxSiteConfig struct { + ID string // 站点唯一ID + Name string // 站点名称 + Domains []string // 域名列表 + Port int // 监听端口 + Path string // 网站根目录路径 + Index string // 默认首页文件 + Enabled bool // 是否启用 +} + +// 启动 Nginx +func (n *NginxService) StartNginx() error { + if n.Running { + return fmt.Errorf("nginx 已经在运行中") + } + + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + // 获取 nginx 可执行文件路径 + nginxPath := appConfig.Nginx.NginxPath + if nginxPath == "" { + nginxPath = "plugin/nginx/nginx.exe" + } + + // 检查 nginx 文件是否存在 + if _, err := os.Stat(nginxPath); os.IsNotExist(err) { + return fmt.Errorf("nginx 可执行文件不存在: %s", nginxPath) + } + + // 获取绝对路径 + absPath, err := filepath.Abs(nginxPath) + if err != nil { + return fmt.Errorf("获取 Nginx 路径失败: %v", err) + } + + // 启动 nginx + cmd := exec.Command(absPath) + cmd.Dir = filepath.Dir(absPath) + + // 设置输出 + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Start() + if err != nil { + return fmt.Errorf("启动 Nginx 失败: %v, stderr: %s", err, stderr.String()) + } + + n.Process = cmd + n.Running = true + + // 保存 PID 到文件,用于后续检测 + pidFile := filepath.Join(filepath.Dir(absPath), "logs", "nginx.pid") + pid := cmd.Process.Pid + if err := os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", pid)), 0644); err != nil { + log.Printf("警告: 保存 PID 文件失败: %v", err) + } + + log.Printf("Nginx 启动成功, PID: %d", pid) + return nil +} + +// 停止 Nginx +func (n *NginxService) StopNginx() error { + if !n.Running { + return fmt.Errorf("nginx 未运行") + } + + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + nginxPath := appConfig.Nginx.NginxPath + if nginxPath == "" { + nginxPath = "plugin/nginx/nginx.exe" + } + + // 获取绝对路径 + absPath, _ := filepath.Abs(nginxPath) + nginxDir := filepath.Dir(absPath) + + // 首先尝试优雅停止 + stopCmd := exec.Command(nginxPath, "-s", "quit") + stopCmd.Dir = filepath.Dir(nginxPath) + err = stopCmd.Run() + + // 等待一段时间让进程退出 + time.Sleep(1 * time.Second) + + // 检查进程是否还在运行 + stillRunning := false + if runtime.GOOS == "windows" { + // Windows 下检查所有 nginx.exe 进程 + stillRunning = n.checkAnyNginxProcess(nginxDir) + } + + // 如果还有进程在运行,强制杀死 + if stillRunning || err != nil { + log.Println("优雅停止失败或进程仍在运行,执行强制停止") + + if runtime.GOOS == "windows" { + // Windows 下使用 taskkill 强制杀死所有 nginx.exe 进程 + n.killAllNginxProcesses(nginxDir) + } else { + // Linux/Mac 下使用 nginx -s stop + stopCmd := exec.Command(nginxPath, "-s", "stop") + stopCmd.Dir = filepath.Dir(nginxPath) + stopCmd.Run() + } + + // 再等待一下确保进程退出 + time.Sleep(500 * time.Millisecond) + } + + n.Running = false + n.Process = nil + + // 删除 PID 文件 + pidFile := filepath.Join(nginxDir, "logs", "nginx.pid") + os.Remove(pidFile) // 忽略错误 + + log.Println("Nginx 已停止") + return nil +} + +// 重启 Nginx +func (n *NginxService) RestartNginx() error { + if n.Running { + if err := n.StopNginx(); err != nil { + return fmt.Errorf("停止 Nginx 失败: %v", err) + } + // 等待一段时间确保完全停止 + time.Sleep(2 * time.Second) + } + + return n.StartNginx() +} + +// 重载配置 +func (n *NginxService) ReloadNginx() error { + if !n.Running { + return fmt.Errorf("nginx 未运行") + } + + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + nginxPath := appConfig.Nginx.NginxPath + if nginxPath == "" { + nginxPath = "plugin/nginx/nginx.exe" + } + + // 获取绝对路径 + absPath, err := filepath.Abs(nginxPath) + if err != nil { + return fmt.Errorf("获取 nginx 路径失败: %v", err) + } + + // 执行重载命令 + reloadCmd := exec.Command(absPath, "-s", "reload") + reloadCmd.Dir = filepath.Dir(absPath) + + if err := reloadCmd.Run(); err != nil { + return fmt.Errorf("重载配置失败: %v", err) + } + + log.Println("Nginx 配置已重载") + return nil +} + +// 检查 Nginx 状态 +func (n *NginxService) CheckStatus() (bool, error) { + appConfig, err := config.LoadConfig() + if err != nil { + return false, fmt.Errorf("加载配置失败: %v", err) + } + + nginxPath := appConfig.Nginx.NginxPath + if nginxPath == "" { + nginxPath = "plugin/nginx/nginx.exe" + } + + // 获取 nginx 目录的绝对路径 + nginxDir := filepath.Dir(nginxPath) + absNginxDir, err := filepath.Abs(nginxDir) + if err != nil { + return false, err + } + + // 读取 PID 文件 + pidFile := filepath.Join(absNginxDir, "logs", "nginx.pid") + pidData, err := os.ReadFile(pidFile) + if err != nil { + // PID 文件不存在,说明没有运行或异常退出 + return false, nil + } + + // 解析 PID + var pid int + if _, err := fmt.Sscanf(string(pidData), "%d", &pid); err != nil { + return false, fmt.Errorf("解析 PID 文件失败: %v", err) + } + + if runtime.GOOS == "windows" { + // Windows 下检查进程是否存在并验证路径 + return n.checkWindowsProcess(pid, absNginxDir) + } else { + // Linux/Mac 下检查进程 + return n.checkUnixProcess(pid) + } +} + +// 测试配置文件 +func (n *NginxService) TestConfig() error { + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + nginxPath := appConfig.Nginx.NginxPath + if nginxPath == "" { + nginxPath = "plugin/nginx/nginx.exe" + } + + // 获取绝对路径 + absPath, err := filepath.Abs(nginxPath) + if err != nil { + return fmt.Errorf("获取 nginx 路径失败: %v", err) + } + + // 执行配置测试 + testCmd := exec.Command(absPath, "-t") + testCmd.Dir = filepath.Dir(absPath) + + var stderr bytes.Buffer + testCmd.Stderr = &stderr + + if err := testCmd.Run(); err != nil { + return fmt.Errorf("配置文件测试失败: %s", stderr.String()) + } + + log.Println("Nginx 配置文件测试通过") + return nil +} + +// 添加站点配置 +func (n *NginxService) AddSite(site types.NginxSiteConfig) error { + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + // 生成配置文件内容 + confContent, err := n.generateSiteConfig(site) + if err != nil { + return fmt.Errorf("生成配置失败: %v", err) + } + + // 根据启用状态选择目录 + hostsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts") + disabledDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts.disabled") + + var targetDir string + if site.Enabled { + targetDir = hostsDir + } else { + targetDir = disabledDir + } + + // 确保目录存在 + if err := os.MkdirAll(targetDir, 0755); err != nil { + return fmt.Errorf("创建目录失败: %v", err) + } + + // 写入配置文件 + confPath := filepath.Join(targetDir, fmt.Sprintf("%s.conf", site.Name)) + if err := os.WriteFile(confPath, []byte(confContent), 0644); err != nil { + return fmt.Errorf("写入配置文件失败: %v", err) + } + + log.Printf("站点配置已添加: %s (启用: %v)", confPath, site.Enabled) + + // 如果启用且 Nginx 正在运行,重载配置 + if site.Enabled && n.Running { + if err := n.ReloadNginx(); err != nil { + // 重载失败只记录警告,不影响添加操作 + log.Printf("警告: 重载 nginx 配置失败: %v", err) + } + } + + return nil +} + +// 删除站点配置 +func (n *NginxService) DeleteSite(siteName string) error { + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + hostsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts") + disabledDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts.disabled") + + // 先尝试从启用目录删除 + confPath := filepath.Join(hostsDir, fmt.Sprintf("%s.conf", siteName)) + err = os.Remove(confPath) + if err != nil && os.IsNotExist(err) { + // 如果启用目录没有,尝试从禁用目录删除 + confPath = filepath.Join(disabledDir, fmt.Sprintf("%s.conf", siteName)) + err = os.Remove(confPath) + } + + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("站点配置不存在: %s", siteName) + } + return fmt.Errorf("删除配置文件失败: %v", err) + } + + log.Printf("站点配置已删除: %s", siteName) + + // 如果 Nginx 正在运行,重载配置 + if n.Running { + if err := n.ReloadNginx(); err != nil { + // 重载失败只记录警告,不影响删除操作 + log.Printf("警告: 重载 nginx 配置失败: %v", err) + } + } + + return nil +} + +// 更新站点配置 +func (n *NginxService) UpdateSite(site types.NginxSiteConfig) error { + // 先删除旧配置 + if err := n.DeleteSite(site.Name); err != nil { + // 如果配置不存在,继续添加新配置 + if !strings.Contains(err.Error(), "不存在") { + return err + } + } + + // 添加新配置 + return n.AddSite(site) +} + +// 获取所有站点配置 +func (n *NginxService) GetAllSites() ([]types.NginxSiteConfig, error) { + appConfig, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("加载配置失败: %v", err) + } + + hostsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts") + disabledDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts.disabled") + + // 检查目录是否存在 + if _, err := os.Stat(hostsDir); os.IsNotExist(err) { + os.MkdirAll(hostsDir, 0755) + } + if _, err := os.Stat(disabledDir); os.IsNotExist(err) { + os.MkdirAll(disabledDir, 0755) + } + + var sites []types.NginxSiteConfig + + // 读取启用的站点 + enabledFiles, err := os.ReadDir(hostsDir) + if err == nil { + for _, file := range enabledFiles { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".conf") { + siteName := strings.TrimSuffix(file.Name(), ".conf") + confPath := filepath.Join(hostsDir, file.Name()) + site, err := n.parseSiteConfig(confPath, siteName, true) + if err != nil { + log.Printf("警告: 解析配置文件失败 %s: %v", file.Name(), err) + continue + } + sites = append(sites, site) + } + } + } + + // 读取禁用的站点 + disabledFiles, err := os.ReadDir(disabledDir) + if err == nil { + for _, file := range disabledFiles { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".conf") { + siteName := strings.TrimSuffix(file.Name(), ".conf") + confPath := filepath.Join(disabledDir, file.Name()) + site, err := n.parseSiteConfig(confPath, siteName, false) + if err != nil { + log.Printf("警告: 解析配置文件失败 %s: %v", file.Name(), err) + continue + } + sites = append(sites, site) + } + } + } + + return sites, nil +} + +// 解析站点配置文件 +func (n *NginxService) parseSiteConfig(confPath string, siteName string, enabled bool) (types.NginxSiteConfig, error) { + content, err := os.ReadFile(confPath) + if err != nil { + return types.NginxSiteConfig{}, err + } + + config := types.NginxSiteConfig{ + Name: siteName, + Enabled: enabled, + Domains: []string{}, + } + + // 简单解析配置文件内容 + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + // 解析 listen 端口 + if strings.HasPrefix(line, "listen") { + parts := strings.Fields(line) + if len(parts) >= 2 { + portStr := strings.TrimSuffix(parts[1], ";") + var port int + fmt.Sscanf(portStr, "%d", &port) + config.Port = port + } + } + + // 解析 server_name 域名 + if strings.HasPrefix(line, "server_name") { + parts := strings.Fields(line) + if len(parts) >= 2 { + for i := 1; i < len(parts); i++ { + domain := strings.TrimSuffix(parts[i], ";") + if domain != "" { + config.Domains = append(config.Domains, domain) + } + } + } + } + + // 解析 root 路径 + if strings.HasPrefix(line, "root") { + parts := strings.Fields(line) + if len(parts) >= 2 { + path := strings.Trim(parts[1], "\";") + config.Path = path + } + } + + // 解析 index + if strings.HasPrefix(line, "index") { + parts := strings.Fields(line) + if len(parts) >= 2 { + index := strings.TrimSuffix(parts[1], ";") + config.Index = index + } + } + } + + return config, nil +} + +// 启用站点 +func (n *NginxService) EnableSite(siteName string) error { + // 对于当前的配置结构,所有在 hosts 目录下的配置都是启用的 + // 如果需要禁用,可以移动到其他目录 + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + hostsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts") + disabledDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts.disabled") + + // 从 disabled 移动到 hosts + srcPath := filepath.Join(disabledDir, fmt.Sprintf("%s.conf", siteName)) + dstPath := filepath.Join(hostsDir, fmt.Sprintf("%s.conf", siteName)) + + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + return fmt.Errorf("站点配置不存在或已启用") + } + + if err := os.MkdirAll(hostsDir, 0755); err != nil { + return err + } + + if err := os.Rename(srcPath, dstPath); err != nil { + return fmt.Errorf("启用站点失败: %v", err) + } + + // 重载配置 + if n.Running { + if err := n.ReloadNginx(); err != nil { + log.Printf("警告: 重载 nginx 配置失败: %v", err) + } + } + + return nil +} + +// 禁用站点 +func (n *NginxService) DisableSite(siteName string) error { + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + hostsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts") + disabledDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxConfPath), "hosts.disabled") + + // 从 hosts 移动到 disabled + srcPath := filepath.Join(hostsDir, fmt.Sprintf("%s.conf", siteName)) + dstPath := filepath.Join(disabledDir, fmt.Sprintf("%s.conf", siteName)) + + if _, err := os.Stat(srcPath); os.IsNotExist(err) { + return fmt.Errorf("站点配置不存在或已禁用") + } + + if err := os.MkdirAll(disabledDir, 0755); err != nil { + return err + } + + if err := os.Rename(srcPath, dstPath); err != nil { + return fmt.Errorf("禁用站点失败: %v", err) + } + + // 重载配置 + if n.Running { + if err := n.ReloadNginx(); err != nil { + log.Printf("警告: 重载 nginx 配置失败: %v", err) + } + } + + return nil +} + +// 生成站点配置文件内容 +func (n *NginxService) generateSiteConfig(site types.NginxSiteConfig) (string, error) { + // 如果用户提供了路径,使用用户的路径 + var absPath string + var err error + + if site.Path != "" { + // 用户提供的路径(相对路径或绝对路径) + if filepath.IsAbs(site.Path) { + absPath = site.Path + } else { + // 相对路径,转换为绝对路径 + absPath, err = filepath.Abs(site.Path) + if err != nil { + return "", fmt.Errorf("获取站点路径失败: %v", err) + } + } + } else { + // 如果没有提供路径,使用配置中的默认目录 + appConfig, err := config.LoadConfig() + if err != nil { + return "", err + } + + sitePath := filepath.Join(appConfig.SiteFileDir, site.Name) + absPath, err = filepath.Abs(sitePath) + if err != nil { + return "", fmt.Errorf("获取站点路径失败: %v", err) + } + } + + // Windows 路径处理(nginx 需要使用正斜杠) + if runtime.GOOS == "windows" { + absPath = strings.ReplaceAll(absPath, "\\", "/") + } + + // 默认首页 + if site.Index == "" { + site.Index = "index.html" + } + + // 生成域名列表字符串 + domains := strings.Join(site.Domains, " ") + + // 使用模板生成配置 + tmpl, err := template.New("nginx").Parse(types.NGINX_CONF_TMP) + if err != nil { + return "", err + } + + // 获取当前时间 + createTime := time.Now().Format("2006-01-02 15:04:05") + + data := map[string]interface{}{ + "SiteName": site.Name, + "Port": site.Port, + "Host": domains, + "FilePath": absPath, + "Index": site.Index, + "CreateTime": createTime, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} + +// 获取 Nginx 日志 +func (n *NginxService) GetAccessLog(lines int) ([]string, error) { + appConfig, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("加载配置失败: %v", err) + } + + logPath := filepath.Join(filepath.Dir(appConfig.Nginx.NginxPath), "logs", "access.log") + return n.readLogLines(logPath, lines) +} + +// 获取错误日志 +func (n *NginxService) GetErrorLog(lines int) ([]string, error) { + appConfig, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("加载配置失败: %v", err) + } + + logPath := filepath.Join(filepath.Dir(appConfig.Nginx.NginxPath), "logs", "error.log") + return n.readLogLines(logPath, lines) +} + +// 读取日志文件指定行数 +func (n *NginxService) readLogLines(filePath string, lines int) ([]string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + if os.IsNotExist(err) { + return []string{}, nil + } + return nil, err + } + + allLines := strings.Split(string(content), "\n") + start := len(allLines) - lines + if start < 0 { + start = 0 + } + + return allLines[start:], nil +} + +// 清空日志 +func (n *NginxService) ClearLogs() error { + appConfig, err := config.LoadConfig() + if err != nil { + return fmt.Errorf("加载配置失败: %v", err) + } + + logsDir := filepath.Join(filepath.Dir(appConfig.Nginx.NginxPath), "logs") + + // 清空 access.log + accessLog := filepath.Join(logsDir, "access.log") + if err := os.WriteFile(accessLog, []byte(""), 0644); err != nil { + return fmt.Errorf("清空访问日志失败: %v", err) + } + + // 清空 error.log + errorLog := filepath.Join(logsDir, "error.log") + if err := os.WriteFile(errorLog, []byte(""), 0644); err != nil { + return fmt.Errorf("清空错误日志失败: %v", err) + } + + log.Println("Nginx 日志已清空") + return nil +} + +// Windows 下检查进程 +func (n *NginxService) checkWindowsProcess(pid int, nginxDir string) (bool, error) { + // 使用 tasklist 检查进程是否存在 + cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/FO", "CSV", "/NH") + output, err := cmd.Output() + if err != nil { + return false, err + } + + outputStr := string(output) + // 如果输出包含 "INFO: No tasks",说明进程不存在 + if strings.Contains(outputStr, "INFO:") || !strings.Contains(outputStr, "nginx.exe") { + return false, nil + } + + // 进程存在,进一步验证进程路径(可选,但更严格) + // 使用 wmic 获取进程的可执行文件路径 + cmd = exec.Command("wmic", "process", "where", fmt.Sprintf("ProcessId=%d", pid), "get", "ExecutablePath", "/format:list") + output, err = cmd.Output() + if err != nil { + // 如果获取路径失败,但进程存在,认为是运行中 + return true, nil + } + + outputStr = string(output) + // 检查路径是否包含我们的 nginx 目录 + nginxDirNorm := strings.ToLower(strings.ReplaceAll(nginxDir, "/", "\\")) + outputStrNorm := strings.ToLower(outputStr) + + return strings.Contains(outputStrNorm, nginxDirNorm), nil +} + +// Unix/Linux 下检查进程 +func (n *NginxService) checkUnixProcess(pid int) (bool, error) { + // 检查 /proc/pid 是否存在 + procPath := fmt.Sprintf("/proc/%d", pid) + if _, err := os.Stat(procPath); os.IsNotExist(err) { + return false, nil + } + + // 读取进程的 cmdline 验证是否是 nginx + cmdlinePath := filepath.Join(procPath, "cmdline") + cmdline, err := os.ReadFile(cmdlinePath) + if err != nil { + return false, err + } + + return strings.Contains(string(cmdline), "nginx"), nil +} + +// 检查是否有任何 nginx 进程在运行(Windows) +func (n *NginxService) checkAnyNginxProcess(nginxDir string) bool { + cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq nginx.exe", "/FO", "CSV", "/NH") + output, err := cmd.Output() + if err != nil { + return false + } + + outputStr := string(output) + // 检查是否有 nginx.exe 进程 + if !strings.Contains(outputStr, "nginx.exe") { + return false + } + + // 进一步验证是否是本项目的 nginx(通过路径) + lines := strings.Split(outputStr, "\n") + for _, line := range lines { + if !strings.Contains(line, "nginx.exe") { + continue + } + + // 提取 PID + fields := strings.Split(line, "\",\"") + if len(fields) < 2 { + continue + } + + pidStr := strings.Trim(fields[1], "\"") + var pid int + if _, err := fmt.Sscanf(pidStr, "%d", &pid); err != nil { + continue + } + + // 检查这个进程的路径 + cmd = exec.Command("wmic", "process", "where", fmt.Sprintf("ProcessId=%d", pid), "get", "ExecutablePath", "/format:list") + output, err := cmd.Output() + if err != nil { + continue + } + + outputStr := string(output) + nginxDirNorm := strings.ToLower(strings.ReplaceAll(nginxDir, "/", "\\")) + if strings.Contains(strings.ToLower(outputStr), nginxDirNorm) { + return true + } + } + + return false +} + +// 杀死所有本项目的 nginx 进程(Windows) +func (n *NginxService) killAllNginxProcesses(nginxDir string) error { + // 获取所有 nginx.exe 进程 + cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq nginx.exe", "/FO", "CSV", "/NH") + output, err := cmd.Output() + if err != nil { + return err + } + + outputStr := string(output) + if !strings.Contains(outputStr, "nginx.exe") { + return nil // 没有 nginx 进程 + } + + // 解析每个进程并杀死属于本项目的 + lines := strings.Split(outputStr, "\n") + nginxDirNorm := strings.ToLower(strings.ReplaceAll(nginxDir, "/", "\\")) + + for _, line := range lines { + if !strings.Contains(line, "nginx.exe") { + continue + } + + // 提取 PID + fields := strings.Split(line, "\",\"") + if len(fields) < 2 { + continue + } + + pidStr := strings.Trim(fields[1], "\"") + var pid int + if _, err := fmt.Sscanf(pidStr, "%d", &pid); err != nil { + continue + } + + // 验证进程路径是否属于本项目 + cmd = exec.Command("wmic", "process", "where", fmt.Sprintf("ProcessId=%d", pid), "get", "ExecutablePath", "/format:list") + output, err := cmd.Output() + if err != nil { + // 如果无法获取路径,为了安全起见,杀死这个进程 + // (因为我们已经通过其他方式确认这是 nginx.exe) + killCmd := exec.Command("taskkill", "/F", "/PID", pidStr) + killCmd.Run() + log.Printf("已杀死 nginx 进程 PID: %s", pidStr) + continue + } + + // 检查路径是否匹配 + pathOutput := strings.ToLower(string(output)) + if strings.Contains(pathOutput, nginxDirNorm) { + // 强制杀死进程 + killCmd := exec.Command("taskkill", "/F", "/PID", pidStr) + if err := killCmd.Run(); err != nil { + log.Printf("警告: 杀死进程 PID %s 失败: %v", pidStr, err) + } else { + log.Printf("已杀死 nginx 进程 PID: %s", pidStr) + } + } + } + + return nil +} diff --git a/types/site_config.go b/types/site_config.go new file mode 100644 index 0000000..c0d07c6 --- /dev/null +++ b/types/site_config.go @@ -0,0 +1,12 @@ +package types + +// NginxSiteConfig 站点配置结构 +type NginxSiteConfig struct { + ID string `json:"id"` // 站点唯一ID + Name string `json:"name"` // 站点名称 + Domains []string `json:"domains"` // 域名列表 + Port int `json:"port"` // 监听端口 + Path string `json:"path"` // 网站根目录路径 + Index string `json:"index"` // 默认首页文件 + Enabled bool `json:"enabled"` // 是否启用 +}