GO HTTP大文件上传后端(MultipartReader流式读取),一次读取上传多个文件

1,136 阅读2分钟

GO HTTP大文件上传后端(MultipartReader流式读取),一次读取上传的多个文件

1. GO代码: main.go

package main

import (
	_ "embed"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"strings"
)

//go:embed index.html
var index []byte

// 流式文件上传,按照顺序读取文件,文件一点点读取到buffer中,相较于req.FormFile在上传大文件时极为节约内存
func upload(w http.ResponseWriter, r *http.Request) {
	mr, _ := r.MultipartReader() // 得到Reader
	var errors = ""
	for {
		fp, err := mr.NextPart() // 循环获取下一个文件
		if err == io.EOF {
			break //读取完成
		}
		errors += saveFile(fp)
	}
	var respStr = []byte("upload ok")
	if errors != "" {
		respStr = []byte(errors)
	}
	w.Write(respStr)
}

func saveFile(fp *multipart.Part) string {
	var errors = ""
	dst, _ := os.OpenFile("./"+fp.FileName(),
		os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) // 创建文件(本演示忽略错误)
	defer dst.Sync()          // 同步到磁盘
	defer dst.Close()         // 关闭文件
	buf := make([]byte, 1024) // 创建一个buffer用于读取http上传的文件
	fmt.Println("start saveing file ", fp.FileName())
	for {
		n, err := fp.Read(buf) // 循环读取文件内容
		if n > 0 {
			_, err = dst.Write(buf[0:n]) // 将buffer的内容写入新创建的文件
			if err != nil {
				errors += err.Error() + "\r\n"
				continue
			}
		}
		if err != nil {
			if err != io.EOF { // 读取遇到非文件读取结束的异常
				errors += err.Error() + "\r\n"
			}
			break
		}
	}
	fmt.Println("end saveing file ", fp.FileName())
	return errors
}

func download(w http.ResponseWriter, req *http.Request) {
	var p = req.URL.Query().Get("path")
	p, _ = url.QueryUnescape(p)
	f, e := os.Open(p)
	if e != nil {
		w.WriteHeader(http.StatusBadRequest)
		_, _ = io.WriteString(w, "Bad request")
		return
	}
	defer f.Close()
	w.Header().Add("Content-type", "application/octet-stream")
	var i = strings.LastIndex(p, "/")
	filename := p[i+1:]
	w.Header().Add("content-disposition", "attachment;filename=\""+filename+"\"")
	_, _ = io.Copy(w, f)
}

func main() {
	fmt.Println("by default server will startup with port 8080, params for port is p=port")
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write(index)
	})
	http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
		os.Exit(0)
	})
	http.HandleFunc("/upload", upload)
	http.HandleFunc("/download", download)
	var port = "8080"
	for _, args := range os.Args {
		if strings.HasPrefix(args, "p=") {
			port = args[2:]
			break
		}
	}
	fmt.Println("server will start on port: ", port)
	err := http.ListenAndServe(":"+port, nil)
	if err != nil {
		fmt.Println("startup error! ", err)
	}
}

2. 前端html&js: index.html

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <title>upload and download</title>
    <script>
        const downPathChange = e => {
            const path = e.value.replaceAll("\\", "/");
            const d = document.getElementById("downBtn")
            d.href = "/download?path=" + encodeURIComponent(path)
            d.download = path.substring(path.lastIndexOf("/") + 1)
        }
        const uploadFileXhr = () => {
            const xhr = new XMLHttpRequest();
            xhr.open('POST', '/upload', true);
            const firstDateTime = new Date().getTime()
            let progressTimeoutCount,progressLastDateTime=firstDateTime,progressLastLoaded=0,first=true
            let size = 0;
            for(const i of fileIn.files){
                size += i.size;
            }
            xhr.upload.onprogress = function (e) {
                if (e.lengthComputable) {
                    const loaded = e.loaded
                    clearTimeout(progressTimeoutCount)
                    let timeLong;
                    progressTimeoutCount=setTimeout(()=>{
                        const time = new Date().getTime()
                        timeLong = time-progressLastDateTime
                        if(first){
                            first=false
                        }else if(timeLong<1000){
                            return
                        }
                        const rate = (loaded/size)*100
                        const speed = (loaded-progressLastLoaded)/1024/1024/timeLong*1000
                        const timeLongToStart = (time-firstDateTime)/1000/60
                        uploadProgress.innerText='已上传'+(e.loaded/1024/1024).toFixed(4)+'MB,'+'进度'+rate.toFixed(4) + '%,速率约'+speed.toFixed(4)+'MB每秒,已耗时'+timeLongToStart.toFixed(1)+'分钟。';
                        progressLastDateTime=time
                        progressLastLoaded=loaded
                    })
                }
            };
            xhr.onload = function () {
                if (this.readyState === 4&&this.status === 200) {
                    uploadProgress.innerText=uploadProgress.innerText+" -> 耗时"+((new Date().getTime()-firstDateTime)/1000/60).toFixed(2)+"分钟后上传完成!"+this.responseText;
                }
            };
            xhr.onerror = function () {
                if(this.status===0){
                    uploadProgress.innerText=uploadProgress.innerText+" -> 异常!网络错误!";
                }else{
                    uploadProgress.innerText=uploadProgress.innerText+" -> 异常!上传错误!"+this.status+","+this.responseText;
                }
            }
            const formData = new FormData();
            for(const i of fileIn.files){
                formData.append("file", i);
            }
            const sizeMb = size/1024/1024;
            document.getElementById('uploadTotal').innerText='总大小:'+(sizeMb<1024?(sizeMb.toFixed(3)+'M'):((sizeMb/1024).toFixed(3)+'G'))+'B,开始时间:'+new Date(firstDateTime).toLocaleString()+'。'
            xhr.send(formData);
        }
    </script>
</head>

<body>
    文件<input type="file" id="fileIn" multiple>
    <button onclick="uploadFileXhr()">上传</button>
    <span id="uploadTotal"></span>
    <span id="uploadProgress"></span>
    <hr>
    文件路径<input id="downPath" onchange="downPathChange(this)">
    <a id="downBtn">下载</a>
    <hr><a href="/shutdown">停止服务</a>
</body>

</html>

3. go语言在windows编译linux可执行程序

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build -o simple_upload