diff --git a/openflare_agent/internal/sync/pages.go b/openflare_agent/internal/sync/pages.go index 80d7804e..e3025442 100644 --- a/openflare_agent/internal/sync/pages.go +++ b/openflare_agent/internal/sync/pages.go @@ -105,6 +105,50 @@ func referencedPagesDeployments(config *protocol.ActiveConfigResponse) ([]pagesD return result, nil } +func findCommonRootPrefix(files []*zip.File) (string, error) { + var firstFilePath string + hasMultipleFiles := false + for _, item := range files { + relativePath, skip, err := normalizePagesArchivePath(item.Name) + if err != nil { + return "", err + } + if skip { + continue + } + normalizedPath := filepath.ToSlash(relativePath) + if firstFilePath == "" { + firstFilePath = normalizedPath + } else { + hasMultipleFiles = true + } + } + if firstFilePath == "" { + return "", nil + } + parts := strings.Split(firstFilePath, "/") + if len(parts) <= 1 { + return "", nil + } + commonPrefix := parts[0] + "/" + if hasMultipleFiles { + for _, item := range files { + relativePath, skip, err := normalizePagesArchivePath(item.Name) + if err != nil { + return "", err + } + if skip { + continue + } + normalizedPath := filepath.ToSlash(relativePath) + if !strings.HasPrefix(normalizedPath, commonPrefix) { + return "", nil + } + } + } + return commonPrefix, nil +} + func extractPagesPackage(packageBytes []byte, releaseDir string, deployment pagesDeploymentSource) error { tmpDir := releaseDir + ".tmp" _ = os.RemoveAll(tmpDir) @@ -116,6 +160,11 @@ func extractPagesPackage(packageBytes []byte, releaseDir string, deployment page _ = os.RemoveAll(tmpDir) return fmt.Errorf("open Pages zip: %w", err) } + commonPrefix, err := findCommonRootPrefix(reader.File) + if err != nil { + _ = os.RemoveAll(tmpDir) + return err + } for _, item := range reader.File { relativePath, skip, err := normalizePagesArchivePath(item.Name) if err != nil { @@ -125,6 +174,12 @@ func extractPagesPackage(packageBytes []byte, releaseDir string, deployment page if skip { continue } + if commonPrefix != "" { + slashPath := filepath.ToSlash(relativePath) + if strings.HasPrefix(slashPath, commonPrefix) { + relativePath = filepath.FromSlash(strings.TrimPrefix(slashPath, commonPrefix)) + } + } if item.FileInfo().Mode()&os.ModeSymlink != 0 { _ = os.RemoveAll(tmpDir) return fmt.Errorf("Pages package contains unsupported symlink: %s", relativePath) diff --git a/openflare_agent/internal/sync/service_test.go b/openflare_agent/internal/sync/service_test.go index 8e29bb10..e10aaf4f 100644 --- a/openflare_agent/internal/sync/service_test.go +++ b/openflare_agent/internal/sync/service_test.go @@ -872,3 +872,52 @@ func testBytesChecksum(data []byte) string { sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } + +func TestSyncOnceDownloadsPagesDeploymentWithTopLevelFolder(t *testing.T) { + packageBytes := testPagesPackage(t, map[string]string{ + "Speed-Test-source/index.html": "hello html", + "Speed-Test-source/assets/app.js": "hello js", + }) + checksum := testBytesChecksum(packageBytes) + client := &fakeClient{ + config: protocol.ActiveConfigResponse{ + Version: "20260309-105", + Checksum: "pages-config-checksum", + SourceConfigJSON: testPagesSourceConfigJSON(77, checksum), + CreatedAt: time.Now().Format(time.RFC3339), + }, + pagesPackages: map[uint][]byte{77: packageBytes}, + } + stateStore := state.NewStore(filepath.Join(t.TempDir(), "state.json")) + nodeID, err := stateStore.EnsureNodeID() + if err != nil { + t.Fatalf("EnsureNodeID failed: %v", err) + } + snapshot, _ := stateStore.Load() + snapshot.NodeID = nodeID + if err = stateStore.Save(snapshot); err != nil { + t.Fatalf("save state failed: %v", err) + } + manager := &fakeManager{currentChecksum: "old-checksum"} + service := New(client, manager, stateStore) + pagesDir := t.TempDir() + service.SetPagesDir(pagesDir) + + if err = service.SyncOnce(context.Background(), &protocol.ActiveConfigMeta{Version: "20260309-105", Checksum: "pages-config-checksum"}); err != nil { + t.Fatalf("SyncOnce failed: %v", err) + } + data, err := os.ReadFile(filepath.Join(pagesDir, "deployments", "77", "current", "index.html")) + if err != nil { + t.Fatalf("expected Pages index.html file to be extracted: %v", err) + } + if string(data) != "hello html" { + t.Fatalf("unexpected Pages index.html content: %s", string(data)) + } + jsData, err := os.ReadFile(filepath.Join(pagesDir, "deployments", "77", "current", "assets", "app.js")) + if err != nil { + t.Fatalf("expected Pages assets/app.js file to be extracted: %v", err) + } + if string(jsData) != "hello js" { + t.Fatalf("unexpected Pages assets/app.js content: %s", string(jsData)) + } +} diff --git a/openflare_server/service/pages.go b/openflare_server/service/pages.go index 9370f48e..ba147291 100644 --- a/openflare_server/service/pages.go +++ b/openflare_server/service/pages.go @@ -22,7 +22,7 @@ import ( const ( pagesMaxDeploymentFiles = 1000 - pagesMaxDeploymentBytes = 25 * 1024 * 1024 + pagesMaxDeploymentBytes = 100 * 1024 * 1024 defaultPagesEntryFile = "index.html" defaultPagesFallbackPath = "/index.html" ) @@ -560,12 +560,60 @@ func persistPagesUploadTemp(fileHeader *multipart.FileHeader) (string, string, e return temp.Name(), hex.EncodeToString(hash.Sum(nil)), nil } +func findCommonRootPrefix(files []*zip.File) (string, error) { + var firstFilePath string + hasMultipleFiles := false + for _, item := range files { + normalizedPath, skip, err := normalizePagesZipPath(item.Name) + if err != nil { + return "", err + } + if skip { + continue + } + if firstFilePath == "" { + firstFilePath = normalizedPath + } else { + hasMultipleFiles = true + } + } + if firstFilePath == "" { + return "", nil + } + parts := strings.Split(firstFilePath, "/") + if len(parts) <= 1 { + return "", nil + } + commonPrefix := parts[0] + "/" + if hasMultipleFiles { + for _, item := range files { + normalizedPath, skip, err := normalizePagesZipPath(item.Name) + if err != nil { + return "", err + } + if skip { + continue + } + if !strings.HasPrefix(normalizedPath, commonPrefix) { + return "", nil + } + } + } + return commonPrefix, nil +} + func inspectPagesZip(zipPath string, entryFile string) (*pagesDeploymentManifest, error) { reader, err := zip.OpenReader(zipPath) if err != nil { return nil, errors.New("Pages 部署包不是有效 zip 文件") } defer reader.Close() + + commonPrefix, err := findCommonRootPrefix(reader.File) + if err != nil { + return nil, err + } + manifest := &pagesDeploymentManifest{ Files: []model.PagesDeploymentFile{}, EntryFile: entryFile, @@ -579,6 +627,11 @@ func inspectPagesZip(zipPath string, entryFile string) (*pagesDeploymentManifest if skip { continue } + + if commonPrefix != "" { + normalizedPath = strings.TrimPrefix(normalizedPath, commonPrefix) + } + if item.FileInfo().Mode()&os.ModeSymlink != 0 { return nil, fmt.Errorf("Pages 部署包不支持符号链接: %s", normalizedPath) } diff --git a/openflare_server/service/pages_test.go b/openflare_server/service/pages_test.go index 3830abe1..f1a4ff01 100644 --- a/openflare_server/service/pages_test.go +++ b/openflare_server/service/pages_test.go @@ -243,3 +243,31 @@ func TestDeletePagesDeploymentRejectsActiveDeployment(t *testing.T) { t.Fatalf("expected active deployment to remain: %v", err) } } + +func TestUploadPagesDeploymentWithTopLevelFolder(t *testing.T) { + setupServiceTestDB(t) + + project, err := CreatePagesProject(PagesProjectInput{ + Name: "Folder Site", + Slug: "folder-site", + Enabled: true, + }) + if err != nil { + t.Fatalf("CreatePagesProject failed: %v", err) + } + // Upload a zip with all files inside a top-level directory "Speed-Test-source/" + uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{ + "Speed-Test-source/index.html": "

Hello Pages

", + "Speed-Test-source/assets/app.js": "console.log('pages')", + })) + deployment, err := UploadPagesDeployment(project.ID, uploadHeader, "index.html", "root") + if err != nil { + t.Fatalf("UploadPagesDeployment with folder failed: %v", err) + } + if deployment.FileCount != 2 { + t.Fatalf("expected 2 files, got %d", deployment.FileCount) + } + if deployment.EntryFile != "index.html" { + t.Fatalf("expected EntryFile to be index.html, got %q", deployment.EntryFile) + } +} diff --git a/openflare_server/web/features/pages/components/pages-page.tsx b/openflare_server/web/features/pages/components/pages-page.tsx index 26102c6b..b0bb72f7 100644 --- a/openflare_server/web/features/pages/components/pages-page.tsx +++ b/openflare_server/web/features/pages/components/pages-page.tsx @@ -114,11 +114,6 @@ export function PagesPage() { } > -

- V1 仅支持 Direct Upload,不执行 Git - 构建或边缘函数运行时。创建后可在项目卡片中上传 zip - 部署包并激活版本。 -

) : ( (projectsQuery.data ?? []).map((project) => ( @@ -262,66 +257,6 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) { } /> - -
- - - -
-

- 项目状态 -

-
- - -
-

- 需要启用且有激活部署 -

-
-
-
setSpaFallbackPath(event.target.value)} /> -
- V1 仅保存项目和部署包,不执行构建任务;请先在本地或 CI - 中完成静态资源构建。 -
{createMutation.error ? (

{createMutation.error.message}