mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 08:36:37 +08:00
修复 Pages 路由发布失败并报 pages module is not available:配置快照发布流程补齐 Pages 项目激活部署解析与 pages_deployment 写入。
This commit is contained in:
@@ -22,6 +22,8 @@ sidebar: false
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 修复 Pages 路由发布失败并报 `pages module is not available`:配置快照发布流程补齐 Pages 项目激活部署解析与 `pages_deployment` 写入。
|
||||||
|
|
||||||
- 修复仪表盘与节点详情「24 小时网络趋势」误按速率展示:改为 OpenResty 入/出站小时流量与近 24 小时总量摘要,Y 轴与 tooltip 自动换算 B/KB/MB/GB。
|
- 修复仪表盘与节点详情「24 小时网络趋势」误按速率展示:改为 OpenResty 入/出站小时流量与近 24 小时总量摘要,Y 轴与 tooltip 自动换算 B/KB/MB/GB。
|
||||||
|
|
||||||
- 修复 Pages 上传或节点同步时报 `pages file size out of bounds`:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。
|
- 修复 Pages 上传或节点同步时报 `pages file size out of bounds`:允许 ZIP 包内的 0 字节文件,并兼容未声明解压大小的 ZIP 条目。
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -398,7 +399,26 @@ func snapshotRouteIdentityEqual(left, right snapshotRoute) bool {
|
|||||||
func snapshotRouteOriginEqual(left, right snapshotRoute) bool {
|
func snapshotRouteOriginEqual(left, right snapshotRoute) bool {
|
||||||
return left.OriginURL == right.OriginURL &&
|
return left.OriginURL == right.OriginURL &&
|
||||||
left.OriginHost == right.OriginHost &&
|
left.OriginHost == right.OriginHost &&
|
||||||
left.UpstreamType == right.UpstreamType
|
left.UpstreamType == right.UpstreamType &&
|
||||||
|
snapshotPagesDeploymentEqual(left.PagesDeployment, right.PagesDeployment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotPagesDeploymentEqual(left, right *openrestyrender.PagesDeployment) bool {
|
||||||
|
if left == nil && right == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if left == nil || right == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
leftJSON, err := json.Marshal(left)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rightJSON, err := json.Marshal(right)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return string(leftJSON) == string(rightJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
func snapshotRoutePolicyEqual(left, right snapshotRoute) bool {
|
func snapshotRoutePolicyEqual(left, right snapshotRoute) bool {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package config_version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPagesSnapshotEntryFile = "index.html"
|
||||||
|
const defaultPagesSnapshotFallbackPath = "/index.html"
|
||||||
|
|
||||||
|
func buildPagesRouteSnapshot(
|
||||||
|
ctx context.Context,
|
||||||
|
route *model.ProxyRoute,
|
||||||
|
) (originURL string, upstreams []string, pagesProjectID *uint, deployment *openrestyrender.PagesDeployment, err error) {
|
||||||
|
if route == nil {
|
||||||
|
return "", nil, nil, nil, errors.New("pages 路由配置无效")
|
||||||
|
}
|
||||||
|
if !model.HasPagesProjectsTable(ctx) {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 模块不可用", route.Domain)
|
||||||
|
}
|
||||||
|
if route.PagesProjectID == nil || *route.PagesProjectID == 0 {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: 未绑定 Pages 项目", route.Domain)
|
||||||
|
}
|
||||||
|
project, err := model.GetPagesProjectByID(ctx, *route.PagesProjectID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目不存在", route.Domain)
|
||||||
|
}
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if !project.Enabled {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目未启用", route.Domain)
|
||||||
|
}
|
||||||
|
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID == 0 {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 项目没有激活部署", route.Domain)
|
||||||
|
}
|
||||||
|
activeDeployment, err := model.GetPagesDeploymentByID(ctx, *project.ActiveDeploymentID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不存在", route.Domain)
|
||||||
|
}
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if activeDeployment.ProjectID != project.ID {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 激活部署不匹配", route.Domain)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(activeDeployment.Checksum) == "" {
|
||||||
|
return "", nil, nil, nil, fmt.Errorf("路由 %s Pages 配置无效: pages 部署校验和缺失", route.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
pagesProjectID = route.PagesProjectID
|
||||||
|
deployment = buildSnapshotPagesDeployment(project, activeDeployment)
|
||||||
|
originURL = fmt.Sprintf("openflare-pages://project/%d", project.ID)
|
||||||
|
return originURL, []string{originURL}, pagesProjectID, deployment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSnapshotPagesDeployment(project *model.PagesProject, activeDeployment *model.PagesDeployment) *openrestyrender.PagesDeployment {
|
||||||
|
if project == nil || activeDeployment == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
entryFile := strings.TrimSpace(project.EntryFile)
|
||||||
|
if entryFile == "" {
|
||||||
|
entryFile = defaultPagesSnapshotEntryFile
|
||||||
|
}
|
||||||
|
fallbackPath := strings.TrimSpace(project.SPAFallbackPath)
|
||||||
|
if fallbackPath == "" {
|
||||||
|
fallbackPath = defaultPagesSnapshotFallbackPath
|
||||||
|
}
|
||||||
|
return &openrestyrender.PagesDeployment{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
ProjectSlug: strings.TrimSpace(project.Slug),
|
||||||
|
DeploymentID: activeDeployment.ID,
|
||||||
|
DeploymentNumber: activeDeployment.DeploymentNumber,
|
||||||
|
Checksum: strings.TrimSpace(activeDeployment.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: fmt.Sprintf(
|
||||||
|
"%s/deployments/%d/current",
|
||||||
|
openrestyrender.PagesDirPlaceholder,
|
||||||
|
activeDeployment.ID,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright 2026 Arctel.net
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package config_version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
|
openrestyrender "github.com/Rain-kl/Wavelet/pkg/render/openresty"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildSnapshotRoutesPages(t *testing.T) {
|
||||||
|
cleanup := setupConfigVersionTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
ctx := context.Background()
|
||||||
|
conn := requireDB(t, ctx)
|
||||||
|
|
||||||
|
project := &model.PagesProject{
|
||||||
|
Name: "Speed Test",
|
||||||
|
Slug: "speedtest",
|
||||||
|
Enabled: true,
|
||||||
|
SPAFallbackEnabled: true,
|
||||||
|
SPAFallbackPath: "/index.html",
|
||||||
|
EntryFile: "index.html",
|
||||||
|
}
|
||||||
|
require.NoError(t, conn.Create(project).Error)
|
||||||
|
|
||||||
|
deployment := &model.PagesDeployment{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
DeploymentNumber: 1,
|
||||||
|
Checksum: "abc123checksum",
|
||||||
|
Status: model.PagesDeploymentStatusActive,
|
||||||
|
FileCount: 1,
|
||||||
|
TotalSize: 12,
|
||||||
|
}
|
||||||
|
require.NoError(t, conn.Create(deployment).Error)
|
||||||
|
require.NoError(t, conn.Model(project).Update("active_deployment_id", deployment.ID).Error)
|
||||||
|
|
||||||
|
route := &model.ProxyRoute{
|
||||||
|
SiteName: "speedtest",
|
||||||
|
Domain: "speedtest.arctel.net",
|
||||||
|
Domains: `["speedtest.arctel.net"]`,
|
||||||
|
OriginURL: "openflare-pages://project/1",
|
||||||
|
Upstreams: `["openflare-pages://project/1"]`,
|
||||||
|
Enabled: true,
|
||||||
|
UpstreamType: "pages",
|
||||||
|
PagesProjectID: &project.ID,
|
||||||
|
}
|
||||||
|
require.NoError(t, model.CreateProxyRouteRecord(ctx, route))
|
||||||
|
|
||||||
|
bundle, err := buildCurrentConfigBundle(ctx, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, bundle.SnapshotRoutes, 1)
|
||||||
|
|
||||||
|
snapshotRoute := bundle.SnapshotRoutes[0]
|
||||||
|
assert.Equal(t, "pages", snapshotRoute.UpstreamType)
|
||||||
|
assert.Equal(t, "openflare-pages://project/1", snapshotRoute.OriginURL)
|
||||||
|
require.NotNil(t, snapshotRoute.PagesDeployment)
|
||||||
|
assert.Equal(t, deployment.ID, snapshotRoute.PagesDeployment.DeploymentID)
|
||||||
|
assert.Equal(t, deployment.Checksum, snapshotRoute.PagesDeployment.Checksum)
|
||||||
|
assert.Equal(t, "__OPENFLARE_PAGES_DIR__/deployments/1/current", snapshotRoute.PagesDeployment.LocalRoot)
|
||||||
|
|
||||||
|
_, err = renderSnapshotConfig(bundle.SnapshotJSON, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
Routes []struct {
|
||||||
|
PagesDeployment *openrestyrender.PagesDeployment `json:"pages_deployment"`
|
||||||
|
} `json:"routes"`
|
||||||
|
}
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(bundle.SnapshotJSON), &decoded))
|
||||||
|
require.NotNil(t, decoded.Routes[0].PagesDeployment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireDB(t *testing.T, ctx context.Context) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
conn := db.DB(ctx)
|
||||||
|
require.NotNil(t, conn)
|
||||||
|
require.NoError(t, conn.AutoMigrate(
|
||||||
|
&model.PagesProject{},
|
||||||
|
&model.PagesDeployment{},
|
||||||
|
))
|
||||||
|
return conn
|
||||||
|
}
|
||||||
@@ -20,35 +20,36 @@ import (
|
|||||||
const supportFilesPerCertificate = 2
|
const supportFilesPerCertificate = 2
|
||||||
|
|
||||||
type snapshotRoute struct {
|
type snapshotRoute struct {
|
||||||
ID uint `json:"id,omitempty"`
|
ID uint `json:"id,omitempty"`
|
||||||
SiteName string `json:"site_name,omitempty"`
|
SiteName string `json:"site_name,omitempty"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Domains []string `json:"domains,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
OriginURL string `json:"origin_url"`
|
OriginURL string `json:"origin_url"`
|
||||||
OriginHost string `json:"origin_host,omitempty"`
|
OriginHost string `json:"origin_host,omitempty"`
|
||||||
Upstreams []string `json:"upstreams,omitempty"`
|
Upstreams []string `json:"upstreams,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
EnableHTTPS bool `json:"enable_https"`
|
EnableHTTPS bool `json:"enable_https"`
|
||||||
CertID *uint `json:"cert_id,omitempty"`
|
CertID *uint `json:"cert_id,omitempty"`
|
||||||
CertIDs []uint `json:"cert_ids,omitempty"`
|
CertIDs []uint `json:"cert_ids,omitempty"`
|
||||||
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
DomainCertIDs []uint `json:"domain_cert_ids,omitempty"`
|
||||||
RedirectHTTP bool `json:"redirect_http"`
|
RedirectHTTP bool `json:"redirect_http"`
|
||||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
||||||
LimitRate string `json:"limit_rate,omitempty"`
|
LimitRate string `json:"limit_rate,omitempty"`
|
||||||
CacheEnabled bool `json:"cache_enabled"`
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
CachePolicy string `json:"cache_policy,omitempty"`
|
CachePolicy string `json:"cache_policy,omitempty"`
|
||||||
CacheRules []string `json:"cache_rules,omitempty"`
|
CacheRules []string `json:"cache_rules,omitempty"`
|
||||||
CustomHeaders []customHeaderInput `json:"custom_headers,omitempty"`
|
CustomHeaders []customHeaderInput `json:"custom_headers,omitempty"`
|
||||||
BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"`
|
BasicAuthEnabled bool `json:"basic_auth_enabled,omitempty"`
|
||||||
BasicAuthUsername string `json:"basic_auth_username,omitempty"`
|
BasicAuthUsername string `json:"basic_auth_username,omitempty"`
|
||||||
BasicAuthPassword string `json:"basic_auth_password,omitempty"`
|
BasicAuthPassword string `json:"basic_auth_password,omitempty"`
|
||||||
Remark string `json:"remark,omitempty"`
|
Remark string `json:"remark,omitempty"`
|
||||||
UpstreamType string `json:"upstream_type,omitempty"`
|
UpstreamType string `json:"upstream_type,omitempty"`
|
||||||
TunnelNodeID *uint `json:"tunnel_node_id,omitempty"`
|
TunnelNodeID *uint `json:"tunnel_node_id,omitempty"`
|
||||||
TunnelTargetAddr string `json:"tunnel_target_addr,omitempty"`
|
TunnelTargetAddr string `json:"tunnel_target_addr,omitempty"`
|
||||||
TunnelTargetProto string `json:"tunnel_target_protocol,omitempty"`
|
TunnelTargetProto string `json:"tunnel_target_protocol,omitempty"`
|
||||||
PagesProjectID *uint `json:"pages_project_id,omitempty"`
|
PagesProjectID *uint `json:"pages_project_id,omitempty"`
|
||||||
|
PagesDeployment *openrestyrender.PagesDeployment `json:"pages_deployment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type snapshotWAFRuleGroup struct {
|
type snapshotWAFRuleGroup struct {
|
||||||
@@ -230,6 +231,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
|||||||
var tunnelTargetAddr string
|
var tunnelTargetAddr string
|
||||||
var tunnelTargetProtocol string
|
var tunnelTargetProtocol string
|
||||||
var pagesProjectID *uint
|
var pagesProjectID *uint
|
||||||
|
var pagesDeployment *openrestyrender.PagesDeployment
|
||||||
switch upstreamType {
|
switch upstreamType {
|
||||||
case "tunnel":
|
case "tunnel":
|
||||||
originURL = resolveTunnelOpenRestyUpstreamURL(ctx)
|
originURL = resolveTunnelOpenRestyUpstreamURL(ctx)
|
||||||
@@ -238,7 +240,10 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
|||||||
tunnelTargetAddr = strings.TrimSpace(route.TunnelTargetAddr)
|
tunnelTargetAddr = strings.TrimSpace(route.TunnelTargetAddr)
|
||||||
tunnelTargetProtocol = normalizeTunnelTargetProtocol(route.TunnelTargetProtocol)
|
tunnelTargetProtocol = normalizeTunnelTargetProtocol(route.TunnelTargetProtocol)
|
||||||
case "pages":
|
case "pages":
|
||||||
return nil, fmt.Errorf("路由 %s Pages 配置无效: pages module is not available", route.Domain)
|
originURL, upstreams, pagesProjectID, pagesDeployment, err = buildPagesRouteSnapshot(ctx, route)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
cacheRules, err := decodeStoredCacheRules(route.CacheRules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,6 +279,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
|||||||
TunnelTargetAddr: tunnelTargetAddr,
|
TunnelTargetAddr: tunnelTargetAddr,
|
||||||
TunnelTargetProto: tunnelTargetProtocol,
|
TunnelTargetProto: tunnelTargetProtocol,
|
||||||
PagesProjectID: pagesProjectID,
|
PagesProjectID: pagesProjectID,
|
||||||
|
PagesDeployment: pagesDeployment,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
|
|||||||
@@ -45,14 +45,8 @@ var (
|
|||||||
GitHubOAuthEnabled = false
|
GitHubOAuthEnabled = false
|
||||||
WeChatAuthEnabled = false
|
WeChatAuthEnabled = false
|
||||||
GitHubClientID = ""
|
GitHubClientID = ""
|
||||||
GitHubClientSecret = ""
|
|
||||||
WeChatServerAddress = ""
|
|
||||||
WeChatServerToken = ""
|
|
||||||
WeChatAccountQRCodeImageURL = ""
|
WeChatAccountQRCodeImageURL = ""
|
||||||
SMTPServer = ""
|
|
||||||
SMTPPort = 587
|
SMTPPort = 587
|
||||||
SMTPAccount = ""
|
|
||||||
SMTPToken = ""
|
|
||||||
AgentDiscoveryToken = ""
|
AgentDiscoveryToken = ""
|
||||||
AgentHeartbeatInterval = 10000
|
AgentHeartbeatInterval = 10000
|
||||||
AgentWebsocketUpgradeEnabled = true
|
AgentWebsocketUpgradeEnabled = true
|
||||||
|
|||||||
Reference in New Issue
Block a user