通过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中并下载出来

标签