通过gin实现文件上传与下载
在计算机网络中,multipart通常指的是multipart/form-data,这是一种用于HTTP协议的MIME类型,用于在HTTP请求中发送包含多个部分的实体内容。这种类型通常用于在HTML表单中上传文件或提交包含多种类型数据的表单,multipart/form-data是基于POST请求来实现,一般用于上传文件,go语言的官方库multipart中也实现了对multipart/form-data的请求,从而实现文件上传
multipart/form-data形式的post与普通post请求的不同之处体现在请求头,请求体2个部分
请求头:必须包含Content-Type信息,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中不同参数的内容(普通post请求的参数分割符默认为&,参数与参数值的分隔符为=),具体的格式如下:
Content-Type: multipart/form-data; boundary=${bound}
其中${bound} 是一个占位符,代表我们规定的具体分割符;可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容,如:–12345678
请求体:它也是一个字符串,不过和普通post请求体不同的是它的构造方式。普通post请求体是简单的键值对连接,格式如下:
a=b&c=d&e=f
而multipart/form-data则是添加了分隔符、参数描述信息等内容的构造体。具体格式如下:
--${bound}
Content-Disposition: form-data; name="Filename" //第一个参数,相当于k1;然后回车;然后是参数的值,即v1
HTTP.pdf //参数值v1
--${bound} //其实${bound}就相当于上面普通post请求体中的&的作用
Content-Disposition: form-data; name="file000"; filename="HTTP协议详解.pdf" //这里说明传入的是文件,下面是文件提
Content-Type: application/octet-stream //传入文件类型,如果传入的是.jpg,则这里会是image/jpeg
%PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload"
Submit Query
--${bound}--
都是以${bound}为开头的,并且最后一个${bound}后面要加–
一、源码分析:
gin文件上传与原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中
1、查看gin Context中已经嵌套了原生的http.Request,如图:

2、Request中的FormFile方法可提供文件上传功能,参数为key的名,如图:

方法FormFile返回三个参数,第一个参数为multipart库的接口File,如图:

File实现了对一个multipart信息中文件记录的访问,只能读取文件而不能写入。它的内容可以保持在内存或者硬盘中,如果保持在硬盘中,底层类型就会是*os.File
第二个参数为描述一个multipart请求的文件记录的信息,如图:

FileHeader中包含了获取文件名字段,文件大小字段以及文件头的字段,相关的方法可参考安装路径下src/mime/multipart/formdata.go的用法
3、也可不使用http.Request中的FormFile方法,直接使用gin.Context的FormFile方法,如图:

FormFile返回两个参数,第一个就是multipart库的FileHeader结构体指针,与第2步中的一样,还有一个错误返回
二、单文件上传:
1、下面例子表示通过原生的http包中的Request的FormFile上传单个文件,如图:

- 第20行:通过gin封装的Request请求FormFile,实际是访问http.Request中的FormFile,上传的文件的key名为image
- 第25行:调用函数返回文件大小
- 第27行:调用函数SaveUploadFile储存文件,第一个参数为FormFile方法的第二个参数,第二个参数为存储路径+文件名,如果路径不存在会报错,因此存储之前需要判断,本例子直接写死的
通过postman发送上传文件请求,上传文件的key为image,名为1.png,如图:

进入/tmp/linshi目录下可以看到已经保存到机器上,如图:

注:生产中很少直接通过文件名存储,可通过md5等方式加密后存储
2、直接通过gin.Context中的FormFile方法实现单个文件上传,添加了打印请求头信息,如图:

请求结果如图:

查看磁盘目录/tmp/下是否存在文件,如图:

请求头可通过res.Header来获取
请求头信息: map[Content-Disposition:[form-data; name="image"; filename="4.png"] Content-Type:[image/png]]
注:如果传入png图片那么Content-Type位置会显示image/png,如果传入jpg图片将显示image/jpep,如果传入文件将显示application/文件后缀信息,总之根据传入不同显示不同
三、多文件上传:
多文件上传需要使用Context.MultipartForm方法,该访问实现对multiForm的解析,并可以获得文件类型的数组,然后遍历文件切片并调用SaveUploadFile保存文件,查看该方法内容,如图:

MultipartForm方法返回两个参数,我们查看multipart的结构体Form,如图:

