golang gin框架实现大文件的流式上传功能

目录
  • upload.html
  • gin_stream_upload_file.go

一般来说,通过c.Request.FormFile()获取文件的时候,所有内容都全部读到了内存。如果是个巨大的文件,则可能内存会爆掉;且,有的时候我们需要一边上传一边处理。
以下的代码实现了大文件流式上传。
还非常不完美,但是可以作为参考:

upload.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload file</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="/gin_upload">
    <input type="file" name="ff" multiple="multiple"/><br/>
    <input type="submit" value="提交"/>
</form>
</body>

gin_stream_upload_file.go

/*
本例子实现了gin框架下的多个大文件流式上传,避免了文件内容存在内存而无法支持大文件的情况
*/
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"os"
	"bytes"
	"io"
	"log"
	"strconv"
	"strings"
)

/// 解析多个文件上传中,每个具体的文件的信息
type FileHeader struct{
	ContentDisposition string
	Name string
	FileName string			///< 文件名
	ContentType string
	ContentLength int64
}

/// 解析描述文件信息的头部
/// @return FileHeader 文件名等信息的结构体
/// @return bool 解析成功还是失败
func ParseFileHeader(h []byte) (FileHeader, bool){
	arr := bytes.Split(h, []byte("\r\n"))
	var out_header FileHeader
	out_header.ContentLength = -1
	const (
		CONTENT_DISPOSITION = "Content-Disposition: "
		NAME = "name=\""
		FILENAME = "filename=\""
		CONTENT_TYPE = "Content-Type: "
		CONTENT_LENGTH = "Content-Length: "
	)
	for _,item := range arr{
		if bytes.HasPrefix(item, []byte(CONTENT_DISPOSITION)){
			l := len(CONTENT_DISPOSITION)
			arr1 := bytes.Split(item[l:], []byte("; "))
			out_header.ContentDisposition = string(arr1[0])
			if bytes.HasPrefix(arr1[1], []byte(NAME)){
				out_header.Name = string(arr1[1][len(NAME):len(arr1[1])-1])
			}
			l = len(arr1[2])
			if bytes.HasPrefix(arr1[2], []byte(FILENAME)) && arr1[2][l-1]==0x22{
				out_header.FileName = string(arr1[2][len(FILENAME):l-1])
			}
		} else if bytes.HasPrefix(item, []byte(CONTENT_TYPE)){
			l := len(CONTENT_TYPE)
			out_header.ContentType = string(item[l:])
		} else if bytes.HasPrefix(item, []byte(CONTENT_LENGTH)){
			l := len(CONTENT_LENGTH)
			s := string(item[l:])
			content_length,err := strconv.ParseInt(s, 10, 64)
			if err!=nil{
				log.Printf("content length error:%s", string(item))
				return out_header, false
			} else {
				out_header.ContentLength = content_length
			}
		} else {
			log.Printf("unknown:%s\n", string(item))
		}
	}
	if len(out_header.FileName)==0{
		return out_header,false
	}
	return out_header,true
}

/// 从流中一直读到文件的末位
/// @return []byte 没有写到文件且又属于下一个文件的数据
/// @return bool 是否已经读到流的末位了
/// @return error 是否发生错误
func ReadToBoundary(boundary []byte, stream io.ReadCloser, target io.WriteCloser)([]byte, bool, error){
	read_data := make([]byte, 1024*8)
	read_data_len := 0
	buf := make([]byte, 1024*4)
	b_len := len(boundary)
	reach_end := false
	for ;!reach_end; {
		read_len, err := stream.Read(buf)
		if err != nil {
			if err != io.EOF && read_len<=0 {
				return nil, true, err
			}
			reach_end = true
		}
		//todo: 下面这一句很蠢,值得优化
		copy(read_data[read_data_len:], buf[:read_len])  //追加到另一块buffer,仅仅只是为了搜索方便
		read_data_len += read_len
		if (read_data_len<b_len+4){
			continue
		}
		loc := bytes.Index(read_data[:read_data_len], boundary)
		if loc>=0{
			//找到了结束位置
			target.Write(read_data[:loc-4])
			return read_data[loc:read_data_len],reach_end, nil
		}

		target.Write(read_data[:read_data_len-b_len-4])
		copy(read_data[0:], read_data[read_data_len-b_len-4:])
		read_data_len = b_len + 4
	}
	target.Write(read_data[:read_data_len])
	return nil, reach_end, nil
}

