mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
migrate
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
// 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:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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/compat"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgradeLogsUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(_ *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// GetLatestReleaseHandler returns the newest GitHub release for the legacy update UI.
|
||||
func GetLatestReleaseHandler(c *gin.Context) {
|
||||
release, err := GetLatestRelease(c.Request.Context(), c.Query("channel"))
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
compat.OK(c, release)
|
||||
}
|
||||
|
||||
// UpgradeServerHandler schedules an automatic upgrade from the latest release.
|
||||
func UpgradeServerHandler(c *gin.Context) {
|
||||
var request upgradeRequest
|
||||
if err := bindOptionalJSON(c.Request.Body, &request); err != nil {
|
||||
compat.Fail(c, "无效的参数")
|
||||
return
|
||||
}
|
||||
|
||||
release, err := ScheduleUpgrade(c.Request.Context(), request.Channel)
|
||||
if err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
okWithMessage(c, release, "服务升级任务已启动,下载完成后将自动重启。")
|
||||
}
|
||||
|
||||
// UploadManualServerBinaryHandler rejects manual uploads (feature disabled upstream).
|
||||
func UploadManualServerBinaryHandler(c *gin.Context) {
|
||||
if err := UploadManualBinary(); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ConfirmManualServerUpgradeHandler rejects manual upgrades (feature disabled upstream).
|
||||
func ConfirmManualServerUpgradeHandler(c *gin.Context) {
|
||||
if err := ConfirmManualUpgrade(); err != nil {
|
||||
compat.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// StreamServerUpgradeLogsHandler streams upgrade progress snapshots over WebSocket.
|
||||
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 okWithMessage(c *gin.Context, data any, message string) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": message,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user