ES6+Golang 实现批量大文件分片上传
2022.04

Html 代码


<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .container{
            display: flex;
            flex-wrap: wrap;
            background: #2c3e50;
            min-height: 50px;
        }
        .file{
            width: 24%;
            height: 100px;
            background: #eee;
            padding: 10px;
            margin: 0.5%;
            box-sizing: border-box;
        }
        .progress{

            height: 20px;
            background-color:#f7f7f7;
            box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);
            border-radius:4px;
            background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);
        }

        .finish{
            background-color: #149bdf;
            background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
            background-size:40px 40px;
            display: inline-block;
            height: 20px;

        }
        form{
            margin-top: 50px;
        }
    </style>
</head>
<body>
<div class="container">
<!--    <div class="file">-->
<!--        <div class="progress">-->
<!--            <span id="finish" style="width: 0%;" progress="0"></span>-->
<!--        </div>-->
<!--        <input type="button" value="停止" id="stop">-->
<!--    </div>-->
</div>



    <input type="file" name="file" id="file" multiple accept="*" >


<script>
    var fileBtn = document.getElementById("file");

    //触发上传开始
    fileBtn.onchange = function(){

        for (let i=0;i<this.files.length;i++){
            file=this.files[i];
               r = new UploadFile(file)
               r = null;
        }

    }

    class UploadFile{
        file
        trigger=true;
        tasks= new Array();

        constructor(file) {
            this.file=file;
            this.tasks= this.regTask(file);
            this.createDom();
            this.upload(this.file,this.tasks);
        }

        //把文件分割注册成为待发送的任务
        regTask(file) {
            const FILE_NAME = file.name; //文件名
            const FILE_SIZE = file.size; //总大小
            const CHUNK_SIZE = 1024 * 1024; // 1MB 分片大小
            const CHUNK_TOTAL_NUM = Math.ceil(FILE_SIZE/CHUNK_SIZE); //文件分片数量


            let start = 0; //切割起始位置
            let end = CHUNK_SIZE; //切割起始位置

            let tasks=new Array();//注册一个任务列表数组
            for (let i=0;i<CHUNK_TOTAL_NUM;i++){
                let task={
                    fileName:FILE_NAME,
                    fileSize:FILE_SIZE,
                    chunkNum:CHUNK_TOTAL_NUM,
                    chunkName:FILE_NAME+"_"+i,
                    start:start,
                    end:end,
                }
                tasks.push(task);

                //更新下一个切割分片的位置
                start = end;
                end = start + CHUNK_SIZE;
            }
            return tasks;

        }

        //切割文件分片
        sliceFile(file,start,end){
            let chunk = file.slice(start,end);
            return chunk;
        };
        //创建上传的dom
        createDom(){
            //从模版创建上传dom
            let template=`
                <div class="file">
                    <div class="progress">
                        <span class="finish" style="width: 0%;" progress="0"></span>
                    </div>
                    <input type="button" value="停止" class="stop">
                </div>
            `;
            let doc =new DOMParser().parseFromString(template,'text/html');
            let node= doc.querySelector('.file');

            let selfClass=this
            node.querySelector('.stop').onclick=function () {
                if (selfClass.trigger==false){
                    selfClass.trigger=true
                    this.value="停止"
                    if (selfClass.file){selfClass.upload();}
                }else{
                    selfClass.trigger=false
                    this.value="继续"
                }
                console.log(selfClass.trigger)
            };
            //
            let container=document.querySelector(".container");
            container.appendChild(node);

            this.node=node;

        }
        //执行上传
        upload() {
            console.log(this.tasks.length)
            console.log(this.file)
            console.log(this.node)
            if (this.tasks.length==0){ return} //任务全部完成后停止
            if (this.trigger==false){ return}  //暂停

            //取任务并把文件切片
            let task=this.tasks.shift()
            let chunk=this.sliceFile(this.file,task.start,task.end)
            //准备ajax发送数据
            let formData = new FormData();
            formData.append('chunk',chunk);
            formData.append('chunkSize',chunk.size);
            formData.append('chunkName',task.chunkName);
            formData.append('chunkNum',task.chunkNum);
            formData.append('fileName',task.fileName);
            formData.append('fileSize',task.fileSize);

            let selfClass=this
            let xhr = new XMLHttpRequest();
            xhr.open('POST', '/upload', true);

            xhr.onload = function(e) {

                let res=JSON.parse(this.response)
                if(this.status == 200 && res.Code==200){
                    //返回成功后更新进度条样式
                    let progress = Math.min(100,((task.chunkNum-selfClass.tasks.length)/task.chunkNum)* 100 ) +'%';
                    selfClass.node.querySelector('.finish').style.width = progress;
                    //服务器返回成功
                    selfClass.upload()
                }else{
                    //服务器返回错误重试
                    selfClass.tasks.unshift(task)
                    selfClass.upload()
                }
            };
            xhr.onerror = function(e){
                //网络错误时重试
                setTimeout(function () {
                    selfClass.upload();
                },1000)
            };

            xhr.send(formData);

        }


    }





</script>
</body>
</html>

Golang伪代码


package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"net/http"
)

func main() {
	http.HandleFunc("/upload", upload)
	http.HandleFunc("/", index)
	//启动
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	fmt.Println(err)
}

func index(res http.ResponseWriter, req *http.Request) {
	t := template.New("")
	t, _ = t.ParseFiles("index.html")
	t.ExecuteTemplate(res, "index.html", map[string]string{})
}

func upload(res http.ResponseWriter, req *http.Request) {
	res.Header().Set("content-type", "application/json")
	fmt.Println(req.FormFile("chunk"))
	fmt.Println(req.FormValue("chunkSize"))
	/*	校验文件是否存在,大小,文件名
			校验块是否存在,大小,块名
			校验块数量是否全匹配
				是:合并块,校验文件大小,删除块
		    读取块写入本地
	*/

	//返回成功状态
	m := struct {
		Code     int //状态码
		Progress int //已获取块数量
	}{200, 50}
	result, _ := json.Marshal(m)

	res.WriteHeader(200)
	res.Write(result)
}


es6