大文件分段上传,前后端写法都有(分别使用node和golang实现)

问题:为什么需要分段上传?

前端项目开发阶段是node环境,上传文件非常迅速,很大的文件也能轻松上传,但是生产环境一般都是用nginx部署,nginx对上传文件大小默认是1M,即使是修改文件大小限制后,由于传输速度问题,要是上传个几兆几十兆的文件问题不大,要是上百兆甚至几百兆,大多数情况会出现超时,导致上传失败,体验很差,分段上传就可以很大程度解决这个问题

大致思路

分段上传需要前后端配合,

前端部分:
  1. 首先是前端要把需要上传的大文件分成若干段,按序号命名,循环上传每一段
  2. 前端循环上传完所有分段后调用合并接口
后端部分:
  1. 递归循环接收前端上传的分段,新建一个和文件同名的文件夹,把接收到的分段都存入该文件夹
  2. 合并接口接收文件夹名为参数,把该文件夹下所有文件按序号排序后合并为一个文件
  3. 合并文件后在同目录生成合并后的文件,并删除分段文件夹

node版本

代码仓库 gitee.com/liyefeng123... 主要是两个接口,上传文件切片接口和合并切片接口 index.js 入口

javascript 复制代码
const express=require("express")
const bodyparser=require("body-parser")
const multiparty=require("multiparty")
const fse=require("fs-extra")
const fs=require("fs")
const path = require('path')
const app =express()
const UPLOAD_DIR=path.resolve(__dirname,"public/upload")
app.use(express.static(__dirname+"/public"))
app.use(bodyparser.urlencoded({extended:true}))

app.post("/upload",function(req,res){
  const form=new multiparty.Form({uploadDir:"template"})
  form.parse(req)
  form.on("file",async(name,chunk)=>{
    
    let chunkDir=`${UPLOAD_DIR}/${chunk.originalFilename.split(".")[0]}`
    if(!fse.existsSync(chunkDir)){
      await fse.mkdirs(chunkDir)
    }
    var dpath=path.join(chunkDir,chunk.originalFilename.split(".")[1])
    await fse.move(chunk.path,dpath,{overwrite:true})
    res.send("文件上传成功")
  })
  
})
app.get("/merge", async function (req,res){
  let name=req.query.name
  let fname=name.split(".")[0]
  let chunDir=path.join(UPLOAD_DIR,fname)
  let chunks=await fse.readdir(chunDir)
  let sortArr=chunks.sort((a,b)=>a-b)
  sortArr.map(chunkPath=>{
    fs.appendFileSync(
      path.join(UPLOAD_DIR,name),
      fs.readFileSync(`${chunDir}/${chunkPath}`)
    )
  })
  fse.removeSync(chunDir)
  res.send({msg:"合并成功",url:`http://localhost:3000/upload/${name}`})
})
app.listen("3000")
console.log("listen 3000")

index.html

xml 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文件上传</title>

</head>

<body>
  <input type="file" id="btnfile">
  <input type="button" value="上传" onclick="upload(0)">
  <button id="btn">0%</button>
  <script src="./axios.js"></script>
  <script>
    let chunkSize = 1024 * 1024
    let index=0
    let size=0
    let radio=0
    function upload(index) {
      let file = btnfile.files[0]
      console.log("file",file)
      size=file.size
      let [fname, fext] = file.name.split(".")
      
      let start = index * chunkSize
      radio=(index/(size/chunkSize))*100
      radio=Math.floor(radio)
      console.log("size/chunkSize",index/(size/chunkSize))
      btn.innerText=`${radio>100?100:radio}%`
      if (start > file.size) {
        merge(file.name)
        return
      }
      let blob = file.slice(start, start + chunkSize)
      let blobName = `${fname}.${index}.${fext}`
      let blobFile = new File([blob], blobName)
      let formData = new FormData()
      formData.append("file", blobFile)
      index++
      axios.post("/upload", formData).then(res => {
        console.log("rea", res)

        upload(index)
      })
    }
    function merge(name){
      axios.get("/merge?name="+name).then(res=>{
        console.log("merge",res)
      })
    }
    window.onload = () => {
      let btnfile = document.getElementById("btnfile")

    }
  </script>
</body>

</html>

golang版本

前端表单

ini 复制代码
<div class="upload_wrap" v-show="percentage<1">
  <input type="file" name="file" id="uploadVideo">
  <input type="button" value="开始上传" @click="uploadBiger">
</div>
<div class="loading"  style="width: 100%" v-show="percentage>1">
  <el-progress  :stroke-width="11" :percentage="percentage" />
</div>
ini 复制代码
/**
 * 上传大文件
 */
