You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
7.8 KiB
Go

package router
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"ziqian/config"
u "ziqian/utils"
"github.com/gin-gonic/gin"
)
// getMimeType 根据文件扩展名获取 MIME 类型
func getMimeType(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
mimeMap := map[string]string{
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".txt": "text/plain",
".html": "text/html",
".json": "application/json",
".zip": "application/zip",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
if mime, ok := mimeMap[ext]; ok {
return mime
}
return "application/octet-stream"
}
// fileExists 检查文件是否存在
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// resolveFilePath 将请求路径转换为存储服务器上的绝对路径
func resolveFilePath(storageRoot, requestPath string) string {
// 去除开头的斜杠,避免 filepath.Join 处理异常
cleanPath := strings.TrimPrefix(requestPath, "/")
return filepath.Join(storageRoot, cleanPath)
}
func SetupRouter() *gin.Engine {
r := gin.Default()
cfg := config.GetConfig()
r.LoadHTMLFiles("templates/index.html")
// 只在开发环境中配置静态文件服务
if gin.Mode() == gin.DebugMode {
r.Static("/static", "./static")
fmt.Println("Development mode: Static file services enabled")
} else {
fmt.Println("Production mode: Static file services disabled")
}
// 添加CORS中间件
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
r.GET("/", func(c *gin.Context) {
u.Zi.Html(c, "index.html", gin.H{
"Title": "Ziqian API",
"Subtitle": "基于 Gin 框架的 API 服务",
"CurrentTime": time.Now().Format("2006-01-02 15:04:05"),
"Version": "1.0.0",
"Year": time.Now().Year(),
})
})
apiGroup := r.Group("/api")
{
apiGroup.Any("/ho", func(c *gin.Context) {
u.Zi.Echo(c, gin.H{
"status": "ok",
})
})
apiGroup.Any("/map", func(c *gin.Context) {
cParam := c.Query("c")
typeList := strings.Split(cParam, ",")
if cParam == "" {
typeList = nil
}
processed := make(map[string]bool)
apiRoutes := make(map[string]string)
protocol := "http"
xForwardedProto := c.GetHeader("X-Forwarded-Proto")
xScheme := c.GetHeader("X-Scheme")
if c.Request.TLS != nil || xForwardedProto == "https" || xScheme == "https" {
protocol = "https"
}
host := c.Request.Host
for _, route := range r.Routes() {
path := route.Path
if !strings.HasPrefix(path, "/api/") || path == "/api/map" || processed[path] {
continue
}
processed[path] = true
keyName := ""
for i, part := range strings.Split(strings.TrimPrefix(path, "/api/"), "/") {
if part == "" {
continue
}
upperPart := strings.ToUpper(string(part[0])) + part[1:]
keyName += func() string {
if i > 0 {
return "_" + upperPart
}
return upperPart
}()
}
if typeList != nil {
typePrefix := strings.ToLower(strings.Split(keyName, "_")[0])
matched := false
for _, t := range typeList {
if strings.ToLower(t) == typePrefix {
matched = true
break
}
}
if !matched {
continue
}
}
apiRoutes[keyName] = fmt.Sprintf("%s://%s%s", protocol, host, path)
}
apiRoutes["Map"] = fmt.Sprintf("%s://%s%s", protocol, host, "/api/map")
apiRoutes["Ho"] = fmt.Sprintf("%s://%s%s", protocol, host, "/api/ho")
u.Zi.Echo(c, apiRoutes)
})
// 上传文件 - base64
apiGroup.POST("/upload/base64", func(c *gin.Context) {
var req struct {
File string `json:"file"`
Path string `json:"path"`
Filename string `json:"filename"`
}
if err := c.ShouldBindJSON(&req); err != nil {
u.Zi.Error(c, "请求参数错误")
return
}
if req.File == "" || req.Filename == "" {
u.Zi.Error(c, "文件内容或文件名不能为空")
return
}
// 解码 base64
decoded, err := base64.StdEncoding.DecodeString(req.File)
if err != nil {
u.Zi.Error(c, "base64 解码失败")
return
}
// 构建完整路径
dirPath := resolveFilePath(cfg.Storage.Root, req.Path)
fullPath := filepath.Join(dirPath, req.Filename)
// 创建目录
if err := os.MkdirAll(dirPath, 0755); err != nil {
u.Zi.Error(c, "创建目录失败: "+err.Error())
return
}
// 写入文件
if err := os.WriteFile(fullPath, decoded, 0644); err != nil {
u.Zi.Error(c, "写入文件失败: "+err.Error())
return
}
// 构建返回 URL
relPath := strings.TrimPrefix(req.Path, "/")
url := fmt.Sprintf("/files/%s/%s", relPath, req.Filename)
u.Zi.Echo(c, gin.H{
"url": url,
})
})
// 上传文件 - multipart
apiGroup.POST("/upload/file", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
u.Zi.Error(c, "获取上传文件失败")
return
}
path := c.PostForm("path")
if path == "" {
u.Zi.Error(c, "路径不能为空")
return
}
// 构建完整路径
dirPath := resolveFilePath(cfg.Storage.Root, path)
fullPath := filepath.Join(dirPath, file.Filename)
// 创建目录
if err := os.MkdirAll(dirPath, 0755); err != nil {
u.Zi.Error(c, "创建目录失败: "+err.Error())
return
}
// 保存文件
if err := c.SaveUploadedFile(file, fullPath); err != nil {
u.Zi.Error(c, "保存文件失败: "+err.Error())
return
}
// 构建返回 URL
relPath := strings.TrimPrefix(path, "/")
url := fmt.Sprintf("/files/%s/%s", relPath, file.Filename)
u.Zi.Echo(c, gin.H{
"url": url,
})
})
}
// 文件服务路由 - 处理 GET/HEAD/DELETE
r.Any("/files/*filepath", func(c *gin.Context) {
filePath := c.Param("filepath")
// 安全保护:不允许空路径或根路径操作
if filePath == "" || filePath == "/" {
c.Status(http.StatusBadRequest)
return
}
fullPath := resolveFilePath(cfg.Storage.Root, filePath)
// 安全保护:确保解析后的路径仍在存储根目录下
cleanRoot := filepath.Clean(cfg.Storage.Root)
cleanFull := filepath.Clean(fullPath)
if !strings.HasPrefix(cleanFull, cleanRoot) {
c.Status(http.StatusForbidden)
return
}
switch c.Request.Method {
case http.MethodGet:
if !fileExists(fullPath) {
c.Status(http.StatusNotFound)
return
}
c.File(fullPath)
case http.MethodHead:
info, err := os.Stat(fullPath)
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.Header("Content-Type", getMimeType(filePath))
c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
c.Header("Last-Modified", info.ModTime().Format(http.TimeFormat))
c.Status(http.StatusOK)
case http.MethodDelete:
if err := os.RemoveAll(fullPath); err != nil {
c.Status(http.StatusInternalServerError)
return
}
c.Status(http.StatusOK)
default:
c.Status(http.StatusMethodNotAllowed)
}
})
// 确保文件上传中间件中不包含日志记录(已移除)
// 如果需要将上传的文件写入到磁盘中的指定路径,请使用上述接口
return r
}