大文件前台切成若干份2M小文件,分别传输给后台,后台分别写入硬盘,在最后一片被后台接收后,读取所有切片并合并成为原始大文件
主要实现两种方式的大数据切片传输
大文件上传时,前端切片,上传后,后端组合
Upload Large Files
Upload
exports.upload=function user(req,res,config){
let fs=require('fs');
let async = require('async');//异步模块
let formidable=require('formidable')
let form=new formidable.IncomingForm();
//设置编辑
form.encoding = 'utf-8';
let dirPath=__dirname+"/../uploadFiles/tep/";
//设置文件存储路径
form.uploadDir = dirPath;
//设置单文件大小限制
// form.maxFilesSize = 200 * 1024 * 1024;
/*form.parse表单解析函数,fields是生成数组用获传过参数,files是bolb文件名称和路径*/
form.parse(req, function (err,fields,files) {
files=files['data'];//获取bolb文件
let index=fields['index'];//当前片数
let total=fields['total'];//总片数
let name=fields['name'];//文件名称
let url= dirPath+'/'+name.split('.')[0]+'_'+index+'.'+name.split('.')[1];//临时bolb文件新名字
fs.renameSync(files.path,url);//修改临时文件名字
try{
if(index==total){//当最后一个分片上传成功,进行合并
/*
检查文件是存在,如果存在,重新设置名称
*/
let uid=uuid.v4()
fs.mkdirSync(__dirname+"/../uploadFiles/"+uid)
let pathname=__dirname+"/../uploadFiles/"+uid+'/'+name;//上传文件存放位置和名称
fs.access(pathname,fs.F_OK,(err) => {
if(!err){
let myDate=Date.now();
pathname=dirPath+'/'+myDate+name;
console.log(pathname);
}
});
//这里定时,是做异步串行,等上执行完后,再执行下面
setTimeout(function(){
/*进行合并文件,先创建可写流,再把所有BOLB文件读出来,
流入可写流,生成文件
fs.createWriteStream创建可写流
aname是存放所有生成bolb文件路径数组:
['Uploads/img/3G.rar1','Uploads/img/3G.rar2',...]
*/
let writeStream=fs.createWriteStream(pathname);
let aname=[];
for(let i=1;i<=total;i++){
let url=dirPath+'/'+name.split('.')[0]+'_'+i+'.'+name.split('.')[1];
aname.push(url);
}
//async.eachLimit进行同步处理
async.eachLimit(aname,1,function(item,callback){
//item 当前路径, callback为回调函数
fs.readFile(item,function(err,data){
if(err)throw err;
//把数据写入流里
writeStream.write(data);
//删除生成临时bolb文件
fs.unlink(item,function(){console.log('删除成功');})
callback();
});
},function(err){
if (err) throw err;
//后面文件写完,关闭可写流文件,文件已经成生完成
writeStream.end();
//返回给客服端,上传成功
let data=JSON.stringify({'code':0,"data": {
"source_store_id": uid,
"file_name": name
}});
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
res.end(data);//返回数据
});
},50);
}else{//还没有上传文件,请继续上传
let data=JSON.stringify({'code':1,'msg':'继续上传'});
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
res.end(data);//返回数据
}
}catch(err){
console.log(err)
}
});
};
关键点在于,Node后台的切片因为没有HTML中的file api, 无法利用File对象继承自Blob的slice方法,以进行切片,进一步去做合并。经过尝试可以利用Node提供的Buffer中的slice进行切片
大部分逻辑同上,故这里只贴出关键代码
let distFile = fs.readFileSync(
__dirname + "/../dataStorage/" + serviceItem.id + ".zip"
);
let largrFile = Buffer.from(distFile);
let name = serviceItem.name+'.zip', //文件名
size = largrFile.length, //总大小
succeed = 0; //当前上传数
let shardSize = 2 * 1024 * 1024, //以2MB为一个分片
shardCount = Math.ceil(size / shardSize); //总片数
/*生成上传分片文件顺充,通过async.eachLimit()进行同步上传
attr里面是[0,1,2,3...,最后一位]
*/
let attr = [];
for (let i = 0; i < shardCount; ++i) {
attr.push(i);
}
let rp=res;
try{
async.eachLimit(
attr,
1,
async function (item, callback) {
let i = item;
let start = i * shardSize, //当前分片开始下标
end = Math.min(size, start + shardSize); //结束下标
let minFile = largrFile.slice(start, end);
let obj = {};
obj["data"] = minFile;
obj["name"] = name;
obj["total"] = shardCount;
obj["index"] = i + 1;
obj["size"] = size;
obj["start"] = start;
obj["end"] = end;
// http://111.229.14.128:8899/largeBKend
// http://localhost:8898/upload 测试
await axios
.post("api", obj, {
timeout: 1000*60*60,
})
.then((axiosRes) => {
++succeed;
/*返回code为0是成功上传,1是请继续上传*/
if (axiosRes.data.code == 0) {
console.log(axiosRes.data.data);
console.log('大文件切上传完成,拿回数据索引,准备转发')
let ws=new WebSocket('ws://111.229.14.128:1708');
let msg={
msg:'Migration',
bk:true,
serviceDownloadId: axiosRes.data.data.source_store_id,
fromToken: req.query.fromToken,
targetToken: req.query.targetToken
}
ws.on('open',()=>{
ws.send(
JSON.stringify(msg)
)
ws.close()
})
ws.on('message',(data)=>{
if(data=='node offline'){
console.log('node offline')
ws.close()
}else{
console.log(data)
}
})
} else if (axiosRes.data.code == 1) {
console.log(axiosRes.data.msg);
let data=JSON.stringify({'code':1});
rp.end(data);//返回数据
}
//生成当前进度百分比
// _this.percentage=Math.round(succeed/shardCount*100);
console.log(
"进度: " + Math.round((succeed / shardCount) * 100)
);
// callback()
});
},
function (err) {
if(err){
console.log(err)
rp.send({code:-1})
return
}
}
);
}catch(err){
console.log(err)
}
let fs=require('fs');
let async = require('async');//异步模块
// 切片的临时存储
let dirPath=__dirname+"/../uploadFiles/tep/";
let index=req.body['index'];//当前片数
let total=req.body['total'];//总片数
let name=req.body['name'];//文件名称
let url= dirPath+'/'+name.split('.')[0]+'_'+index+'.'+name.split('.')[1];//临时bolb文件新名字
try{
if(index==total){//当最后一个分片上传成功,进行合并
/*
检查文件是存在,如果存在,重新设置名称
*/
let bf=Buffer.from(req.body['data'])
fs.writeFileSync(url,bf)
let uid=uuid.v4()
fs.mkdirSync(__dirname+"/../uploadFiles/"+uid)
let pathname=__dirname+"/../uploadFiles/"+uid+'/'+name;//上传文件存放位置和名称
//这里定时,是做异步串行,等上执行完后,再执行下面
setTimeout(function(){
let writeStream=fs.createWriteStream(pathname);
let aname=[];
for(let i=1;i<=total;i++){
let url=dirPath+'/'+name.split('.')[0]+'_'+i+'.'+name.split('.')[1];
aname.push(url);
}
let bf=[]
//async.eachLimit进行同步处理
async.eachLimit(aname,1,function(item,callback){
//item 当前路径, callback为回调函数
fs.readFile(item,function(err,data){
if(err)throw err;
//把数据写入流里,这里有两种方式
// 第一种是利用stream边读边写,这种方式相对于第二种对于内存更加友好
writeStream.write(data);
// 第二种是利用拼接buffer进行,但由于是将所有文件读取buffer并放在bf数组中进行拼接,可能由于bf数组过大导致内存溢出,所以舍弃
// bf.push(data)
//删除生成临时bolb文件
fs.unlink(item,function(){console.log('删除成功');})
callback();
});
},function(err){
if (err) throw err;
//后面文件写完,关闭可写流文件,文件已经成生完成
// 这里同时有两种方式进行文件合并
//第一种,是关闭流,由于利用stream是边读边写的,对内存友好
writeStream.end();
// 第二种,利用Buffer的concat函数进行buffer拼接,这种方式可能会造成内存溢出,故舍弃
// let re=Buffer.concat(bf)
// fs.writeFileSync(pathname,re)
//返回给客服端,上传成功
let data=JSON.stringify({'code':0,"data": {
"source_store_id": uid,
"file_name": name
}});
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
res.end(data);//返回数据
});
},50)
}else{//还没有上传文件,请继续上传
// bf.fill(req.body['data'])
let bf=Buffer.from(req.body['data'])
fs.writeFileSync(url,bf)
let data=JSON.stringify({'code':1,'msg':'继续上传'});
res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8'});
res.end(data);//返回数据
}
}catch(err){
console.log(err)
}