🎨 Optimize update services
This commit is contained in:
1 parent
cc98e556c6
commit
286b0159d7
4 files changed
+172
-322
No files matched your search
@@ -80,7 +80,7 @@ function animationLoop() {
|
|||||||
// 等待一段时间后重置动画
|
// 等待一段时间后重置动画
|
||||||
resetTimeoutId = window.setTimeout(() => {
|
resetTimeoutId = window.setTimeout(() => {
|
||||||
reset();
|
reset();
|
||||||
}, 750);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,8 @@ onBeforeUnmount(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: var(--voidraft-bg-gradient, radial-gradient(#222922, #000500));
|
//background: var(--voidraft-bg-gradient, rgba(0, 5, 0, 0.15));
|
||||||
|
//backdrop-filter: blur(2px);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, readonly, ref, shallowRef, onScopeDispose } from 'vue';
|
import { computed, readonly, ref, onScopeDispose } from 'vue';
|
||||||
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
||||||
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
|
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
|
||||||
import { useConfigStore } from './configStore';
|
import { useConfigStore } from './configStore';
|
||||||
|
|||||||
@@ -55,9 +55,11 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
|
|
||||||
<div ref="editorElement" class="editor"></div>
|
<div ref="editorElement" class="editor"></div>
|
||||||
<Toolbar/>
|
<Toolbar/>
|
||||||
|
<transition name="loading-fade">
|
||||||
|
<LoadingScreen v-if="editorStore.isLoading && enableLoadingAnimation" text="VOIDRAFT"/>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,4 +87,15 @@ onBeforeUnmount(() => {
|
|||||||
:deep(.cm-scroller) {
|
:deep(.cm-scroller) {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载动画过渡效果
|
||||||
|
.loading-fade-enter-active,
|
||||||
|
.loading-fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-fade-enter-from,
|
||||||
|
.loading-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -4,13 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/creativeprojects/go-selfupdate"
|
"github.com/creativeprojects/go-selfupdate"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,55 +32,57 @@ type SelfUpdateResult struct {
|
|||||||
type SelfUpdateService struct {
|
type SelfUpdateService struct {
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
badgeService *dock.DockService // 直接使用Wails原生badge服务
|
badgeService *dock.DockService
|
||||||
notificationService *notifications.NotificationService // 通知服务
|
notificationService *notifications.NotificationService
|
||||||
config *models.AppConfig
|
|
||||||
|
|
||||||
// 状态管理
|
mu sync.Mutex // 保护更新状态
|
||||||
isUpdating bool
|
isUpdating bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSelfUpdateService 创建自我更新服务实例
|
// NewSelfUpdateService 创建自我更新服务实例
|
||||||
func NewSelfUpdateService(configService *ConfigService, badgeService *dock.DockService, notificationService *notifications.NotificationService, logger *log.LogService) *SelfUpdateService {
|
func NewSelfUpdateService(configService *ConfigService, badgeService *dock.DockService, notificationService *notifications.NotificationService, logger *log.LogService) *SelfUpdateService {
|
||||||
// 获取配置
|
return &SelfUpdateService{
|
||||||
appConfig, err := configService.GetConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := &SelfUpdateService{
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
badgeService: badgeService,
|
badgeService: badgeService,
|
||||||
notificationService: notificationService,
|
notificationService: notificationService,
|
||||||
config: appConfig,
|
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return service
|
// getConfig 获取最新配置
|
||||||
|
func (s *SelfUpdateService) getConfig() (*models.AppConfig, error) {
|
||||||
|
config, err := s.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get config failed: %w", err)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckForUpdates 检查更新
|
// CheckForUpdates 检查更新
|
||||||
func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateResult, error) {
|
func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateResult, error) {
|
||||||
|
config, err := s.getConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
result := &SelfUpdateResult{
|
result := &SelfUpdateResult{
|
||||||
CurrentVersion: s.config.Updates.Version,
|
CurrentVersion: config.Updates.Version,
|
||||||
HasUpdate: false,
|
HasUpdate: false,
|
||||||
UpdateApplied: false,
|
UpdateApplied: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首先尝试主要更新源
|
// 尝试主要更新源
|
||||||
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
|
primaryResult, err := s.checkSourceForUpdates(ctx, config.Updates.PrimarySource, config)
|
||||||
if err == nil && primaryResult != nil {
|
if err == nil && primaryResult != nil {
|
||||||
s.handleUpdateBadge(primaryResult)
|
s.handleUpdateBadge(primaryResult)
|
||||||
return primaryResult, nil
|
return primaryResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果主要更新源失败,尝试备用更新源
|
// 尝试备用更新源
|
||||||
backupResult, backupErr := s.checkSourceForUpdates(ctx, s.config.Updates.BackupSource)
|
backupResult, backupErr := s.checkSourceForUpdates(ctx, config.Updates.BackupSource, config)
|
||||||
if backupErr != nil {
|
if backupErr != nil {
|
||||||
// 如果备用源也失败,返回主要源的错误信息
|
result.Error = fmt.Sprintf("both sources failed: %v; %v", err, backupErr)
|
||||||
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
|
|
||||||
// 确保在检查失败时也调用handleUpdateBadge来清除可能存在的badge
|
|
||||||
s.handleUpdateBadge(result)
|
s.handleUpdateBadge(result)
|
||||||
return result, errors.New(result.Error)
|
return result, errors.New(result.Error)
|
||||||
}
|
}
|
||||||
@@ -88,17 +92,16 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkSourceForUpdates 根据更新源类型检查更新
|
// checkSourceForUpdates 根据更新源类型检查更新
|
||||||
func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType) (*SelfUpdateResult, error) {
|
func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*SelfUpdateResult, error) {
|
||||||
// 创建带超时的上下文
|
timeout := config.Updates.UpdateTimeout
|
||||||
timeout := s.config.Updates.UpdateTimeout
|
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 30 // 默认30秒
|
timeout = 30
|
||||||
}
|
}
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
result := &SelfUpdateResult{
|
result := &SelfUpdateResult{
|
||||||
CurrentVersion: s.config.Updates.Version,
|
CurrentVersion: config.Updates.Version,
|
||||||
HasUpdate: false,
|
HasUpdate: false,
|
||||||
UpdateApplied: false,
|
UpdateApplied: false,
|
||||||
Source: string(sourceType),
|
Source: string(sourceType),
|
||||||
@@ -110,320 +113,164 @@ func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceTyp
|
|||||||
|
|
||||||
switch sourceType {
|
switch sourceType {
|
||||||
case models.UpdateSourceGithub:
|
case models.UpdateSourceGithub:
|
||||||
release, found, err = s.checkGithubUpdates(timeoutCtx)
|
release, found, err = s.checkGithubUpdates(timeoutCtx, config)
|
||||||
case models.UpdateSourceGitea:
|
case models.UpdateSourceGitea:
|
||||||
release, found, err = s.checkGiteaUpdates(timeoutCtx)
|
release, found, err = s.checkGiteaUpdates(timeoutCtx, config)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported update source type: %s", sourceType)
|
return nil, fmt.Errorf("unsupported source: %s", sourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = fmt.Sprintf("Failed to check for updates: %v", err)
|
return result, fmt.Errorf("check failed: %w", err)
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
result.Error = fmt.Sprintf("No release found for %s/%s on %s",
|
return result, fmt.Errorf("no release for %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
runtime.GOOS, runtime.GOARCH, s.getRepoName(sourceType))
|
|
||||||
return result, errors.New(result.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.LatestVersion = release.Version()
|
result.LatestVersion = release.Version()
|
||||||
result.AssetURL = release.AssetURL
|
result.AssetURL = release.AssetURL
|
||||||
result.ReleaseNotes = release.ReleaseNotes
|
result.ReleaseNotes = release.ReleaseNotes
|
||||||
|
result.HasUpdate = release.GreaterThan(config.Updates.Version)
|
||||||
// 比较版本
|
|
||||||
if release.GreaterThan(s.config.Updates.Version) {
|
|
||||||
result.HasUpdate = true
|
|
||||||
} else {
|
|
||||||
s.logger.Info("Current version is up to date")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createGithubUpdater 创建GitHub更新器
|
// createGithubUpdater 创建GitHub更新器
|
||||||
func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) {
|
func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) {
|
||||||
// 使用默认的GitHub源
|
return selfupdate.NewUpdater(selfupdate.Config{})
|
||||||
updaterConfig := selfupdate.Config{}
|
|
||||||
|
|
||||||
return selfupdate.NewUpdater(updaterConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createGiteaUpdater 创建Gitea更新器
|
// createGiteaUpdater 创建Gitea更新器
|
||||||
func (s *SelfUpdateService) createGiteaUpdater() (*selfupdate.Updater, error) {
|
func (s *SelfUpdateService) createGiteaUpdater(config *models.AppConfig) (*selfupdate.Updater, error) {
|
||||||
giteaConfig := s.config.Updates.Gitea
|
|
||||||
|
|
||||||
// 创建Gitea源
|
|
||||||
source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{
|
source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{
|
||||||
BaseURL: giteaConfig.BaseURL,
|
BaseURL: config.Updates.Gitea.BaseURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create Gitea source: %w", err)
|
return nil, fmt.Errorf("create gitea source failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建使用Gitea源的更新器
|
return selfupdate.NewUpdater(selfupdate.Config{Source: source})
|
||||||
updaterConfig := selfupdate.Config{
|
|
||||||
Source: source,
|
|
||||||
}
|
|
||||||
|
|
||||||
return selfupdate.NewUpdater(updaterConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkGithubUpdates 检查GitHub更新
|
// checkGithubUpdates 检查GitHub更新
|
||||||
func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
|
func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) {
|
||||||
// 创建GitHub更新器
|
|
||||||
updater, err := s.createGithubUpdater()
|
updater, err := s.createGithubUpdater()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
githubConfig := s.config.Updates.Github
|
repo := selfupdate.NewRepositorySlug(config.Updates.Github.Owner, config.Updates.Github.Repo)
|
||||||
repository := selfupdate.NewRepositorySlug(githubConfig.Owner, githubConfig.Repo)
|
return updater.DetectLatest(ctx, repo)
|
||||||
|
|
||||||
// 检测最新版本
|
|
||||||
return updater.DetectLatest(ctx, repository)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkGiteaUpdates 检查Gitea更新
|
// checkGiteaUpdates 检查Gitea更新
|
||||||
func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context) (*selfupdate.Release, bool, error) {
|
func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) {
|
||||||
// 创建Gitea更新器
|
updater, err := s.createGiteaUpdater(config)
|
||||||
updater, err := s.createGiteaUpdater()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaConfig := s.config.Updates.Gitea
|
repo := selfupdate.NewRepositorySlug(config.Updates.Gitea.Owner, config.Updates.Gitea.Repo)
|
||||||
repository := selfupdate.NewRepositorySlug(giteaConfig.Owner, giteaConfig.Repo)
|
return updater.DetectLatest(ctx, 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 应用更新
|
// ApplyUpdate 应用更新
|
||||||
func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) {
|
func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) {
|
||||||
|
s.mu.Lock()
|
||||||
if s.isUpdating {
|
if s.isUpdating {
|
||||||
return nil, errors.New("update is already in progress")
|
s.mu.Unlock()
|
||||||
|
return nil, errors.New("update in progress")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.isUpdating = true
|
s.isUpdating = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
s.mu.Lock()
|
||||||
s.isUpdating = false
|
s.isUpdating = false
|
||||||
|
s.mu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// 获取可执行文件路径
|
config, err := s.getConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
exe, err := selfupdate.ExecutablePath()
|
exe, err := selfupdate.ExecutablePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &SelfUpdateResult{
|
return nil, fmt.Errorf("locate executable failed: %w", err)
|
||||||
CurrentVersion: s.config.Updates.Version,
|
|
||||||
Error: fmt.Sprintf("Could not locate executable path: %v", err),
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建带超时的上下文,仅用于检测最新版本
|
// 尝试主要源
|
||||||
timeout := s.config.Updates.UpdateTimeout
|
result, err := s.performUpdate(ctx, config.Updates.PrimarySource, exe, config)
|
||||||
if timeout <= 0 {
|
if err == nil {
|
||||||
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) {
|
|
||||||
result.LatestVersion = primaryRelease.Version()
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新结果信息
|
// 尝试备用源
|
||||||
result.LatestVersion = primaryRelease.Version()
|
result, err = s.performUpdate(ctx, config.Updates.BackupSource, exe, config)
|
||||||
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 {
|
if err != nil {
|
||||||
// 尝试从备用源更新
|
return nil, fmt.Errorf("update failed from both sources: %w", err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行配置迁移
|
|
||||||
if err := s.configService.MigrateConfig(); err != nil {
|
|
||||||
s.logger.Error("Failed to migrate config after update", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新成功,移除badge
|
|
||||||
if s.badgeService != nil {
|
|
||||||
if err := s.badgeService.RemoveBadge(); err != nil {
|
|
||||||
s.logger.Error("failed to remove update badge after successful update", "error", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateFromSource 从指定源尝试下载并应用更新
|
// performUpdate 执行更新操作(包括检测、备份、下载、应用)
|
||||||
func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType models.UpdateSourceType, exe string) (*SelfUpdateResult, error) {
|
func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models.UpdateSourceType, exe string, config *models.AppConfig) (*SelfUpdateResult, error) {
|
||||||
// 创建带超时的上下文,仅用于检测最新版本
|
timeout := config.Updates.UpdateTimeout
|
||||||
checkTimeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.Updates.UpdateTimeout)*time.Second)
|
if timeout <= 0 {
|
||||||
|
timeout = 30
|
||||||
|
}
|
||||||
|
checkCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
// 获取更新器和版本信息
|
||||||
|
updater, release, found, err := s.getUpdateFromSource(checkCtx, sourceType, config)
|
||||||
|
if err != nil || !found {
|
||||||
|
return nil, fmt.Errorf("detect release failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
result := &SelfUpdateResult{
|
result := &SelfUpdateResult{
|
||||||
CurrentVersion: s.config.Updates.Version,
|
CurrentVersion: config.Updates.Version,
|
||||||
|
LatestVersion: release.Version(),
|
||||||
|
AssetURL: release.AssetURL,
|
||||||
|
ReleaseNotes: release.ReleaseNotes,
|
||||||
Source: string(sourceType),
|
Source: string(sourceType),
|
||||||
|
HasUpdate: release.GreaterThan(config.Updates.Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("Attempting to update from source", "source", sourceType)
|
// 无更新
|
||||||
|
if !result.HasUpdate {
|
||||||
// 获取更新信息
|
|
||||||
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记有更新可用
|
// 创建备份
|
||||||
result.HasUpdate = true
|
|
||||||
|
|
||||||
// 备份当前可执行文件(如果启用且尚未备份)
|
|
||||||
var backupPath string
|
var backupPath string
|
||||||
if s.config.Updates.BackupBeforeUpdate {
|
if config.Updates.BackupBeforeUpdate {
|
||||||
s.logger.Info("Creating backup before update...")
|
|
||||||
var err error
|
|
||||||
backupPath, err = s.createBackup(exe)
|
backupPath, err = s.createBackup(exe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = fmt.Sprintf("Failed to create backup: %v", err)
|
return nil, fmt.Errorf("backup failed: %w", err)
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if backupPath != "" {
|
||||||
|
s.cleanupBackup(backupPath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试下载并应用更新,不设置超时
|
// 下载并应用更新
|
||||||
err = updater.UpdateTo(ctx, release, exe)
|
if err := updater.UpdateTo(ctx, release, exe); err != nil {
|
||||||
|
return nil, fmt.Errorf("apply update failed: %w", err)
|
||||||
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
|
result.UpdateApplied = true
|
||||||
|
s.handleUpdateSuccess(result)
|
||||||
// 更新成功后清理备份文件
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新成功,移除badge
|
|
||||||
if s.badgeService != nil {
|
|
||||||
if err := s.badgeService.RemoveBadge(); err != nil {
|
|
||||||
s.logger.Error("failed to remove update badge after successful update", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUpdateFromSource 从指定源获取更新信息
|
// getUpdateFromSource 从指定源获取更新信息
|
||||||
func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType) (*selfupdate.Updater, *selfupdate.Release, bool, error) {
|
func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*selfupdate.Updater, *selfupdate.Release, bool, error) {
|
||||||
var updater *selfupdate.Updater
|
var updater *selfupdate.Updater
|
||||||
var release *selfupdate.Release
|
var release *selfupdate.Release
|
||||||
var found bool
|
var found bool
|
||||||
@@ -433,50 +280,50 @@ func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType
|
|||||||
case models.UpdateSourceGithub:
|
case models.UpdateSourceGithub:
|
||||||
updater, err = s.createGithubUpdater()
|
updater, err = s.createGithubUpdater()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, false, fmt.Errorf("failed to create GitHub updater: %w", err)
|
return nil, nil, false, err
|
||||||
}
|
}
|
||||||
release, found, err = s.checkGithubUpdates(ctx)
|
release, found, err = s.checkGithubUpdates(ctx, config)
|
||||||
case models.UpdateSourceGitea:
|
case models.UpdateSourceGitea:
|
||||||
updater, err = s.createGiteaUpdater()
|
updater, err = s.createGiteaUpdater(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, false, fmt.Errorf("failed to create Gitea updater: %w", err)
|
return nil, nil, false, err
|
||||||
}
|
}
|
||||||
release, found, err = s.checkGiteaUpdates(ctx)
|
release, found, err = s.checkGiteaUpdates(ctx, config)
|
||||||
default:
|
default:
|
||||||
return nil, nil, false, fmt.Errorf("unsupported update source type: %s", sourceType)
|
return nil, nil, false, fmt.Errorf("unsupported source: %s", sourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return updater, release, found, err
|
return updater, release, found, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartApplication 重启应用程序
|
// handleUpdateSuccess 处理更新成功后的操作
|
||||||
func (s *SelfUpdateService) RestartApplication() error {
|
func (s *SelfUpdateService) handleUpdateSuccess(result *SelfUpdateResult) {
|
||||||
return s.restartApplication()
|
// 更新配置版本
|
||||||
}
|
if err := s.configService.Set("updates.version", result.LatestVersion); err != nil {
|
||||||
|
s.logger.Error("update config version failed", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
// updateConfigVersion 更新配置中的版本号
|
// 执行配置迁移
|
||||||
func (s *SelfUpdateService) updateConfigVersion(version string) error {
|
if err := s.configService.MigrateConfig(); err != nil {
|
||||||
// 使用configService更新配置中的版本号
|
s.logger.Error("migrate config failed", "error", err)
|
||||||
if err := s.configService.Set("updates.version", version); err != nil {
|
}
|
||||||
return fmt.Errorf("failed to update config version: %w", err)
|
|
||||||
|
// 移除badge
|
||||||
|
if s.badgeService != nil {
|
||||||
|
s.badgeService.RemoveBadge()
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// createBackup 创建当前可执行文件的备份
|
// createBackup 创建可执行文件备份
|
||||||
func (s *SelfUpdateService) createBackup(executablePath string) (string, error) {
|
func (s *SelfUpdateService) createBackup(executablePath string) (string, error) {
|
||||||
backupPath := executablePath + ".backup"
|
backupPath := executablePath + ".backup"
|
||||||
|
|
||||||
// 读取原文件
|
|
||||||
data, err := os.ReadFile(executablePath)
|
data, err := os.ReadFile(executablePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read executable: %w", err)
|
return "", fmt.Errorf("read executable failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入备份文件
|
if err := os.WriteFile(backupPath, data, 0755); err != nil {
|
||||||
err = os.WriteFile(backupPath, data, 0755)
|
return "", fmt.Errorf("write backup failed: %w", err)
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to create backup: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return backupPath, nil
|
return backupPath, nil
|
||||||
@@ -485,31 +332,34 @@ func (s *SelfUpdateService) createBackup(executablePath string) (string, error)
|
|||||||
// cleanupBackup 清理备份文件
|
// cleanupBackup 清理备份文件
|
||||||
func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
|
func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
|
||||||
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(backupPath); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("failed to remove backup file: %w", err)
|
s.logger.Error("cleanup backup failed", "error", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateBadge 处理更新通知badge和通知
|
// RestartApplication 重启应用程序
|
||||||
func (s *SelfUpdateService) handleUpdateBadge(result *SelfUpdateResult) {
|
func (s *SelfUpdateService) RestartApplication() error {
|
||||||
if result != nil && result.HasUpdate {
|
return s.restartApplication()
|
||||||
// 有更新时显示更新badge
|
}
|
||||||
if s.badgeService != nil {
|
|
||||||
if err := s.badgeService.SetBadge("●"); err != nil {
|
|
||||||
s.logger.Error("failed to set update badge", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送简单通知
|
// handleUpdateBadge 处理更新徽章和通知
|
||||||
s.sendUpdateNotification(result)
|
func (s *SelfUpdateService) handleUpdateBadge(result *SelfUpdateResult) {
|
||||||
} else {
|
if result == nil || !result.HasUpdate {
|
||||||
// 没有更新或出错时移除badge
|
|
||||||
if s.badgeService != nil {
|
if s.badgeService != nil {
|
||||||
if err := s.badgeService.RemoveBadge(); err != nil {
|
s.badgeService.RemoveBadge()
|
||||||
s.logger.Error("failed to remove update badge", "error", err)
|
}
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示徽章
|
||||||
|
if s.badgeService != nil {
|
||||||
|
if err := s.badgeService.SetBadge("●"); err != nil {
|
||||||
|
s.logger.Error("set badge failed", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发送通知
|
||||||
|
s.sendUpdateNotification(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendUpdateNotification 发送更新通知
|
// sendUpdateNotification 发送更新通知
|
||||||
@@ -518,34 +368,20 @@ func (s *SelfUpdateService) sendUpdateNotification(result *SelfUpdateResult) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查通知授权(macOS需要)
|
// 检查授权
|
||||||
authorized, err := s.notificationService.CheckNotificationAuthorization()
|
authorized, err := s.notificationService.CheckNotificationAuthorization()
|
||||||
if err != nil {
|
if err != nil || !authorized {
|
||||||
s.logger.Error("Failed to check notification authorization", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !authorized {
|
|
||||||
authorized, err = s.notificationService.RequestNotificationAuthorization()
|
authorized, err = s.notificationService.RequestNotificationAuthorization()
|
||||||
if err != nil || !authorized {
|
if err != nil || !authorized {
|
||||||
s.logger.Error("Failed to get notification authorization", "error", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建简单通知内容
|
// 发送通知
|
||||||
title := "Voidraft Update Available"
|
s.notificationService.SendNotification(notifications.NotificationOptions{
|
||||||
body := fmt.Sprintf("New version %s available (current: %s)", result.LatestVersion, result.CurrentVersion)
|
|
||||||
|
|
||||||
// 发送简单通知
|
|
||||||
err = s.notificationService.SendNotification(notifications.NotificationOptions{
|
|
||||||
ID: "update_available",
|
ID: "update_available",
|
||||||
Title: title,
|
Title: "Voidraft Update Available",
|
||||||
Subtitle: "New version available",
|
Subtitle: "New version available",
|
||||||
Body: body,
|
Body: fmt.Sprintf("Version %s available (current: %s)", result.LatestVersion, result.CurrentVersion),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("Failed to send notification", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user