golang實(shí)現(xiàn)大文件上傳功能全過程
前言
在我們的工作中,上傳功能是一個比較常見的功能,但是當(dāng)上傳文件過大就可能上傳不成功,或者花費(fèi)時間過長或失敗。
這個時候我就需要將大文件進(jìn)行分割成小文件上傳,然后在合并成一個大文件,提供上傳的容錯率。
現(xiàn)將大文件上傳功能記錄與此。
實(shí)現(xiàn)邏輯:
1.將文件分割成n個文件,并將他們?nèi)可蟼鞯椒?wù)器,可以給文件hash一個值,確保n個文件是同一個文件的一部分。
2.上傳完成后,服務(wù)器進(jìn)行合并,根據(jù)hash。
具體實(shí)現(xiàn):
1.前段html index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>upload file</title> </head> <body id="app"> <h1 style="text-align: center">大文件切片上傳-實(shí)例</h1> <form method="post" enctype="multipart/form-data" onsubmit="return false" style="left: 10vw;position: relative;display: flex;height: 30vh;flex-direction: column;width: 80vw;margin: 20px;text-align: center;"> <input type="file" id="file" name="ff" multiple="multiple" style="margin-left: 30px"/><br/> <input type="submit" value="提交" id="xx" onclick="upload()" style="margin-left: 30px;width:70px"/> </form> <div style="height: 30px; width:80vw;left: 10vw;position: relative;"><span>上傳過程:</span></div> <div style="display: block;height: 40vh; width:80vw;overflow: scroll; background: darkgray;left: 10vw;position: relative;"> <textarea id="ct" style="height: 100%;width:100%;"></textarea> </div> </body> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.0.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script> <script > const chunkSize = 2 * 1024 * 1024; // 每個chunk的大小,設(shè)置為2兆 const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; const hashFile = (file) => { return new Promise((resolve, reject) => { const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); const loadNext = () => { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } fileReader.onload = e => { spark.append(e.target.result); // Append array buffer currentChunk++; if (currentChunk < chunks) { loadNext(); ct = document.getElementById("ct") ct.textContent = ct.textContent + `第${currentChunk}分片解析完成,開始解析${currentChunk + 1}分片\n\r` console.log(`第${currentChunk}分片解析完成,開始解析${currentChunk + 1}分片`); } else { console.log('finished loading'); const result = spark.end(); // 如果單純的使用result 作為hash值的時候, 如果文件內(nèi)容相同,而名稱不同的時候 // 想保留兩個文件無法保留。所以把文件名稱加上。 const sparkMd5 = new SparkMD5(); sparkMd5.append(result); sparkMd5.append(file.name); const hexHash = sparkMd5.end(); resolve(hexHash); } }; fileReader.onerror = () => { console.warn('文件讀取失?。?); }; loadNext(); }).catch(err => { console.log(err); }); } const upload = async () => { const fileDom = $('#file')[0]; // 獲取到的files為一個File對象數(shù)組,如果允許多選的時候,文件為多個 const files = fileDom.files; const file = files[0]; if (!file) { alert('沒有獲取文件'); return; } // alert("文件大小:"+ file.size / 1024 / 1024) // console.log(file) const blockCount = Math.ceil(file.size / chunkSize); // 分片總數(shù) const axiosPromiseArray = []; // axiosPromise數(shù)組 const hash = await hashFile(file); //文件 hash // 獲取文件hash之后,如果需要做斷點(diǎn)續(xù)傳,可以根據(jù)hash值去后臺進(jìn)行校驗(yàn)。 // 看看是否已經(jīng)上傳過該文件,并且是否已經(jīng)傳送完成以及已經(jīng)上傳的切片。 for (let i = 0; i < blockCount; i++) { const start = i * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; // 構(gòu)建表單 const form = new FormData(); form.append('file', blobSlice.call(file, start, end)); form.append('name', file.name); form.append('total', blockCount); form.append('index', i); form.append('size', file.size); form.append('hash', hash); console.log(blockCount, blobSlice.call(file, start, end), i, start, end, file.size); // ajax提交 分片,此時 content-type 為 multipart/form-data const axiosOptions = { onUploadProgress: e => { // 處理上傳的進(jìn)度 // console.log(blockCount, i, e, file); ct = document.getElementById("ct") ct.textContent = ct.textContent + `第${i}分片上傳完成\n\r` }, }; // 加入到 Promise 數(shù)組中 axiosPromiseArray.push(axios.post('/uploadFile', form, axiosOptions)); } await axios.all(axiosPromiseArray).then((result) => { // 合并chunks const data = { size: file.size, name: file.name, total: blockCount, hash }; const form = new FormData(); form.append('size', file.size); form.append('name', file.name); form.append('total', blockCount); form.append('hash', hash); console.log(result); axios.post("/file/chunks", form).then(res => { //console.log(res) ct = document.getElementById("ct") ct.textContent = ct.textContent + `上傳完成\n\r` console.log("全部上傳完畢"); }) }).catch((err) => { }); } </script> </html>
2.后端代碼
package main import ( "bufio" "encoding/json" "fmt" "html/template" "io" "io/ioutil" "log" "net/http" "os" "path" "path/filepath" "strconv" "strings" "sync" "syscall" ) var dir, _ = os.Getwd() var uploadPath = path.Join(dir, "uploads") var uploadTempPath = path.Join(uploadPath, "temp") // 加載html前段頁面 func home(w http.ResponseWriter, r *http.Request) { r.ParseForm() t, err := template.ParseFiles("static/index.html") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } t.Execute(w, "") return } // PathExists 判斷文件夾是否存在 func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err } func uploadFile(w http.ResponseWriter, r *http.Request) { file, _, err := r.FormFile("file") index := r.PostFormValue("index") hash := r.PostFormValue("hash") // 獲取uploads下所有的文件夾 nameList, err := ioutil.ReadDir(uploadPath) m := map[string]interface{}{ "code": 46900, "msg": "文件已上傳", } result, _ := json.MarshalIndent(m, "", " ") // 循環(huán)判斷hash是否在文件里如果有就返回上傳已完成 for _, name := range nameList { tmpName := strings.Split(name.Name(), "_")[0] if tmpName == hash { fmt.Fprintf(w, string(result)) return } } chunksPath := path.Join(uploadTempPath, hash, "/") isPathExists, err := PathExists(chunksPath) if !isPathExists { err = os.MkdirAll(chunksPath, os.ModePerm) } destFile, err := os.OpenFile(path.Join(chunksPath+"/"+hash+"-"+index), syscall.O_CREAT|syscall.O_WRONLY, 0777) reader := bufio.NewReader(file) writer := bufio.NewWriter(destFile) buf := make([]byte, 1024*1024) // 1M buf for { n, err := reader.Read(buf) if err == io.EOF { writer.Flush() break } else if err != nil { return } else { writer.Write(buf[:n]) } } defer file.Close() defer destFile.Close() if err != nil { log.Fatal("%v", err) } fmt.Printf("第%s:%s塊上傳完成\n", index, destFile.Name()) } // 合并文件 func chunks(w http.ResponseWriter, r *http.Request) { size, _ := strconv.ParseInt(r.PostFormValue("size"), 10, 64) hash := r.PostFormValue("hash") name := r.PostFormValue("name") toSize, _ := getDirSize(path.Join(uploadTempPath, hash, "/")) if size != toSize { fmt.Fprintf(w, "文件上傳錯誤") } chunksPath := path.Join(uploadTempPath, hash, "/") files, _ := ioutil.ReadDir(chunksPath) // 排序 filesSort := make(map[string]string) for _, f := range files { nameArr := strings.Split(f.Name(), "-") filesSort[nameArr[1]] = f.Name() } saveFile := path.Join(uploadPath, name) if exists, _ := PathExists(saveFile); exists { os.Remove(saveFile) } fs, _ := os.OpenFile(saveFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModeAppend|os.ModePerm) var wg sync.WaitGroup filesCount := len(files) if filesCount != len(filesSort) { fmt.Fprintf(w, "文件上傳錯誤2") } wg.Add(filesCount) for i := 0; i < filesCount; i++ { // 這里一定要注意按順序讀取不然文件就會損壞 fileName := path.Join(chunksPath, "/"+filesSort[strconv.Itoa(i)]) data, err := ioutil.ReadFile(fileName) fmt.Println(err) fs.Write(data) wg.Done() } wg.Wait() os.RemoveAll(path.Join(chunksPath, "/")) m := map[string]interface{}{ "code": 20000, "msg": "上傳成功", } result, _ := json.MarshalIndent(m, "", " ") fmt.Fprintf(w, string(result)) defer fs.Close() } // 獲取整體文件夾大小 func getDirSize(path string) (int64, error) { var size int64 err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { if !info.IsDir() { size += info.Size() } return err }) return size, err } func main() { http.HandleFunc("/", home) // set router http.HandleFunc("/uploadFile", uploadFile) http.HandleFunc("/file/chunks", chunks) err := http.ListenAndServe(":8080", nil) // set listen port if err != nil { log.Fatal("Error while starting GO http server on port - 8080 : ", err) //log error and exit in case of error at server boot up } }
效果:
JS改進(jìn):
將文件分割和上傳同步進(jìn)行,提升整體上傳速度
const chunkSize = 2 * 1024 * 1024; // 每個chunk的大小,設(shè)置為2兆 const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; const spark = new SparkMD5.ArrayBuffer(); const getHash = (file) => { const result = spark.end(); const sparkMd5 = new SparkMD5(); sparkMd5.append(result); sparkMd5.append(file.name); return sparkMd5.end(); } const uploadFile = async (file, hash) => { return } const upload = async () => { const fileDom = $('#file')[0]; // 獲取到的files為一個File對象數(shù)組,如果允許多選的時候,文件為多個 const files = fileDom.files; const file = files[0]; if (!file) { alert('沒有獲取文件'); return; } const blockCount = Math.ceil(file.size / chunkSize); // 分片總數(shù) const axiosPromiseArray = []; // axiosPromise數(shù)組 const hash = getHash(file); //文件 hash // 獲取文件hash之后,如果需要做斷點(diǎn)續(xù)傳,可以根據(jù)hash值去后臺進(jìn)行校驗(yàn)。 // 看看是否已經(jīng)上傳過該文件,并且是否已經(jīng)傳送完成以及已經(jīng)上傳的切片。 new Promise((resolve, reject) => { const chunks = Math.ceil(file.size / chunkSize); let currentChunk = 0; const fileReader = new FileReader(); const loadNext = () => { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; // 構(gòu)建表單 const form = new FormData(); form.append('file', blobSlice.call(file, start, end)); form.append('name', file.name); form.append('total', blockCount); form.append('index', currentChunk); form.append('size', file.size); form.append('hash', hash); // ajax提交 分片,此時 content-type 為 multipart/form-data const axiosOptions = { onUploadProgress: e => { ct = document.getElementById("ct") ct.textContent = ct.textContent + `第${currentChunk}分片上傳完成\n\r` }, }; // 加入到 Promise 數(shù)組中 axiosPromiseArray.push(axios.post('/uploadFile', form, axiosOptions)); } while (currentChunk < chunks){ loadNext() currentChunk++; } }).catch(err => { console.log(err); }); await axios.all(axiosPromiseArray).then((result) => { // 合并chunks const data = { size: file.size, name: file.name, total: blockCount, hash }; const form = new FormData(); form.append('size', file.size); form.append('name', file.name); form.append('total', blockCount); form.append('hash', hash); console.log(result); axios.post("/file/chunks", form).then(res => { //console.log(res) ct = document.getElementById("ct") ct.textContent = ct.textContent + `上傳完成\n\r` console.log("全部上傳完畢"); }) }).catch((err) => { }); }
總結(jié)
到此這篇關(guān)于golang實(shí)現(xiàn)大文件上傳功能的文章就介紹到這了,更多相關(guān)golang實(shí)現(xiàn)大文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang中sync.Map并發(fā)創(chuàng)建、讀取問題實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于golang中sync.Map并發(fā)創(chuàng)建、讀取問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07golang實(shí)現(xiàn)簡單工廠、方法工廠、抽象工廠三種設(shè)計(jì)模式
這篇文章介紹了golang實(shí)現(xiàn)簡單工廠、方法工廠、抽象工廠三種設(shè)計(jì)模式的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04Go中Goroutines輕量級并發(fā)的特性及效率探究
這篇文章主要為大家介紹了Go中Goroutines輕量級并發(fā)的特性及效率探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Golang中Channel實(shí)戰(zhàn)技巧與一些說明
channel是Go語言內(nèi)建的first-class類型,也是Go語言與眾不同的特性之一,下面這篇文章主要給大家介紹了關(guān)于Golang中Channel實(shí)戰(zhàn)技巧與一些說明的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11詳解go-micro微服務(wù)consul配置及注冊中心
這篇文章主要為大家介紹了go-micro微服務(wù)consul配置及注冊中心示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01