From 84d7caea8c8410cc350b64046e73fb0b273f6ec0 Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Wed, 10 Sep 2025 16:48:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BC=A0=E8=BE=93=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E8=B7=AF=E7=94=B1=E8=AE=BE=E7=BD=AE=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=96=E9=83=A8=E5=89=8D=E7=AB=AF=E7=9B=AE=E5=BD=95?= =?UTF-8?q?|=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E9=80=9F=E5=BA=A6=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .chuan.env.example | 23 ++++++++ cmd/config.go | 124 +++++++++++++++++++++++++++++++++++++++ cmd/main.go | 103 ++++---------------------------- cmd/router.go | 60 +++++++++++++++++++ cmd/server.go | 77 ++++++++++++++++++++++++ go.sum | 2 - internal/web/frontend.go | 97 +++++++++++++++++++++++++++--- 7 files changed, 385 insertions(+), 101 deletions(-) create mode 100644 .chuan.env.example create mode 100644 cmd/config.go create mode 100644 cmd/router.go create mode 100644 cmd/server.go diff --git a/.chuan.env.example b/.chuan.env.example new file mode 100644 index 0000000..af0047c --- /dev/null +++ b/.chuan.env.example @@ -0,0 +1,23 @@ +# 文件传输服务器配置文件 +# 这个文件会被自动加载,支持 KEY=VALUE 格式 + +# 服务器端口 +PORT=8080 + +# 外部前端文件目录 (可选) +# 如果设置了这个路径,服务器会使用指定目录的前端文件 +# 而不是内嵌在二进制文件中的前端文件 +# FRONTEND_DIR=./chuan-next/out +# FRONTEND_DIR=/var/www/chuan-frontend + +# 示例: Docker 容器内的路径 +# FRONTEND_DIR=/app/frontend + +# 示例: 开发环境 +# FRONTEND_DIR=./chuan-next/dist + +# 注意: +# 1. 环境变量的优先级高于配置文件 +# 2. 命令行参数的优先级最高 +# 3. 空行和以 # 开头的行会被忽略 +# 4. 值可以用单引号或双引号包围 diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..1d06858 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,124 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + "strconv" + "strings" +) + +// Config 应用配置结构 +type Config struct { + Port int + FrontendDir string +} + +// loadEnvFile 加载环境变量文件 +func loadEnvFile(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // 跳过空行和注释行 + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 解析 KEY=VALUE 格式 + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + + // 移除值两端的引号 + if (strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"")) || + (strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'")) { + value = value[1 : len(value)-1] + } + + // 只有当环境变量不存在时才设置 + if os.Getenv(key) == "" { + os.Setenv(key, value) + } + } + } + + return scanner.Err() +} + +// showHelp 显示帮助信息 +func showHelp() { + fmt.Println("文件传输服务器") + fmt.Println("用法:") + fmt.Println(" 配置文件:") + fmt.Println(" .chuan.env - 自动加载的配置文件") + fmt.Println(" 环境变量:") + fmt.Println(" PORT=8080 - 服务器监听端口") + fmt.Println(" FRONTEND_DIR=/path - 外部前端文件目录 (可选)") + fmt.Println(" 命令行参数:") + flag.PrintDefaults() + fmt.Println("") + fmt.Println("配置优先级: 命令行参数 > 环境变量 > 配置文件 > 默认值") + fmt.Println("") + fmt.Println("示例:") + fmt.Println(" ./file-transfer-server") + fmt.Println(" ./file-transfer-server -port 3000") + fmt.Println(" PORT=8080 FRONTEND_DIR=./dist ./file-transfer-server") +} + +// loadConfig 加载应用配置 +func loadConfig() *Config { + // 首先尝试加载 .chuan.env 文件 + if err := loadEnvFile(".chuan.env"); err == nil { + log.Printf("📄 已加载配置文件: .chuan.env") + } + + // 从环境变量获取配置,如果没有则使用默认值 + defaultPort := 8080 + if envPort := os.Getenv("PORT"); envPort != "" { + if port, err := strconv.Atoi(envPort); err == nil { + defaultPort = port + } + } + + // 定义命令行参数 + var port = flag.Int("port", defaultPort, "服务器监听端口 (可通过 PORT 环境变量设置)") + var help = flag.Bool("help", false, "显示帮助信息") + flag.Parse() + + // 显示帮助信息 + if *help { + showHelp() + os.Exit(0) + } + + config := &Config{ + Port: *port, + FrontendDir: os.Getenv("FRONTEND_DIR"), + } + + return config +} + +// logConfig 记录配置信息 +func logConfig(config *Config) { + // 记录前端配置信息 + if config.FrontendDir != "" { + if info, err := os.Stat(config.FrontendDir); err == nil && info.IsDir() { + log.Printf("✅ 使用外部前端目录: %s", config.FrontendDir) + } else { + log.Printf("⚠️ 外部前端目录不可用: %s, 回退到内嵌文件", config.FrontendDir) + } + } else { + log.Printf("📦 使用内嵌前端文件") + } +} diff --git a/cmd/main.go b/cmd/main.go index 3a17bf0..421af20 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,104 +1,25 @@ package main import ( - "context" - "flag" - "fmt" - "log" - "net/http" "os" - "os/signal" - "syscall" - "time" - - "chuan/internal/handlers" - "chuan/internal/web" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" ) func main() { - // 定义命令行参数 - var port = flag.Int("port", 8080, "服务器监听端口") - var help = flag.Bool("help", false, "显示帮助信息") - flag.Parse() - - // 显示帮助信息 - if *help { - fmt.Println("文件传输服务器") - fmt.Println("用法:") - flag.PrintDefaults() - os.Exit(0) + // 检查是否需要显示帮助 + if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") { + showHelp() + return } - // 初始化处理器 - h := handlers.NewHandler() + // 加载配置 + config := loadConfig() - // 创建路由 - r := chi.NewRouter() + // 记录配置信息 + logConfig(config) - // 中间件 - r.Use(middleware.Logger) - r.Use(middleware.Recoverer) - r.Use(middleware.Compress(5)) + // 设置路由 + router := setupRouter() - // CORS 配置 - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - MaxAge: 300, - })) - - // 嵌入式前端文件服务 - r.Handle("/*", web.CreateFrontendHandler()) - - // WebRTC信令WebSocket路由 - r.Get("/api/ws/webrtc", h.HandleWebRTCWebSocket) - r.Get("/ws/webrtc", h.HandleWebRTCWebSocket) - - // WebRTC房间API - r.Post("/api/create-room", h.CreateRoomHandler) - r.Get("/api/room-info", h.WebRTCRoomStatusHandler) - r.Get("/api/webrtc-room-status", h.WebRTCRoomStatusHandler) - - // 构建服务器地址 - addr := fmt.Sprintf(":%d", *port) - - // 启动服务器 - srv := &http.Server{ - Addr: addr, - Handler: r, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 120 * time.Second, - } - - // 优雅关闭 - go func() { - log.Printf("服务器启动在端口 %s", addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("服务器启动失败: %v", err) - } - }() - - // 等待中断信号 - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("正在关闭服务器...") - - // 设置关闭超时 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("服务器强制关闭:", err) - } - - log.Println("服务器已退出") + // 运行服务器(包含启动和优雅关闭) + RunServer(config, router) } diff --git a/cmd/router.go b/cmd/router.go new file mode 100644 index 0000000..00b2bfd --- /dev/null +++ b/cmd/router.go @@ -0,0 +1,60 @@ +package main + +import ( + "net/http" + + "chuan/internal/handlers" + "chuan/internal/web" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" +) + +// setupRouter 设置路由和中间件 +func setupRouter() http.Handler { + // 初始化处理器 + h := handlers.NewHandler() + + router := chi.NewRouter() + + // 设置中间件 + setupMiddleware(router) + + // 设置API路由 + setupAPIRoutes(router, h) + + // 设置前端路由 + router.Handle("/*", web.CreateFrontendHandler()) + + return router +} + +// setupMiddleware 设置中间件 +func setupMiddleware(r *chi.Mux) { + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.Compress(5)) + + // CORS 配置 + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, + })) +} + +// setupAPIRoutes 设置API路由 +func setupAPIRoutes(r *chi.Mux, h *handlers.Handler) { + // WebRTC信令WebSocket路由 + r.Get("/api/ws/webrtc", h.HandleWebRTCWebSocket) + r.Get("/ws/webrtc", h.HandleWebRTCWebSocket) + + // WebRTC房间API + r.Post("/api/create-room", h.CreateRoomHandler) + r.Get("/api/room-info", h.WebRTCRoomStatusHandler) + r.Get("/api/webrtc-room-status", h.WebRTCRoomStatusHandler) +} diff --git a/cmd/server.go b/cmd/server.go new file mode 100644 index 0000000..409ef13 --- /dev/null +++ b/cmd/server.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" +) + +// Server 服务器结构 +type Server struct { + httpServer *http.Server + config *Config +} + +// NewServer 创建新的服务器实例 +func NewServer(config *Config, handler http.Handler) *Server { + return &Server{ + httpServer: &http.Server{ + Addr: fmt.Sprintf(":%d", config.Port), + Handler: handler, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + }, + config: config, + } +} + +// Start 启动服务器 +func (s *Server) Start() error { + log.Printf("🚀 服务器启动在端口 :%d", s.config.Port) + return s.httpServer.ListenAndServe() +} + +// Stop 停止服务器 +func (s *Server) Stop(ctx context.Context) error { + log.Println("🛑 正在关闭服务器...") + return s.httpServer.Shutdown(ctx) +} + +// WaitForShutdown 等待关闭信号并优雅关闭 +func (s *Server) WaitForShutdown() { + // 等待中断信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + // 设置关闭超时 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := s.Stop(ctx); err != nil { + log.Fatal("❌ 服务器强制关闭:", err) + } + + log.Println("✅ 服务器已退出") +} + +// RunServer 运行服务器(包含启动和优雅关闭) +func RunServer(config *Config, handler http.Handler) { + server := NewServer(config, handler) + + // 启动服务器 + go func() { + if err := server.Start(); err != nil && err != http.ErrServerClosed { + log.Fatalf("❌ 服务器启动失败: %v", err) + } + }() + + // 等待关闭信号 + server.WaitForShutdown() +} diff --git a/go.sum b/go.sum index 635b2d9..a07d7b1 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,5 @@ github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/internal/web/frontend.go b/internal/web/frontend.go index aaa4d4b..46460a7 100644 --- a/internal/web/frontend.go +++ b/internal/web/frontend.go @@ -5,7 +5,9 @@ import ( "io" "io/fs" "net/http" + "os" "path" + "path/filepath" "strings" ) @@ -25,6 +27,15 @@ func hasFrontendFiles() bool { // CreateFrontendHandler 创建前端文件处理器 func CreateFrontendHandler() http.Handler { + // 检查是否配置了外部前端目录 + if frontendDir := os.Getenv("FRONTEND_DIR"); frontendDir != "" { + if info, err := os.Stat(frontendDir); err == nil && info.IsDir() { + // 使用外部前端目录 + return &externalSpaHandler{baseDir: frontendDir} + } + } + + // 使用内嵌的前端文件 if !hasFrontendFiles() { return &placeholderHandler{} } @@ -59,6 +70,7 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { pre { margin: 0; overflow-x: auto; } .api-list { margin: 20px 0; } .api-item { margin: 10px 0; padding: 10px; background: #e3f2fd; border-radius: 4px; } + .env-config { background: #e8f5e8; padding: 15px; border-radius: 4px; margin: 20px 0; } @@ -69,11 +81,21 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ⚠️ 前端界面未构建,当前显示的是后端 API 服务。 -

