Files
voidraft/internal/services/self_update_service.go

465 lines
14 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 services
import (
"context"
"errors"
"fmt"
"github.com/creativeprojects/go-selfupdate"
"github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"runtime"
"time"
"voidraft/internal/models"
)
// SelfUpdateResult 自我更新结果
type SelfUpdateResult struct {
HasUpdate bool `json:"hasUpdate"` // 是否有更新
CurrentVersion string `json:"currentVersion"` // 当前版本
LatestVersion string `json:"latestVersion"` // 最新版本
UpdateApplied bool `json:"updateApplied"` // 是否已应用更新
AssetURL string `json:"assetURL"` // 下载链接
ReleaseNotes string `json:"releaseNotes"` // 发布说明
Error string `json:"error"` // 错误信息
Source string `json:"source"` // 更新源github/gitea
}
// SelfUpdateService 自我更新服务
type SelfUpdateService struct {
logger *log.Service
configService *ConfigService
config *models.AppConfig
// 状态管理
isUpdating bool
}
// NewSelfUpdateService 创建自我更新服务实例
func NewSelfUpdateService(configService *ConfigService, logger *log.Service) (*SelfUpdateService, error) {
// 获取配置
appConfig, err := configService.GetConfig()
if err != nil {
return nil, fmt.Errorf("failed to get config: %w", err)
}
service := &SelfUpdateService{
logger: logger,
configService: configService,
config: appConfig,
isUpdating: false,
}
return service, nil
}
// CheckForUpdates 检查更新
func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateResult, error) {
result := &SelfUpdateResult{
CurrentVersion: s.config.Updates.Version,
HasUpdate: false,
UpdateApplied: false,
}
// 首先尝试主要更新源
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
if err == nil && primaryResult != nil {
return primaryResult, nil
}
// 如果主要更新源失败,尝试备用更新源
backupResult, backupErr := s.checkSourceForUpdates(ctx, s.config.Updates.BackupSource)
if backupErr != nil {
// 如果备用源也失败,返回主要源的错误信息
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
return result, errors.New(result.Error)
}
return backupResult, nil
}
// checkSourceForUpdates 根据更新源类型检查更新
func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType) (*SelfUpdateResult, error) {
// 创建带超时的上下文
timeout := s.config.Updates.UpdateTimeout
if timeout <= 0 {
timeout = 30 // 默认30秒
}
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
result := &SelfUpdateResult{
CurrentVersion: s.config.Updates.Version,
HasUpdate: false,
UpdateApplied: false,
Source: string(sourceType),
}
var release *selfupdate.Release
var found bool
var err error
switch sourceType {
case models.UpdateSourceGithub:
release, found, err = s.checkGithubUpdates(timeoutCtx)
case models.UpdateSourceGitea:
release, found, err = s.checkGiteaUpdates(timeoutCtx)
default:
return nil, fmt.Errorf("unsupported update source type: %s", sourceType)
}
if err != nil {
result.Error = fmt.Sprintf("Failed to check for updates: %v", err)
return result, err
}
if !found {
result.Error = fmt.Sprintf("No release found for %s/%s on %s",
runtime.GOOS, runtime.GOARCH, s.getRepoName(sourceType))
return result, errors.New(result.Error)
}
result.LatestVersion = release.Version()
result.AssetURL = release.AssetURL
result.ReleaseNotes = release.ReleaseNotes
// 比较版本
if release.GreaterThan(s.config.Updates.Version) {
result.HasUpdate = true
} else {
s.logger.Info("Current version is up to date")
}
return result, nil
}
// createGithubUpdater 创建GitHub更新器
func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) {
// 使用默认的GitHub源
updaterConfig := selfupdate.Config{}
return selfupdate.NewUpdater(updaterConfig)
}
// createGiteaUpdater 创建Gitea更新器
func (s *SelfUpdateService) createGiteaUpdater() (*selfupdate.Updater, error) {
giteaConfig := s.config.Updates.Gitea
// 创建Gitea源
source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{
BaseURL: giteaConfig.BaseURL,
})
if err != nil {
return nil, fmt.Errorf("failed to create Gitea source: %w", err)
}
// 创建使用Gitea源的更新器
updaterConfig := selfupdate.Config{
Source: source,
}
return selfupdate.NewUpdater(updaterConfig)
}
// checkGithubUpdates 检查GitHub更新
func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
// 创建GitHub更新器
updater, err := s.createGithubUpdater()
if err != nil {
return nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
}
githubConfig := s.config.Updates.Github
repository := selfupdate.NewRepositorySlug(githubConfig.Owner, githubConfig.Repo)
// 检测最新版本
return updater.DetectLatest(ctx, repository)
}
// checkGiteaUpdates 检查Gitea更新
func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
// 创建Gitea更新器
updater, err := s.createGiteaUpdater()
if err != nil {
return nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
}
giteaConfig := s.config.Updates.Gitea
repository := selfupdate.NewRepositorySlug(giteaConfig.Owner, giteaConfig.Repo)
// 检测最新版本
return updater.DetectLatest(ctx, repository)
}
// getRepoName 获取当前更新源的仓库名称
func (s *SelfUpdateService) getRepoName(sourceType models.UpdateSourceType) string {
switch sourceType {
case models.UpdateSourceGithub:
return s.config.Updates.Github.Repo
case models.UpdateSourceGitea:
return s.config.Updates.Gitea.Repo
default:
return "unknown"
}
}
// ApplyUpdate 应用更新
func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) {
if s.isUpdating {
return nil, errors.New("update is already in progress")
}
s.isUpdating = true
defer func() {
s.isUpdating = false
}()
// 获取可执行文件路径
exe, err := selfupdate.ExecutablePath()
if err != nil {
return &SelfUpdateResult{
CurrentVersion: s.config.Updates.Version,
Error: fmt.Sprintf("Could not locate executable path: %v", err),
}, err
}
// 创建带超时的上下文,仅用于检测最新版本
timeout := s.config.Updates.UpdateTimeout
if timeout <= 0 {
timeout = 30 // 默认30秒
}
checkTimeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
result := &SelfUpdateResult{
CurrentVersion: s.config.Updates.Version,
}
// 首先尝试从主要更新源获取更新信息
primarySourceType := s.config.Updates.PrimarySource
backupSourceType := s.config.Updates.BackupSource
result.Source = string(primarySourceType)
// 从主更新源获取更新信息
primaryUpdater, primaryRelease, primaryFound, err := s.getUpdateFromSource(checkTimeoutCtx, primarySourceType)
if err != nil || !primaryFound {
// 主更新源失败,直接尝试备用源
return s.updateFromSource(ctx, backupSourceType, exe)
}
// 检查是否有可用更新
if !primaryRelease.GreaterThan(s.config.Updates.Version) {
s.logger.Info("Current version is up to date, no need to apply update")
result.LatestVersion = primaryRelease.Version()
return result, nil
}
// 更新结果信息
result.LatestVersion = primaryRelease.Version()
result.AssetURL = primaryRelease.AssetURL
result.ReleaseNotes = primaryRelease.ReleaseNotes
result.HasUpdate = true
// 备份当前可执行文件(如果启用)
var backupPath string
if s.config.Updates.BackupBeforeUpdate {
var err error
backupPath, err = s.createBackup(exe)
if err != nil {
result.Error = fmt.Sprintf("Failed to create backup: %v", err)
return result, err
}
}
// 从主要源尝试下载并应用更新,不设置超时
err = primaryUpdater.UpdateTo(ctx, primaryRelease, exe)
// 如果主要源下载失败,尝试备用源
if err != nil {
// 尝试从备用源更新
backupResult, backupErr := s.updateFromSource(ctx, backupSourceType, exe)
// 如果备用源也失败,清理并返回错误
if backupErr != nil {
if backupPath != "" {
s.cleanupBackup(backupPath)
}
result.Error = fmt.Sprintf("Update failed from both sources: primary error: %v; backup error: %v", err, backupErr)
return result, errors.New(result.Error)
}
// 备用源成功
return backupResult, nil
}
// 主要源更新成功
result.UpdateApplied = true
// 更新成功后清理备份文件
if backupPath != "" {
if err := s.cleanupBackup(backupPath); err != nil {
s.logger.Error("Failed to cleanup backup", "error", err)
}
}
// 更新配置中的版本号
if err := s.updateConfigVersion(result.LatestVersion); err != nil {
s.logger.Error("Failed to update config version", "error", err)
}
return result, nil
}
// updateFromSource 从指定源尝试下载并应用更新
func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType models.UpdateSourceType, exe string) (*SelfUpdateResult, error) {
// 创建带超时的上下文,仅用于检测最新版本
checkTimeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.Updates.UpdateTimeout)*time.Second)
defer cancel()
result := &SelfUpdateResult{
CurrentVersion: s.config.Updates.Version,
Source: string(sourceType),
}
s.logger.Info("Attempting to update from source", "source", sourceType)
// 获取更新信息
updater, release, found, err := s.getUpdateFromSource(checkTimeoutCtx, sourceType)
if err != nil {
result.Error = fmt.Sprintf("Failed to detect latest release from %s: %v", sourceType, err)
return result, err
}
if !found {
result.Error = fmt.Sprintf("Latest release not found from %s", sourceType)
return result, errors.New(result.Error)
}
// 更新结果信息
result.LatestVersion = release.Version()
result.AssetURL = release.AssetURL
result.ReleaseNotes = release.ReleaseNotes
// 检查是否有更新
if !release.GreaterThan(s.config.Updates.Version) {
s.logger.Info("Current version is up to date, no need to apply update")
return result, nil
}
// 标记有更新可用
result.HasUpdate = true
// 备份当前可执行文件(如果启用且尚未备份)
var backupPath string
if s.config.Updates.BackupBeforeUpdate {
s.logger.Info("Creating backup before update...")
var err error
backupPath, err = s.createBackup(exe)
if err != nil {
result.Error = fmt.Sprintf("Failed to create backup: %v", err)
return result, err
}
}
// 尝试下载并应用更新,不设置超时
s.logger.Info("Downloading update...", "source", sourceType)
err = updater.UpdateTo(ctx, release, exe)
if err != nil {
result.Error = fmt.Sprintf("Failed to apply update from %s: %v", sourceType, err)
// 移除下载失败时恢复备份的逻辑,让用户手动处理
if backupPath != "" {
s.logger.Info("Update failed, backup is available at: " + backupPath)
}
return result, err
}
result.UpdateApplied = true
// 更新成功后清理备份文件
if backupPath != "" {
if err := s.cleanupBackup(backupPath); err != nil {
s.logger.Error("Failed to cleanup backup", "error", err)
}
}
// 更新配置中的版本号
if err := s.updateConfigVersion(result.LatestVersion); err != nil {
s.logger.Error("Failed to update config version", "error", err)
}
return result, nil
}
// getUpdateFromSource 从指定源获取更新信息
func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType) (*selfupdate.Updater, *selfupdate.Release, bool, error) {
var updater *selfupdate.Updater
var release *selfupdate.Release
var found bool
var err error
switch sourceType {
case models.UpdateSourceGithub:
updater, err = s.createGithubUpdater()
if err != nil {
return nil, nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
}
release, found, err = s.checkGithubUpdates(ctx)
case models.UpdateSourceGitea:
updater, err = s.createGiteaUpdater()
if err != nil {
return nil, nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
}
release, found, err = s.checkGiteaUpdates(ctx)
default:
return nil, nil, false, fmt.Errorf("unsupported update source type: %s", sourceType)
}
return updater, release, found, err
}
// RestartApplication 重启应用程序
func (s *SelfUpdateService) RestartApplication() error {
return s.restartApplication()
}
// updateConfigVersion 更新配置中的版本号
func (s *SelfUpdateService) updateConfigVersion(version string) error {
// 使用configService更新配置中的版本号
if err := s.configService.Set("updates.version", version); err != nil {
return fmt.Errorf("failed to update config version: %w", err)
}
return nil
}
// createBackup 创建当前可执行文件的备份
func (s *SelfUpdateService) createBackup(executablePath string) (string, error) {
backupPath := executablePath + ".backup"
// 读取原文件
data, err := os.ReadFile(executablePath)
if err != nil {
return "", fmt.Errorf("failed to read executable: %w", err)
}
// 写入备份文件
err = os.WriteFile(backupPath, data, 0755)
if err != nil {
return "", fmt.Errorf("failed to create backup: %w", err)
}
return backupPath, nil
}
// cleanupBackup 清理备份文件
func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove backup file: %w", err)
}
return nil
}