329 lines
8.2 KiB
Go
329 lines
8.2 KiB
Go
package service
|
||
|
||
import (
|
||
"archive/zip"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"time"
|
||
)
|
||
|
||
// InstallResult 安装结果
|
||
type InstallResult struct {
|
||
Success bool `json:"success"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// getExecutablePath 获取当前可执行文件路径
|
||
func getExecutablePath() (string, error) {
|
||
execPath, err := os.Executable()
|
||
if err != nil {
|
||
return "", fmt.Errorf("获取可执行文件路径失败: %v", err)
|
||
}
|
||
return execPath, nil
|
||
}
|
||
|
||
// installExe 安装 exe 文件
|
||
func installExe(exePath string) error {
|
||
execPath, err := getExecutablePath()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return replaceExecutableFile(exePath, execPath)
|
||
}
|
||
|
||
// installZip 安装 ZIP 压缩包
|
||
func installZip(zipPath string) error {
|
||
zipReader, err := zip.OpenReader(zipPath)
|
||
if err != nil {
|
||
return fmt.Errorf("打开 ZIP 文件失败: %v", err)
|
||
}
|
||
defer zipReader.Close()
|
||
|
||
execPath, err := getExecutablePath()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
execDir := filepath.Dir(execPath)
|
||
execName := filepath.Base(execPath)
|
||
|
||
// 解压到临时目录
|
||
tempDir := filepath.Join(execDir, ".update-temp")
|
||
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
||
return fmt.Errorf("创建临时目录失败: %v", err)
|
||
}
|
||
defer os.RemoveAll(tempDir)
|
||
|
||
// 解压文件
|
||
for _, file := range zipReader.File {
|
||
filePath := filepath.Join(tempDir, file.Name)
|
||
if file.FileInfo().IsDir() {
|
||
os.MkdirAll(filePath, file.FileInfo().Mode())
|
||
continue
|
||
}
|
||
|
||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||
return fmt.Errorf("创建目录失败: %v", err)
|
||
}
|
||
|
||
rc, err := file.Open()
|
||
if err != nil {
|
||
return fmt.Errorf("打开 ZIP 文件项失败: %v", err)
|
||
}
|
||
|
||
targetFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
|
||
if err != nil {
|
||
rc.Close()
|
||
return fmt.Errorf("创建目标文件失败: %v", err)
|
||
}
|
||
|
||
if _, err := io.Copy(targetFile, rc); err != nil {
|
||
targetFile.Close()
|
||
rc.Close()
|
||
return fmt.Errorf("复制文件失败: %v", err)
|
||
}
|
||
targetFile.Close()
|
||
rc.Close()
|
||
}
|
||
|
||
// 查找新的可执行文件
|
||
newExecPath := filepath.Join(tempDir, execName)
|
||
if _, err := os.Stat(newExecPath); os.IsNotExist(err) {
|
||
files, _ := os.ReadDir(tempDir)
|
||
for _, f := range files {
|
||
if !f.IsDir() {
|
||
newExecPath = filepath.Join(tempDir, f.Name())
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 替换文件(使用与 installExe 相同的逻辑)
|
||
return replaceExecutableFile(newExecPath, execPath)
|
||
}
|
||
|
||
// 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 := getExecutablePath()
|
||
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 := getExecutablePath()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return copyFile(backupPath, execPath)
|
||
}
|
||
|
||
// BackupApplication 备份当前应用
|
||
func BackupApplication() (string, error) {
|
||
execPath, err := getExecutablePath()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
homeDir, err := os.UserHomeDir()
|
||
if err != nil {
|
||
return "", fmt.Errorf("获取用户目录失败: %v", err)
|
||
}
|
||
|
||
backupDir := filepath.Join(homeDir, ".ssq-desk", "backups")
|
||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||
return "", fmt.Errorf("创建备份目录失败: %v", err)
|
||
}
|
||
|
||
timestamp := time.Now().Format("20060102-150405")
|
||
backupFileName := fmt.Sprintf("ssq-desk-backup-%s%s", timestamp, filepath.Ext(execPath))
|
||
backupPath := filepath.Join(backupDir, backupFileName)
|
||
|
||
sourceFile, err := os.Open(execPath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("打开源文件失败: %v", err)
|
||
}
|
||
defer sourceFile.Close()
|
||
|
||
backupFile, err := os.Create(backupPath)
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建备份文件失败: %v", err)
|
||
}
|
||
defer backupFile.Close()
|
||
|
||
if _, err := backupFile.ReadFrom(sourceFile); err != nil {
|
||
return "", fmt.Errorf("复制文件失败: %v", err)
|
||
}
|
||
|
||
return backupPath, nil
|
||
}
|