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 }