添加前端页面以及相关打包脚本和内置 web 的逻辑

This commit is contained in:
2026-01-14 23:09:16 +08:00
parent e456d3a823
commit 9b646321e1
45 changed files with 583 additions and 10 deletions

127
main.go
View File

@@ -10,8 +10,17 @@ import (
"FileRelay/internal/model"
"FileRelay/internal/task"
"context"
"embed"
"flag"
"fmt"
"io"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
@@ -19,6 +28,9 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
)
//go:embed all:web
var webFS embed.FS
// @title 文件暂存柜 API
// @version 1.0
// @description 自托管的文件暂存柜后端系统 API 文档
@@ -44,11 +56,50 @@ import (
// @description Type "Bearer <API-Token>" to authenticate. Required scope depends on the endpoint.
func main() {
// 注册常用 MIME 类型
mime.AddExtensionType(".js", "application/javascript")
mime.AddExtensionType(".css", "text/css")
mime.AddExtensionType(".woff", "font/woff")
mime.AddExtensionType(".woff2", "font/woff2")
mime.AddExtensionType(".svg", "image/svg+xml")
// 解析命令行参数
configPath := flag.String("config", "config/config.yaml", "path to config file")
flag.Parse()
// 1. 加载配置
if err := config.LoadConfig("config/config.yaml"); err != nil {
if err := config.LoadConfig(*configPath); err != nil {
log.Fatalf("Failed to load config: %v", err)
}
port := 8080
// 打印配置信息
fmt.Println("========================================")
fmt.Println("FileRelay 服务启动中...")
fmt.Printf("配置文件路径: %s\n", *configPath)
fmt.Printf("监听端口: %d\n", port)
fmt.Printf("数据库文件: %s\n", config.GlobalConfig.Database.Path)
fmt.Printf("存储模式: %s\n", config.GlobalConfig.Storage.Type)
webPath := config.GlobalConfig.Web.Path
useExternalWeb := false
if webPath != "" {
if info, err := os.Stat(webPath); err == nil && info.IsDir() {
useExternalWeb = true
}
}
if useExternalWeb {
fmt.Printf("前端资源来源: 外部目录 (%s)\n", webPath)
} else {
fmt.Printf("前端资源来源: 内置嵌入 (嵌入 fs)\n")
if webPath != "" {
fmt.Printf("提示: 配置的外部前端路径 %s 不存在,已回退到内置资源\n", webPath)
}
}
fmt.Println("========================================")
// 2. 初始化
bootstrap.InitDB()
@@ -82,7 +133,9 @@ func main() {
api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText)
api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup)
api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch)
// 文件下载保持 /files/:id/download 风格
// 文件下载路由,支持直观的文件名结尾
api.GET("/files/:file_id/:filename", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
// 保持旧路由兼容性
api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
// 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新)
@@ -114,8 +167,74 @@ func main() {
adm.POST("/api-tokens/:id/revoke", tokenHandler.RevokeToken)
}
// 静态资源服务 (放在最后,确保 API 路由优先)
webSub, _ := fs.Sub(webFS, "web")
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// 如果请求的是 API 或 Swagger则不处理静态资源 (让其返回 404)
// 注意:此处不排除 /admin因为 /admin 通常是前端 SPA 的路由地址
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/swagger") {
return
}
// 辅助函数:尝试从外部或嵌入服务文件
serveFile := func(relPath string, allowExternal bool) bool {
// 1. 优先尝试外部路径
if allowExternal && config.GlobalConfig.Web.Path != "" {
fullPath := filepath.Join(config.GlobalConfig.Web.Path, relPath)
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
c.File(fullPath)
return true
}
}
// 2. 尝试嵌入式文件
f, err := webSub.Open(relPath)
if err == nil {
defer f.Close()
stat, err := f.Stat()
if err == nil && !stat.IsDir() {
// 使用 http.ServeContent 避免 c.FileFromFS 重定向问题
if rs, ok := f.(io.ReadSeeker); ok {
// 显式设置 Content-Type防止某些环境下识别失败
ext := filepath.Ext(relPath)
ctype := mime.TypeByExtension(ext)
if ctype != "" {
c.Header("Content-Type", ctype)
}
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), rs)
return true
}
}
}
return false
}
// 1. 尝试直接请求的文件 (如果是 / 则尝试 index.html)
requestedPath := strings.TrimPrefix(path, "/")
if requestedPath == "" {
requestedPath = "index.html"
}
if serveFile(requestedPath, true) {
return
}
// 2. SPA 支持:对于非文件请求(没有后缀或不包含点),尝试返回 index.html
// 如果请求的是 assets 目录下的文件(包含点)却没找到,不应该回退到 index.html
isAsset := strings.Contains(requestedPath, ".")
if !isAsset || requestedPath == "index.html" {
if serveFile("index.html", true) {
return
}
}
// 最终找不到则返回 404
c.Status(http.StatusNotFound)
})
// 5. 运行
port := 8080
fmt.Printf("Server is running on port %d\n", port)
r.Run(fmt.Sprintf(":%d", port))
}