目前項目中需要上傳大文件,瀏覽器端上傳大文件的常用做法就是分片上傳,項目前端用的是vue element,服務器用的是golang的開源框架 echo。
上傳大文件如果傳到一般斷掉了,在全部重新上傳的話那就太抓狂了,所以不只是分片上傳還要斷點續傳。
前端
既然是vue+element 那肯定就是通過npm + webpack構建的vue 項目了, 什麼是 npm ,什麼是webpack這裡不介紹,網上的介紹很詳細。 首先通過npm安裝 vue-simple-uploader 安裝命令 npm -i vue-simple-uploade r ,vue-simple-uploader 就是一個基於 simple-uploader.js 和 Vue 結合做的一個上傳組件,自帶 UI,可覆蓋、自定義UI,如下圖
具體使用方法
初始化:
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
上傳組件
<template>
<uploader>
:attrs="attrs"
:options="options"
:file-status-text="statusText"
class="uploader-example"
@file-added="onFileAdded"
@file-success="onFileSuccess">
<uploader-unsupport>
<uploader-drop>
<uploader-btn>選擇文件/<uploader-btn>
<uploader-list>
/<uploader>
import SparkMD5 from 'spark-md5'
import { getToken } from '@/utils/auth'
export default {
name: 'LbUploader',
data() {
return {
options: {
target: '//localhost:1323/admin/upload',
testChunks: true,
chunkSize: 5242880,
checkChunkUploadedByResponse: function(chunck, message) {
const objMessage = JSON.parse(message)
if (objMessage.data.uploaded) {
return objMessage.data.uploaded.indexOf((chunck.offset + 1) + '') >= 0
}
return false
},
headers: {
Authorization: 'Bearer ' + getToken()
}
},
attrs: [
'.zip', '.rar'
],
collapse: false,
statusText: {
success: '上傳成功',
error: '出錯了',
uploading: '上傳中',
paused: '暫停中',
waiting: '等待中'
}
}
},
methods: {
onFileAdded(file) {
this.computeMD5(file)
},
onFileSuccess(rootFile, file, response, chunk) {
response = JSON.parse(response)
if (response.data.fileUrl) {
this.$emit('uploadSuccess', response.data.fileUrl)
}
},
/**
* 計算md5,實現斷點續傳及秒傳
*/
computeMD5(file) {
const fileReader = new FileReader()
// const time = new Date().getTime()
let md5 = ''
file.pause()
fileReader.readAsArrayBuffer(file.file)
fileReader.onload = e => {
if (file.size !== e.target.result.byteLength) {
this.error('Browser reported success but could not read the file until the end.')
return
}
md5 = SparkMD5.ArrayBuffer.hash(e.target.result)
file.uniqueIdentifier = md5
file.resume()
}
fileReader.onerror = function() {
this.error('FileReader onerror was triggered, maybe the browser aborted due to high memory usage.')
}
}
}
}
服務端
上傳之前有一個分片檢測是GET 請求,上傳是POST請求,所以都是一個方法裡面處理判斷了一些是get 還是post
服務端就不BB了 直接貼代碼
/分片上傳
func Upload(c echo.Context) error {
var (
chunkNumber int //當前片數
chunkSize int64 //總分片總
currentChunkSize int64 //當前分片文件大小
totalSize int64 //文件總大小
identifier string //文件ID
filename string
totalChunks int //文件總大小
fileHeader *multipart.FileHeader
file multipart.File
dst *os.File
err error
currentPath string //上傳目錄
fileUrl string //保存文件完整路徑
)
currentPath = utils.GetCurrentPath() + global.UPLOADDIR
if len(c.QueryParams()) > 0 { //分片檢測,有上傳的片不上傳,提高上傳效率
identifier = c.QueryParams().Get("identifier")
totalChunks, _ = strconv.Atoi(c.QueryParams().Get("totalChunks"))
_, names := utils.GetDirList(fmt.Sprintf("%s/%s/", currentPath, identifier))
if totalChunks == len(names)-1 {
filename = c.QueryParams().Get("filename")
currentPath := utils.GetCurrentPath() + global.UPLOADDIR
localPath := utils.CreateDateDir(currentPath)
fileUrl = fmt.Sprintf("%s%s%s_%s", localPath, global.SEPARATOR, utils.GetRandomString(12), filename) //文件保存路徑,對文件重命名
mergeFile(fileUrl, identifier)
names = append(names[:0], names[0+1:]...)
return utils.ResponseSuccess(c, map[string]interface{}{"uploaded": names, "fileUrl": strings.Replace(strings.Replace(fileUrl, currentPath, "", 1), "\\\\", "/", -1)})
}
return utils.ResponseSuccess(c, map[string]interface{}{"uploaded": names})
}
identifier = c.FormValue("identifier")
if fileHeader, err = c.FormFile("file"); err != nil {
return utils.ResponseError(c, err.Error())
}
if chunkNumber, err = strconv.Atoi(c.FormValue("chunkNumber")); err != nil {
return utils.ResponseError(c, err.Error())
}
chunkSize, err = strconv.ParseInt(c.FormValue("chunkSize"), 10, 64)
currentChunkSize, err = strconv.ParseInt(c.FormValue("currentChunkSize"), 10, 64)
totalSize, err = strconv.ParseInt(c.FormValue("totalSize"), 10, 64)
totalChunks, err = strconv.Atoi(c.FormValue("totalChunks"))
if file, err = fileHeader.Open(); err != nil {
return utils.ResponseError(c, err.Error())
}
defer func() {
file.Close()
dst.Close()
if chunkNumber == totalChunks { //上傳完成,開始合併文件
if chunkSize*int64(chunkNumber-1)+currentChunkSize == totalSize {
mergeFile(fileUrl, identifier)
}
}
}()
if dst, err = os.Create(fmt.Sprintf("%s/%d", utils.CreateDir(currentPath, identifier), chunkNumber)); err != nil {
return utils.ResponseError(c, err.Error())
}
if _, err = io.Copy(dst, file); err != nil {
return utils.ResponseError(c, err.Error())
}
if chunkNumber == totalChunks {
filename = c.FormValue("filename")
currentPath := utils.GetCurrentPath() + global.UPLOADDIR
localPath := utils.CreateDateDir(currentPath)
fileUrl = fmt.Sprintf("%s%s%s_%s", localPath, global.SEPARATOR, utils.GetRandomString(12), filename) //文件保存路徑,對文件重命名
}
return utils.ResponseSuccess(c, map[string]interface{}{"fileUrl": strings.Replace(strings.Replace(fileUrl, currentPath, "", 1), "\\\\", "/", -1)})
}
/*
分片上傳合併文件
*/
func mergeFile(fileUrl string, identifier string) {
var (
body []byte
localFile *os.File
err error
)
//文件合併
if localFile, err = os.OpenFile(fileUrl, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755); err != nil {
return
}
eachDir, _ := filepath.Split(fileUrl)
eachDir += identifier
filepath.Walk(eachDir, func(path string, f os.FileInfo, err error) error {
if f == nil {
return err
}
if f.IsDir() {
return nil
}
if body, err = ioutil.ReadFile(path); err != nil {
return err
}
localFile.Write(body)
return nil
})
localFile.Close()
remPath := utils.GetCurrentPath() + global.UPLOADDIR + global.SEPARATOR + identifier
err = os.RemoveAll(remPath) //合併完成刪除臨時文件
}
裡面用到了一些封裝的函數 GetCurrentPath 獲取當前路徑 CreateDateDir:根據日期創建路徑 ResponseSuccess ,ResponseError 響應到客戶端的數據
閱讀更多 guorenweitianxia 的文章