golang 实现文件分块上传

该代码示例展示了如何在Go中进行分块上传文件,包括计算文件大小,定义每块文件大小,读取并上传文件块,以及在上传失败时的重试机制。使用了`multipart`包创建表单数据,通过HTTPPost方法提交到指定URL,并处理上传响应以获取文件ID和URL。
摘要由CSDN通过智能技术生成

分块上传主要逻辑如下:

1、计算文件总大小

2、定义每块文件的大小

3、计算出总共有多少块

4、读取每块文件,分批次上传

5、上传失败时,添加重试上传机制

涉及到的代码如下:

package utils

import (
	"NativeAndroidProxyQS865V2/logconfig"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"mime/multipart"
	"net/http"
	"os"
)

var fileId int32 = 0
var fileUrl string

/*
*
分块上传文件
*/
func UploadFile(fileName string, fileDir string, uploadUrl string) (string, bool) {

	logconfig.SugarLogger.Infof("begin UploadFile,fileName=%s,fileDir=%s,uploadUrl=%s", fileName, fileDir, uploadUrl)
	//const fileChunk = 4 << 20 // 4MB

	fileChunk := 10 * 1024 * 1024 //10MB
	uploadCount := 1 //上传次数

	//打开文件句柄操作
	file, err := os.Open(fileDir + fileName)
	if err != nil {
		fmt.Println("error opening file")
		logconfig.SugarLogger.Errorf("PostFile() opening file err:%v", err)
		return "", false
	}
	defer file.Close()

	fi, err := file.Stat()
	if err != nil {
		logconfig.SugarLogger.Errorf("PostFile() file.Stat err:%v", err)
		return "", false
	}

	//文件块数
	numChunks := int(math.Ceil(float64(fi.Size()) / float64(fileChunk)))

	//计算上传文件md5
	getMd5Cmd := fmt.Sprintf("md5sum %s | awk '{print $1}'", fileDir+fileName)
	fileMd5Value, _ := LocalCommand(getMd5Cmd, "PostFile")
	logconfig.SugarLogger.Infof("PostFile() file=%s,md5=%s", fileDir+fileName, fileMd5Value)

	//文件切割
	for i := 0; i < numChunks; i++ {
		uploadCount = i + 1
		bodyBuf := &bytes.Buffer{}
		bodyWriter := multipart.NewWriter(bodyBuf)

		chunkSize := int(math.Min(float64(fileChunk), float64(fi.Size()-int64(i*fileChunk))))

		//计算单次要上传的文件
		buf := make([]byte, chunkSize)
		_, err := file.ReadAt(buf, int64(i*fileChunk))
		if err != nil && err != io.EOF {
			panic(err)
		}

		//关键的一步操作,这里写入每次要上传的文件
		fileWriter, err := bodyWriter.CreateFormFile("file", fileName)
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--file  err:%v", err)
			return "", false
		}

		_, err = io.Copy(fileWriter, bytes.NewReader(buf))
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--file Copy err:%v", err)
			return "", false
		}

		currentBlockWriter, err := bodyWriter.CreateFormField("currentBlock")
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--currentBlock  err:%v", err)
			return "", false
		}
		currentBlockValue, _ := json.Marshal(i + 1)
		io.Copy(currentBlockWriter, bytes.NewReader(currentBlockValue))

		totalBlockWriter, err := bodyWriter.CreateFormField("totalBlock")
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--totalBlock  err:%v", err)
			return "", false
		}
		totalBlockValue, _ := json.Marshal(numChunks)
		io.Copy(totalBlockWriter, bytes.NewReader(totalBlockValue))

		totalSizeWriter, err := bodyWriter.CreateFormField("totalSize")
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--totalSize  err:%v", err)
			return "", false
		}
		totalSizeValue, _ := json.Marshal(fi.Size())
		io.Copy(totalSizeWriter, bytes.NewReader(totalSizeValue))

		//上传类型 1:应用 2:文件
		typeWriter, err := bodyWriter.CreateFormField("type")
		if err != nil {
			logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--type  err:%v", err)
			return "", false
		}
		typeValue, _ := json.Marshal(2)
		io.Copy(typeWriter, bytes.NewReader(typeValue))

		if fileId != 0 {
			idWriter, err := bodyWriter.CreateFormField("id")
			if err != nil {
				logconfig.SugarLogger.Errorf("PostFile() CreateFormFile--id  err:%v", err)
				return "", false
			}
			idValue, _ := json.Marshal(fileId)
			io.Copy(idWriter, bytes.NewReader(idValue))
		}

		bodyWriter.WriteField("fileName", fileName)

		bodyWriter.WriteField("md5", fileMd5Value)

		contentType := bodyWriter.FormDataContentType()
		bodyWriter.Close()

		logconfig.SugarLogger.Infof("PostFile() 开始上传第%d段", i+1)

        //上传失败重传机制
		isUploadSuccess, postErr := postFileForm(uploadUrl, contentType, bodyBuf)
		if isUploadSuccess == false || postErr != nil {
			for num := 1; num < 4; num++ {
				logconfig.SugarLogger.Errorf("PostFile() 开始上传第%d段--fail,第%d次重试", i+1, num)
				isUploadSuccess, postErr := postFileForm(uploadUrl, contentType, bodyBuf)
				if isUploadSuccess == true && postErr == nil {
					break
				} else {
					if num == 3 {
						return fileUrl, numChunks == uploadCount
					}
				}
			}
		}
	}

	return fileUrl, numChunks == uploadCount
}