/// 解析表单的头部
/// @param read_data 已经从流中读到的数据
/// @param read_total 已经从流中读到的数据长度
/// @param boundary 表单的分割字符串
/// @param stream 输入流
/// @return FileHeader 文件名等信息头
///			[]byte 已经从流中读到的部分
///			error 是否发生错误
func ParseFromHead(read_data []byte, read_total int, boundary []byte, stream io.ReadCloser)(FileHeader, []byte, error){
	buf := make([]byte, 1024*4)
	found_boundary := false
	boundary_loc := -1
	var file_header FileHeader
	for {
		read_len, err := stream.Read(buf)
		if err!=nil{
			if err!=io.EOF{
				return file_header, nil, err
			}
			break
		}
		if read_total+read_len>cap(read_data){
			return file_header, nil, fmt.Errorf("not found boundary")
		}
		copy(read_data[read_total:], buf[:read_len])
		read_total += read_len
		if !found_boundary {
			boundary_loc = bytes.Index(read_data[:read_total], boundary)
			if -1 == boundary_loc {
				continue
			}
			found_boundary = true
		}
		start_loc := boundary_loc+len(boundary)
		file_head_loc := bytes.Index(read_data[start_loc:read_total], []byte("\r\n\r\n"))
		if -1==file_head_loc{
			continue
		}
		file_head_loc += start_loc
		ret := false
		file_header,ret = ParseFileHeader(read_data[start_loc:file_head_loc])
		if !ret{
			return file_header,nil,fmt.Errorf("ParseFileHeader fail:%s", string(read_data[start_loc:file_head_loc]))
		}
		return file_header, read_data[file_head_loc+4:read_total], nil
	}
	return file_header,nil,fmt.Errorf("reach to sream EOF")
}

func main(){
	log.SetFlags(log.LstdFlags | log.Lshortfile)
	r := gin.Default()
	r.StaticFile("/upload.html", "./upload.html")

	r.POST("/gin_upload", func(c *gin.Context) {
		var content_length int64
		content_length = c.Request.ContentLength
		if content_length<=0 || content_length>1024*1024*1024*2{
			log.Printf("content_length error\n")
			return
		}
		content_type_,has_key := c.Request.Header["Content-Type"]
		if  !has_key{
			log.Printf("Content-Type error\n")
			return
		}
		if len(content_type_)!=1{
			log.Printf("Content-Type count error\n")
			return
		}
		content_type := content_type_[0]
		const BOUNDARY string = "; boundary="
		loc := strings.Index(content_type, BOUNDARY)
		if -1==loc{
			log.Printf("Content-Type error, no boundary\n")
			return
		}
		boundary := []byte(content_type[(loc+len(BOUNDARY)):])
		log.Printf("[%s]\n\n", boundary)
		//
		read_data := make([]byte, 1024*12)
		var read_total int = 0
		for {
			file_header, file_data, err := ParseFromHead(read_data, read_total, append(boundary, []byte("\r\n")...), c.Request.Body)
			if err != nil {
				log.Printf("%v", err)
				return
			}
			log.Printf("file :%s\n", file_header.FileName)
			//
			f, err := os.Create(file_header.FileName)
			if err != nil {
				log.Printf("create file fail:%v\n", err)
				return
			}
			f.Write(file_data)
			file_data = nil

			//需要反复搜索boundary
			temp_data, reach_end, err := ReadToBoundary(boundary, c.Request.Body, f)
			f.Close()
			if err != nil {
				log.Printf("%v\n", err)
				return
			}
			if reach_end{
				break
			} else {
				copy(read_data[0:], temp_data)
				read_total = len(temp_data)
				continue
			}
		}
		//
		c.JSON(200, gin.H{
			"message": fmt.Sprintf("%s", "ok"),
		})
	})
	r.Run()
}

