package system import ( "archive/tar" "compress/gzip" "context" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path" "path/filepath" "runtime" "strings" "time" "github.com/ayflying/p2p/internal/service" "github.com/gogf/gf/v2/encoding/gcompress" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gcmd" "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gfile" ) // 本地版本号(建议从编译参数注入,如 -ldflags "-X main.version=v0.1.3") const versionFile = "version.txt" var localVersion = "v0.0.0" // 对应 GitHub API 响应的核心字段(按需精简) type GitHubRelease struct { Url string `json:"url"` AssetsUrl string `json:"assets_url"` UploadUrl string `json:"upload_url"` HtmlUrl string `json:"html_url"` Id int `json:"id"` TagName string `json:"tag_name"` Assets []*Assets `json:"assets"` NodeId string `json:"node_id"` TargetCommitish string `json:"target_commitish"` Name string `json:"name"` Draft bool `json:"draft"` Immutable bool `json:"immutable"` Prerelease bool `json:"prerelease"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` PublishedAt time.Time `json:"published_at"` TarballUrl string `json:"tarball_url"` ZipballUrl string `json:"zipball_url"` Body string `json:"body"` } func (s *sSystem) Update(ctx context.Context, gzFile string) (err error) { //拼接操作系统和架构(格式:OS_ARCH) platform := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) runFile := gcmd.GetArg(0).String() oldFile, err := service.System().RenameRunningFile(runFile) g.Log().Debugf(ctx, "执行文件改名为%v", oldFile) if gzFile == "" { gzFile = path.Join("download", platform+".gz") } //结束后删除压缩包 defer gfile.RemoveFile(gzFile) ext := gfile.Ext(gzFile) if ext == ".zip" { g.Log().Debugf(ctx, "zip解压%v到%v", gzFile, gfile.Dir(runFile)) err = gcompress.UnZipFile(gzFile, gfile.Dir(runFile)) } else { g.Log().Debugf(ctx, "gzip解压%v到%v", gzFile, gfile.Dir(runFile)) err = s.UnTarGz(gzFile, gfile.Dir(runFile)) } if err != nil { return } //修改文件权限为755 err = gfile.Chmod(runFile, 0755) go func() { log.Println("5秒后开始重启...") time.Sleep(5 * time.Second) if err = service.System().RestartSelf(); err != nil { log.Fatalf("重启失败:%v", err) } }() return } // UnTarGz 解压tar.gz文件到指定目录 func (s *sSystem) UnTarGz(tarGzFileName, targetDir string) (err error) { // 打开tar.gz文件 file, err := os.Open(tarGzFileName) if err != nil { return err } defer file.Close() // 创建gzip reader gzr, err := gzip.NewReader(file) if err != nil { return err } defer gzr.Close() // 创建tar reader tr := tar.NewReader(gzr) // 遍历tar中的每个文件 for { hdr, err := tr.Next() if err == io.EOF { // 到达文件末尾,退出循环 break } if err != nil { return err } // 构建解压后的文件路径 targetPath := targetDir + string(os.PathSeparator) + hdr.Name // 如果是目录,创建目录 if hdr.Typeflag == tar.TypeDir { err := os.MkdirAll(targetPath, 0755) if err != nil { return err } continue } // 如果是文件,创建文件并写入内容 outFile, err := os.Create(targetPath) if err != nil { return err } defer outFile.Close() _, err = io.Copy(outFile, tr) if err != nil { return err } } return } // RestartSelf 实现 Windows 平台下的程序自重启 func (s *sSystem) RestartSelf() error { ctx := gctx.New() // 判断是否为linux平台 if runtime.GOOS == "linux" { err := ghttp.RestartAllServer(ctx, os.Args[0]) if err != nil { g.Log().Errorf(ctx, "重启失败:%v", err) } return err } // 1. 获取当前程序的绝对路径 exePath, err := os.Executable() if err != nil { return err } // 处理路径中的符号链接(确保路径正确) exePath, err = filepath.EvalSymlinks(exePath) if err != nil { return err } // 2. 获取命令行参数(os.Args[0] 是程序名,实际参数从 os.Args[1:] 开始) args := os.Args[1:] // 3. 构建新进程命令(路径为当前程序,参数为原参数) cmd := exec.Command(exePath, args...) // 设置新进程的工作目录与当前进程一致 cmd.Dir, err = os.Getwd() if err != nil { return err } // 新进程的输出继承当前进程的标准输出(可选,根据需求调整) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin // 4. 启动新进程(非阻塞,Start() 后立即返回) if err := cmd.Start(); err != nil { return err } // 5. 新进程启动成功后,退出当前进程 os.Exit(0) return nil // 理论上不会执行到这里 } // RenameRunningFile 重命名正在运行的程序文件(如 message.exe → message.exe~) func (s *sSystem) RenameRunningFile(exePath string) (string, error) { // 目标备份文件名(message.exe → message.exe~) backupPath := exePath + "~" // 先删除已存在的备份文件(若有) if _, err := os.Stat(backupPath); err == nil { if err := os.Remove(backupPath); err != nil { return "", fmt.Errorf("删除旧备份文件失败: %v", err) } } // 重命名正在运行的 exe 文件 // 关键:Windows 允许对锁定的文件执行重命名操作 if err := os.Rename(exePath, backupPath); err != nil { return "", fmt.Errorf("重命名运行中文件失败: %v", err) } return backupPath, nil } // 简化版版本对比(仅适用于 vX.Y.Z 格式) func (s *sSystem) isNewVersion(local, latest string) bool { // 移除前缀 "v",按 "." 分割成数字切片 localParts := strings.Split(strings.TrimPrefix(local, "v"), ".") latestParts := strings.Split(strings.TrimPrefix(latest, "v"), ".") // 逐段对比版本号(如 0.1.3 vs 0.1.4 → 后者更新) for i := 0; i < len(localParts) && i < len(latestParts); i++ { if localParts[i] < latestParts[i] { return true } else if localParts[i] > latestParts[i] { return false } } // 若前缀相同,长度更长的版本更新(如 0.1 vs 0.1.1) return len(localParts) < len(latestParts) } func (s *sSystem) getLatestVersion() (string, []*Assets, error) { apiURL := "https://api.github.com/repos/ayflying/p2p/releases/latest" resp, err := http.Get(apiURL) if err != nil { return "", nil, fmt.Errorf("请求失败:%v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", nil, fmt.Errorf("API 响应错误:%d", resp.StatusCode) } var release GitHubRelease if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return "", nil, fmt.Errorf("解析响应失败:%v", err) } return release.TagName, release.Assets, nil } func (s *sSystem) CheckUpdate() (err error) { ctx := gctx.New() latestVersion, assets, err := s.getLatestVersion() if err != nil { fmt.Printf("检查更新失败:%v\n", err) return } localVersion = gfile.GetContents(versionFile) if s.isNewVersion(localVersion, latestVersion) { g.Log().Printf(ctx, "发现新版本:%s(当前版本:%s)", latestVersion, localVersion) //拼接操作系统和架构(格式:OS_ARCH) platform := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) //name := fmt.Sprintf("p2p_%s_%s.tar.gz", latestVersion, platform) fmt.Println("下载链接:") for _, asset := range assets { if strings.Contains(fmt.Sprintf("_%s.", asset.Name), platform) { fmt.Printf("- %s\n", asset.BrowserDownloadUrl) // 下载更新文件 fileDownload, err2 := g.Client().Get(ctx, asset.BrowserDownloadUrl) if err2 != nil { return } updateFile := path.Join("download", asset.Name) err = gfile.PutBytes(updateFile, fileDownload.ReadAll()) err = s.Update(ctx, updateFile) if err != nil { return } // 保存最新版本号到文件 gfile.PutContents(versionFile, latestVersion) break } } } else { fmt.Printf("当前已是最新版本:%s\n", localVersion) } return }