结构体Form的字段类型都是map类型,第二个字段File获取就是上传的文件内容,key为上传时指定的名,值为multipart.FileHeader结构体指针切片(上方有FileHeader说明)
1、下面例子定义多文件上传,如图:

通过postman请求,如图:

请求后查看磁盘目录/tmp/linshi,已经上传成功,如图:

2、定义多文件上传,并判断是否上传了文件以及文件大小,如下:
package main
import (
"log"
"net/http"
"path"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
router.POST("/upload", func(c *gin.Context) {
mf, err := c.MultipartForm() //判断是否上传了文件
if len(mf.File) == 0 {
ginJSON(c, "请至少选择一个文件上传")
}
if err != nil {
log.Fatal(err)
}
for _, v := range mf.File["file"] {
if v.Size > 4194304 { //判断文件是否过大
ginJSON(c, "文件过大,不能超过4M,请重新选择!!")
}
dst := path.Join("/tmp/test", v.Filename)
err := c.SaveUploadedFile(v, dst)
if err != nil {
log.Fatal(err)
}
ginJSON(c, "文件上传成功")
}
})
router.Run()
}
func ginJSON(c *gin.Context, s string) {
c.JSON(http.StatusOK, gin.H{
"code": 1000,
"data": s,
})
}
四、文件下载
文件下载可借助GET()方法以及File()方法来实现
1、下面例子表示通过GET()接口下载/tmp目录下的文件a.txt,如下:
package main
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
//定义动态路由
router.GET("/getFile/:filename", func(c *gin.Context) {
f := c.Param("filename")
filePath := "/tmp/" + f
_, err := os.Stat(filePath)
if os.IsNotExist(err) { //判断文件是否存在
c.String(http.StatusNotFound, "file not found")
return
}
//Content-Disposition:指示客户端如何处理响应正文。在这种情况下,它告诉客户端将响应正文作为附件进行下载,并指定下载文件名
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", f))
//Content-Description:描述了响应正文的性质,通常用于指定文件下载
c.Header("Content-Description", "File Transfer")
//Content-Type:指示响应正文的MIME类型。在这种情况下,它指定文件的二进制数据流
c.Header("Content-Type", "application/octet-stream")
//将本地文件作为HTTP响应的正文发送给客户端
c.File(filePath)
})
router.Run()
}
运行后通过http://ip:8080/getFile/a.txt即可下载/tmp目录下的a.txt文件
2、下面例子表示批量下载文件后打包为zip压缩包并下载,如下:
package main
import (
"archive/zip"
"io"
"log"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
//定义动态路由
router.GET("/getFile", func(c *gin.Context) {
fileslice := []string{"/tmp/linshi/a.txt", "/tmp/linshi/b.txt", "/tmp/linshi/c.txt"}
//Content-Disposition:指示客户端如何处理响应正文。在这种情况下,它告诉客户端将响应正文作为附件进行下载,并指定下载文件名
c.Header("Content-Disposition", "attachment; filename=download.zip")
//Content-Description:描述了响应正文的性质,通常用于指定文件下载
c.Header("Content-Description", "File Transfer")
//Content-Type:指示响应正文的MIME类型。在这种情况下,它指定文件的二进制数据流
c.Header("Content-Type", "application/octet-stream")
//创建zip文件写入器,注意,是需要关闭的
zipWriter := zip.NewWriter(c.Writer)
defer zipWriter.Close()
for _, v := range fileslice {
file, err := os.Open(v)
if err != nil {
log.Fatal(err)
}
fileinfo, err := file.Stat()
if err != nil {
log.Fatal(err)
}
//描述zip文件中的文件或目录的元数据信息
header := &zip.FileHeader{
Name: fileinfo.Name(),
Method: zip.Deflate, //压缩方法,0为不压缩,8为使用DEFLATE算法压缩,zip.Deflate就是使用8来压缩
}
//向zip文件中添加一个具有指定元数据的文件或目录,返回io.Writer接口
wr, err := zipWriter.CreateHeader(header)
if err != nil {
log.Fatal(err)
}
//将file的内容复制到wr中,最后关闭file
_, err = io.Copy(wr, file)
if err != nil {
log.Fatal(err)
}
defer file.Close()
}
})
router.Run()
}
浏览器请求http://ip:8080/getFile将把三个文件打进download.zip中并下载出来


