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
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
|
|
} |