Golang配合Vue 分片上傳

目前項目中需要上傳大文件,瀏覽器端上傳大文件的常用做法就是分片上傳,項目前端用的是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,如下圖

Golang配合Vue 分片上傳

具體使用方法

初始化:

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

Golang配合Vue 分片上傳

服務端就不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 響應到客戶端的數據


分享到:


相關文章: