mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 07:26:36 +08:00
merge: remove legacy openflare update API
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package openflare
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/apiutil"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/update"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func registerUpdateRoutes(apiGroup *gin.RouterGroup) {
|
||||
updateRoute := apiGroup.Group("/update")
|
||||
updateRoute.Use(apiutil.AdminMiddlewares()...)
|
||||
{
|
||||
updateRoute.GET("/latest-release", update.GetLatestReleaseHandler)
|
||||
updateRoute.GET("/logs/ws", update.StreamServerUpgradeLogsHandler)
|
||||
updateRoute.POST("/manual-upload", update.UploadManualServerBinaryHandler)
|
||||
updateRoute.POST("/manual-upgrade", update.ConfirmManualServerUpgradeHandler)
|
||||
updateRoute.POST("/upgrade", update.UpgradeServerHandler)
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,4 @@ func RegisterV1Routes(apiV1Router *gin.RouterGroup) {
|
||||
registerPagesRoutes(group)
|
||||
registerDashboardRoutes(group)
|
||||
registerObservabilityRoutes(group)
|
||||
registerUpdateRoutes(group)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user