到此这篇关于golang gin框架中实现大文件的流式上传的文章就介绍到这了,更多相关golang 大文件流式上传内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang Gin框架实现文件下载功能的示例代码

    目录 Layui框架实现文件上传 Gin框架获取前端上传的文件 Gin框架的文件下载 Layui框架实现文件上传 基本的思路就是随便创建一个元素,然后使用layui的upload组件对创建的元素进行渲染,详见代码 <!DOCTYPE html> <html lang="en"> <head> <script src="jquery-3.5.0.min.js" type="text/javascript"&

  • golang Gin上传文件返回前端及中间件实现示例

    目录 上传文件 文件返回给前端 中间件 中间件调用两种方式 单个中间件 多个中间件 上传文件 package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() // 给表单限制上传大小 (默认 32 MiB) // router.MaxMultipartMemory = 8 << 20 // 8 MiB router.POST(&q

  • Golang详细讲解常用Http库及Gin框架的应用

    目录 1. Http标准库 1.1 http客户端 1.2 自定义请求头 1.3 检查请求重定向 1.4 http服务器性能分析 2. JSON数据处理 2.1 实体序列化 2.2 处理字段为小写下划线 2.3 省略空字段 2.4 反序列化 3. 自然语言处理 3.1 使用Map处理 3.2 定义实体处理 4. http框架 4.1 gin 4.1.1 启动服务 4.1.2 middleware 4.1.3 设置请求ID 1. Http标准库 1.1 http客户端 func main() {

  • golang gin框架实现大文件的流式上传功能

    目录 upload.html gin_stream_upload_file.go 一般来说,通过c.Request.FormFile()获取文件的时候,所有内容都全部读到了内存.如果是个巨大的文件,则可能内存会爆掉:且,有的时候我们需要一边上传一边处理.以下的代码实现了大文件流式上传.还非常不完美,但是可以作为参考: upload.html <!DOCTYPE html> <html lang="en"> <head> <meta charse

  • 美图秀秀web开放平台--PHP流式上传和表单上传示例分享

    废话少说,直接上代码: <?php /** * Note:for octet-stream upload * 这个是流式上传PHP文件 * Please be amended accordingly based on the actual situation */ $post_input = 'php://input'; $save_path = dirname(__FILE__); $postdata = file_get_contents($post_input); if (isset($p

  • python中watchdog文件监控与检测上传功能

    引言 上一篇介绍完了观察者模式的原理,本篇想就此再介绍一个小应用,虽然我也就玩了一下午,是当时看observer正好找到的,以及还有Django-observer,但Django很久没用了,所以提下这个作为一个笔记. watchdog介绍 Watchdog的中文的"看门狗",有保护的意思.最早引入Watchdog是在单片机系统中,由于单片机的工作环境容易受到外界磁场的干扰,导致程序"跑飞",造成整个系统无法正常工作,因此,引入了一个"看门狗",对

  • 配置php.ini实现PHP文件上传功能

    昨天分享了在PHP网站开发中如何在php.ini中配置实现session功能的PHP教程,今天继续分享在利用PHP实现文件上传功能时几点关键php.ini的配置. 说到在php.ini中的文件上传的配置,其实在之前介绍PHP文件上传功能代码实例教程以及Jquery AjaxUpload实现文件上传功能代码实例教程时我都有所提及.PHP文件上传功能配置主要涉及php.ini配置文件中的upload_tmp_dir.upload_max_filesize.post_max_size等选项. php.

  • Asp.Net上传文件并配置可上传大文件的方法

    ASP.NET 包含两个控件可以使用户向网页服务器上传文件.一旦服务器接受了上传的文件数据,那么应用程序就可以进行保存,进行检查或者忽略它. HtmlInputFile - HTML 服务器控件 FileUpload - ASP.NET 网页控件 两种控件都允许文件上传,但是 FileUpload 控件自动设置编码格式,然而 HtmlInputFile 控件并不会这样. 1.使用HtmlInputFile文件上传 前台 <form enctype="multipart/form- data

  • 使用jQuery.form.js/springmvc框架实现文件上传功能

    使用的技术有jquery.form.js框架, 以及springmvc框架.主要实现异步文件上传的同时封装对象,以及一些注意事项. 功能本身是很简单的,但是涉及到一些传递参数类型的问题.例如:jquery的ajax方法与jquery.form.js中的ajaxSubmit方法的参数,具体细节将在下一篇博客中分享. 重点: html表格三要素: action="fileUpload/fileUpload" method="post" enctype="mul

  • Spring框架实现文件上传功能

    在Java中实现文件的上传有多种方式,如smartUpload或是使用Strus2,本文与大家分享使用Spring框架中的MultipartFile类来实例文件的上传. 不啰嗦了,直接上干货.先是编写了一个实现文件上传的类FileUploadingUtil,此类中定义了两个对外公开的方法,upload和getFileMap. 前者需要传入一个Map参数,是用户提交的表单中的文件列表,最终返回值的也是一个Map类型对象,其键名为上传的文件名称,键值为文件在服务器上的存储路径:后者主要是用于测试用途

  • Struts2实现文件上传功能

    Servlet 3.0规范的HttpServletRequest已经提供了方法来处理文件上传但这种上传需要在Servlet中完成.而Struts2则提供了更简单的封装. Struts2默认使用的是Jakarta的Common-FileUpload的文件上传框架,因此使用Struts2的文件上传功能,则需要添加两个jar包,即commons-io-2.2.jar和commons-fileupload-1.3.1.jar. Struts2简单文件上传示例: 1.文件上传页面 为了能上传文件,表单的m

  • 基于nodejs+express(4.x+)实现文件上传功能

    Nodejs是一个年轻的编程框架,充满了活力和无限激情,一直都在保持着快速更新.基于Nodejs的官方Web开发库Express也在同步发展着,每年升级一个大版本,甚至对框架底层都做了大手术.在Express4时,替换掉中件间库connect,而改用多个更细粒度的库来取代.带来的好处是明显地,这些中间件能更自由的更新和发布,不会受到Express发布周期的影响:但问题也是很的棘手,不兼容于之前的版本,升级就意味着要修改代码. 通过一段时间的查阅资料.摸索,我发现实现上传的方式有:1.expres

随机推荐