//post上传文件

func postFileForm(uploadUrl string, contentType string, bodyBuf *bytes.Buffer) (bool, error) {
	resp, err := http.Post(uploadUrl, contentType, bodyBuf)
	if err != nil {
		logconfig.SugarLogger.Errorf("postFileForm() Post Err:%v", err)
		return false, err
	}

	defer resp.Body.Close()

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logconfig.SugarLogger.Errorf("postFileForm() ReadAll Err:%v", err)
		return false, err
	}

	logconfig.SugarLogger.Infof("resp.Status:%v,,,消息返回体%s", resp.Status, string(respBody))
    //解析返回的消息
	uploadFileCallback := &UploadFileCallback{}
	parseErr := json.Unmarshal(respBody, uploadFileCallback)
	if parseErr != nil {
		logconfig.SugarLogger.Errorf("postFileForm() parseErr:%v", parseErr)
		return false, parseErr
	}

	//0:成功、1:失败
	if uploadFileCallback.Status == 0 {
		fileId = uploadFileCallback.ResponseData.Id
		fileUrl = uploadFileCallback.ResponseData.Url
		logconfig.SugarLogger.Infof("postFileForm() fileId:%d", fileId)
		return true, nil
	} else {
		logconfig.SugarLogger.Errorf("postFileForm() uploadFileCallback.Status callback fail,status!=0")
		return false, nil
	}
}

type UploadFileCallback struct {
	Status       int16        `json:"status"`
	Msg          string       `json:"msg"`
	RetCode      int16        `json:"retCode"`
	ResponseData ResponseData `json:"data"`
}

type ResponseData struct {
	Id  int32  `json:"id"`
	Url string `json:"url"`
}

注:post表单上传涉及多种类型参数时,WriteField()只能传递string类型值,其他类型值用CreateFormField()方式,使用方法参照上面代码

基于go的大文件切片上传、断点续传、秒传.zip 1、如何唯一标示一个文件文件的信息后端会存储在mysql数据库表中。 在上传之前,前端通过 spark-md5.js 计算文件的md5值以此去唯一的标示一个文件。 spark-md5.js 地址:satazor/js-spark-md5 README.md中有spark-md5.js的使用demo,可以去看看。 2、断点续传是如何实现的? 断点续传可以实现这样的功能,比如RD上传200M的文件,当用户上传完199M时,断网了,有了断点续传的功能,我们允许RD再次上传时,能从第199M的位置重新上传实现原理: 实现断点续传的前提是,大文件切片上传。然后前端得问后端哪些chunk曾经上传过,让前端跳过这些上传过的chunk就好了。 前端的上传器(uploader.js)在上传时会先发送一个GET请求,这个请求不会携带任何chunk数据,作用就是向后端询问哪些chunk曾经上传过。 后端会将这些数据保存在mysql数据库表中。比如按这种格式:1:2:3:5表示,曾经上传过的分片有1,2,3,5。第四片没有被上传,前端会跳过1,2,3,5。 仅仅会将第四个chunk发送给后端。 3、秒传是如何实现的? 秒传实现的功能是:当RD重复上传一份相同的文件时,除了第一次上传会正常发送上传请求后,其他的上传都会跳过真正的上传,直接显示秒成功。 实现方式: 后端存储着当前文件的相关信息。为了实现秒传,我们需要搞一个字段(isUploaded)表示当前md5对应的文件是否曾经上传过。 后端在处理 前端的上传器(uploader.js)发送的第一个GET请求时,会将这个字段发送给前端,比如 isUploaded = true。前端看到这个信息后,直接跳过上传,显示上传成功。 4、上传暂停是如何实现的? 上传的暂停:并不是去暂停一个已经发送出去的正在进行数据传输的http请求~ 而是暂停发送起发送下一个http请求。 就我们的项目而言,因为我们的文件本来就是先切片,对于我们来说,暂停文件上传,本质上就是暂停发送下一个chunk。 5、前端上传并发数是多少? 前端的uploader.js中默认会三条线程启动并发上传,前端会在同一时刻并发 发送3个chunk,后端就会相应的为每个请求开启三个协程处理上传的过来的chunk。 在我们的项目中,会将前端并发数调整成了1。原因如下: 因为考虑到了断点续传的实现,后端需要记录下曾经上传过哪些切片。(这个记录在mysql的数据库表中,以 ”1:2:3:4:5“ )这种格式记录。 Mysql5.7默认的存储引擎是innoDB,默认的隔离级别是RR。如果我们将前端的并发数调大,就会出现下面的异常情况: 1. goroutine1 获取开启事物,读取当前上传到记录是 1:2 (未提交事物) 2. goroutine1 在现有的记录上加上自己处理的分片3,并和现有的1:2拼接在一起成1:2:3 (未提交事物) 3. goroutine2 获取开启事物,(因为RR,所以它读不到1:2:3)读取当前上传到记录是 1:2 (未提交事物) 4. goroutine1 提交事物,将1:2:3写回到mysql 5. goroutine2 在现有的记录上加上自己处理的分片4,并和现有的1:2拼接在一起成1:2:4 (提交事物) 可以看到,如果前端并发上传,后端就会出现分片丢失的问题。 故前端将并发数置为1。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

粤M温同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值