mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat: 添加文件传输服务器配置和路由设置,支持外部前端目录|添加文件传输速度计算
This commit is contained in:
23
.chuan.env.example
Normal file
23
.chuan.env.example
Normal file
@@ -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. 值可以用单引号或双引号包围
|
||||||
124
cmd/config.go
Normal file
124
cmd/config.go
Normal file
@@ -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("📦 使用内嵌前端文件")
|
||||||
|
}
|
||||||
|
}
|
||||||
103
cmd/main.go
103
cmd/main.go
@@ -1,104 +1,25 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"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() {
|
func main() {
|
||||||
// 定义命令行参数
|
// 检查是否需要显示帮助
|
||||||
var port = flag.Int("port", 8080, "服务器监听端口")
|
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
|
||||||
var help = flag.Bool("help", false, "显示帮助信息")
|
showHelp()
|
||||||
flag.Parse()
|
return
|
||||||
|
|
||||||
// 显示帮助信息
|
|
||||||
if *help {
|
|
||||||
fmt.Println("文件传输服务器")
|
|
||||||
fmt.Println("用法:")
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化处理器
|
// 加载配置
|
||||||
h := handlers.NewHandler()
|
config := loadConfig()
|
||||||
|
|
||||||
// 创建路由
|
// 记录配置信息
|
||||||
r := chi.NewRouter()
|
logConfig(config)
|
||||||
|
|
||||||
// 中间件
|
// 设置路由
|
||||||
r.Use(middleware.Logger)
|
router := setupRouter()
|
||||||
r.Use(middleware.Recoverer)
|
|
||||||
r.Use(middleware.Compress(5))
|
|
||||||
|
|
||||||
// CORS 配置
|
// 运行服务器(包含启动和优雅关闭)
|
||||||
r.Use(cors.Handler(cors.Options{
|
RunServer(config, router)
|
||||||
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("服务器已退出")
|
|
||||||
}
|
}
|
||||||
|
|||||||
60
cmd/router.go
Normal file
60
cmd/router.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
77
cmd/server.go
Normal file
77
cmd/server.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
2
go.sum
2
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/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 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +27,15 @@ func hasFrontendFiles() bool {
|
|||||||
|
|
||||||
// CreateFrontendHandler 创建前端文件处理器
|
// CreateFrontendHandler 创建前端文件处理器
|
||||||
func CreateFrontendHandler() http.Handler {
|
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() {
|
if !hasFrontendFiles() {
|
||||||
return &placeholderHandler{}
|
return &placeholderHandler{}
|
||||||
}
|
}
|
||||||
@@ -59,6 +70,7 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
pre { margin: 0; overflow-x: auto; }
|
pre { margin: 0; overflow-x: auto; }
|
||||||
.api-list { margin: 20px 0; }
|
.api-list { margin: 20px 0; }
|
||||||
.api-item { margin: 10px 0; padding: 10px; background: #e3f2fd; border-radius: 4px; }
|
.api-item { margin: 10px 0; padding: 10px; background: #e3f2fd; border-radius: 4px; }
|
||||||
|
.env-config { background: #e8f5e8; padding: 15px; border-radius: 4px; margin: 20px 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -69,11 +81,21 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
⚠️ 前端界面未构建,当前显示的是后端 API 服务。
|
⚠️ 前端界面未构建,当前显示的是后端 API 服务。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>📋 可用的 API 接口</h2>
|
<h2><EFBFBD> 环境变量配置</h2>
|
||||||
|
<div class="env-config">
|
||||||
|
<strong>FRONTEND_DIR</strong> - 指定外部前端文件目录<br>
|
||||||
|
<strong>PORT</strong> - 自定义服务端口 (默认: 8080)<br><br>
|
||||||
|
<strong>示例:</strong><br>
|
||||||
|
<pre>export FRONTEND_DIR=/path/to/frontend
|
||||||
|
export PORT=3000
|
||||||
|
./file-transfer-server</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2><3E>📋 可用的 API 接口</h2>
|
||||||
<div class="api-list">
|
<div class="api-list">
|
||||||
<div class="api-item"><strong>POST</strong> /api/create-text-room - 创建文本传输房间</div>
|
<div class="api-item"><strong>POST</strong> /api/create-room - 创建WebRTC房间</div>
|
||||||
<div class="api-item"><strong>GET</strong> /api/get-text-content/* - 获取文本内容</div>
|
<div class="api-item"><strong>GET</strong> /api/room-info - 获取房间信息</div>
|
||||||
<div class="api-item"><strong>WebSocket</strong> /ws/webrtc - WebRTC 信令连接</div>
|
<div class="api-item"><strong>WebSocket</strong> /api/ws/webrtc - WebRTC 信令连接</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>🛠️ 构建前端</h2>
|
<h2>🛠️ 构建前端</h2>
|
||||||
@@ -82,14 +104,18 @@ func (h *placeholderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
cd chuan-next
|
cd chuan-next
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
yarn install
|
npm install
|
||||||
|
|
||||||
# 构建静态文件
|
# 构建静态文件
|
||||||
yarn build:ssg
|
npm run build
|
||||||
|
|
||||||
# 重新构建 Go 项目以嵌入前端文件
|
# 方法1: 重新构建 Go 项目以嵌入前端文件
|
||||||
cd ..
|
cd ..
|
||||||
go build -o file-transfer-server ./cmd</pre>
|
go build -o file-transfer-server ./cmd
|
||||||
|
|
||||||
|
# 方法2: 使用外部前端目录
|
||||||
|
export FRONTEND_DIR=./chuan-next/out
|
||||||
|
./file-transfer-server</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p><strong>提示:</strong> 构建完成后刷新页面即可看到完整的前端界面。</p>
|
<p><strong>提示:</strong> 构建完成后刷新页面即可看到完整的前端界面。</p>
|
||||||
@@ -99,6 +125,61 @@ go build -o file-transfer-server ./cmd</pre>
|
|||||||
`))
|
`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 应用处理器
|
// spaHandler SPA 应用处理器
|
||||||
type spaHandler struct {
|
type spaHandler struct {
|
||||||
fs fs.FS
|
fs fs.FS
|
||||||
|
|||||||
Reference in New Issue
Block a user