Private
Public Access
1
0
Files
u-desk/internal/service/update.go
绝尘 f7d648ea52 新增:文件系统导航面包屑
功能:
- 新增 PathBreadcrumb 组件,支持路径快速跳转
- 新增 DropdownItem 通用下拉菜单组件

优化:
- 版本升级流程优化(Pinia 状态管理、进度节流、完整下载验证)
- 模块延迟初始化(数据库、文件系统按需启动)
- API 数据格式统一(蛇形转驼峰)
- CodeMirror 语言包按需动态加载
- Markdown 渲染增强(支持锚点跳转)

重构:
- 迁移到 Pinia 状态管理(stores/config.ts、stores/theme.ts、stores/update.ts)
- 简化 UpdatePanel、UpdateNotification、ThemeToggle 逻辑
- 优化表结构加载逻辑

清理:
- 删除测试组件 index-simple.vue
- 删除旧的 useTheme.ts
2026-02-05 00:17:32 +08:00

396 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"u-desk/internal/common"
)
// ==================== 类型定义 ====================
// 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"`
FileSize int64 `json:"file_size"`
}
// 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"`
FileSize int64 `json:"file_size"`
}
// 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) {
config, err := LoadUpdateConfig()
if err != nil {
return nil, fmt.Errorf("加载配置失败: %v", err)
}
// 获取当前版本(使用缓存)
currentVersion, err := ParseVersion(GetCurrentVersion())
if err != nil {
return nil, fmt.Errorf("解析当前版本失败: %v", err)
}
// 请求远程版本信息
remoteInfo, err := s.fetchRemoteVersionInfo()
if err != nil {
return nil, fmt.Errorf("获取远程版本信息失败: %v", err)
}
// 解析远程版本号
remoteVersion, err := ParseVersion(remoteInfo.Version)
if err != nil {
return nil, fmt.Errorf("解析远程版本号失败: %v", err)
}
// 比较版本
hasUpdate := remoteVersion.IsNewerThan(currentVersion)
// 更新最后检查时间
config.UpdateLastCheckTime()
return &UpdateCheckResult{
HasUpdate: hasUpdate,
CurrentVersion: GetCurrentVersion(),
LatestVersion: remoteInfo.Version,
DownloadURL: remoteInfo.DownloadURL,
Changelog: remoteInfo.Changelog,
ForceUpdate: remoteInfo.ForceUpdate,
ReleaseDate: remoteInfo.ReleaseDate,
FileSize: remoteInfo.FileSize,
}, nil
}
// fetchRemoteVersionInfo 获取远程版本信息
func (s *UpdateService) fetchRemoteVersionInfo() (*RemoteVersionInfo, error) {
if s.checkURL == "" {
return nil, fmt.Errorf("版本检查 URL 未配置,请先设置检查地址")
}
// 添加时间戳参数防止缓存
timestamp := time.Now().UnixMilli()
var requestURL string
if strings.Contains(s.checkURL, "?") {
requestURL = fmt.Sprintf("%s&t=%d", s.checkURL, timestamp)
} else {
requestURL = fmt.Sprintf("%s?t=%d", s.checkURL, timestamp)
}
// 创建 HTTP 客户端,设置超时
client := &http.Client{
Timeout: 10 * time.Second,
}
// 发送请求
resp, err := client.Get(requestURL)
if err != nil {
return nil, fmt.Errorf("网络请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
}
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
// 解析 JSON
var remoteInfo RemoteVersionInfo
if err := json.Unmarshal(body, &remoteInfo); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
if remoteInfo.Version == "" {
return nil, fmt.Errorf("远程版本信息不完整")
}
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
}
backupDir := filepath.Join(common.GetUserDataDir(), "backups")
if err := os.MkdirAll(backupDir, 0755); err != nil {
return "", fmt.Errorf("创建备份目录失败: %v", err)
}
timestamp := time.Now().Format("20060102-150405")
backupFileName := fmt.Sprintf("u-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
}