松鼠的博客

导航

统计

大文件切片上传和断点续传

大文件分片上传
前端知识点

md5加密算法用于确保信息传输完整一致
spark md5在散列大量数据(例如文件)时表现得更好。可以使用 FileReader 和 Blob 读取块中的文件,并将每个块附加到 md5

//创建一个spark md5计算arrayBuffer的对象 spark = new SparkMD5.ArrayBuffer()
// 添加到array buffer
spark.append(e.target.result);
//根据arrayBuffer内容生成最终哈希值
spark.end()

根据文件内容生成哈希文件名,这样即使上传的文件名不一样但是内容一样也不会重复上传。

nodejs知识点

fs 文件系统相关:
existsSync路径是否存在的同步版本
appendFileSync 将数据同步添加到文件(持续添加进去)
writeFileSync 同步的写入文件(只写入最后一次的)
readdirSyn 读取一个目录的文件,返回不包含.的文件名列表
path路径:
path.extname(path)文件后缀名
path.dirname(path)文件目录名
path.resolve() 把一个路径或路径片段的序列解析为一个绝对路径。
框架:
express.static()创建静态资源服务器 ,对外开放静态资源
express.use(模糊匹配)和express.all(精准匹配),
请求->中间件->相应

大文件切片上传过程
获取到文件dom,获取到file,以及file的类型,名字,大小以后可以使用new 一个spark MD5.ArrayBuffer,每次都将chunk,append到arraybuffer里,在最后一个分片传输时用spark.end()生成最终的hash文件名传给后端,然后使用file.slice对文件进行切片,需要保存已经上传的文件大小,分片大小,当上传的文件大小小于文件本身大小时循环切片上传。将文件大小,文件类型,文件名,已上传的文件大小和chunk文件通过formdata传给后端。
后端接收时检查是否存在这个文件,如果没有就是第一次上传,writefile创建文件,不然就直接appendfile。
涉及对路径的处理,将路径通过resolve保存为绝对路径,路径不写死而是通过dirname获取当前目录。

代码
前端

import {API,UPLOAD_INFO,ALLOWED_TYPE,CHUNK_SIZE} from './config.js'


;((doc)=>{
const oProgress=doc.querySelector("#uploadProgress")
const oUploder=doc.querySelector("#videoUploader")
const oInfo=doc.querySelector("#uploadInfo")
const oBtn=doc.querySelector("#uploadBtn")

//当前已经上传的文件大小
let uploadedSize=0

let uploadedResult=null

const init=()=>{
bindEvent()
}

function bindEvent(){
oBtn.addEventListener('click',uploadVideo,false)

}

async function uploadVideo(){
const {files:[file]}=oUploder
if(!file){
oInfo.innerText=UPLOAD_INFO['NO_FILE']
return
}

if(!ALLOWED_TYPE[file.type]){
oInfo.innerText=UPLOAD_INFO['INVAILD_TYPE']
return
}
const {name,size,type}=file
const fileName=new Date().getTime()+"_"+name
oProgress.max=size
oInfo.innerText=''

while(uploadedSize<size){
const fileChunk=file.slice(uploadedSize,uploadedSize+CHUNK_SIZE)
let formData=createFormData({name,type,size,fileName,uploadedSize,file: fileChunk})
try {
uploadedResult=await axios.post(API.UPLOAD_VIDED,formData)
} catch (error) {
oInfo.innerText=`${UPLOAD_INFO['UPLOAD_FAILED']} (${error.message})`
return
}
uploadedSize+=fileChunk.size
oProgress.value=uploadedSize
}
oInfo.innerText=`${file.name}+${UPLOAD_INFO['UPLOAD_SUCESS']}`
oUploder.value=null
createVideo(uploadedResult.data.video_url)
}

function createFormData({
name,
type,
size,
fileName,
uploadedSize,
file
}) {
const fd=new FormData()
fd.append('name',name)
fd.append('type',type)
fd.append('size',size)
fd.append('fileName',fileName)
fd.append('uploadedSize',uploadedSize)
fd.append('file',file)
return fd
}

function createVideo(src){
const oVideo = document.createElement('video')
oVideo.controls=true
oVideo.width='500'
oVideo.src=src
document.body.appendChild(oVideo)
}

init()
}
)(document);

