1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 大文件分片上传 断点续传 秒传 实现

大文件分片上传 断点续传 秒传 实现

时间:2021-06-19 10:58:36

相关推荐

大文件分片上传 断点续传 秒传 实现

前段时间做视频上传业务,通过网页上传视频到服务器。 视频大小 小则几十M,大则 1G+,以一般的HTTP请求发送数据的方式的话,会遇到的问题:1,文件过大,超出服务端的请求大小限制;2,请求时间过长,请求超时;3,传输中断,必须重新上传导致前功尽弃; 解决方案: 1,修改服务端上传的限制配置;Nginx 以及 PHP 的上传文件限制 不宜过大,一般5M 左右为好; 2,大文件分片,一片一片的传到服务端,再由服务端合并。这么做的好处在于一旦上传失败只是损失一个分片而已,不用整个文件重传,而且每个分片的大小可以控制在4MB以内,服务端限制在4M即可。

前端

Web前端可使用百度的webuploaderH5大文件分片上传插件;官网地址:/webuploader/

<div class="section section6 section5"><div class="part1"><a href="javascript:;" target="_blank" class="part1__btn">批量删除</a><span class="part1__txt"><em class="part1__num" id="upload_num">0</em>个视频,共 <em class="part1__num" id="upload_size">0M</em></span></div><table class="section5__table"><tbody id="thelist"><tr class="thead"><th class="col1 allCkeck"><input type="checkbox" name="" class="col1__checkBox"/>视频名称</th><th class="col2">视频大小</th><th class="col3">视频分类</th><th class="col4">状态</th><th class="col5">进度</th><th>操作</th></tr></tbody></table><div class="selFile" id="selFile"><div id="drag_tips"><div id="btns__add2"></div><h2 class="txt1">选择视频文件</h2><span class="txt2">或直接将文件拖拽至此窗口</span></div></div><div class="btns"><span class="btns__add" id="btns__add">+添加视频文件</span><span class="btns__upload btns__upload-start" id="uploadBtn"><i class="btns__upload_icon"></i>开始上传视频</span></div></div>

//引入插件

<script type="text/javascript" src="media/js/lib/webuploader/js/webuploader.min.js"></script>

upload.js

