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" "sync" "time" "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, err := url.Parse(uri) if err != nil { log.Printf("解析URL失败: %v", err) return false } var File utils.File hostname := parsed.Hostname() // 统计总数和成功数 totalCount := 0 successCount := 0 failedCount := 0 // 定义下载任务结构 type downloadTask struct { url string resType string index int isHTML bool } // 收集所有下载任务 var tasks []downloadTask // CSS资源 for k, v := range obj.CSS { if u, err := url.Parse(v); err == nil && u.Hostname() == hostname { tasks = append(tasks, downloadTask{url: v, resType: "css", index: k, isHTML: false}) } } // Script资源 for k, v := range obj.Script { if u, err := url.Parse(v); err == nil && u.Hostname() == hostname { tasks = append(tasks, downloadTask{url: v, resType: "script", index: k, isHTML: false}) } } // Image资源 for k, v := range obj.Image { if u, err := url.Parse(v); err == nil && u.Hostname() == hostname { tasks = append(tasks, downloadTask{url: v, resType: "image", index: k, isHTML: false}) } } // Video资源 for k, v := range obj.Video { if u, err := url.Parse(v); err == nil && u.Hostname() == hostname { tasks = append(tasks, downloadTask{url: v, resType: "video", index: k, isHTML: false}) } } // DOM资源(HTML页面) for k, v := range obj.Dom { if u, err := url.Parse(v); err == nil && u.Hostname() == hostname { tasks = append(tasks, downloadTask{url: v, resType: "dom", index: k, isHTML: true}) } } totalCount = len(tasks) if totalCount == 0 { a.app.Event.Emit("download:complete", map[string]interface{}{ "total": 0, "success": 0, "failed": 0, }) return true } // 并发控制:最多同时下载10个文件 maxConcurrent := 10 sem := make(chan struct{}, maxConcurrent) var wg sync.WaitGroup var mu sync.Mutex // 下载开始时间 startTime := time.Now() for _, task := range tasks { wg.Add(1) sem <- struct{}{} // 获取信号量 go func(t downloadTask) { defer wg.Done() defer func() { <-sem }() // 释放信号量 // 下载文件 var filePath string if t.isHTML { filePath = File.HTMLDownload(t.url) } else { filePath = File.Download(t.url) } // 更新统计 mu.Lock() if filePath != "" { successCount++ } else { failedCount++ log.Printf("下载失败: %s", t.url) } mu.Unlock() // 发送进度事件(兼容旧格式) a.app.Event.Emit("download:"+t.resType, t.index) }(task) } // 等待所有下载完成 wg.Wait() // 计算下载耗时 duration := time.Since(startTime) // 保存下载记录到数据库 if a.store != nil { record := storage.DownloadRecord{ SiteName: hostname, URL: uri, TotalFiles: totalCount, Downloaded: successCount, Duration: int64(duration.Seconds()), Status: "success", StartTime: startTime, EndTime: time.Now(), } if err := a.store.AddDownloadRecord(record); err != nil { log.Printf("保存下载记录失败: %v", err) } } // 发送完成事件 a.app.Event.Emit("download:complete", map[string]interface{}{ "total": totalCount, "success": successCount, "failed": failedCount, "duration": duration.Seconds(), "siteName": hostname, }) log.Printf("下载完成: 总数=%d, 成功=%d, 失败=%d, 耗时=%.2fs", totalCount, successCount, failedCount, duration.Seconds()) return successCount > 0 } // GetDownloadOptions 获取下载配置 func (a *App) GetDownloadOptions() types.DownloadOptions { return types.DefaultDownloadOptions() } // DownloadSiteWithOptions 带配置选项的下载网站资源 func (a *App) DownloadSiteWithOptions(uri string, obj services.ResourcesList, options types.DownloadOptions) bool { parsed, err := url.Parse(uri) if err != nil { log.Printf("解析URL失败: %v", err) return false } var File utils.File hostname := parsed.Hostname() // 统计总数和成功数 totalCount := 0 successCount := 0 failedCount := 0 // 定义下载任务结构 type downloadTask struct { url string resType string index int isHTML bool } // 收集所有下载任务 var tasks []downloadTask // CSS资源 for k, v := range obj.CSS { if u, err := url.Parse(v); err == nil { resHost := u.Hostname() if u.Port() != "" { resHost += ":" + u.Port() } if options.ShouldDownloadExternal(resHost, hostname, "css") { tasks = append(tasks, downloadTask{url: v, resType: "css", index: k, isHTML: false}) } } } // Script资源 for k, v := range obj.Script { if u, err := url.Parse(v); err == nil { resHost := u.Hostname() if u.Port() != "" { resHost += ":" + u.Port() } if options.ShouldDownloadExternal(resHost, hostname, "script") { tasks = append(tasks, downloadTask{url: v, resType: "script", index: k, isHTML: false}) } } } // Image资源 for k, v := range obj.Image { if u, err := url.Parse(v); err == nil { resHost := u.Hostname() if u.Port() != "" { resHost += ":" + u.Port() } if options.ShouldDownloadExternal(resHost, hostname, "image") { tasks = append(tasks, downloadTask{url: v, resType: "image", index: k, isHTML: false}) } } } // Video资源 for k, v := range obj.Video { if u, err := url.Parse(v); err == nil { resHost := u.Hostname() if u.Port() != "" { resHost += ":" + u.Port() } if options.ShouldDownloadExternal(resHost, hostname, "video") { tasks = append(tasks, downloadTask{url: v, resType: "video", index: k, isHTML: false}) } } } // DOM资源(HTML页面) for k, v := range obj.Dom { if u, err := url.Parse(v); err == nil { resHost := u.Hostname() if u.Port() != "" { resHost += ":" + u.Port() } // HTML页面总是下载本站的 if resHost == hostname { tasks = append(tasks, downloadTask{url: v, resType: "dom", index: k, isHTML: true}) } } } totalCount = len(tasks) if totalCount == 0 { a.app.Event.Emit("download:complete", map[string]interface{}{ "total": 0, "success": 0, "failed": 0, }) return true } // 并发控制:最多同时下载10个文件 maxConcurrent := 10 sem := make(chan struct{}, maxConcurrent) var wg sync.WaitGroup var mu sync.Mutex // 下载开始时间 startTime := time.Now() for _, task := range tasks { wg.Add(1) sem <- struct{}{} // 获取信号量 go func(t downloadTask) { defer wg.Done() defer func() { <-sem }() // 释放信号量 // 下载文件(使用配置) var filePath string if t.isHTML { filePath = File.HTMLDownloadWithOptions(t.url, &options) } else { filePath = File.DownloadWithOptions(t.url, &options) } // 更新统计 mu.Lock() if filePath != "" { successCount++ } else { failedCount++ log.Printf("下载失败: %s", t.url) } mu.Unlock() // 发送进度事件(兼容旧格式) a.app.Event.Emit("download:"+t.resType, t.index) }(task) } // 等待所有下载完成 wg.Wait() // 计算下载耗时 duration := time.Since(startTime) // 保存下载记录到数据库 if a.store != nil { record := storage.DownloadRecord{ SiteName: hostname, URL: uri, TotalFiles: totalCount, Downloaded: successCount, Duration: int64(duration.Seconds()), Status: "success", StartTime: startTime, EndTime: time.Now(), } if err := a.store.AddDownloadRecord(record); err != nil { log.Printf("保存下载记录失败: %v", err) } } // 发送完成事件 a.app.Event.Emit("download:complete", map[string]interface{}{ "total": totalCount, "success": successCount, "failed": failedCount, "duration": duration.Seconds(), "siteName": hostname, }) log.Printf("下载完成(配置模式=%s): 总数=%d, 成功=%d, 失败=%d, 耗时=%.2fs", options.Mode, totalCount, successCount, failedCount, duration.Seconds()) return successCount > 0 } // 获取本地已下载网站列表 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) version, _ := packConfig["version"].(string) author, _ := packConfig["author"].(string) description, _ := packConfig["description"].(string) outputDir, _ := packConfig["outputDir"].(string) width, _ := packConfig["width"].(float64) height, _ := packConfig["height"].(float64) // 平台参数处理 platformsInterface, _ := packConfig["platforms"].([]interface{}) platforms := make([]string, 0) for _, p := range platformsInterface { if platform, ok := p.(string); ok { platforms = append(platforms, platform) } } if sitePath == "" || appName == "" { return fmt.Errorf("缺少必要参数: 网站路径和应用名称不能为空") } // 检查网站路径是否存在 if _, err := os.Stat(sitePath); os.IsNotExist(err) { return fmt.Errorf("网站路径不存在: %s", sitePath) } // 发送打包进度 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "preparing", "percent": 5, "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) } // 设置默认值 if version == "" { version = "1.0.0" } if description == "" { description = appName } if len(platforms) == 0 { platforms = []string{"windows"} } if width == 0 { width = 1280 } if height == 0 { height = 800 } // 设置输出目录 if outputDir == "" { appConfig, _ := config.LoadConfig() outputDir = appConfig.PackSiteFileDir if outputDir == "" { outputDir = "site-dist" } } // 创建临时项目目录 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "creating", "percent": 15, "message": "正在创建Wails项目结构...", }) tempDir := filepath.Join(os.TempDir(), "wails-pack-"+appName) os.RemoveAll(tempDir) // 清理旧的 err = os.MkdirAll(tempDir, 0755) if err != nil { return fmt.Errorf("创建临时目录失败: %v", err) } // 使用打包服务创建项目 packService := &services.PackService{} config := services.PackConfig{ SitePath: sitePath, AppName: appName, Version: version, Author: author, Description: description, Platforms: platforms, Width: int(width), Height: int(height), OutputDir: outputDir, } log.Printf("开始打包网站: %s", sitePath) log.Printf("应用名称: %s", appName) log.Printf("临时目录: %s", tempDir) log.Printf("输出目录: %s", outputDir) a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "copying", "percent": 30, "message": "正在复制网站文件...", }) // 创建Wails项目结构 if err := packService.CreateWailsProject(config, tempDir); err != nil { a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "error", "percent": 0, "error": fmt.Sprintf("创建项目失败: %v", err), }) return fmt.Errorf("创建Wails项目失败: %v", err) } a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "building", "percent": 50, "message": "正在编译应用(这可能需要几分钟)...", }) // 构建应用 if err := packService.BuildWailsApp(tempDir, outputDir, platforms); err != nil { a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "error", "percent": 0, "error": fmt.Sprintf("构建失败: %v", err), }) return fmt.Errorf("构建应用失败: %v", err) } a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "completed", "percent": 100, "message": fmt.Sprintf("打包完成! 应用已保存到: %s", outputDir), }) log.Printf("打包完成! 输出目录: %s", outputDir) return nil }