js文件分段上传
2017-10-13 14:25 muamaker 阅读(395) 评论(0) 编辑 收藏 举报前端代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" lang="zh-CN"> <head> <title>分割大文件上传</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> #test{ width: 200px; height: 100px; border: 1px solid green; display: none; } #img{ width: 50px; height: 50px; display: none; } #upimg{ text-align: center; font: 8px/10px '微软雅黑','黑体',sans-serif; width: 300px; height: 10px; border: 1px solid green; } #load{ width: 0%; height: 100%; background: green; text-align: center; } </style> </head> <body> <form enctype="multipart/form-data" action="file.php" method="post"> <!-- <input type="file" name="pic" /> <div id="img"></div> <input type="button" value="uploadimg" onclick="upimg();" /><br /> --> <div id="upimg"> <div id="load"></div> </div> <input type="file" name="mof" multiple="multiple"/> <input type="button" value="uploadfile" onclick="upfile();" /> <input type="submit" value="submit" /> </form> <div id="test"> 测试是否DIV消失 </div> <script type="text/javascript"> var xhr=null; if (window.XMLHttpRequest) {// code for all new browsers xhr=new XMLHttpRequest(); } else if (window.ActiveXObject) {// code for IE5 and IE6 xhr=new ActiveXObject("Microsoft.XMLHTTP"); } if(xhr == null){ alert("Your browser does not support XMLHTTP."); } if (window.File && window.FileReader && window.FileList && window.Blob) { // Great success! All the File APIs are supported. } else { alert('The File APIs are not fully supported in this browser.'); } var fd; var des=document.getElementById('load'); var file; const LENGTH=2*1024*1024; var start; var end; var blob; var pecent; var filename; //var pending; //var clock; function upfile(){ start=0; end=LENGTH+start; //pending=false; file=document.getElementsByName('mof')[0].files[0]; //filename = file.name; if(!file){ alert('请选择文件'); return; } //clock=setInterval('up()',1000); up(); } function up(){ /* if(pending){ return; } */ if(start<file.size){ xhr.open('POST','file.php',true); //xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xhr.onreadystatechange=function(){ if(this.readyState==4){ if(this.status>=200&&this.status<300 || this.status == 304){ if(this.responseText.indexOf('failed') >= 0){ if(this.responseText.indexOf('success') >= 0){ alert('文件发送成功'); }else{ alert('文件发送失败,请重新发送'); des.style.width='0%'; } }else{ start=end; end=start+LENGTH; setTimeout('up()',100); } } } } xhr.upload.onprogress=function(ev){ if(ev.lengthComputable){ pecent=100*(ev.loaded+start)/file.size; if(pecent>100){ pecent=100; } //num.innerHTML=parseInt(pecent)+'%'; des.style.width=pecent+'%'; des.innerHTML = parseInt(pecent)+'%' } } //分割文件核心部分slice if(file.slice){ blob=file.slice(start,end); }else if (file.webkitSlice) { blob = file.webkitSlice(start, stop + 1); } else if (file.mozSlice) { blob = file.mozSlice(start, stop + 1); }else{ alert('不支持slice上传'); return; } fd=new FormData(); fd.append('mof',blob); fd.append('test',file.name); //console.log(fd); //pending=true; xhr.send(fd); }else{ alert('上传成功'); } } xhr.onerror = function(e){ console.log(e); alert('服务器错误'); } function change(){ des.style.width='0%'; } </script> </body> </html>
PHP代码
<?php /**** waited ****/ //print_r($_FILES);exit; $file = $_FILES['mof']; $type = trim(strrchr($_POST['test'], '.'),'.'); // print_r($_POST['test']);exit; if($file['error']==0){ if(!file_exists('./upload/upload.'.$type)){ if(!move_uploaded_file($file['tmp_name'],'./upload/.'.$type)){ echo 'failed'; } }else{ $content=file_get_contents($file['tmp_name']); if (!file_put_contents('./upload/.'.$type, $content,FILE_APPEND)) { echo 'failed'; } } }else{ echo 'failed success'; } ?>
使用md5加密参数
使用es6的封装,支持断点续传
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>文件上传</title> <script src="https://code.jquery.com/jquery-3.4.1.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script> </head> <body> <h1>大文件上传测试</h1> <div> <h3>自定义上传文件</h3> <input id="file" type="file" name="avatar"/> <div> <input id="submitBtn" type="button" value="提交"> </div> </div> <script type="text/javascript"> //开始插件代码 const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; class FlieSlice { constructor(opt){ let def = { progress: function(){}, success: function(){}, fail: function(){}, chunkSize:2*1024*1024, // 每个chunk的大小,设置为2兆 userId:"" ,//如果传了用户的 id,通过 hash 记录在 localstorage ,就可以断点续传,不传 userid,则不考虑断点续传 autoUpload:true //自动上传 }; def = Object.assign(def,opt); def.blockCount = Math.ceil(opt.file.size / def.chunkSize); // 分片总数 this.def = def; this.paused = false; this.current = 0; this.iscontinue = false; if(def.autoUpload){ this.send(); } } async send(){ try{ this.hash = await this.getHash(); //这个是文件的hash,可以再加上用户id,这样就可以做断点续传 if(this.def.userId){ //需要断点续传的判断 const prevIndex = window.localStorage.getItem(this.hash); const index = Number(prevIndex); if(prevIndex && index < this.def.blockCount){ //如果存在,说明之前这个文件没有传完 this.iscontinue = true; this.upload(index); }else{ //如果不存在,则不会继续了 this.upload(); } }else{ this.upload(); } }catch(e){ console.warn(e); this.paused = true; } } upload(i){ i = i || 0; const me = this; const def = me.def; const {file,blockCount,chunkSize} = def; const hash = me.hash; me.current = i; if(i >= blockCount || me.paused){ return ; } const start = i * chunkSize; const end = Math.min(file.size, start + chunkSize); if(start >= end){ return ; } console.log(`start:${start}_end:${end},${i}`); def.progress && def.progress({ name:file.name, file:blobSlice.call(file, start, end), // 当前的块 done: i >= blockCount-1, // 是否已经发送完 hash:hash, // hash size:file.size, // 文件的总大小 index:i, // 当前传到了第几块 count:blockCount, // 总块数 iscontinue:me.iscontinue, //这次上,属于断点续传 next(err){ // 下次迭代. if(err){ me.paused = true; window.localStorage.removeItem(hash); return ; } ++i; if(i < blockCount){ //每次上传,都记录 hash window.localStorage.setItem(hash,i); me.upload(i); }else{ window.localStorage.removeItem(hash); def.success && def.success(); } } }); } getHash(){ const me = this; const {file,blockCount,chunkSize,userId} = me.def; return new Promise((resolve, reject) => { let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); function loadNext() { const start = currentChunk * chunkSize; const end = Math.min(file.size, start + chunkSize); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } fileReader.onload = e => { spark.append(e.target.result); // Append array buffer currentChunk += 1; if (currentChunk < blockCount) { loadNext(); } else { const result = spark.end(); // 如果单纯的使用result 作为hash值的时候, 如果文件内容相同,而名称不同的时候 // 想保留两个文件无法保留。所以把文件名称加上。 const sparkMd5 = new SparkMD5(); sparkMd5.append(result); sparkMd5.append(file.name); if(userId){ sparkMd5.append(userId); } const hexHash = sparkMd5.end(); resolve(hexHash); } }; fileReader.onerror = (e) => { console.warn('文件读取失败!'); reject(e); }; loadNext(); }).catch(err => { console.log(err); }); } paused(){ //停止上传 this.paused = true; } play(){ //开始上传 this.paused = false; this.upload(this.current); } } //插件代码结束 const submitBtn = $('#submitBtn'); const fileDom = $('#file')[0]; submitBtn.on('click',() => { // 获取到的files为一个File对象数组,如果允许多选的时候,文件为多个 const files = fileDom.files; const file = files[0]; if (!file) { alert('没有获取文件'); return; } new FlieSlice({ file:file, userId:"mannymyu", progress(obj){ console.log(obj); const form = new FormData(); form.append('file', obj.file); form.append("name",obj.name); form.append('count', obj.count); form.append('index', obj.index); form.append('size', obj.size); form.append('hash', obj.hash); form.append("done",obj.done); $.ajax({ url: 'http://127.0.0.1:3000', type: 'post', data: form, contentType: false, processData: false, success: function (res) { console.info(res); //在这里 执行自己的 ajax 上传,执行成功后调用 obj.next 执行下次 if(res.code == 200){ obj.next(); }else{ obj.next(res); } }, error: function (error) { console.info(error); obj.next(error); } }) }, success(){ console.log("success"); } }) }) </script> </body> </html>
使用node 的 koa 来完成后端
const os = require('os'); const path = require('path'); const koaBody = require('koa-body'); const Koa = require('koa'); const app = new Koa(); const cors = require('koa2-cors'); const promisify = require("util").promisify; const fs = require("fs"); const readFile = promisify(fs.readFile); const fsextra = require('fs-extra') //递归的创建文件夹 function mkdirs(dirpath) { if (!fs.existsSync(path.dirname(dirpath))) { mkdirs(path.dirname(dirpath)); } fs.mkdirSync(dirpath); } function createDir(myPath){ fs.existsSync(myPath) == false && mkdirs(myPath); } //合并文件 function mergeFile(target,arr){ return new Promise((resolve,reject)=>{ function write(curr){ fs.stat(target,function (err,stat) { if(err){ let WStream = fs.createWriteStream(target); let readStream = fs.createReadStream(curr); readStream.pipe(WStream); run() }else if(stat.isFile()){ let size = stat.size; let WSoptions = { start: size, flags: "r+" } let WStream = fs.createWriteStream(target,WSoptions) let readStream = fs.createReadStream(curr); readStream.pipe(WStream); run() }else{ reject("不是一个文件"); } }) } function run(){ if(arr.length > 0){ write(arr.shift()); }else{ resolve(target); } } run(); }); } const main = async function(ctx) { const filePaths = []; const files = ctx.request.files || {}; const params = ctx.request.body; const temp = path.join(__dirname, "tmp" ,params.hash); const filePath = path.join(temp , `${params.hash}_${params.index}`); createDir(temp); for (let key in files) { const file = files[key]; if(Object.prototype.toString.call(file) == '[object Array]'){ ctx.body = { code:403, msg:"分片上传不允许多文件上传" } return ; }else{ const reader = fs.createReadStream(file.path); const writer = fs.createWriteStream(filePath); reader.pipe(writer); let staticDir; console.log(params); if(params.done == "true"){ //如果已经完成了---合成文件 staticDir = path.join(__dirname, "static",params.name); //最后文件存的地址 var arr = []; for(var i = 0; i <= params.index; i++){ arr.push( path.join(temp , `${params.hash}_${i}`) ); } try{ let res = await mergeFile(staticDir,arr); ctx.body = { code:200, data: { done:true, path:staticDir }, msg:"成功" }; }catch(e){ console.log("错误--",e); ctx.body = { code:500, msg:"合并文件错误" }; } //删除临时文件 fsextra.remove(temp, err => { if (err) return console.error("删除文件是失败",err) console.log('删除文件成功!') }); }else{ ctx.body = { code:200, data: { done:false, path: filePath }, msg:"成功" }; } } return ; } }; app.use(cors()); app.use(koaBody({ multipart: true })); app.use(main); app.listen(3000);
typescript 前端版本
import SparkMD5 from "spark-md5"; //开始插件代码 const blobSlice = File.prototype.slice ; interface IDEF{ progress?: (params:IProgress)=>void; success?:()=>void; fail?:()=>void; chunkSize?: number; userId?:string; autoUpload?:boolean; blockCount?: number; file: File | Blob ; } interface IProgress{ next:(err:Error)=>void; file: File | Blob ; iscontinue:boolean; hash:string; name:string; done:boolean; size: number; index: number; count: number; } export default class FlieSlice{ def:IDEF; paused:boolean; current:number; iscontinue:boolean; hash:string; constructor(opt:IDEF){ let def = { progress: function(a:IProgress){}, success: function(){}, fail: function(){}, chunkSize:2*1024*1024, // 每个chunk的大小,设置为2兆 userId:"" ,//如果传了用户的 id,通过 hash 记录在 localstorage ,就可以断点续传,不传 userid,则不考虑断点续传 autoUpload:true , //自动上传 blockCount: 0, file:null, }; def = Object.assign(def,opt); def.blockCount = Math.ceil(opt.file.size / def.chunkSize); // 分片总数 this.def = def; this.paused = false; this.current = 0; this.iscontinue = false; if(def.autoUpload){ this.send(); } } async send(){ try{ this.hash = await this.getHash() as string; //这个是文件的hash,可以再加上用户id,这样就可以做断点续传 if(this.def.userId){ //需要断点续传的判断 const prevIndex = window.localStorage.getItem(this.hash); const index = Number(prevIndex); if(prevIndex && index < this.def.blockCount){ //如果存在,说明之前这个文件没有传完 this.iscontinue = true; this.upload(index); }else{ //如果不存在,则不会继续了 this.upload(); } }else{ this.upload(); } }catch(e){ console.warn(e); this.paused = true; } } upload(i:number = 0){ const me = this; const def = me.def; const {file,blockCount,chunkSize} = def; const hash = me.hash ; me.current = i; if(i >= blockCount || me.paused){ return ; } const start = i * chunkSize; const end = Math.min(file.size, start + chunkSize); if(start >= end){ return ; } console.log(`start:${start}_end:${end},${i}`); def.progress && def.progress({ name: (file as File).name, file:blobSlice.call(file, start, end), // 当前的块 done: i >= blockCount-1, // 是否已经发送完 hash:hash, // hash size:file.size, // 文件的总大小 index:i, // 当前传到了第几块 count:blockCount, // 总块数 iscontinue:me.iscontinue, //这次上,属于断点续传 next(err:Error){ // 下次迭代. if(err){ me.paused = true; window.localStorage.removeItem(hash); return ; } ++i; if(i < blockCount){ //每次上传,都记录 hash window.localStorage.setItem(hash,String(i)); me.upload(i); }else{ window.localStorage.removeItem(hash); def.success && def.success(); } } }); } getHash(){ const me = this; const {file,blockCount,chunkSize,userId} = me.def; return new Promise((resolve, reject) => { let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); function loadNext() { const start = currentChunk * chunkSize; const end = Math.min(file.size, start + chunkSize); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } fileReader.onload = e => { spark.append(e.target.result); // Append array buffer currentChunk += 1; if (currentChunk < blockCount) { loadNext(); } else { const result = spark.end(); // 如果单纯的使用result 作为hash值的时候, 如果文件内容相同,而名称不同的时候 // 想保留两个文件无法保留。所以把文件名称加上。 const sparkMd5 = new SparkMD5(); sparkMd5.append(result); sparkMd5.append((file as File).name); if(userId){ sparkMd5.append(userId); } const hexHash = sparkMd5.end(); resolve(hexHash); } }; fileReader.onerror = (e) => { console.warn('文件读取失败!'); reject(e); }; loadNext(); }).catch(err => { console.log(err); }); } pause(){ //停止上传 this.paused = true; } play(){ //开始上传 this.paused = false; this.upload(this.current); } } //插件代码结束