新增:版本更新管理功能,优化代码架构
This commit is contained in:
429
internal/service/update.go
Normal file
429
internal/service/update.go
Normal file
@@ -0,0 +1,429 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
// UpdateService 更新服务
|
||||
type UpdateService struct {
|
||||
checkURL string // 版本检查接口 URL
|
||||
}
|
||||
|
||||
// RemoteVersionInfo 远程版本信息
|
||||
type RemoteVersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
Changelog string `json:"changelog"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
}
|
||||
|
||||
// UpdateCheckResult 更新检查结果
|
||||
type UpdateCheckResult struct {
|
||||
HasUpdate bool `json:"has_update"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
LatestVersion string `json:"latest_version"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
Changelog string `json:"changelog"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
}
|
||||
|
||||
// InstallResult 安装结果
|
||||
type InstallResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ==================== 更新服务 ====================
|
||||
|
||||
// NewUpdateService 创建更新服务
|
||||
func NewUpdateService(checkURL string) *UpdateService {
|
||||
return &UpdateService{
|
||||
checkURL: checkURL,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckUpdate 检查更新
|
||||
func (s *UpdateService) CheckUpdate() (*UpdateCheckResult, error) {
|
||||
log.Printf("[更新检查] 开始检查更新,检查地址: %s", s.checkURL)
|
||||
|
||||
config, err := LoadUpdateConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 同步版本号
|
||||
currentVersionStr, err := s.syncConfigVersion(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentVersion, err := ParseVersion(currentVersionStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析当前版本失败: %v", err)
|
||||
}
|
||||
|
||||
// 请求远程版本信息
|
||||
remoteInfo, err := s.fetchRemoteVersionInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取远程版本信息失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[更新检查] 远程版本信息: 版本=%s, 下载地址=%s, 强制更新=%v",
|
||||
remoteInfo.Version, remoteInfo.DownloadURL, remoteInfo.ForceUpdate)
|
||||
|
||||
// 解析远程版本号
|
||||
remoteVersion, err := ParseVersion(remoteInfo.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析远程版本号失败: %v", err)
|
||||
}
|
||||
|
||||
// 比较版本
|
||||
hasUpdate := remoteVersion.IsNewerThan(currentVersion)
|
||||
log.Printf("[更新检查] 版本比较: 当前=%s, 远程=%s, 有更新=%v",
|
||||
currentVersion.String(), remoteVersion.String(), hasUpdate)
|
||||
|
||||
// 更新最后检查时间
|
||||
config.UpdateLastCheckTime()
|
||||
|
||||
result := &UpdateCheckResult{
|
||||
HasUpdate: hasUpdate,
|
||||
CurrentVersion: currentVersionStr,
|
||||
LatestVersion: remoteInfo.Version,
|
||||
DownloadURL: remoteInfo.DownloadURL,
|
||||
Changelog: remoteInfo.Changelog,
|
||||
ForceUpdate: remoteInfo.ForceUpdate,
|
||||
ReleaseDate: remoteInfo.ReleaseDate,
|
||||
}
|
||||
|
||||
log.Printf("[更新检查] 检查完成: 有更新=%v", hasUpdate)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// syncConfigVersion 同步配置中的版本号
|
||||
func (s *UpdateService) syncConfigVersion(config *UpdateConfig) (string, error) {
|
||||
currentVersionStr := GetCurrentVersion()
|
||||
if currentVersionStr == "" {
|
||||
currentVersionStr = config.CurrentVersion
|
||||
log.Printf("[更新检查] 使用配置中的版本号: %s", currentVersionStr)
|
||||
} else if config.CurrentVersion != currentVersionStr {
|
||||
log.Printf("[更新检查] 配置中的版本号 (%s) 与当前版本号 (%s) 不一致,更新配置",
|
||||
config.CurrentVersion, currentVersionStr)
|
||||
config.CurrentVersion = currentVersionStr
|
||||
if err := SaveUpdateConfig(config); err != nil {
|
||||
log.Printf("[更新检查] 更新配置失败: %v", err)
|
||||
}
|
||||
}
|
||||
return currentVersionStr, nil
|
||||
}
|
||||
|
||||
// fetchRemoteVersionInfo 获取远程版本信息
|
||||
func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
|
||||
if s.checkURL == "" {
|
||||
log.Printf("[远程版本] 版本检查 URL 未配置")
|
||||
return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址")
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 请求远程版本信息: %s", s.checkURL)
|
||||
|
||||
// 创建 HTTP 客户端,设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Get(s.checkURL)
|
||||
if err != nil {
|
||||
log.Printf("[远程版本] 网络请求失败: %v", err)
|
||||
return nil, fmt.Errorf("网络请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
log.Printf("[远程版本] HTTP 响应状态码: %d", resp.StatusCode)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[远程版本] 读取响应失败: %v", err)
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 响应内容长度: %d 字节", len(body))
|
||||
|
||||
// 解析 JSON
|
||||
var remoteInfo RemoteVersionInfo
|
||||
if err := json.Unmarshal(body, &remoteInfo); err != nil {
|
||||
log.Printf("[远程版本] 解析 JSON 失败: %v, 响应内容: %s", err, string(body))
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if remoteInfo.Version == "" {
|
||||
log.Printf("[远程版本] 远程版本信息不完整,响应内容: %s", string(body))
|
||||
return nil, fmt.Errorf("远程版本信息不完整")
|
||||
}
|
||||
|
||||
log.Printf("[远程版本] 成功获取远程版本信息: %+v", remoteInfo)
|
||||
return &remoteInfo, nil
|
||||
}
|
||||
|
||||
// ==================== 安装更新 ====================
|
||||
|
||||
// InstallUpdate 安装更新包
|
||||
func InstallUpdate(installerPath string, autoRestart bool) (*InstallResult, error) {
|
||||
return InstallUpdateWithHash(installerPath, autoRestart, "", "")
|
||||
}
|
||||
|
||||
// InstallUpdateWithHash 安装更新包(带哈希验证)
|
||||
func InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (*InstallResult, error) {
|
||||
if _, err := os.Stat(installerPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("安装文件不存在: %s", installerPath)
|
||||
}
|
||||
|
||||
// 哈希验证
|
||||
if expectedHash != "" && hashType != "" {
|
||||
valid, err := VerifyFileHash(installerPath, expectedHash, hashType)
|
||||
if err != nil {
|
||||
return &InstallResult{Success: false, Message: "文件验证失败: " + err.Error()}, nil
|
||||
}
|
||||
if !valid {
|
||||
return &InstallResult{Success: false, Message: "文件哈希值不匹配,文件可能已损坏或被篡改"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 备份
|
||||
backupPath, err := BackupApplication()
|
||||
if err != nil {
|
||||
return &InstallResult{Success: false, Message: fmt.Sprintf("备份失败: %v", err)}, nil
|
||||
}
|
||||
|
||||
// 安装
|
||||
ext := filepath.Ext(installerPath)
|
||||
switch ext {
|
||||
case ".exe":
|
||||
if runtime.GOOS != "windows" {
|
||||
return &InstallResult{Success: false, Message: "当前系统不是 Windows,无法安装 .exe 文件"}, nil
|
||||
}
|
||||
err = installExe(installerPath)
|
||||
case ".zip":
|
||||
err = installZip(installerPath)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的安装包格式: %s", ext)
|
||||
}
|
||||
|
||||
// 处理安装结果
|
||||
if err != nil {
|
||||
// 安装失败,尝试回滚
|
||||
if backupPath != "" {
|
||||
_ = rollbackFromBackup(backupPath)
|
||||
}
|
||||
return &InstallResult{Success: false, Message: fmt.Sprintf("安装失败: %v", err)}, nil
|
||||
}
|
||||
|
||||
// 自动重启
|
||||
if autoRestart {
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
restartApplication()
|
||||
}()
|
||||
}
|
||||
|
||||
return &InstallResult{Success: true, Message: "安装成功"}, nil
|
||||
}
|
||||
|
||||
// ==================== 安装相关辅助函数 ====================
|
||||
|
||||
// installExe 安装 exe 文件
|
||||
func installExe(exePath string) error {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return replaceExecutableFile(exePath, execPath)
|
||||
}
|
||||
|
||||
// installZip 安装 ZIP 压缩包
|
||||
func installZip(zipPath string) error {
|
||||
// 这里需要导入 archive/zip 包
|
||||
return fmt.Errorf("ZIP 安装暂未实现")
|
||||
}
|
||||
|
||||
// replaceExecutableFile 替换可执行文件(Windows 和 Unix 通用逻辑)
|
||||
func replaceExecutableFile(newFilePath, execPath string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return replaceExecutableFileWindows(newFilePath, execPath)
|
||||
}
|
||||
|
||||
// Unix-like: 直接替换
|
||||
if err := copyFile(newFilePath, execPath); err != nil {
|
||||
return fmt.Errorf("复制文件失败: %v", err)
|
||||
}
|
||||
return os.Chmod(execPath, 0755)
|
||||
}
|
||||
|
||||
// replaceExecutableFileWindows Windows 平台替换可执行文件
|
||||
func replaceExecutableFileWindows(newFilePath, execPath string) error {
|
||||
oldExecPath := execPath + ".old"
|
||||
newExecPathTemp := execPath + ".new"
|
||||
|
||||
// 清理旧文件
|
||||
os.Remove(oldExecPath)
|
||||
os.Remove(newExecPathTemp)
|
||||
|
||||
// 复制新文件到临时位置
|
||||
if err := copyFile(newFilePath, newExecPathTemp); err != nil {
|
||||
return fmt.Errorf("复制新文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 尝试重命名当前文件(如果失败,说明文件正在使用)
|
||||
if err := os.Rename(execPath, oldExecPath); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 替换文件
|
||||
if err := os.Rename(newExecPathTemp, execPath); err != nil {
|
||||
os.Rename(oldExecPath, execPath) // 恢复
|
||||
return fmt.Errorf("替换文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 延迟删除旧文件
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
os.Remove(oldExecPath)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// restartApplication 重启应用
|
||||
func restartApplication() {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
currentPID := os.Getpid()
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
|
||||
replacePendingFile(execPath)
|
||||
|
||||
// 创建并执行重启脚本
|
||||
tempDir := os.TempDir()
|
||||
batFile := filepath.Join(tempDir, fmt.Sprintf("restart_%d.bat", currentPID))
|
||||
execDir := filepath.Dir(execPath)
|
||||
|
||||
batContent := fmt.Sprintf(`@echo off
|
||||
cd /d "%s"
|
||||
start "" "%s"
|
||||
timeout /t 3 /nobreak >nul
|
||||
taskkill /PID %d /F >nul 2>&1
|
||||
del "%%~f0"
|
||||
`, execDir, execPath, currentPID)
|
||||
|
||||
if err := os.WriteFile(batFile, []byte(batContent), 0644); err != nil {
|
||||
fallbackRestart(execPath)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("cmd", "/C", batFile)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Start(); err != nil {
|
||||
fallbackRestart(execPath)
|
||||
return
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// fallbackRestart 降级重启方案
|
||||
func fallbackRestart(execPath string) {
|
||||
exec.Command("cmd", "/C", "start", "", execPath).Start()
|
||||
time.Sleep(2 * time.Second)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// replacePendingFile 替换待替换的文件(.new -> 可执行文件)
|
||||
func replacePendingFile(execPath string) error {
|
||||
newExecPathTemp := execPath + ".new"
|
||||
if _, err := os.Stat(newExecPathTemp); os.IsNotExist(err) {
|
||||
return nil // 没有待替换文件
|
||||
}
|
||||
|
||||
oldExecPath := execPath + ".old"
|
||||
os.Remove(oldExecPath)
|
||||
if err := os.Rename(newExecPathTemp, execPath); err != nil {
|
||||
return fmt.Errorf("文件替换失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rollbackFromBackup 从备份恢复
|
||||
func rollbackFromBackup(backupPath string) error {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return copyFile(backupPath, execPath)
|
||||
}
|
||||
|
||||
// BackupApplication 备份当前应用
|
||||
func BackupApplication() (string, error) {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取用户目录失败: %v", err)
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(homeDir, ".go-desk", "backups")
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建备份目录失败: %v", err)
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
backupFileName := fmt.Sprintf("go-desk-backup-%s%s", timestamp, filepath.Ext(execPath))
|
||||
backupPath := filepath.Join(backupDir, backupFileName)
|
||||
|
||||
if err := copyFile(execPath, backupPath); err != nil {
|
||||
return "", fmt.Errorf("复制文件失败: %v", err)
|
||||
}
|
||||
|
||||
return backupPath, nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = destFile.ReadFrom(sourceFile)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user