在开发《工单地图》的时候,后台平面图上传的功能需要处理10M以上大小的文件上传,单个超大文件上传的时候容易出现各种问题,后来采用了分片上传的思路。将大文件分成多个小的文件分片,逐个上传到服务器之后再合并还原成大文件。
效果演示
1、选择待上传的文件
2、点击开始上传按钮
点击开始上传按钮之后,会在前端将文件分割成固定大小的分片,逐个上传到服务器,返回服务器上的相对路径,从网络请求日志里可以看到有4个上传文件的请求,分别返回的路径则在页面通过表单显示出来。
3、合并文件
点击合并按钮,向服务器发送合并请求,将上述4个分片文件合并为一个大的文件,并将相对路径返回,完成大文件上传的过程。
这是一种比较简单的分片文件上传方法。由于整个过程存在文件上传、写入、删除和合并,而且是需要前端和后端配合才能完成整个上传操作,因此在部署到生产环境时需要在代码里针对于潜在的安全漏洞进行防护。
源代码分析
1、前端使用上传组件是百度的webuploader。
<script type="text/javascript" src="=$base_url?>static/plugins/webuploader/webuploader.js">script><script> $list_map_json = $("#fileList_map_json"), $btn_map_json = $("#btn-star_map_json"), state = "pending", uploader_map_json; var uploader_map_json = WebUploader.create({ auto: false, swf: '=$base_url?>static/plugins/webuploader/Uploader.swf', // 文件接收服务端。 server: '=$base_url?>admin_map/webuploader', chunked:true, chunkSize:2000000, threads:1, // 选择文件的按钮。可选。 // 内部根据当前运行是创建,可能是input元素,也可能是flash. pick: { id: '#filePicker_map_json', innerHTML: '点击选择文件', multiple:false }, // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传! resize: false, // 只允许选择图片文件。 accept: { title: 'file', extensions: 'json', mimeTypes: 'application/*' } }); uploader_map_json.on( 'fileQueued', function( file ) { var $li = $( '
' + '
'+
'
' + file.name + '
' +
'
等待上传...
'+
'
' ), $img = $li.find('img'); $list_map_json.append( $li ); // 创建缩略图 // 如果为非图片文件,可以不用调用此方法。 // thumbnailWidth x thumbnailHeight 为 100 x 100 var thumbnailWidth = 100; var thumbnailHeight = 100; uploader_map_json.makeThumb( file, function( error, src ) { if ( error ) { $img.replaceWith('不能预览'); return; } $img.attr( 'src', src ); }, thumbnailWidth, thumbnailHeight ); }); // 文件上传过程中创建进度条实时显示。 uploader_map_json.on( 'uploadProgress', function( file, percentage ) { var $li = $( '#'+file.id ), $percent = $li.find('.progress-box .sr-only'); // 避免重复创建 if ( !$percent.length ) { $percent = $(').appendTo( $li ).find('.sr-only'); } $li.find(".state").text("上传中"); $percent.css( 'width', percentage * 100 + '%' ); }); var str; // 文件上传成功,给item添加成功class, 用样式标记上传成功。 uploader_map_json.on( 'uploadAccept', function( file,rep) { var response = eval(rep); if(response.code === 201) { $( '#'+file.id ).addClass('upload-state-success').find(".state").text("已上传"); str = ' '; $("#input_group").append(str); }else { $( '#'+file.id ).addClass('upload-state-error').find(".state").text(response.message); } }); // 文件上传成功,给item添加成功class, 用样式标记上传成功。 uploader_map_json.on( 'uploadSuccess', function( file,rep) { var response = eval(rep); if(response.code === 201) { $( '#'+file.id ).addClass('upload-state-success').find(".state").text("已上传"); }else { $( '#'+file.id ).addClass('upload-state-error').find(".state").text(response.message); } }); // 文件上传失败,显示上传出错。 uploader_map_json.on( 'uploadError', function( file ) { $( '#'+file.id ).addClass('upload-state-error').find(".state").text("上传出错"); }); // 完成上传完了,成功或者失败,先删除进度条。 uploader_map_json.on( 'uploadComplete', function( file ) { $( '#'+file.id ).find('.progress-box').fadeOut(); }); uploader_map_json.on('all', function (type) { if (type === 'startUpload') { state = 'uploading'; } else if (type === 'stopUpload') { state = 'paused'; } else if (type === 'uploadFinished') { state = 'done'; } if (state === 'uploading') { $btn_map_json.text('暂停上传'); } else { $btn_map_json.text('开始上传'); } }); $btn_map_json.on('click', function () { if (state === 'uploading') { uploader_map_json.stop(); } else { $("#input_group").empty(); uploader_map_json.upload(); } return false; });script><script type="text/javascript">$(function(){ $(".select2").select2(); }); function hebin(){ var file_list = new Array(); $("#input_group input[type='text']").each(function(){ var file; file = $(this).val(); file_list.push(file); }); var file_list_post = new Array(); file_list_post = { 'file_list[]':file_list }; $.ajax({ type:"POST", url:"=$base_url?>admin_map/map_merge", data:file_list_post, traditional:true, dataType:"json", success:function(result){ $("#map_json").val(result.map_json); } });}script>
文件分片的功能是上传组件内置的,只需要在创建对象的时候配置即可。
var uploader_map_json = WebUploader.create({ auto: false, swf: '=$base_url?>static/plugins/webuploader/Uploader.swf', // 文件接收服务端。 server: '=$base_url?>admin_map/webuploader', chunked:true, chunkSize:2000000, threads:1,
在代码中将分片的大小配置为2M,线程设置为1,这样可以控制上传的顺序和进度。
2、后端代码用php实现
public function webuploader(){ $config['upload_path'] = './svg/fjs'; $config['allowed_types'] = 'json|'; $config['max_size'] = 100000; $config['file_ext_tolower'] = TRUE; $config['overwrite'] = TRUE; $config['encrypt_name'] = TRUE; $this->load->library('upload', $config); $this->output->set_header('Content-Type: application/json;charset=utf-8'); if ( ! $this->upload->do_upload('file')) { $error = array('error' => $this->upload->display_errors()); echo '{"jsonrpc" : "2.0", "code": 101, "message": "'.implode($error).'"}'; print_r($this->upload->data()); } else { $upload_data = $this->upload->data(); $uploads = $upload_data['full_path']; $uploads = str_replace(str_replace('\\','/', getcwd()), '', $uploads); echo '{"jsonrpc" : "2.0", "code": 201, "message": "'.$uploads.'"}'; } } public function map_merge(){ $file_list_post = $this->input->post(); $file_list = $file_list_post['file_list']; $file_tmp = $file_list[0]; $file_path_root = dirname(dirname(dirname(__FILE__))); $file_path = dirname($file_tmp); $file_name_tmp = basename($file_tmp,'.json'); $file_name_temp = md5($file_name_tmp); $file_name_return = $file_path.'/'.$file_name_temp.'.json'; $file_name = $file_path_root.$file_path.'/'.$file_name_temp.'.json'; $fp = fopen($file_name,"ab"); foreach ($file_list as $key => $value) { if ( ! empty($value)) { $handle = fopen($file_path_root.$value,"rb"); fwrite($fp, fread($handle, filesize($file_path_root.$value))); fclose($handle); unset($handle); unlink($file_path_root.$value); } } fclose($fp); $res = array( 'map_json'=>$file_name_return ); $this->output->set_header('Content-Type: application/json;charset=utf-8'); echo json_encode($res); }
后端由于使用的是框架,上传和文件合并的代码只需要简单几行。在上传的时候严格限制上传的后缀名,保存文件到服务器上时也将后见面死死的写住,合并的时候前端传过来的文件参数进行多重校验,确保文件操作安全。