diff --git a/openflare_server/model/goose/goose_202606040002_add_pages_project_root_dir.go b/openflare_server/model/goose/goose_202606040002_add_pages_project_root_dir.go new file mode 100644 index 00000000..dd63df3b --- /dev/null +++ b/openflare_server/model/goose/goose_202606040002_add_pages_project_root_dir.go @@ -0,0 +1,43 @@ +package goose + +import ( + "fmt" + + presslygoose "github.com/pressly/goose/v3" + "gorm.io/gorm" +) + +const versionPagesProjectRootDir int64 = 202606040002 + +// migration202606040002 adds RootDir and EntryFile fields to Pages projects. +func migration202606040002(backend string, ctx Context) *presslygoose.Migration { + return newGORMMigration( + versionPagesProjectRootDir, + "202606040002_add_pages_project_root_dir.go", + backend, + ctx, + migratePagesProjectRootDir, + ) +} + +func migratePagesProjectRootDir(ctx Context, db *gorm.DB, backend string) error { + if err := ctx.ApplyCurrentSchema(db, backend); err != nil { + return err + } + // Verify that the columns exist + if !db.Migrator().HasColumn("pages_projects", "root_dir") { + return fmt.Errorf("column pages_projects.root_dir is missing") + } + if !db.Migrator().HasColumn("pages_projects", "entry_file") { + return fmt.Errorf("column pages_projects.entry_file is missing") + } + // Backfill existing rows: if entry_file is empty, set to 'index.html' + type PagesProject struct { + ID uint `gorm:"primaryKey"` + EntryFile string `gorm:"size:512;not null;default:'index.html'"` + } + if err := db.Model(&PagesProject{}).Where("entry_file = '' OR entry_file IS NULL").Update("entry_file", "index.html").Error; err != nil { + return fmt.Errorf("failed to backfill pages_projects.entry_file: %w", err) + } + return nil +} diff --git a/openflare_server/model/goose/migrations.go b/openflare_server/model/goose/migrations.go index de78b289..a32e208a 100644 --- a/openflare_server/model/goose/migrations.go +++ b/openflare_server/model/goose/migrations.go @@ -46,6 +46,7 @@ func registeredMigrations(backend string, ctx Context) []*presslygoose.Migration migration202606030003(backend, ctx), migration202606030004(backend, ctx), migration202606040001(backend, ctx), + migration202606040002(backend, ctx), } } diff --git a/openflare_server/model/pages.go b/openflare_server/model/pages.go index 75fb2731..5b792a28 100644 --- a/openflare_server/model/pages.go +++ b/openflare_server/model/pages.go @@ -20,6 +20,8 @@ type PagesProject struct { APIProxyPass string `json:"api_proxy_pass" gorm:"size:2048;not null;default:''"` APIProxyRewrite string `json:"api_proxy_rewrite" gorm:"size:255;not null;default:''"` ActiveDeploymentID *uint `json:"active_deployment_id" gorm:"index"` + RootDir string `json:"root_dir" gorm:"size:512;not null;default:''"` + EntryFile string `json:"entry_file" gorm:"size:512;not null;default:'index.html'"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/openflare_server/service/pages.go b/openflare_server/service/pages.go index f5673e58..74ae1ac6 100644 --- a/openflare_server/service/pages.go +++ b/openflare_server/service/pages.go @@ -41,6 +41,8 @@ type PagesProjectInput struct { APIProxyPath string `json:"api_proxy_path"` APIProxyPass string `json:"api_proxy_pass"` APIProxyRewrite string `json:"api_proxy_rewrite"` + RootDir string `json:"root_dir"` + EntryFile string `json:"entry_file"` } type PagesProjectView struct { @@ -55,6 +57,8 @@ type PagesProjectView struct { APIProxyPath string `json:"api_proxy_path"` APIProxyPass string `json:"api_proxy_pass"` APIProxyRewrite string `json:"api_proxy_rewrite"` + RootDir string `json:"root_dir"` + EntryFile string `json:"entry_file"` ActiveDeploymentID *uint `json:"active_deployment_id"` ActiveDeployment *PagesDeploymentView `json:"active_deployment,omitempty"` DeploymentCount int64 `json:"deployment_count"` @@ -151,6 +155,8 @@ func UpdatePagesProject(id uint, input PagesProjectInput) (*PagesProjectView, er "api_proxy_path": project.APIProxyPath, "api_proxy_pass": project.APIProxyPass, "api_proxy_rewrite": project.APIProxyRewrite, + "root_dir": project.RootDir, + "entry_file": project.EntryFile, }).Error; err != nil { if model.IsUniqueConstraintError(err) { return nil, errors.New("Pages 项目标识已存在") @@ -241,11 +247,11 @@ func UploadPagesDeployment(projectID uint, fileHeader *multipart.FileHeader, roo if !strings.EqualFold(filepath.Ext(fileHeader.Filename), ".zip") { return nil, errors.New("Pages 部署包必须是 .zip 文件") } - rootDir, err = validateAndNormalizePagesRootDir(rootDir) + rootDir, err = validateAndNormalizePagesRootDir(project.RootDir) if err != nil { return nil, err } - entryFile = normalizePagesEntryFile(entryFile) + entryFile = normalizePagesEntryFile(project.EntryFile) tempPath, checksum, err := persistPagesUploadTemp(fileHeader) if err != nil { return nil, err @@ -485,6 +491,13 @@ func buildPagesProject(project *model.PagesProject, input PagesProjectInput) (*m project.APIProxyPass = apiProxyPass project.APIProxyRewrite = apiProxyRewrite + rootDir, err := validateAndNormalizePagesRootDir(input.RootDir) + if err != nil { + return nil, err + } + project.RootDir = rootDir + project.EntryFile = normalizePagesEntryFile(input.EntryFile) + return project, nil } @@ -504,6 +517,8 @@ func buildPagesProjectView(project *model.PagesProject) (*PagesProjectView, erro APIProxyPath: project.APIProxyPath, APIProxyPass: project.APIProxyPass, APIProxyRewrite: project.APIProxyRewrite, + RootDir: project.RootDir, + EntryFile: project.EntryFile, ActiveDeploymentID: project.ActiveDeploymentID, CreatedAt: project.CreatedAt, UpdatedAt: project.UpdatedAt, diff --git a/openflare_server/service/pages_test.go b/openflare_server/service/pages_test.go index d2b32fa4..9e79314f 100644 --- a/openflare_server/service/pages_test.go +++ b/openflare_server/service/pages_test.go @@ -331,15 +331,17 @@ func TestUploadPagesDeploymentWithRootDir(t *testing.T) { setupServiceTestDB(t) project, err := CreatePagesProject(PagesProjectInput{ - Name: "App Site", - Slug: "app-site", - Enabled: true, + Name: "App Site", + Slug: "app-site", + Enabled: true, + RootDir: "build", + EntryFile: "index.html", }) if err != nil { t.Fatalf("CreatePagesProject failed: %v", err) } - // 1. Upload a zip with files inside a subfolder, specifying "build" as rootDir. + // 1. Upload a zip with files inside a subfolder. uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{ "build/index.html": "
+ {file.name} +
++ 大小: {formatBytes(file.size)} +
++ 点击或拖拽 zip 部署包到此处 +
++ 仅支持 zip,Server 会校验文件数量、体积、路径逃逸和入口文件 +
+部署数
-- {project.deployment_count} -
-当前激活 diff --git a/openflare_server/web/features/pages/schemas.ts b/openflare_server/web/features/pages/schemas.ts index 4738a7ea..54c36d0e 100644 --- a/openflare_server/web/features/pages/schemas.ts +++ b/openflare_server/web/features/pages/schemas.ts @@ -25,6 +25,17 @@ export const pagesProjectSchema = z api_proxy_path: z.string().trim(), api_proxy_pass: z.string().trim(), api_proxy_rewrite: z.string().trim(), + root_dir: z + .string() + .trim() + .max(512, '根目录不能超过 512 个字符') + .optional() + .or(z.literal('')), + entry_file: z + .string() + .trim() + .min(1, '请输入入口文件') + .max(512, '入口文件不能超过 512 个字符'), }) .superRefine((data, ctx) => { if (data.spa_fallback_enabled) { diff --git a/openflare_server/web/features/pages/types.ts b/openflare_server/web/features/pages/types.ts index 1f366ef8..d6616ea9 100644 --- a/openflare_server/web/features/pages/types.ts +++ b/openflare_server/web/features/pages/types.ts @@ -25,6 +25,8 @@ export interface PagesProject { api_proxy_path: string; api_proxy_pass: string; api_proxy_rewrite: string; + root_dir?: string; + entry_file: string; active_deployment_id?: number | null; active_deployment?: PagesDeployment | null; deployment_count: number; @@ -43,4 +45,6 @@ export interface PagesProjectPayload { api_proxy_path: string; api_proxy_pass: string; api_proxy_rewrite: string; + root_dir?: string; + entry_file: string; }