mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
migrate
This commit is contained in:
BIN
Binary file not shown.
@@ -4,20 +4,23 @@
|
||||
package pages
|
||||
|
||||
const (
|
||||
errPagesProjectNotFound = "Pages 项目不存在"
|
||||
errPagesSlugExists = "Pages 项目标识已存在"
|
||||
errPagesNameRequired = "Pages 项目名称不能为空"
|
||||
errPagesSlugInvalid = "Pages 项目标识只能包含小写字母、数字和连字符"
|
||||
errPagesDeleteReferenced = "Pages 项目已被规则引用,不能删除"
|
||||
errPagesDeploymentNotFound = "Pages 部署不存在"
|
||||
errPagesDeploymentMismatch = "Pages 部署不属于该项目"
|
||||
errPagesDeleteActiveDeploy = "不能删除当前激活的 Pages 部署"
|
||||
errPagesPackageMissing = "缺少 Pages 部署包"
|
||||
errPagesPackageNotZip = "Pages 部署包必须是 .zip 文件"
|
||||
errPagesPackageInvalidZip = "Pages 部署包不是有效 zip 文件"
|
||||
errPagesPackageEmpty = "Pages 部署包不能为空"
|
||||
errPagesAPIProxyPathRequired = "启用 API 反代时,匹配路径不能为空"
|
||||
errPagesAPIProxyPathPrefix = "API 反代匹配路径必须以 '/' 开头"
|
||||
errPagesAPIProxyPassRequired = "启用 API 反代时,后端服务地址不能为空"
|
||||
errPagesAPIProxyPassInvalid = "API 反代后端服务地址必须是有效的 HTTP/HTTPS URL"
|
||||
errPagesProjectNotFound = "Pages 项目不存在"
|
||||
errPagesSlugExists = "Pages 项目标识已存在"
|
||||
errPagesNameRequired = "Pages 项目名称不能为空"
|
||||
errPagesSlugInvalid = "Pages 项目标识只能包含小写字母、数字和连字符"
|
||||
errPagesDeleteReferenced = "Pages 项目已被规则引用,不能删除"
|
||||
errPagesDeploymentNotFound = "Pages 部署不存在"
|
||||
errPagesDeploymentMismatch = "Pages 部署不属于该项目"
|
||||
errPagesDeleteActiveDeploy = "不能删除当前激活的 Pages 部署"
|
||||
errPagesPackageMissing = "缺少 Pages 部署包"
|
||||
errPagesPackageNotZip = "Pages 部署包必须是 .zip 文件"
|
||||
errPagesPackageInvalidZip = "Pages 部署包不是有效 zip 文件"
|
||||
errPagesPackageEmpty = "Pages 部署包不能为空"
|
||||
errPagesAPIProxyPathRequired = "启用 API 反代时,匹配路径不能为空"
|
||||
errPagesAPIProxyPathPrefix = "API 反代匹配路径必须以 '/' 开头"
|
||||
errPagesAPIProxyPassRequired = "启用 API 反代时,后端服务地址不能为空"
|
||||
errPagesAPIProxyPassInvalid = "API 反代后端服务地址必须是有效的 HTTP/HTTPS URL"
|
||||
errPagesPackagePathEmpty = "Pages 部署包路径为空"
|
||||
errPagesPackageNotInActiveConfig = "Pages 部署尚未进入激活配置"
|
||||
errPagesInvalidSnapshotFormat = "配置快照格式无效"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ package pages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
@@ -341,6 +342,77 @@ func ActivateDeployment(ctx context.Context, projectID uint, deploymentID uint)
|
||||
return GetProject(ctx, project.ID)
|
||||
}
|
||||
|
||||
// GetDeploymentPackagePath returns the on-disk artifact path and download filename for an agent package request.
|
||||
func GetDeploymentPackagePath(ctx context.Context, deploymentID uint) (string, string, error) {
|
||||
deployment, err := model.GetPagesDeploymentByID(ctx, deploymentID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if err = ensureDeploymentInActiveSnapshot(ctx, deployment.ID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if strings.TrimSpace(deployment.ArtifactPath) == "" {
|
||||
return "", "", errors.New(errPagesPackagePathEmpty)
|
||||
}
|
||||
if _, err = os.Stat(deployment.ArtifactPath); err != nil {
|
||||
return "", "", fmt.Errorf("Pages 部署包不存在: %w", err)
|
||||
}
|
||||
return deployment.ArtifactPath, fmt.Sprintf("pages-deployment-%d.zip", deployment.ID), nil
|
||||
}
|
||||
|
||||
func ensureDeploymentInActiveSnapshot(ctx context.Context, deploymentID uint) error {
|
||||
version, err := model.GetActiveConfigVersion(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(errPagesPackageNotInActiveConfig)
|
||||
}
|
||||
return err
|
||||
}
|
||||
routes, err := parseSnapshotRoutes(version.SnapshotJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, route := range routes {
|
||||
if route.UpstreamType != "pages" || route.PagesDeployment == nil {
|
||||
continue
|
||||
}
|
||||
if route.PagesDeployment.DeploymentID == deploymentID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New(errPagesPackageNotInActiveConfig)
|
||||
}
|
||||
|
||||
type snapshotPagesDeployment struct {
|
||||
DeploymentID uint `json:"deployment_id"`
|
||||
}
|
||||
|
||||
type snapshotRouteRef struct {
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
PagesDeployment *snapshotPagesDeployment `json:"pages_deployment"`
|
||||
}
|
||||
|
||||
func parseSnapshotRoutes(snapshotJSON string) ([]snapshotRouteRef, error) {
|
||||
text := strings.TrimSpace(snapshotJSON)
|
||||
if text == "" {
|
||||
return []snapshotRouteRef{}, nil
|
||||
}
|
||||
if strings.HasPrefix(text, "[") {
|
||||
var routes []snapshotRouteRef
|
||||
if err := json.Unmarshal([]byte(text), &routes); err != nil {
|
||||
return nil, errors.New(errPagesInvalidSnapshotFormat)
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
var snapshot struct {
|
||||
Routes []snapshotRouteRef `json:"routes"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(text), &snapshot); err != nil {
|
||||
return nil, errors.New(errPagesInvalidSnapshotFormat)
|
||||
}
|
||||
return snapshot.Routes, nil
|
||||
}
|
||||
|
||||
// DeleteDeployment 删除 Pages 部署。
|
||||
func DeleteDeployment(ctx context.Context, projectID uint, deploymentID uint) error {
|
||||
project, err := model.GetPagesProjectByID(ctx, projectID)
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
@@ -26,6 +32,7 @@ func setupPagesTestDB(t *testing.T) func() {
|
||||
&model.PagesProject{},
|
||||
&model.PagesDeployment{},
|
||||
&model.PagesDeploymentFile{},
|
||||
&model.ConfigVersion{},
|
||||
))
|
||||
|
||||
db.SetDB(sqliteDB)
|
||||
@@ -82,3 +89,80 @@ func TestCreateProjectRejectsUnsafeFallbackPath(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "回退路径")
|
||||
}
|
||||
|
||||
func TestGetDeploymentPackagePathRequiresActiveConfigSnapshot(t *testing.T) {
|
||||
cleanup := setupPagesTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
project, err := CreateProject(ctx, Input{
|
||||
Name: "Published Site",
|
||||
Slug: "published-site",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"index.html": "ok",
|
||||
})), "root")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ActivateDeployment(ctx, project.ID, deployment.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = GetDeploymentPackagePath(ctx, deployment.ID)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "激活配置")
|
||||
|
||||
require.NoError(t, db.DB(ctx).Create(&model.ConfigVersion{
|
||||
Version: "v2026-001",
|
||||
SnapshotJSON: fmt.Sprintf(`{"routes":[{"upstream_type":"pages","pages_deployment":{"deployment_id":%d}}]}`, deployment.ID),
|
||||
MainConfig: "",
|
||||
RenderedConfig: "",
|
||||
SupportFilesJSON: "[]",
|
||||
Checksum: "test-checksum",
|
||||
IsActive: true,
|
||||
CreatedBy: "test",
|
||||
}).Error)
|
||||
|
||||
filePath, fileName, err := GetDeploymentPackagePath(ctx, deployment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, filePath)
|
||||
assert.Equal(t, "pages-deployment-"+strconv.FormatUint(uint64(deployment.ID), 10)+".zip", fileName)
|
||||
}
|
||||
|
||||
func testPagesZip(t *testing.T, files map[string]string) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
writer := zip.NewWriter(&buffer)
|
||||
for name, content := range files {
|
||||
file, err := writer.Create(name)
|
||||
require.NoError(t, err)
|
||||
_, err = file.Write([]byte(content))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, writer.Close())
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func testPagesMultipartFile(t *testing.T, fileName string, content []byte) *multipart.FileHeader {
|
||||
t.Helper()
|
||||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile("package", fileName)
|
||||
require.NoError(t, err)
|
||||
_, err = part.Write(content)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
req := httptest.NewRequest("POST", "/", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
require.NoError(t, req.ParseMultipartForm(int64(len(content))+1024))
|
||||
|
||||
file, header, err := req.FormFile("package")
|
||||
require.NoError(t, err)
|
||||
file.Close()
|
||||
return header
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user