后端

const express =require('express')
const bodyParser =require('body-parser')
const uploader=require('express-fileupload')
const {extname,resolve}=require('path')
const {existsSync,appendFileSync,writeFileSync}=require('fs')

const app = express()

const ALLOWED_TYPE={
'video/mp4':'mp4',
'video/ogg':'ogg'
}

app.use(bodyParser.urlencoded({extended:true}))
app.use(bodyParser.json())
app.use(uploader())
//静态文件对外开放
app.use('/',express.static('uploaded_temp'))

// 中间件
app.all('*',(req,res,next)=>{
res.header('Access-Control-Allow-origin','*')
res.header('Access-Control-Allow-Methods','POST,GET')
next()
})


const PORT=8080

// 监听post请求
app.post('/upload_video',(req,res)=>{
const { name,
type,
size,
fileName,
uploadedSize,
} =req.body
const {file}=req.files

if(!file){
res.send({
msg:'no file',
code:1001
})
return
}
if(!ALLOWED_TYPE[type]){
res.send({
msg:'type error',
code:1002
})
return
}
const _fileName=fileName+extname(name)
const filePath=resolve(__dirname,'./uploaded_temp/'+_fileName)

//如果已经上传文件大小不为0,说明已经上传过一部分了,文件存在,直接append
if(uploadedSize !='0'){
// 防止已经上传的被删除
if(!existsSync(filePath)){
res.send({
msg:'no file',
code:1003
})
return
}
appendFileSync(filePath,file.data)
res.send({
msg:'file appended',
code:0,
video_url:'http://localhost:8080/'+_fileName

})
return
}
// 说明第一次上传,所以创建文件
writeFileSync(filePath,file.data)
res.send({
msg:'file created',
code:0
})
})

// 启动web服务器,端口port
app.listen(PORT,()=>{
console.log('Server is running on'+PORT)
})

断点上传过程
上述过程需要修改,切片名称由文件hash和序号构成,后端文件会存放所有临时切片,如果最终全部切片上传完成,前端发送合并请求给后端,后端通过appendFileSync将所有切片合并,删除临时chunk。前端监听上传文件框的change事件,每次change就执行函数去生成hash文件名以及向后端发送请求获取该文件已经上传的切片列表。后端可以通过readdirsyn读取文件夹里保存的chunk返回给前端。前端在每次分片上传之前判断切片是否上传,不重复上传。后端在接收到合并请求后将切片排序并合并。

多文件上传
通过file添加multple同时上传多个文件,获取到dom的一个文件数组保存files,通过innerHtml添加到下面作为列表展示,同时添加移除功能,通过事件委托实现dom移除和files数组的移除,files数组实现删除需要对数组重构,每一个文件对象都有一个key属性,key通过随机数加日期生成。多文件发送ajax请求可以通过promise.all来处理,只要有一个上传失败就reject。

图片缩略图和视频预览
图片可以通过base64预览
视频可以通过合成切片以后后端express内置中间件的express.static返回预览

参考文章:http://blog.ncmem.com/wordpress/2023/09/23/%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%87%e7%89%87%e4%b8%8a%e4%bc%a0%e5%92%8c%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/

欢迎入群一起讨论

 

 

posted on   Xproer-松鼠  阅读(56)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
历史上的今天:
2020-09-23 word图文混排复制到CKEditor图片不显示
2020-09-23 word图文混排复制到UMEditor图片不显示
2020-09-23 word图文混排复制到百度UEditor图片不显示
2020-09-23 ckeditor粘贴word图片自动上传源代码
2020-09-23 ckeditor粘贴word图片自动上传源码
2020-09-23 ckeditor粘贴word图片自动上传代码
2020-09-23 ckeditor粘贴word图片自动上传插件
点击右上角即可分享
微信分享提示