📋 可用的 API 接口

+

� 环境变量配置

+
+ FRONTEND_DIR - 指定外部前端文件目录
+ PORT - 自定义服务端口 (默认: 8080)

+ 示例:
+
export FRONTEND_DIR=/path/to/frontend
+export PORT=3000
+./file-transfer-server
+
+ +

�📋 可用的 API 接口

-
POST /api/create-text-room - 创建文本传输房间
-
GET /api/get-text-content/* - 获取文本内容
-
WebSocket /ws/webrtc - WebRTC 信令连接
+
POST /api/create-room - 创建WebRTC房间
+
GET /api/room-info - 获取房间信息
+
WebSocket /api/ws/webrtc - WebRTC 信令连接

🛠️ 构建前端

@@ -82,14 +104,18 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { cd chuan-next # 安装依赖 -yarn install +npm install # 构建静态文件 -yarn build:ssg +npm run build -# 重新构建 Go 项目以嵌入前端文件 +# 方法1: 重新构建 Go 项目以嵌入前端文件 cd .. -go build -o file-transfer-server ./cmd +go build -o file-transfer-server ./cmd + +# 方法2: 使用外部前端目录 +export FRONTEND_DIR=./chuan-next/out +./file-transfer-server

提示: 构建完成后刷新页面即可看到完整的前端界面。

@@ -99,6 +125,61 @@ go build -o file-transfer-server ./cmd `)) } +// externalSpaHandler 外部文件目录处理器 +type externalSpaHandler struct { + baseDir string +} + +func (h *externalSpaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 清理路径 + upath := strings.TrimPrefix(r.URL.Path, "/") + if upath == "" { + upath = "index.html" + } + + // 构建完整文件路径 + fullPath := filepath.Join(h.baseDir, upath) + + // 安全检查:确保文件在基础目录内 + absBasePath, err := filepath.Abs(h.baseDir) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + absFullPath, err := filepath.Abs(fullPath) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if !strings.HasPrefix(absFullPath, absBasePath) { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + // 检查文件是否存在 + if _, err := os.Stat(fullPath); os.IsNotExist(err) { + // 文件不存在,对于 SPA 应用返回 index.html + h.serveIndexHTML(w, r) + return + } + + // 服务文件 + http.ServeFile(w, r, fullPath) +} + +// serveIndexHTML 服务外部目录的 index.html 文件 +func (h *externalSpaHandler) serveIndexHTML(w http.ResponseWriter, r *http.Request) { + indexPath := filepath.Join(h.baseDir, "index.html") + if _, err := os.Stat(indexPath); os.IsNotExist(err) { + http.NotFound(w, r) + return + } + + http.ServeFile(w, r, indexPath) +} + // spaHandler SPA 应用处理器 type spaHandler struct { fs fs.FS