let uploadStep=ref(0)
let chunkSize = 1024 * 1024
let uploadSize=0
let index=0
let size=0
let percentage=ref(0)
const uploadBiger=()=>{
  const uploadVideo=document.getElementById("uploadVideo")
  let file=uploadVideo.files[0]
  size=file.size
  uploadSize=file.size
  let start = index * chunkSize
  let [fname, fext] = file.name.split(".")
  percentage.value=Math.floor((index/(size/chunkSize))*100)
  percentage.value=percentage.value>=100?100:percentage.value
  if (start > file.size) {
    mergeVideo(file.name)
    return
  }
  let blob = file.slice(start, start + chunkSize)
  let blobName = `${fname}.${index}.${fext}`
  let blobFile = new File([blob], blobName)
  let formData = new FormData()
  formData.append("file", blobFile)
  index++
  server.post("/admin/uploadVideo",formData).then(res=>{
    console.log("up",res)
    if(res.status==200){
      uploadBiger(index)
    }
  })
  
}
const mergeVideo=(fileName)=>{
  let data={
    fileName
  }
  server.post("/admin/mergeVideo",data).then(res=>{
    if(res.status==200){
      form.url=res.data.url
    }
  })
}

golang后端代码

go 复制代码
func (this VideoController) UploadVideo(ctx *gin.Context) {
	file, err := ctx.FormFile("file")
	if err == nil {
            config, _ := ini.Load("./conf/app.ini")
            musiclocal := config.Section("").Key("mlocal").String()
            println(musiclocal)
            nameStr := strings.Split(file.Filename, ".")
            dsn := path.Join("./static/upload/video/"+nameStr[0]+"/", nameStr[1])
            if err := ctx.SaveUploadedFile(file, dsn); err == nil {
                ctx.JSON(200, gin.H{
                        "success": true,
                        "message": "上传成功",
                })
            } else {
                ctx.JSON(400, gin.H{
                        "success": false,
                        "message": "上传失败",
                })
            }
	} else {
            ctx.JSON(400, gin.H{
                    "success": false,
                    "message": "上传失败",
            })
	}
}
func (this VideoController) appendFile(fout *os.File, infile string) {
    fin, err := os.Open(infile)
    if err != nil {
        panic(err)
    }
    defer fin.Close()
    buffer := make([]byte, 1024)
    for {
        n, err := fin.Read(buffer)
        if err != nil {
            if err == io.EOF {
                if n > 0 {
                    fout.Write(buffer[:n])
                }
            } else {
                println(err)
            }
            break
        } else {
            fout.Write(buffer[:n])
        }
    }
}
func (this VideoController) MergeVideos(ctx *gin.Context) {
    name := ctx.PostForm("fileName")
    fileName := strings.Split(name, ".")
    config, _ := ini.Load("./conf/app.ini")
    videolocal := config.Section("").Key("mlocal").String()
    dsn := path.Join("./static/upload/video/" + fileName[0] + "/")
    files, err := ioutil.ReadDir(dsn)
    if err != nil {
        ctx.JSON(400, gin.H{
                "success": false,
                "message": "合并失败",
        })
    } else {
        fileDsn := dsn + "." + fileName[1]
        fout, err := os.OpenFile(fileDsn, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
        if err != nil {
            panic(err)
        }
        defer fout.Close()
        if fileInfos, err := os.ReadDir(dsn); err != nil {
            panic(err)
        } else {
            sort.Slice(fileInfos, func(i, j int) bool {
                return files[i].Name() < files[j].Name()
            })
            for _, fileInfo := range fileInfos {
                if fileInfo.Type().IsRegular() {
                    infile := filepath.Join(dsn, fileInfo.Name())
                    this.appendFile(fout, infile)
                }
            }
            fn := path.Join("./static/upload/video/" + fileName[0])
            err3 := os.RemoveAll(fn)
            if err3 != nil {
                println("文件删除失败")
            }
            ctx.JSON(200, gin.H{
                "success": true,
                "message": "合并成功",
                "url":     videolocal + "/static/upload/video/" + name,
            })
        }
    }

}

方法写的有点简略,有感兴趣的小伙伴可以单聊

相关推荐
wangruofeng3 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
脚本自动化开发3 小时前
玩影之刃零,两张四千元级显卡怎么挑
后端
wangruofeng3 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈6 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技6 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569156 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
努力的小雨7 小时前
别再复制日志给AI了!我让Codex自己去观测云查报错
后端
风骏时光牛马8 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng9 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
afumu9 小时前
Go 写一个只监听 127.0.0.1 的控制服务:鉴权、白名单、危险命令拦截
后端·安全·go