1 // 文件上传 2 jQuery(function() { 3var $ = jQuery, 4 $list = $('#thelist'), 5 $btn = $('#upload-start'), 6 $thead = $('.thead'), 7 $part_btn = $('.part1__btn'), //批量上传按钮 8 state = 'pending', 9 fileCount = 0, //上传文件总数 10 fileSize = 0,//上传文件的总大小 11// 上传按钮 12 $upload = $('#uploadBtn'), 13// 所有文件的进度信息,key为file id 14 percentages = {}, 15// 所有文件的md5,key为file id 16 md5Obj = {}, 17// 可能有pedding, ready, uploading, confirm, done. 18 state = 'pedding', 19 uploader; 20 21//浏览器关闭提醒 22window.is_confirm = false; 23$(window).bind('beforeunload', function(){ 24 // 只有在标识变量is_confirm不为false时,才弹出确认提示 25 if(window.is_confirm !== false) 26 return '正在上传视频,该操作将丢失视频,是否继续?'; 27}) 28 29if ( !WebUploader.Uploader.support() ) { 30 alert( 'Web Uploader 不支持您的浏览器!如果你使用的是IE浏览器,请尝试升级浏览器'); 31 throw new Error( 'WebUploader does not support the browser you are using.' ); 32} 33 34$(".pop2 .btns__sure").click(function(){ 35 $('.popup,.pop').hide(); 36}); 37 38uploader = WebUploader.create({ 39 //拖拽容器 40 dnd:'#selFile', 41 42 // 不压缩image 43 resize: false, 44 45 // swf文件路径 46 swf: '/media/js/lib/webuploader/js/Uploader.swf', 47 48 // 文件接收服务端。 49 server: '/service/upload/upload_file', 50 //server:'http://vod./service/upload/ssl_upload_file', 51 formData: { 52 file_id: 'file', 53 guid:new Date().getTime() + Math.ceil(Math.random()*100), 54 file_name:'' 55 }, 56 57 // 选择文件的按钮。可选。 58 // 内部根据当前运行是创建,可能是input元素,也可能是flash. 59 pick: { 60 id:'#btns__add', 61 innerHTML:"+添加视频文件" 62 }, 63 64 // 开起分片上传。 65 chunked: true, 66 67 //如果要分片,分多大一片2M 68 chunkSize:2*1024*1024, 69 70 //上传文件的类型 71 accept:{ 72 title: 'Videos', 73 extensions: 'mp4,avi,flv', 74 mimeTypes: 'video/*' 75 }, 76 //验证文件总数量, 超出则不允许加入队列。 77 fileNumLimit: 10, 78 //单个文件上传的大小限制 2G 79 fileSingleSizeLimit:2*1024*1024*1024, 80 81}); 82 83//添加文件具体函数 84function addFile( file ){ 85 var data = new Date(), 86 month = (data.getMonth()+1)<10 ? '0'+(data.getMonth()+1) : (data.getMonth()+1), 87 day = data.getDate()<10 ? '0'+ data.getDate(): data.getDate(), 88 time = data.getFullYear() + "-" + month + "-" + day, 89 $tr = $('<tr class="toBeUploaded" id="'+file.id+'"></tr>'), 90 $td = $('<td class="col1"><input type="checkbox" name="" class="col1__checkBox"/><input type="text" value="'+ file.name +'" name="" class="name"/></td><td class="col2">'+convert_size(file.size)+'</td><td class="col3"><select class="class_id">'+ class_options +'</select></td><td class="col4">读取视频中</td><td class="col5">0%</td><td class="col6"><ul><li class="view"><a target="_blank" href="javascript:;">查看</a></li><li class="delete">删除</li></ul></td>').appendTo($tr), 91 $state = $tr.find('td.col4'), 92 $prgress = $tr.find('td.col5'), 93 $delbtn = $tr.find('li.delete'); 94 95 $("#selFile").hide(); 96 97 if ( file.getStatus() === 'invalid' ) { 98 switch( file.statusText ) { 99 case 'exceed_size':100 text = '文件大小超出';101 break;102 103 case 'interrupt':104 text = '上传暂停';105 break;106 107 default:108 text = '上传失败,请重试';109 break;110 }111 showError(text);112 } else {113 // @todo lazyload114 percentages[ file.id ] = [ file.size, 0 ];115 file.rotation = 0;116 }117 118 file.on('statuschange', function( cur, prev ) {119 if ( prev === 'progress' ) {120 //上传成功121 } else if ( prev === 'queued' ) {122 // 开始上传123 }124 125 // 成功126 if ( cur === 'error' || cur === 'invalid' ) {127 console.log( file.statusText );128 showError( file.statusText );129 percentages[ file.id ][ 1 ] = 1;130 } else if ( cur === 'interrupt' ) {131 showError( 'interrupt' );132 } else if ( cur === 'queued' ) {133 percentages[ file.id ][ 1 ] = 0;134 } else if ( cur === 'progress' ) {135 // 正在上传136 137 } else if ( cur === 'complete' ) {138 // 上传完成139 140 }141 142 $tr.removeClass( 'state-' + prev ).addClass( 'state-' + cur );143 });144 $delbtn.on('click',function(){145 uploader.removeFile( file );146 });147 $tr.appendTo($list);148 //$tr.insertAfter($thead);149}150 151// 负责view的销毁152function removeFile( file ) {153 var $tr = $('#'+file.id);154 155 delete percentages[ file.id ];156 $tr.off().find('.col6').off().end().remove();157}158 159function setState( val ) {160 var file, stats;161 162 if ( val === state ) {163 return;164 }165 166 $upload.removeClass( 'state-' + state );167 $upload.addClass( 'state-' + val );168 state = val;169 170 switch ( state ) {171 case 'pedding':172 uploader.refresh();173 break;174 175 case 'ready':176 uploader.refresh();177 break;178 179 case 'uploading':180 $upload.text( '暂停上传' );181 break;182 case 'paused':183 $upload.text( '继续上传' );184 break;185 186 case 'confirm':187 //$progress.hide();188 $upload.text( '开始上传' ).addClass( 'disabled' );189 190 stats = uploader.getStats();191 if ( stats.successNum && !stats.uploadFailNum ) {192 setState( 'finish' );193 return;194 }195 break;196 case 'finish':197 stats = uploader.getStats();198 if ( stats.successNum ) {199 alert( '上传成功' );200 } else {201 // 没有成功的图片,重设202 state = 'done';203 location.reload();204 }205 break;206 }207}208 209 210// 当有文件添加进来的时候211uploader.on( 'fileQueued', function( file ) {212 fileCount++;213 fileSize += file.size;214 $("#upload_num").text(fileCount);215 $("#upload_size").text(convert_size(fileSize));216 md5Obj[ file.id ] = '';217 //获取文件MD5 值218 uploader.md5File( file )219 // 及时显示进度220 .progress(function(percentage) {221 $( '#'+file.id ).find('.col4').text('读取文件'+parseInt(percentage*100)+"%");222 })223 // 完成224 .then(function(val) {225 console.log('md5 result:', val);226 md5Obj[ file.id ] = val;227 $( '#'+file.id ).find('.col4').text('待上传');228 setState( 'ready' );229 });230 addFile( file );231});232 233// 删除文件234uploader.onFileDequeued = function( file ) {235 fileCount--;236 fileSize -= file.size;237 $("#upload_num").text(fileCount);238 $("#upload_size").text(convert_size(fileSize));239 if ( !fileCount ) {240 setState( 'pedding' );241 }242 removeFile( file );243 244};245 246// 添加“添加文件”的按钮,247uploader.addButton({248 id: '#btns__add2',249 label: ''250});251 252// 文件上传过程中创建进度实时显示。253uploader.onUploadProgress = function( file, percentage ) {254 var $tr = $('#'+file.id),255 $percent = $tr.find('td.col5'),256 $state = $tr.find('td.col4');257 percentage = parseInt(percentage*100);258 if(! (percentage == 0 && percentage == 100)){259 $state.text("正在上传");260 }261 $percent.text( percentage + "%")262 percentages[ file.id ][ 1 ] = percentage;263};264 265//上传前,请求服务端 判断文件是否已经上传过266uploader.on( 'uploadStart', function( file ) {267 var type = 'POST';268 var url = '/service/upload/determine_video_exist';269 var request_data = {270 'md5': md5Obj[ file.id ],271 'type':1272 };273 var success = function(r) {274 uploader.upload( file );275 console.log(r);276 if(r.code == 1) {277 uploader.skipFile( file );278 $( '#'+file.id ).find('.col4').text('视频已存在');279 $( '#'+file.id ).find('.col5').text('100%');280 $('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ r.data.video_id);281 $('.pop2 .video_game').text("所在游戏:"+r.data.game_name);282 $('.pop2 .create_time').text("上传时间:"+r.data.create_time);283 $('.pop').hide();284 $('.pop2').show();285 $('.popup').show();286 }else if(r.code <= 0) {287 showError(r.msg);288 }else {289 290 }291 };292 request(type, url, request_data, success);293});294 295//当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。296uploader.on('uploadBeforeSend', function (obj, data, headers) {297 $tr = $("#"+data.id);298 var file_name = $tr.find(".name").val();299 var class_id = $tr.find("select.class_id").val();300 var reg = /[1-9][0-9]*/g;301 data.md5 = md5Obj[ obj.file.id ];302 data.file_name = file_name;303 data.class_id = class_id;304 data.guid = data.guid + data.id.replace(/[^0-9]+/g, '');305});306 307uploader.on( 'uploadSuccess', function( file ,res) {308 if(res.code == 1){309 $( '#'+file.id ).find('.col4').text('成功上传');310 console.log(res);311 $('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ res.data.video_id);312 }else if(res.code == 2) {313 $( '#'+file.id ).find('.col4').text('视频已存在');314 console.log(res);315 $('#'+file.id).find('.view').find('a').attr('href',playmain +'/?video_id='+ res.data.video_id);316 }else {317 showError(res.msg);318 }319});320 321uploader.on( 'uploadError', function( file,reason ) {322 $( '#'+file.id ).find('.col4').text('上传失败');323 console.log(reason);324});325 326uploader.on( 'uploadComplete', function( file ) {327 $( '#'+file.id ).find('.progress').fadeOut();328});329 330uploader.on( 'all', function( type ) {331 if ( type === 'startUpload' ) {332 state = 'uploading';333 } else if ( type === 'stopUpload' ) {334 state = 'paused';335 } else if ( type === 'uploadFinished' ) {336 state = 'done';337 }338 339 if ( state === 'uploading' ) {340 window.is_confirm = true;341 $('.toBeUploaded').addClass("uploaded").removeClass("toBeUploaded");342 $('input.name').attr("disabled","disabled");343 $('input.col1__checkBox').hide();344 $('input').attr("disabled","disabled");345 $('select.class_id').attr("disabled","disabled");346 $('.btns__add').remove();347 $upload.addClass("btns__upload-ing").removeClass("btns__upload-start").html('<i class="btns__upload_icon"></i>正在上传视频');348 349 } else if(state === 'done') {350 window.is_confirm = false;351 console.log("上传完成");352 $upload.addClass("btns__upload-start btns__upload-refresh").removeClass("btns__upload-ing").html('<i class="btns__upload_icon"></i>开始上传视频');353 }354});355 356/**357* 验证文件格式以及文件大小358*/359uploader.on("error",function (type){360 var msg = ''361 switch (type){362 case "Q_TYPE_DENIED": msg = "请上传mp4格式文件";break;363 case "F_EXCEED_SIZE": msg = "文件大小不能超过1G";break;364 case "Q_EXCEED_NUM_LIMIT" : msg = "一次最多能上传10个文件";break;365 default: msg='';366 }367 if(msg != ''){368 showError(msg);369 }370});371 372$part_btn.on('click',function(){373 $('td .col1__checkBox').each(function(){374 if($(this).is(':checked')){375 var $tr = $(this).parents('tr');376 var id = $tr.attr('id');377 uploader.removeFile( id );378 }379 });380});381$upload.on('click', function() {382 var isbreak = false;383 $(".name").each(function(){384 if(!$(this).val()|| $(this).val() == ''){385 isbreak = true;386 }387 })388 if(isbreak){389 showError("文件名不能存在为空");390 return;391 }392 $("select.class_id").each(function(){393 if(!$(this).val()|| $(this).val() == ''){394 isbreak = true;395 }396 })397 if(isbreak){398 showError("分类不能为空,请先添加分类");399 return;400 }401 if ( $(this).hasClass( 'btns__upload-refresh' ) ) {402 location.reload();403 }404 if ( $(this).hasClass( 'btns__upload-ing' ) ) {405 return false;406 }407 var md5Ready = true;408 $.each(md5Obj,function(index,item){409 if(!item || item==''){410 md5Ready = false;411 }412 });413 if(!md5Ready){414 showError('文件尚未读取完成,请耐心等待');415 return false;416 }417 if ( state === 'ready' && md5Ready ) {418 uploader.upload();419 } else if ( state === 'paused' ) {420 uploader.upload();421 } else if ( state === 'uploading' ) {422 uploader.stop();423 }424});425$upload.addClass( 'state-' + state );426 427 });

