Files
u-desk/internal/api/update_api.go
T
lxy c7978a5bfd 修复: 版本更新与设置面板走查问题
- 更新链路: 检查记录远端SHA256,下载完成事件回传,安装时带哈希校验
- 续传: 服务器忽略Range返回200时截断重下防追加损坏,文件名去query
- 定时器: 自动检查可重入,配置变更免重启生效,关闭即停,下载加互斥
- 前端: 支持跳过此版本并按版本去重,手动检查不受门禁限制,启动未就绪延时重试
- 更新面板: 新增自动检查开关/间隔/地址配置与进度条状态映射
- 版本历史: cache参数强制刷新与加载超时重试兜底
- 配置: 侧边栏分区全链路持久化,保存请求复用api类型防字段丢失
- 侧边栏: 区块定位改显式类名,不再依赖排序位置
- 绑定: 重新生成并与镜像目录同步
2026-10-03 10:18:27 +08:00

206 lines
6.1 KiB
Go

package api
import (
"context"
"sync/atomic"
"time"
"u-desk/internal/service"
)
// UpdateAPI 版本更新 API
type UpdateAPI struct {
updateService *service.UpdateService
ctx context.Context
eventEmitter func(name string, data ...any)
configChanged func() // 更新配置变更回调(重置自动检查定时器)
expectedSHA256 string // 最近一次检查得到的远端包 SHA256
downloading atomic.Bool // 下载进行中标志(并发互斥)
}
// NewUpdateAPI 创建版本更新 API
func NewUpdateAPI(checkURL string) (*UpdateAPI, error) {
return &UpdateAPI{
updateService: service.NewUpdateService(checkURL),
}, nil
}
// SetEventEmitter 设置事件发射器(由 App.ServiceStartup 注入)
func (api *UpdateAPI) SetEventEmitter(emitter func(name string, data ...any)) {
api.eventEmitter = emitter
}
// SetConfigChangedCallback 设置配置变更回调(由 App 注入,用于重置自动检查定时器)
func (api *UpdateAPI) SetConfigChangedCallback(cb func()) {
api.configChanged = cb
}
// SetContext 设置上下文(用于事件推送)
func (api *UpdateAPI) SetContext(ctx context.Context) {
api.ctx = ctx
}
// emit 发送事件到前端
func (api *UpdateAPI) emit(name string, data ...any) {
if api.eventEmitter != nil {
api.eventEmitter(name, data...)
}
}
// successResponse 构造成功响应
func successResponse(data interface{}) map[string]interface{} {
return map[string]interface{}{"success": true, "data": data}
}
// errorResponse 构造错误响应
func errorResponse(message string) map[string]interface{} {
return map[string]interface{}{"success": false, "message": message}
}
// CheckUpdate 检查更新
func (api *UpdateAPI) CheckUpdate() (map[string]interface{}, error) {
result, err := api.updateService.CheckUpdate()
if err != nil {
return errorResponse(err.Error()), nil
}
// 记录远端包哈希,供下载完成事件比对
api.expectedSHA256 = result.SHA256
return successResponse(result), nil
}
// GetCurrentVersion 获取当前版本号
func (api *UpdateAPI) GetCurrentVersion() (map[string]interface{}, error) {
version := service.GetCurrentVersion()
return successResponse(map[string]interface{}{
"version": version,
}), nil
}
// GetUpdateConfig 获取更新配置
func (api *UpdateAPI) GetUpdateConfig() (map[string]interface{}, error) {
config, err := service.LoadUpdateConfig()
if err != nil {
return errorResponse(err.Error()), nil
}
return successResponse(map[string]interface{}{
"current_version": config.CurrentVersion,
"last_check_time": config.LastCheckTime.Format("2006-01-02 15:04:05"),
"auto_check_enabled": config.AutoCheckEnabled,
"check_interval_minutes": config.CheckIntervalMinutes,
"check_url": config.CheckURL,
}), nil
}
// SetUpdateConfig 设置更新配置
func (api *UpdateAPI) SetUpdateConfig(autoCheckEnabled bool, checkIntervalMinutes int, checkURL string) (map[string]interface{}, error) {
config, err := service.LoadUpdateConfig()
if err != nil {
return errorResponse(err.Error()), nil
}
config.AutoCheckEnabled = autoCheckEnabled
config.CheckIntervalMinutes = checkIntervalMinutes
if checkURL != "" {
config.CheckURL = checkURL
api.updateService = service.NewUpdateService(checkURL)
}
if err := service.SaveUpdateConfig(config); err != nil {
return errorResponse(err.Error()), nil
}
// 配置已变更,通知重置自动检查定时器(免重启生效)
if api.configChanged != nil {
api.configChanged()
}
return successResponse(map[string]interface{}{
"message": "配置保存成功",
}), nil
}
// DownloadUpdate 下载更新包(异步,通过事件推送进度)
func (api *UpdateAPI) DownloadUpdate(downloadURL string) (map[string]interface{}, error) {
if downloadURL == "" {
return errorResponse("下载地址不能为空"), nil
}
// 并发互斥:已有下载任务进行中则拒绝
if !api.downloading.CompareAndSwap(false, true) {
return errorResponse("已有下载任务进行中"), nil
}
go func() {
defer api.downloading.Store(false)
progressCallback := func(progress float64, speed float64, downloaded int64, total int64) {
progressInfo := map[string]interface{}{
"progress": progress,
"speed": speed,
"downloaded": downloaded,
"total": total,
}
api.emit("download-progress", progressInfo)
}
time.Sleep(100 * time.Millisecond)
result, err := service.DownloadUpdate(downloadURL, progressCallback)
if err != nil {
errorInfo := map[string]interface{}{"error": err.Error()}
api.emit("download-complete", errorInfo)
} else {
resultInfo := map[string]interface{}{
"success": true,
"file_path": result.FilePath,
"file_size": result.FileSize,
"sha256": result.SHA256Hash,
"expected_sha256": api.expectedSHA256,
}
api.emit("download-complete", resultInfo)
}
}()
return successResponse(map[string]interface{}{
"message": "下载已开始",
}), nil
}
// InstallUpdate 安装更新包
func (api *UpdateAPI) InstallUpdate(installerPath string, autoRestart bool) (map[string]interface{}, error) {
return api.InstallUpdateWithHash(installerPath, autoRestart, "", "")
}
// InstallUpdateWithHash 安装更新包(带哈希验证)
func (api *UpdateAPI) InstallUpdateWithHash(installerPath string, autoRestart bool, expectedHash string, hashType string) (map[string]interface{}, error) {
if installerPath == "" {
return errorResponse("安装文件路径不能为空"), nil
}
result, err := service.InstallUpdateWithHash(installerPath, autoRestart, expectedHash, hashType)
if err != nil {
return errorResponse(err.Error()), nil
}
return successResponse(result), nil
}
// VerifyUpdateFile 验证更新文件哈希值
func (api *UpdateAPI) VerifyUpdateFile(filePath string, expectedHash string, hashType string) (map[string]interface{}, error) {
if filePath == "" {
return errorResponse("文件路径不能为空"), nil
}
valid, err := service.VerifyFileHash(filePath, expectedHash, hashType)
if err != nil {
return errorResponse(err.Error()), nil
}
return successResponse(map[string]interface{}{
"valid": valid,
}), nil
}