package main import ( "bytes" "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" "path/filepath" "github.com/go-rod/rod/lib/proto" "github.com/wailsapp/wails/v3/pkg/application" ) type App struct { 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 { // return nil // } // func (a *App) ServiceShutdown(ctx context.Context) error { // return nil // } // // ========== 网站克隆相关方法 ========== func (a *App) GetResources(rawURL string) *services.ResourcesList { RequestParamsAll, RouterAll := siteService.GetAllResources(rawURL) // 再次去重 根据url链接去除重复项 dataList := siteService.DeduplicationRequestByUrl(RequestParamsAll) resources := &services.ResourcesList{ Dom: RouterAll, } // 将页面及资源一起返回 for _, v := range dataList { if v.Type == proto.NetworkResourceTypeImage { resources.Image = append(resources.Image, v.URL) } if v.Type == proto.NetworkResourceTypeStylesheet { resources.CSS = append(resources.CSS, v.URL) } if v.Type == proto.NetworkResourceTypeScript { resources.Script = append(resources.Script, v.URL) } if v.Type == proto.NetworkResourceTypeMedia { resources.Video = append(resources.Video, v.URL) } } return resources } // 下载网站资源 func (a *App) DownloadSite(uri string, obj services.ResourcesList) bool { // 将页面及资源一起返回 parsed, _ := url.Parse(uri) var File utils.File if len(obj.CSS) > 0 { for k, v := range obj.CSS { u, _ := url.Parse(v) if parsed.Hostname() == u.Hostname() { File.Download(v) } a.app.Event.Emit("download:css", k) } } if len(obj.Script) > 0 { for k, v := range obj.Script { u, _ := url.Parse(v) if parsed.Hostname() == u.Hostname() { File.Download(v) } a.app.Event.Emit("download:script", k) } } if len(obj.Image) > 0 { for k, v := range obj.Image { u, _ := url.Parse(v) if parsed.Hostname() == u.Hostname() { File.Download(v) } a.app.Event.Emit("download:image", k) } } if len(obj.Video) > 0 { for k, v := range obj.Video { u, _ := url.Parse(v) if parsed.Hostname() == u.Hostname() { File.Download(v) } a.app.Event.Emit("download:video", k) } } if len(obj.Dom) > 0 { for k, v := range obj.Dom { u, _ := url.Parse(v) if parsed.Hostname() == u.Hostname() { File.HTMLDownload(v) } a.app.Event.Emit("download:dom", k) } } return true } // 获取本地已下载网站列表 func (a *App) GetDownloadList() []utils.FileDir { return siteService.GetLocalSiteList() } // 打开网站文件夹 func (a *App) OpenSiteFileDir(pathDir string) bool { appConfig, _ := config.LoadConfig() newPath := filepath.Join(appConfig.SiteFileDir, pathDir) cmd := exec.Command("explorer", newPath) err := cmd.Start() if err != nil { fmt.Println("打开文件夹失败:", err) return false } return true } // 删除网站文件夹 func (a *App) DeleteSiteFileDir(pathDir string) bool { appConfig, _ := config.LoadConfig() newPath := filepath.Join(appConfig.SiteFileDir, pathDir) err := os.RemoveAll(newPath) if err != nil { fmt.Println("删除文件夹失败:", err) return false } 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) } // SelectFolder 打开文件夹选择对话框 func (a *App) SelectFolder() (string, error) { result, err := a.app.Dialog.OpenFileWithOptions(&application.OpenFileDialogOptions{ CanChooseDirectories: true, CanChooseFiles: false, Title: "选择网站文件夹", ButtonText: "选择", }).PromptForSingleSelection() if err != nil { return "", err } return result, nil } // ========== 打包相关方法 ========== // CheckEnvironment 检查Go和Wails环境 func (a *App) CheckEnvironment() (*utils.EnvStatus, error) { return utils.GetEnvStatus() } // InstallGo 安装Go环境 func (a *App) InstallGo(version string) error { scriptPath, err := utils.GetInstallScriptPath("go") if err != nil { return err } // 构建命令参数 args := []string{"/c", scriptPath} if version != "" { args = append(args, version) } // 发送安装进度事件 a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "go", "step": "downloading", "percent": 10, "message": "正在下载 Go...", }) // 在Windows上使用cmd.exe执行bat文件 cmd := exec.Command("cmd.exe", args...) cmd.Dir = filepath.Dir(scriptPath) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // 启动命令但不等待完成(bat脚本会暂停) err = cmd.Start() if err != nil { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "go", "step": "error", "percent": 0, "error": fmt.Sprintf("启动安装脚本失败: %v", err), }) return fmt.Errorf("安装失败: %v", err) } // 在后台等待完成 go func() { err := cmd.Wait() if err != nil { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "go", "step": "error", "percent": 0, "error": stderr.String(), }) } else { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "go", "step": "completed", "percent": 100, "message": "Go 安装完成", }) } }() return nil } // InstallWails 安装Wails3环境 func (a *App) InstallWails() error { scriptPath, err := utils.GetInstallScriptPath("wails") if err != nil { return err } // 发送安装进度事件 a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "wails", "step": "installing", "percent": 10, "message": "正在安装 Wails3...", }) // 在Windows上使用cmd.exe执行bat文件 cmd := exec.Command("cmd.exe", "/c", scriptPath) cmd.Dir = filepath.Dir(scriptPath) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // 启动命令但不等待完成 err = cmd.Start() if err != nil { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "wails", "step": "error", "percent": 0, "error": fmt.Sprintf("启动安装脚本失败: %v", err), }) return fmt.Errorf("安装失败: %v", err) } // 在后台等待完成 go func() { err := cmd.Wait() if err != nil { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "wails", "step": "error", "percent": 0, "error": stderr.String(), }) } else { a.app.Event.Emit("install:progress", map[string]interface{}{ "tool": "wails", "step": "completed", "percent": 100, "message": "Wails3 安装完成", }) } }() return nil } // PackApp 打包应用 func (a *App) PackApp(packConfig map[string]interface{}) error { // 获取配置参数 sitePath, _ := packConfig["sitePath"].(string) appName, _ := packConfig["appName"].(string) outputDir, _ := packConfig["outputDir"].(string) if sitePath == "" || appName == "" { return fmt.Errorf("缺少必要参数") } // 发送打包进度 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "preparing", "percent": 10, "message": "正在准备打包环境...", }) // 检查环境 envStatus, err := utils.GetEnvStatus() if err != nil { return fmt.Errorf("检查环境失败: %v", err) } if !envStatus.HasGo || !envStatus.HasWails { return fmt.Errorf("缺少必要的环境: Go=%v, Wails=%v", envStatus.HasGo, envStatus.HasWails) } // 创建临时项目目录 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "creating", "percent": 30, "message": "正在创建项目结构...", }) // 设置输出目录 if outputDir == "" { appConfig, _ := config.LoadConfig() outputDir = appConfig.PackSiteFileDir if outputDir == "" { outputDir = "site-dist" } } // 确保输出目录存在 os.MkdirAll(outputDir, 0755) tempDir := filepath.Join(os.TempDir(), "wails-pack-"+appName) os.RemoveAll(tempDir) // 清理旧的 err = os.MkdirAll(tempDir, 0755) if err != nil { return fmt.Errorf("创建临时目录失败: %v", err) } // TODO: 创建Wails项目结构,复制网站文件等 // 这里是完整打包逻辑的占位符 // 实际需要: // 1. 创建wails项目结构 (go.mod, main.go等) // 2. 复制网站文件到项目的assets目录 // 3. 配置wails项目参数 log.Printf("准备打包网站: %s 到应用: %s", sitePath, appName) log.Printf("临时目录: %s", tempDir) log.Printf("输出目录: %s", outputDir) a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "building", "percent": 60, "message": "正在编译应用...", }) // 使用Wails构建 wailsCmd := envStatus.WailsPath if wailsCmd == "" { wailsCmd = "wails3" } // 模拟构建过程(实际项目中这里应该真正调用wails build) // cmd := exec.Command(wailsCmd, "build", "-o", filepath.Join(outputDir, appName+".exe")) // cmd.Dir = tempDir // 暂时模拟成功 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "packaging", "percent": 80, "message": "正在生成安装包...", }) // 模拟延迟 // time.Sleep(2 * time.Second) a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "completed", "percent": 100, "message": fmt.Sprintf("打包完成! 输出目录: %s", outputDir), }) return nil }