merge: remove legacy openflare update API

This commit is contained in:
ryan
2026-06-19 11:18:15 +08:00
16 changed files with 3601 additions and 2147 deletions
@@ -1,262 +0,0 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package update
import (
"context"
"errors"
"fmt"
"runtime"
"strings"
"sync"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/admin/updater"
"github.com/Rain-kl/Wavelet/pkg/logger"
)
const (
channelStable = "stable"
channelPreview = "preview"
)
// UpgradeLogRecord is a single upgrade log entry for the legacy update API.
type UpgradeLogRecord struct {
Level string `json:"level"`
Message string `json:"message"`
CreatedAt time.Time `json:"created_at"`
}
// LatestReleaseView mirrors the legacy OpenFlare latest-release payload.
type LatestReleaseView struct {
TagName string `json:"tag_name"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
PublishedAt string `json:"published_at"`
Channel string `json:"channel"`
Prerelease bool `json:"prerelease"`
CurrentVersion string `json:"current_version"`
HasUpdate bool `json:"has_update"`
UpgradeSupported bool `json:"upgrade_supported"`
InProgress bool `json:"in_progress"`
UpgradeStatus string `json:"upgrade_status"`
UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"`
}
// StreamSnapshot is pushed over the upgrade logs websocket.
type StreamSnapshot struct {
InProgress bool `json:"in_progress"`
UpgradeStatus string `json:"upgrade_status"`
UpgradeLogs []UpgradeLogRecord `json:"upgrade_logs"`
}
type upgradeRequest struct {
Channel string `json:"channel"`
}
var upgradeState struct {
sync.Mutex
inProgress bool
status string
logs []UpgradeLogRecord
}
var upgradeSubscribers struct {
sync.Mutex
nextID int
listeners map[int]chan StreamSnapshot
}
func init() {
upgradeSubscribers.listeners = make(map[int]chan StreamSnapshot)
upgradeState.status = "idle"
}
func normalizeChannel(channel string) string {
switch strings.ToLower(strings.TrimSpace(channel)) {
case channelPreview:
return channelPreview
default:
return channelStable
}
}
func isDevBuild(version string) bool {
version = strings.TrimSpace(version)
return version == "" || strings.EqualFold(version, "dev")
}
func mapStatusToLatestRelease(status updater.Status, channel string) *LatestReleaseView {
inProgress, upgradeStatus, logs := snapshotUpgradeState()
view := &LatestReleaseView{
TagName: status.LatestVersion,
Body: status.ReleaseNotes,
HTMLURL: status.ReleaseURL,
PublishedAt: status.PublishedAt,
Channel: channel,
Prerelease: status.Prerelease,
CurrentVersion: status.CurrentVersion,
HasUpdate: status.UpdateAvailable,
UpgradeSupported: !isDevBuild(status.CurrentVersion) && runtime.GOOS != "windows",
InProgress: inProgress || updater.IsUpgrading(),
UpgradeStatus: upgradeStatus,
UpgradeLogs: logs,
}
if channel == channelPreview && status.Prerelease {
view.HasUpdate = !isDevBuild(status.CurrentVersion)
}
return view
}
// GetLatestRelease returns the newest upstream release for the requested channel.
func GetLatestRelease(ctx context.Context, channel string) (*LatestReleaseView, error) {
normalizedChannel := normalizeChannel(channel)
status, err := updater.GetStatus(ctx)
if err != nil {
return nil, err
}
return mapStatusToLatestRelease(status, normalizedChannel), nil
}
// ScheduleUpgrade downloads the latest release and restarts with the staged binary.
func ScheduleUpgrade(ctx context.Context, channel string) (*LatestReleaseView, error) {
normalizedChannel := normalizeChannel(channel)
upgradeState.Lock()
if upgradeState.inProgress || updater.IsUpgrading() {
upgradeState.Unlock()
return nil, errors.New("服务升级正在执行中,请稍后再试")
}
resetUpgradeLogsLocked()
upgradeState.inProgress = true
upgradeState.status = "running"
appendUpgradeLogLocked("info", fmt.Sprintf("Automatic upgrade scheduled for channel: %s.", normalizedChannel))
upgradeState.Unlock()
broadcastUpgradeSnapshot()
executable, stagedBinary, status, err := updater.PrepareUpgrade(ctx)
if err != nil {
recordUpgradeFailure(err)
return nil, err
}
view := mapStatusToLatestRelease(status, normalizedChannel)
view.InProgress = true
view.UpgradeStatus = "running"
view.UpgradeLogs = snapshotUpgradeLogs()
appendUpgradeLogLocked("info", fmt.Sprintf("Upgrade package prepared: %s.", status.LatestVersion))
broadcastUpgradeSnapshot()
go func() {
time.Sleep(time.Second)
if err := updater.ApplyPreparedUpgrade(executable, stagedBinary); err != nil {
updater.FinishUpgrade()
recordUpgradeFailure(err)
logger.ErrorF(context.Background(), "[Update] replace and restart failed: %v", err)
}
}()
return view, nil
}
// UploadManualBinary is disabled in the legacy OpenFlare server.
func UploadManualBinary() error {
return errors.New("手动升级功能已禁用")
}
// ConfirmManualUpgrade is disabled in the legacy OpenFlare server.
func ConfirmManualUpgrade() error {
return errors.New("手动升级功能已禁用")
}
// SubscribeUpgradeStream registers a listener for upgrade websocket snapshots.
func SubscribeUpgradeStream() (<-chan StreamSnapshot, func()) {
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
id := upgradeSubscribers.nextID
upgradeSubscribers.nextID++
ch := make(chan StreamSnapshot, 1)
upgradeSubscribers.listeners[id] = ch
unsubscribe := func() {
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
if listener, ok := upgradeSubscribers.listeners[id]; ok {
delete(upgradeSubscribers.listeners, id)
close(listener)
}
}
select {
case ch <- currentUpgradeSnapshot():
default:
}
return ch, unsubscribe
}
func snapshotUpgradeState() (bool, string, []UpgradeLogRecord) {
upgradeState.Lock()
defer upgradeState.Unlock()
return upgradeState.inProgress || updater.IsUpgrading(), upgradeState.status, cloneUpgradeLogsLocked()
}
func snapshotUpgradeLogs() []UpgradeLogRecord {
upgradeState.Lock()
defer upgradeState.Unlock()
return cloneUpgradeLogsLocked()
}
func currentUpgradeSnapshot() StreamSnapshot {
inProgress, status, logs := snapshotUpgradeState()
return StreamSnapshot{
InProgress: inProgress,
UpgradeStatus: status,
UpgradeLogs: logs,
}
}
func resetUpgradeLogsLocked() {
upgradeState.logs = nil
}
func appendUpgradeLogLocked(level, message string) {
upgradeState.logs = append(upgradeState.logs, UpgradeLogRecord{
Level: level,
Message: message,
CreatedAt: time.Now().UTC(),
})
}
func cloneUpgradeLogsLocked() []UpgradeLogRecord {
if len(upgradeState.logs) == 0 {
return []UpgradeLogRecord{}
}
cloned := make([]UpgradeLogRecord, len(upgradeState.logs))
copy(cloned, upgradeState.logs)
return cloned
}
func recordUpgradeFailure(err error) {
upgradeState.Lock()
upgradeState.inProgress = false
upgradeState.status = "failed"
appendUpgradeLogLocked("error", err.Error())
upgradeState.Unlock()
broadcastUpgradeSnapshot()
}
func broadcastUpgradeSnapshot() {
snapshot := currentUpgradeSnapshot()
upgradeSubscribers.Lock()
defer upgradeSubscribers.Unlock()
for _, listener := range upgradeSubscribers.listeners {
select {
case listener <- snapshot:
default:
}
}
}
@@ -1,157 +0,0 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package update
import (
"encoding/json"
"errors"
"io"
"net/http"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
"github.com/Rain-kl/Wavelet/internal/common/response"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgradeLogsUpgrader = websocket.Upgrader{
CheckOrigin: func(_ *http.Request) bool { return true },
}
// GetLatestReleaseHandler 获取最新 GitHub 发布版本。
// @Summary 获取最新服务端发布版本
// @Description 查询 OpenFlare 服务端最新 GitHub Release 及升级状态,需要管理员权限
// @Tags openflare-update
// @Produce json
// @Security SessionCookie
// @Param channel query string false "发布渠道(stable/preview)"
// @Success 200 {object} response.Any{data=update.LatestReleaseView} "最新发布信息"
// @Failure 400 {object} response.Any "参数错误"
// @Failure 401 {object} response.Any "未登录"
// @Failure 403 {object} response.Any "无管理员权限"
// @Failure 500 {object} response.Any "内部错误"
// @Router /api/v1/d/update/latest-release [get]
func GetLatestReleaseHandler(c *gin.Context) {
release, err := GetLatestRelease(c.Request.Context(), c.Query("channel"))
if apiutil.AbortBadRequestOnError(c, err) {
return
}
c.JSON(http.StatusOK, response.OK(release))
}
// UpgradeServerHandler 调度自动升级任务。
// @Summary 触发服务端自动升级
// @Description 从最新 Release 调度 OpenFlare 服务端自动升级,需要管理员权限
// @Tags openflare-update
// @Accept json
// @Produce json
// @Security SessionCookie
// @Param request body update.upgradeRequest false "升级参数"
// @Success 200 {object} response.Any{data=update.LatestReleaseView} "升级任务已调度"
// @Failure 400 {object} response.Any "参数错误"
// @Failure 401 {object} response.Any "未登录"
// @Failure 403 {object} response.Any "无管理员权限"
// @Failure 500 {object} response.Any "内部错误"
// @Router /api/v1/d/update/upgrade [post]
func UpgradeServerHandler(c *gin.Context) {
var request upgradeRequest
if err := bindOptionalJSON(c.Request.Body, &request); err != nil {
response.AbortBadRequest(c, "无效的参数")
return
}
release, err := ScheduleUpgrade(c.Request.Context(), request.Channel)
if apiutil.AbortBadRequestOnError(c, err) {
return
}
c.JSON(http.StatusOK, response.OK(release))
}
// UploadManualServerBinaryHandler 上传手动升级二进制(已禁用)。
// @Summary 上传手动升级二进制
// @Description 上传服务端二进制以进行手动升级(当前功能已禁用),需要管理员权限
// @Tags openflare-update
// @Accept multipart/form-data
// @Produce json
// @Security SessionCookie
// @Param binary formData file true "服务端二进制文件"
// @Failure 400 {object} response.Any "功能已禁用或参数错误"
// @Failure 401 {object} response.Any "未登录"
// @Failure 403 {object} response.Any "无管理员权限"
// @Router /api/v1/d/update/manual-upload [post]
func UploadManualServerBinaryHandler(c *gin.Context) {
if apiutil.AbortBadRequestOnError(c, UploadManualBinary()) {
return
}
}
// ConfirmManualServerUpgradeHandler 确认手动升级(已禁用)。
// @Summary 确认手动服务端升级
// @Description 确认并执行手动上传的服务端升级(当前功能已禁用),需要管理员权限
// @Tags openflare-update
// @Accept json
// @Produce json
// @Security SessionCookie
// @Failure 400 {object} response.Any "功能已禁用或参数错误"
// @Failure 401 {object} response.Any "未登录"
// @Failure 403 {object} response.Any "无管理员权限"
// @Router /api/v1/d/update/manual-upgrade [post]
func ConfirmManualServerUpgradeHandler(c *gin.Context) {
if apiutil.AbortBadRequestOnError(c, ConfirmManualUpgrade()) {
return
}
}
// StreamServerUpgradeLogsHandler 通过 WebSocket 推送升级日志。
// @Summary 流式获取服务端升级日志
// @Description 通过 WebSocket 推送升级进度快照,需要管理员权限
// @Tags openflare-update
// @Security SessionCookie
// @Success 101 {object} update.StreamSnapshot "WebSocket 升级日志流"
// @Failure 401 {object} response.Any "未登录"
// @Failure 403 {object} response.Any "无管理员权限"
// @Router /api/v1/d/update/logs/ws [get]
func StreamServerUpgradeLogsHandler(c *gin.Context) {
conn, err := upgradeLogsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer func() {
_ = conn.Close()
}()
updates, unsubscribe := SubscribeUpgradeStream()
defer unsubscribe()
heartbeatTicker := time.NewTicker(15 * time.Second)
defer heartbeatTicker.Stop()
for {
select {
case snapshot, ok := <-updates:
if !ok {
return
}
if err := conn.WriteJSON(snapshot); err != nil {
return
}
case <-heartbeatTicker.C:
if err := conn.WriteJSON(StreamSnapshot{}); err != nil {
return
}
case <-c.Request.Context().Done():
return
}
}
}
func bindOptionalJSON(body io.Reader, target any) error {
if err := json.NewDecoder(body).Decode(target); err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
}