Files
OpenFlare/internal/apps/openflare/pages/rebind.go
T
ryan 699e95f12c perfsprint 18→0:strconv.Itoa/FormatInt/FormatUint/FormatBool 替代 fmt.Sprintf、无动词 fmt.Errorf→errors.New、纯字符串拼接。全部语义等价(已核对 diff)。修正 fixer 遗留的 import 问题(引入 goimports 统一整理)。
Result: {"status":"keep","total_issues":56,"golint_canonicalheader":8,"golint_errname":1,"golint_errorlint":12,"golint_forcetypeassert":3,"golint_gosec":0,"golint_intrange":3,"golint_modernize":5,"golint_nilnil":3,"golint_perfsprint":0,"golint_prealloc":3,"golint_recvcheck":7,"golint_usestdlibvars":3,"golint_wastedassign":7,"golint_total":55,"eslint_problems":1,"eslint_errors":0,"eslint_warnings":1,"tsc_errors":0,"measure_s":47}
2026-08-16 17:34:44 +08:00

251 lines
7.5 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package pages
import (
"context"
"encoding/json"
"errors"
"fmt"
"path"
"strings"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/model"
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
"gorm.io/gorm"
)
// RebindSnapshotPagesToCurrentActive rewrites pages_deployment fields so every
// pages route points at the project's current active deployment.
//
// Main config versions and Pages deployments are independent. Rolling back a
// main config version must not require old Pages packages; Agents always follow
// the live active deployment for each referenced project.
//
// Returns the original JSON unchanged when there are no pages routes. Does not
// mutate stored config_versions rows. Non-pages route fields are preserved.
func RebindSnapshotPagesToCurrentActive(ctx context.Context, snapshotJSON string) (string, error) {
text := strings.TrimSpace(snapshotJSON)
if text == "" {
return snapshotJSON, nil
}
if strings.HasPrefix(text, "[") {
var routes []map[string]json.RawMessage
if err := json.Unmarshal([]byte(text), &routes); err != nil {
return "", fmt.Errorf("parse pages snapshot routes: %w", err)
}
changed, err := rebindPagesRouteMaps(ctx, routes)
if err != nil {
return "", err
}
if !changed {
return snapshotJSON, nil
}
encoded, err := json.Marshal(routes)
if err != nil {
return "", err
}
return string(encoded), nil
}
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(text), &raw); err != nil {
return "", fmt.Errorf("parse pages snapshot document: %w", err)
}
routesRaw, ok := raw["routes"]
if !ok || len(routesRaw) == 0 {
return snapshotJSON, nil
}
var routes []map[string]json.RawMessage
if err := json.Unmarshal(routesRaw, &routes); err != nil {
return "", fmt.Errorf("parse pages snapshot routes: %w", err)
}
changed, err := rebindPagesRouteMaps(ctx, routes)
if err != nil {
return "", err
}
if !changed {
return snapshotJSON, nil
}
encodedRoutes, err := json.Marshal(routes)
if err != nil {
return "", err
}
raw["routes"] = encodedRoutes
encoded, err := json.Marshal(raw)
if err != nil {
return "", err
}
return string(encoded), nil
}
func rebindPagesRouteMaps(ctx context.Context, routes []map[string]json.RawMessage) (bool, error) {
changed := false
for index := range routes {
route := routes[index]
if route == nil {
continue
}
upstreamType, _ := rawJSONString(route["upstream_type"])
if !strings.EqualFold(strings.TrimSpace(upstreamType), "pages") {
continue
}
siteName, _ := rawJSONString(route["site_name"])
projectID, err := resolveProjectIDFromRouteMap(route)
if err != nil {
if siteName == "" {
siteName = "pages"
}
return false, fmt.Errorf("路由 %s %w", siteName, err)
}
project, activeDeployment, err := loadActivePagesProject(ctx, projectID, siteName)
if err != nil {
return false, err
}
deployment, err := buildLivePagesDeployment(project, activeDeployment)
if err != nil {
return false, err
}
projectIDCopy := project.ID
originURL := fmt.Sprintf("openflare-pages://project/%d", project.ID)
if err := putJSON(route, "pages_project_id", projectIDCopy); err != nil {
return false, err
}
if err := putJSON(route, "pages_deployment", deployment); err != nil {
return false, err
}
if err := putJSON(route, "origin_url", originURL); err != nil {
return false, err
}
if err := putJSON(route, "upstreams", []string{originURL}); err != nil {
return false, err
}
routes[index] = route
changed = true
}
return changed, nil
}
const jsonNullLiteral = "null"
func isPresentJSON(raw json.RawMessage) bool {
return len(raw) > 0 && string(raw) != jsonNullLiteral
}
func resolveProjectIDFromRouteMap(route map[string]json.RawMessage) (uint, error) {
if raw, ok := route["pages_project_id"]; ok && isPresentJSON(raw) {
var projectID uint
if err := json.Unmarshal(raw, &projectID); err == nil && projectID != 0 {
return projectID, nil
}
}
if raw, ok := route["pages_deployment"]; ok && isPresentJSON(raw) {
var deployment struct {
ProjectID uint `json:"project_id"`
}
if err := json.Unmarshal(raw, &deployment); err == nil && deployment.ProjectID != 0 {
return deployment.ProjectID, nil
}
}
return 0, errors.New("pages 配置无效: 缺少 pages_project_id")
}
func loadActivePagesProject(ctx context.Context, projectID uint, siteName string) (*model.PagesProject, *model.PagesDeployment, error) {
if siteName == "" {
siteName = "pages"
}
project, err := repository.GetPagesProjectByID(ctx, projectID)
if err != nil {
if errorsIsNotFound(err) {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", siteName)
}
return nil, nil, err
}
if !project.Enabled {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目未启用", siteName)
}
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", siteName)
}
activeDeployment, err := repository.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
if err != nil {
if errorsIsNotFound(err) {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", siteName)
}
return nil, nil, err
}
if activeDeployment.ProjectID != project.ID {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不匹配", siteName)
}
if strings.TrimSpace(activeDeployment.Checksum) == "" {
return nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 部署校验和缺失", siteName)
}
return project, activeDeployment, nil
}
func buildLivePagesDeployment(
project *model.PagesProject,
active *model.PagesDeployment,
) (*openrestyrender.PagesDeployment, error) {
rootDir, err := validateAndNormalizePagesRootDir(project.RootDir)
if err != nil {
return nil, err
}
entryFile, err := validateAndNormalizePagesEntryFile(project.EntryFile)
if err != nil {
return nil, err
}
fallbackPath := strings.TrimSpace(project.SPAFallbackPath)
if fallbackPath == "" {
fallbackPath = defaultPagesFallbackPath
}
localRoot := openrestyrender.PagesProjectLocalRoot(project.ID)
if rootDir != "" {
localRoot = path.Join(localRoot, rootDir)
}
return &openrestyrender.PagesDeployment{
ProjectID: project.ID,
ProjectSlug: strings.TrimSpace(project.Slug),
DeploymentID: active.ID,
DeploymentNumber: active.DeploymentNumber,
Checksum: strings.TrimSpace(active.Checksum),
EntryFile: entryFile,
SPAFallbackEnabled: project.SPAFallbackEnabled,
SPAFallbackPath: fallbackPath,
APIProxyEnabled: project.APIProxyEnabled,
APIProxyPath: strings.TrimSpace(project.APIProxyPath),
APIProxyPass: strings.TrimSpace(project.APIProxyPass),
APIProxyRewrite: strings.TrimSpace(project.APIProxyRewrite),
LocalRoot: localRoot,
}, nil
}
func rawJSONString(raw json.RawMessage) (string, bool) {
if !isPresentJSON(raw) {
return "", false
}
var value string
if err := json.Unmarshal(raw, &value); err != nil {
return "", false
}
return value, true
}
func putJSON(route map[string]json.RawMessage, key string, value any) error {
encoded, err := json.Marshal(value)
if err != nil {
return err
}
route[key] = encoded
return nil
}
func errorsIsNotFound(err error) bool {
return err != nil && (err == gorm.ErrRecordNotFound || strings.Contains(strings.ToLower(err.Error()), "record not found"))
}