feat(deploy): 在线更新 - 新增后台检查、强制备份迁移与失败自动回退

This commit is contained in:
ddCat
2026-08-30 22:15:17 +08:00
parent 8227921c68
commit 694cb974fb
60 changed files with 4124 additions and 93 deletions

View File

@@ -0,0 +1,89 @@
package updaterclient
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"infinite-canvas/backend/internal/hostupdate"
)
type Client struct {
baseURL string
token string
http *http.Client
}
func New(socketPath, token string) *Client {
dialer := &net.Dialer{Timeout: 3 * time.Second}
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialContext(ctx, "unix", socketPath)
},
DisableKeepAlives: true,
}
return &Client{baseURL: "http://unix", token: strings.TrimSpace(token), http: &http.Client{Transport: transport, Timeout: 15 * time.Second}}
}
func (c *Client) Status(ctx context.Context) (hostupdate.Status, error) {
return c.request(ctx, http.MethodGet, "/v1/status", nil)
}
func (c *Client) Check(ctx context.Context) (hostupdate.Status, error) {
return c.request(ctx, http.MethodPost, "/v1/check", struct{}{})
}
func (c *Client) Start(ctx context.Context, targetVersion string) (hostupdate.Status, error) {
return c.request(ctx, http.MethodPost, "/v1/update", hostupdate.StartRequest{TargetVersion: targetVersion})
}
func (c *Client) Rollback(ctx context.Context, reason string) (hostupdate.Status, error) {
return c.request(ctx, http.MethodPost, "/v1/rollback", hostupdate.RollbackRequest{Reason: reason})
}
func (c *Client) request(ctx context.Context, method, path string, payload any) (hostupdate.Status, error) {
var body io.Reader
if payload != nil {
encoded, err := json.Marshal(payload)
if err != nil {
return hostupdate.Status{}, err
}
body = bytes.NewReader(encoded)
}
request, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
if err != nil {
return hostupdate.Status{}, err
}
request.Header.Set("Authorization", "Bearer "+c.token)
request.Header.Set("Accept", "application/json")
if payload != nil {
request.Header.Set("Content-Type", "application/json")
}
response, err := c.http.Do(request)
if err != nil {
return hostupdate.Status{}, fmt.Errorf("连接 Host Updater:%w", err)
}
defer response.Body.Close()
limited := io.LimitReader(response.Body, 2<<20)
if response.StatusCode < 200 || response.StatusCode >= 300 {
var failure struct {
Error string `json:"error"`
Data hostupdate.Status `json:"data"`
}
if err := json.NewDecoder(limited).Decode(&failure); err == nil && failure.Error != "" {
return failure.Data, fmt.Errorf("Host Updater:%s", failure.Error)
}
return hostupdate.Status{}, fmt.Errorf("Host Updater 返回 HTTP %d", response.StatusCode)
}
var status hostupdate.Status
if err := json.NewDecoder(limited).Decode(&status); err != nil {
return hostupdate.Status{}, fmt.Errorf("解析 Host Updater 响应:%w", err)
}
return status, nil
}