前端实现文件切片上传的方式很简单,原理就是将一个完整的文件对象切割成一段一段的独立文件,然后将这一段一段的独立文件对像上传到后端服务器(上传方式和普通文件对象上传方式一样,放在formdata中上传就行了),
后端等所有的分段文件对象上传完毕后,然后将这些分段文件对象进行合并成一个就行了
以上是处理思路,但是有两个疑问。
1.怎么将完整的文件对象切割成一个个的片段文件对象呢
2.这么多片段文件,假如还有其他人也在上传其他文件,那么后端在合并的时候怎么找到对应的文件片段集合,然后将其合并呢
首先解决第一点疑问:
在我们通过input标签获取到的完整的文件对象中,有一个属性size代表这个文件的总体大小。然后原型上有一个slice方法,通过文件对象原型上的slice方法可以将该文件切割成一段一段的独立文件对象
例如我们通过input获取到一个初识的完整的文件对象,该文件对象的size为1024000,则代表该文件大小为1m。我们要将其平均切割成10份的话,只需要下面这样做
const 片段文件对象1= 初识完整的文件对象.slice(0,102400),const 片段文件对象2= 初识完整的文件对象.slice(102400,204800),......., const 片段文件对象10= 初识完整的文件对象.slice(9*102400,102400)
第二个问题,如果同时上传两个同名文件,那么怎么保证其切割的文件片段的名称唯一,
可以通过spark-md5跟据文件buffer数据内容生成唯一的hash值,然后将各个文件片段命名为 文件hash值_片段index.后缀 然后最后上传完毕将hash值传给后端,后端根据hash找到对应的文件片段,然后将所有的文件片段合并
后端这里使用node实现的,思路如下
首先将分段文件对象存储在指定目录下,待所有分段文件上传完毕,再通过流的形式去指定目录下读取所有得到分段文件然后写入在新的文件中
1.前端代码
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<input type="file" name="" id="file_uplaod" value="" />
<script src="node_modules/axios/dist/axios.min.js"></script>
<script src="node_modules/spark-md5/spark-md5.min.js"></script>
<script src="./utils.js" ></script>
<script>
document.getElementById('file_uplaod').addEventListener('change',async (e)=>{
var file = e.target.files[0]
console.log(file)
var fileChunks=[]//保存所有的文件切片数据
var fileSize = file.size
var fileName = file.name//文件原名称
var fileMax=1024*1000 //限制每一个文件切片最大为1mb
var currentSize=0 //当前文件切片之前已经切过的大小
var {Hash,suffix} =await parseFileHash(file)
for(var i=1;i<=100000;i++){
var fm = new FormData()
if(i*fileMax>fileSize){
fm.append('file',file.slice((i-1)*fileMax,fileSize))
fm.append('fileHashName',`${Hash}_${i}.${suffix}`)
fm.append('fileName',fileName)
fm.append('isFinal',true)
fm.append('Hash',Hash)
fm.append('suffix',suffix)
fileChunks.push(fm)
break;
}else{
fm.append('file',file.slice((i-1)*fileMax,i*fileMax))
fm.append('fileHashName',`${Hash}_${i}.${suffix}`)
fm.append('fileName',fileName)
fm.append('isFinal',false)
fm.append('Hash',Hash)
fm.append('suffix',suffix)
fileChunks.push(fm)
currentSize+=i*fileMax
}
}
console.log(fileChunks[0].get('file'))
for(var i=0;i<fileChunks.length;i++){
const res= await axios({
url:'http://localhost:3535/bxg/upload_file',
method:'post',
headers:{
'Content-Type':'multipart/form-data'
},
data:fileChunks[i]
})
console.log(res)
}
})
</script>
</body>
</html>
utils.js
(function (window) {
window.parseFileHash=function (file) {
return new Promise((resolve,reject)=>{
const fileReader=new FileReader()
fileReader.readAsArrayBuffer(file) //读取文件的buffer类型数据
fileReader.onload=(val)=>{
var buffer = val.target.result //获取file读取后的buffer数据
var spark=new SparkMD5.ArrayBuffer() //创建spark插件实例,该插件可以将文件buffer类型数据转为hash值
spark.append(file)
var Hash=spark.end()
var suffix=/\.([\d\D]+$)/.exec(file.name)[1]//获取文件后缀名
resolve({
buffer,
Hash,
suffix,
filename:`${Hash}.${suffix}`
})
}
})
}
})(window);
2.后端代码
//文件分片上传
const koaRouter =require('koa-router')
const router =koaRouter({
prefix:'/bxg'
})
const multer = require('koa-multer');
const upload = multer({ dest: 'upload/' });
const fs=require('fs')
//开启http长链接
router.use(async (ctx, next) => {
ctx.response.set('X-Powered-By', 'xxx')
ctx.response.set('connection', 'keep-alive')
await next()
})
router.post('/upload_file',upload.single('file'),async (ctx,next)=>{
// console.log(ctx.req.file.destination)
var reg=/([\/])/gm
var fileFragmentHashName=ctx.req.file.destination.replace(reg,'')+'\\'+ctx.req.body.fileHashName //片段文件hash名称
fs.renameSync(ctx.req.file.path,fileFragmentHashName);//修改图片之前的名字
if(ctx.req.body.isFinal==='true'){
console.log(ctx.req.body.isFinal)
await ParseMergeFiles(fileFragmentHashName,ctx.req.body.Hash,ctx.req.body.suffix,ctx.req.body.fileName)
ctx.body={
code:200,
data:{
msg:ctx.req.body.fileName+'上传成功'
}
}
}else{
ctx.body={
code:200,
data:{
msg:ctx.req.body.fileName+'的文件分片上传成功'
}
}
}
})
//准备合并大文件
//第一步拿到所有的文件片段对象进行循环 一个个写入一个新建的文件
async function ParseMergeFiles(fileFragmentHashName,Hash,suffix,fileName){
return new Promise(async (resolve,reject)=>{
var fileFragmentHashNameCount=0 //文件片段数量
var reg=/\_([\d\D]+)\.([\d\D]+)$/
//我这里将文件写入resource_upload目录下,自行根据需要进行调整
const fileWriteStream = fs.createWriteStream('resource_upload'+'\\'+fileName); // 创建一个可写流
fileFragmentHashNameCount=reg.exec(fileFragmentHashName)[1]
for(let i=1;i<=fileFragmentHashNameCount;i++){
var newfileFragmentHashName=fileFragmentHashName.replace(reg,($1,$2,$3)=>{
return `_${i}.${$3}`
})
const res= await mergeFiles(fileWriteStream,fileName,newfileFragmentHashName)
}
fileWriteStream.end()
resolve()
})
}
//开始合并
function mergeFiles(fws,fileName,newfileFragmentHashName){
return new Promise((resolve,reject) => {
var rs = fs.createReadStream(newfileFragmentHashName);
rs.on('data', function (data) {
fws.write(data)
});
rs.on('end',function () {
resolve('xx')
})
})
}
module.exports=router