golang断点续传
一、原理
把文件以固定大小拆分为多个分片,每次上传一个文件分片。我以每个分片2M(2*1024*1024)
我把功能分为两个接口:
1.检查文件上传状态接口
2.上传文件分片接口
二、接口描述
1.检查文件上传状态接口
描述:根据上传文件的md5和文件大小获取上传状态
类型:post
请求参数:
参数名 | 类型 | 是否必填 | 描述 |
md5 | string | 是 | 文件MD5 |
file_len | int64 | 是 | 文件长度 |
返回参数:
参数名 | 类型 | 是否必填 | 描述 |
countIndex | int64 | 是 | 一共有多少个分片 |
dataId | int64 | 是 | 数据记录ID |
fileSize | int64 | 是 | 文件真实大小 |
startIndex | int64 | 是 | 当前上传第几个分片 |
status | int | 否 |
例子:
{
"code": 200,
"data": {
"countIndex": 1,
"dataId": 36,
"fileSize": 1206023,
"startIndex": 0,
"status": 0
},
"msg": "Success"
}
2.上传文件分片接口
描述:每个分片2M(2*1024*1024)每次上传一个分片
类型:post
请求参数:
参数名 | 类型 | 是否必填 | 描述 |
md5 | string | 是 | 文件MD5 |
data | blob | 是 | 文件分片 |
fileName | string | 是 | 文件名 |
fileSize | int64 | 是 | 文件长度 |
shardSize | int64 | 是 | 分片大小 |
shardCount | int64 | 是 | 分片总数 |
indexCurrent | int64 | 是 | 当前第几个分片 |
dataId | int64 | 是 | 数据记录ID |
返回参数:
参数名 | 类型 | 是否必填 | 描述 |
code | int64 | 是 | 状态码,区分上分片上传成功,还是全部上传成功 |
三、代码
前端代码
upload.vue
<template>
<a-modal
title="上传"
:visible="visible"
:confirm-loading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-upload name="file" id="uploadFile" @beforeUpload="customRequest" @change="handleChange" >
<a-button>上传文件</a-button>
</a-upload>
</a-modal>
</template>
<script>
import {isUpload} from "./model"
import SparkMD5 from 'spark-md5'
export default {
name: "upload",
data() {
return {
visible: true,
loading: false,
uploadFile:null,
md5File:"",
fileSize:0
};
},
methods: {
handleOk() {
var vm = this;
this.visible = true;
console.log(vm.md5File);
isUpload({
"md5":vm.md5File,
"file_len":vm.fileSize
}).done(res=>{
console.log("res:"+JSON.stringify(res));
if(res.code==200){
if(res.data.status==0){
this.postFile(vm.uploadFile.file,res.data.startIndex,res.data.dataId);
}else if(res.data.status==1){
this.visible = false;
}
}
});
},
getToken(i) {
return '?token=' + (localStorage.token || '')+"&i="+i;
},
async postFile(file,i,dataId){
var vm = this;
var name = file.name, //文件名
size = file.size, //总大小shardSize = 2 * 1024 * 1024,
shardSize = 2 * 1024 * 1024, //以2MB为一个分片,每个分片的大小
shardCount = Math.ceil(size / shardSize); //总片数
if(i >= shardCount){
return;
}
//console.log(size,i+1,shardSize); //文件总大小,第一次,分片大小//
var start = i * shardSize ;
var end = start + shardSize;
end = end>size?size:end;
var packet = file.originFileObj.slice(start, end); //将文件进行切片
/* 构建form表单进行提交 */
var form = new FormData();
form.append("md5", vm.md5File)
form.append("data", packet); //slice方法用于切出文件的一部分
form.append("fileName", encodeURI(name));
form.append("fileSize", size);
form.append("shardSize", shardSize);
form.append("shardCount", shardCount); //总片数
form.append("indexCurrent", i + 1); //当前是第几片
form.append("dataId", dataId); //当前是第几片
await vm.uploadAjax(form,file,i,dataId);
form = '';
},
uploadAjax(form,file,i,dataId){
var vm = this;
$.ajax({
url: "./datasource/file/uploadContinuation",
type: "POST",
data: form,
//timeout:"10000", //超时10秒
async: true, //异步
dataType:"json",
processData: false, //很重要,告诉jquery不要对form进行处理
contentType: false, //很重要,指定为false才能形成正确的Content-Type
success: function (msg) {
console.log(msg);
if (msg.code == -400253) {
i++;
console.log("request upload i:"+i);
vm.postFile(file, i,dataId);
} else if (msg.code == -400254) {
/* 失败后,每2秒继续传一次分片文件 */
setInterval(function () { vm.postFile(file, i,dataId) }, 2000);
}else if (msg.code == -400255) {
/* 上传完成后,md5比对失败*/
alert("上传失败");
}else if (msg.code == 200) {
vm.visible = false;
alert("上传成功");
} else if (msg.code == 500) {
console.log('第'+msg.i+'次,上传文件有误!');
} else {
console.log('未知错误');
}
},
error:function(msg){
console.log("request upload failed:"+msg);
vm.uploadAjax(form,file,i,dataId);
}
})
},
handleCancel() {
this.visible = false;
},
fileChange(file) {
var vm = this;
vm.fileSize= file.size
var fileReader=new FileReader()
var Spark=new SparkMD5.ArrayBuffer();
fileReader.readAsArrayBuffer(file)
fileReader.onload=function(e){
Spark.append(e.target.result)
vm.md5File=Spark.end()
}
},
customRequest(data){
return false;
},
handleChange(info) {
var vm = this;
vm.uploadFile=info;
vm.fileChange(vm.uploadFile.file.originFileObj);
}
},
};
</script>
<style lang="less" scoped>
</style>
model.js
'use strict';
import http from '../../commons/js/http.js'
define([], function() {
var isUpload = function(param) {
return http.postForm("./file/uploadStatus",param);
};
return {
isUpload:isUpload
};
});
后台代码
action.go
// @Summary 数据源文件上传状态
// @Tags 数据导入
// @Produce json
// @Param file_path formData string true "文件路径"
// @Success 200 {object} data_type.Response
// @Router /upload/start [post]
func (ds *DataSource) SourceUploadStatus(context *gin.Context) {
md5 := context.GetString(consts.ValidatorPrefix + "md5")
fileLen, _ := strconv.ParseInt(context.GetString(consts.ValidatorPrefix+"file_len"), 10, 64)
fmt.Println("start upload status,md5: ", md5, ",fileSize:", fileLen)
fileData := data_source.Md5ByUploadFile(md5, fileLen)
dataId := data_source.SaveDataSource()
fileData["dataId"] = dataId
response.Success(context, consts.CurdStatusOkMsg, fileData)
}
// @Summary 数据源文件上传
// @Tags 数据导入
// @Produce json
// @Param file_path formData string true "文件路径"
// @Success 200 {object} data_type.Response
// @Router /upload/start [post]
func (ds *DataSource) SourceUploadContinuation(context *gin.Context) {
fmt.Println("start upload continuation.......")
formData, err := context.MultipartForm()
if err != nil {
response.Fail(context, consts.FilesUploadPartFailCode, consts.FilesUploadPartFialMsg, "")
return
}
partData := formData.File["data"][0]
md5 := context.GetString(consts.ValidatorPrefix + "md5")
fileName := context.GetString(consts.ValidatorPrefix + "fileName")
fileSize := context.GetFloat64(consts.ValidatorPrefix + "fileSize")
shardSize := context.GetFloat64(consts.ValidatorPrefix + "shardSize")
shardCount := context.GetFloat64(consts.ValidatorPrefix + "shardCount")
indexCurrent := context.GetFloat64(consts.ValidatorPrefix + "indexCurrent")
dataId := context.GetFloat64(consts.ValidatorPrefix + "dataId")
fmt.Printf("fileName:%s ,md5:%s ,fileSize:%f ,shardSize:%f , shardCount:%f ,indexCurrent:%f ", fileName, md5, fileSize, shardSize, shardCount, indexCurrent)
filePath := data_source.Md5ByUploadFileSavePart(md5, fileSize, shardSize, shardCount, indexCurrent, partData)
if shardCount == indexCurrent {
//获取MD5
fileMD5 := data_source.GetFileMD5(filePath)
//md5 比较
if strings.Compare(fileMD5, md5) == 0 {
fileNameResult := filePath + "_" + fileName
os.Rename(filePath, fileNameResult)
//to_do 更新数据库记录和状态
response.Success(context, consts.FilesUploadingMsg, "")
return
} else {
os.Remove(filePath)
}
response.Interactive(context, consts.FilesUploadPartFinishFailCode, consts.FilesUploadPartFinishFialMsg, "")
} else {
response.Interactive(context, consts.FilesUploadingCode, consts.FilesUploadingMsg, "")
}
}
data_source.go
func Md5ByUploadFile(md5 string, fileLen int64) map[string]interface{} {
savePath := variable.ConfigYml.GetString("UploadFile.savePath")
filePath := path.Join(savePath, md5)
fmt.Println(filePath)
exists, err := files.PathExists(filePath)
countIndex := (fileLen / consts.UploadFilePartSize) + 1
file_status := make(map[string]interface{})
file_status["startIndex"] = 0
file_status["fileSize"] = fileLen
file_status["countIndex"] = countIndex
file_status["status"] = 0
if err == nil || exists {
file_size := files.FileSize(filePath)
startIndex := file_size / consts.UploadFilePartSize
if startIndex == 0 {
file_status["startIndex"] = 0
} else {
file_status["startIndex"] = startIndex - 1
}
}
return file_status
}
func Md5ByUploadFileSavePart(md5 string, fileSize float64, shardSize float64, shardCount float64, indexCurrent float64, partData *multipart.FileHeader) string {
savePath := variable.ConfigYml.GetString("UploadFile.savePath")
filePath := path.Join(savePath, md5)
fmt.Println(filePath)
start := (indexCurrent - 1) * shardSize
WriteFile(partData, filePath, start)
return filePath
}
func GetFileMD5(pathName string) string {
f, err := os.Open(pathName)
if err != nil {
fmt.Println("Open", err)
return ""
}
defer f.Close()
md5hash := md5.New()
if _, err := io.Copy(md5hash, f); err != nil {
fmt.Println("Copy", err)
return ""
}
has := md5hash.Sum(nil)
md5str := fmt.Sprintf("%x", has)
return md5str
}
/*
*
保存文件
*/
func WriteFile(partData *multipart.FileHeader, filePath string, start float64) (string, error) {
filepoint, err := partData.Open() //打开文件
if err != nil {
return "", err
}
defer filepoint.Close()
//创建新文件进行存储
newfile, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return "", err
}
defer newfile.Close()
newfile.Seek(int64(start), 0)
//把旧文件的内容放入新文件
var context []byte = make([]byte, 1024)
for {
n, err := filepoint.Read(context)
newfile.Write(context[:n])
if err != nil {
if err == io.EOF {
return filePath, nil
} else {
return "", err
}
}
}
}