单文件上传:FormFile 与 SaveUploadedFile

本章解决什么问题:把客户端发来的一个文件存到服务器磁盘,是表单开发的基本功。本章讲单文件上传的完整流程:取文件、存文件、处理大小限制,并提示文件名的安全隐患。

上传流程

客户端通常以 multipart/form-data 提交文件,字段名自定(下面用 file)。Gin 把“拿到文件描述”和“落盘”拆成两个 API:c.FormFile(name) 返回 *multipart.FileHeader 与 error;c.SaveUploadedFile(header, dst) 负责把内容写入目标路径:

package main

import (
	"net/http"
	"path/filepath"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.MaxMultipartMemory = 8 << 20 // 8 MiB:超出部分写入临时文件而非内存

	r.POST("/upload", func(c *gin.Context) {
		file, err := c.FormFile("file") // 与表单字段名一致
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "no file"})
			return
		}
		dst := filepath.Join("uploads", file.Filename) // 客户端文件名不可信,正式项目需清洗
		if err := c.SaveUploadedFile(file, dst); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(200, gin.H{"saved": dst, "size": file.Size})
	})

	r.Run(":8080")
}

用 curl 模拟客户端:curl -F "file=@a.txt" http://127.0.0.1:8080/upload。file.Filename 是客户端给的文件名,file.Size 是文件字节数。

内存与磁盘的阈值

multipart 解析时,小于阈值的部分驻留内存,超出部分以临时文件落盘。阈值就是引擎的 MaxMultipartMemory 字段(默认约 32 MiB,gin.New 时预设),按需调大或调小。临时文件会在请求处理结束后由 net/http 自动清理,无需手动干预。

大小限制的正确姿势

MaxMultipartMemory 控制的是“内存/磁盘阈值”,不是文件大小上限。真正限制上传体积,推荐两层做法:

  • 限制整个请求体:用 http.MaxBytesReader 包住 c.Request.Body,可放在中间件里对指定路由生效;
  • 限制单文件:FormFile 之后检查 file.Size 是否超限。

超出限制时应尽早返回 4xx,并考虑前端同步提示。

文件名安全

file.Filename 来自客户端,可能包含路径分隔符或恶意内容,直接拼进 dst 有目录穿越风险。正式项目至少要做:只保留 basename、限定扩展名白名单、重命名为服务端生成的随机名并另存原扩展名。

关键点

  • 取文件用 c.FormFile,存文件用 c.SaveUploadedFile,字段名要与表单一致。
  • MaxMultipartMemory 是内存/磁盘阈值,不是体积上限;限体积用 http.MaxBytesReader 或检查 file.Size。
  • 上传目录与文件名策略要按“不可信输入”对待,写入前做清洗与白名单。

小结

单文件上传只需 FormFile + SaveUploadedFile 两步,难点在阈值理解与安全收口。下一章处理多文件,并深入 multipart 的解析细节。

笔记加载中…