golang實(shí)現(xiàn)大文件上傳功能全過程
前言
在我們的工作中,上傳功能是一個(gè)比較常見的功能,但是當(dāng)上傳文件過大就可能上傳不成功,或者花費(fèi)時(shí)間過長(zhǎng)或失敗。
這個(gè)時(shí)候我就需要將大文件進(jìn)行分割成小文件上傳,然后在合并成一個(gè)大文件,提供上傳的容錯(cuò)率。
現(xiàn)將大文件上傳功能記錄與此。
實(shí)現(xiàn)邏輯:
1.將文件分割成n個(gè)文件,并將他們?nèi)可蟼鞯椒?wù)器,可以給文件hash一個(gè)值,確保n個(gè)文件是同一個(gè)文件的一部分。
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; // 每個(gè)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值的時(shí)候, 如果文件內(nèi)容相同,而名稱不同的時(shí)候
// 想保留兩個(gè)文件無法保留。所以把文件名稱加上。
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為一個(gè)File對(duì)象數(shù)組,如果允許多選的時(shí)候,文件為多個(gè)
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值去后臺(tái)進(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提交 分片,此時(shí) 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前段頁(yè)面
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, "文件上傳錯(cuò)誤")
}
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, "文件上傳錯(cuò)誤2")
}
wg.Add(filesCount)
for i := 0; i < filesCount; i++ {
// 這里一定要注意按順序讀取不然文件就會(huì)損壞
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; // 每個(gè)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為一個(gè)File對(duì)象數(shù)組,如果允許多選的時(shí)候,文件為多個(gè)
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值去后臺(tái)進(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提交 分片,此時(shí) 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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)Dijkstra算法過程詳解
Dijkstra 算法是一種用于計(jì)算無向圖的最短路徑的算法,它是基于貪心策略的,每次選擇當(dāng)前距離起始節(jié)點(diǎn)最近的未訪問節(jié)點(diǎn)進(jìn)行訪問,并更新其相鄰節(jié)點(diǎn)的距離值,以得到最短路徑,這篇文章主要介紹了Golang實(shí)現(xiàn)Dijkstra算法,需要的朋友可以參考下2023-05-05
golang 如何獲取pem格式RSA公私鑰長(zhǎng)度
這篇文章主要介紹了golang 如何獲取pem格式RSA公私鑰長(zhǎng)度操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Go標(biāo)準(zhǔn)庫(kù)-ServeMux的使用與模式匹配深入探究
這篇文章主要為大家介紹了Go標(biāo)準(zhǔn)庫(kù)-ServeMux的使用與模式匹配深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
基于Go語言實(shí)現(xiàn)一個(gè)并發(fā)端口掃描器
這篇文章主要介紹了如何使用 Go 實(shí)現(xiàn)一個(gè)并發(fā)端口掃描器,通過 Goroutine 并發(fā)掃描多個(gè)端口,極大地提升端口掃描的效率,本文不僅講解了如何使用 Go 的并發(fā)特性,還涉及了如何處理超時(shí)和錯(cuò)誤,保證端口掃描的健壯性和效率,需要的朋友可以參考下2025-08-08