后台(PHP)【仅分片上传相关代码】

1public function action_upload_file(){ 2 $file_id = R::string('file_id', 'file'); 3 $keepFileName = R::string('keepFileName', 0); 4 $unsize_change = R::numeric('unsize_change',0); 5 $id = R::string('id'); //插件每上传一个视频自带id 6 $guid = R::string('guid'); //标识视频 7 $chunks = R::numeric('chunks'); // 分片数 8 $chunk = R::numeric('chunk'); //分片号 9 $file_name = R::string('file_name'); 10 $file = isset($_FILES[$file_id])?$_FILES[$file_id]:''; 11 $md5 = R::string('md5'); 12 $this->upload = new Common_Upload(); 13 14 if(empty($guid) || empty($file_name) || empty($md5)){ 15 $this->response_msg(-1, 'guid 或 file_name 或 md5 不能为空'); 16 return; 17 } 18 19 if(empty($file['name'])){ 20 $this->response_msg(-1, '请上传一个文件'); 21 return; 22 }else{ 23 if($chunks){ 24 $res = $this->upload->saveFile_chunks($file,$chunks, $chunk, $guid); 25 if(empty($res)){ 26 $this->response_msg(-2, '分片上传失败'); 27 return; 28 } 29 30 }else if($keepFileName){ 31 $res = $this->upload->saveFile_nochunks($file, '', '', $keepFileName); 32 }else{ 33 $res = $this->upload->saveFile_nochunks($file); 34 } 35 if(empty($res)){ 36 $err = $this->upload->getError(); 37 $this->response_msg(-3, '上传文件出错!msg:'.print_r($err, true)); 38 return; 39 } 40 if($unsize_change){ 41 $size = $res['size']; 42 }else{ 43 $size = $this->convert_size($res['size']); 44 } 45 46 //视频上传完成 47 if($chunks && $res['last_chunk']){ 48 $domain = Kohana::$config->load('domain'); 49 $video_domain = $domain[RUN_MOD]['VIDEO']; 50 51 if(!empty($file_name)){ 52 $res['name'] = $file_name; 53 } 54 $video_data = array( 55 'video_name'=> $file_name, 56 'video_url'=> $video_domain."/".$res['path'], 57 'size'=>$res['size'], 58 'create_time'=> date('y-m-d H:i:s',time()), 59 'update_time'=> date('y-m-d H:i:s',time()), 60 'duration'=> $res['time'], 61 'md5'=>$md5 62 ); 63 $video_mod = new Model_Videoinfo(); 64 $video = $video_mod->save_video($video_data,$guid); 65 $res = array( 66 'path'=>$res['path'], 67 'chunks'=>$chunks, 68 'chunk'=>$chunk, 69 'size'=>$size, 70 'guid'=> $guid, 71 'video_id'=>$video[0], 72 'file'=>$file, 73 'id'=>$id 74 ); 75 $this->response_msg(1,'视频上传成功',$res); 76 return; 77 } 78 //非分片上传 79 if(!$chunks){ 80 $domain = Kohana::$config->load('domain'); 81 $video_domain = $domain[RUN_MOD]['VIDEO']; 82 if(!empty($file_name)){ 83 $res['name'] = $file_name; 84 } 85 $video_data = array( 86 'video_name'=> $file_name, 87 'video_url'=> $video_domain."/".$res['path'], 88 'size'=>$res['size'], 89 'create_time'=> date('y-m-d H:i:s',time()), 90 'update_time'=> date('y-m-d H:i:s',time()), 91 'duration'=> $res['time'], 92 'md5'=>$md5 93 ); 94 $video_mod = new Model_Videoinfo(); 95 $video = $video_mod->save_video($video_data,$guid); 96 if(empty($video)){ 97 $this->response_msg(-6, '视频信息保存失败'); 98 return; 99 }100 $res = array(101 'path'=>$res['path'],102 'video_data'=>$video_data,103 'size'=>$size,104 'guid'=> $guid,105 'video_id'=>$video[0],106 'file'=>$file,107 'id'=>$id108 );109 $this->response_msg(1,'视频上传成功',$res);110 return;111 }112 //分片上传成功(未全部分片上传完成)113 $res = array(114 'chunks'=>$chunks,115 'chunk'=>$chunk,116 );117 $this->response_msg(2, '分片上传成功',$res);118 }119}

1/** 2* 保存分片文件(注意先验证文件是否合法) 3* 4* @param array $file 单个文件 5* @param string $attachdir 上传文件路径 6* @param string $upload_type 上传文件类型 7* @param bool $keepFileName 是否保持文件名,默认不不保持 8* @return bool 9*/ 10public function saveFile_chunks($file,$chunks, $chunk, $guid) 11{ 12 if(empty($guid) || empty($file) ){ 13 return false; 14 } 15 $file_name = (string)$guid . $chunk; 16 //保存分片文件 17 $file_info = $this->saveFile($file, '', '', false, $file_name,true); 18 if ($file_info) { 19 $cache = Cache::instance('memcache'); 20 //记录已上传的分片编号,上传顺序并不是按编号顺序进行上传 21 $chunks_list_pre = $cache->get($guid); 22 if(empty($chunks_list_pre)){ 23 $strarr = array(); 24 for($i=0;$i<$chunks;$i++){ 25 $strarr[] = $guid.$i; 26 } 27 $cache->set($guid,$strarr,60 * 60 * 24); 28 } 29 $file_path = $cache->set($guid.$chunk,$file_info['path'],60 * 60 * 24); 30 31 $chunk_path_array= array(); 32 for($i=0;$i<$chunks;$i++){ 33 if($cache->get($guid.$i)){ 34 $chunk_path_array[$i] = $cache->get($guid.$i); 35 } 36 } 37 list($Y,$M,$D,$H,$I,$S) = explode('-',date("Y-m-d-H-i-s", time())); 38 $file_info['chunks_path_count'] = count($chunk_path_array); 39 $file_info['last_chunk'] = false; 40 if (count($chunk_path_array) == $chunks) { 41 //按目录类型存储 42 $dirType = substr($file_info['type'], 1, strlen($file_info['type']));; 43 //目录类型前面加上前缀url 44 $dirType = $this->pre_url.$dirType; 45 //按年月二级存储 46 $month_file_path = $Y.'/'.$M; 47 $saveName ='upload/mp4/'.$month_file_path.'/original/'.$guid.$file_info['type']; 48 $join_file_name =$this->attachDIR.$saveName; 49 if(!is_dir($this->attachDIR.'upload/mp4/'.$month_file_path.'/original/')){ 50 mkdir($this->attachDIR.'upload/mp4/'.$month_file_path.'/original/',0755,true); 51 } 52 if(! file_exists($join_file_name)){ 53 $fp = fopen($join_file_name, "ab"); 54 //合并过程中对文件加锁,防止同时操作而出错 55 if (flock($fp,LOCK_EX)){ 56for ($i = 0; $i < $chunks; $i++) { 57 $tmp_file = $this->attachDIR . $chunk_path_array[$i]; 58 $handle = fopen($tmp_file, "rb"); 59 fwrite($fp, fread($handle, filesize($tmp_file))); 60 fclose($handle); 61 unset($handle); 62 unlink($tmp_file);//合并完毕的文件就删除 63}//组装分片 64$cache->delete($guid); 65for($i=0;$i<$chunks;$i++){ 66 $cache->delete($guid.$i); 67} 68$time = $this->getTime($join_file_name,$file_info['type']); 69$file_info['time'] = $time; 70$file_info['path'] = $saveName; 71$file_info['size'] = filesize($join_file_name); 72$file_info['last_chunk'] = true; 73 74$model_mod = new Model_Base(); 75$model_mod->disconnect(); 76$pid = pcntl_fork(); 77//父进程和子进程都会执行下面代码 78if ($pid == -1) { 79 //错误处理:创建子进程失败时返回-1. 80 die('could not fork'); 81} else if ($pid) { 82 $model_mod->connect(); 83 //对上传完成的视频进行排队转码 84 $this->thread($join_file_name,$file_info['type'],$guid); 85 //父进程会得到子进程号,所以这里是父进程执行的逻辑 86 pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。 87} else { 88 return $file_info; 89 //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。 90} 91 92 } 93 } 94 95 } 96 return $file_info; 97 } else { 98 $this->error[] = '分片上传失败'; 99 return false;100 }101 /*}}}*/102 }

1,实现了分片上传;

2,同时在上传前检查视频md5 是否在库,如已存在可实现“秒传” 功能,即直接复制数据信息,指向同一个文件,不必再上传;

3,可实现断点续传,上传过程中中断;之前上传的分片已保留在服务器,只需重新上传尚未上传的分片即可;

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。