Jfinal下使用WebUploader进行分片上传

背景

大文件直传易受网络波动而失败,同时运维人员一直维护失败的队列不合理,需要实现分片上传,webuploader已经是很成熟的分片上传插件了,这里使用webuploader进行分片。

前端

分片通过常规配置可得,主要交互通过以下两个事件可得:

  • beforeSend:分片发送前校验文件,分片若已存在则跳过(这里保证了失败重新传能快速跳过已上传部分)
  • uploadSuccess:文件整体上传完成后进行分片的合并
WebUploader.Uploader.register({ 
	            "before-send":"beforeSend"
	        },{  
	            beforeSend:function(block){
	                var deferred = WebUploader.Deferred();  
	                $.ajax({  
	                    type: "POST",  
	                    url: '/checkChunk',  
	                    async: false,
	                    data: {  
	                        //文件名称  
	                        fileName: block.file.name,
	                        //当前分块下标  
	                        chunk: block.chunk,
	                        //当前分块大小  
	                        chunkSize: block.end - block.start,
	                        //总大小
	                        totalSize: block.total
	                    },  
	                    dataType: "json",  
	                    success: function(res){  
	                        if(res.code == 'YES'){
	                            //分块存在,跳过  
	                            deferred.reject();
	                        }else{  
	                            //分块不存在或不完整,重新发送该分块内容  
	                            deferred.resolve();  
	                        }  
	                    }  
	                });
	                
	                return deferred.promise();  
	            }
	        });
			
			var uploader = WebUploader.create({
		        auto: true, // 选完文件后,是否自动上传
		        resize: false, // 不压缩image
		        duplicate: false,
 		    	chunked: true,  //分片处理
		        chunkSize: 25 * 1024 * 1024, //每片25M
		        chunkRetry: false,//如果失败,则不重试
		        threads:'1',        //同时运行5个线程传输
		        fileNumLimit:'100',  //文件总数量只能选择100个 
		        swf: '/Uploader.swf', // swf文件路径
		        server: /uploadFile', // 文件接收服务端。
		        pick: '#upload', // 选择文件的按钮。可选
		        accept:{
					title: '',
					extensions: 'tif|tiff'
				},
				timeout: 0
		    });
		
		    // 文件上传成功
		    uploader.on('uploadSuccess', function(file) {
		    	//如果分块上传成功,则通知后台合并分块  
                $.ajax({  
                    type: "POST",
                    url: '/mergeChunk',
                    async: false,
                    data:{  
                    	//文件名称  
                        fileName: file.name,
                        //总大小
                        totalSize: file.size
                    }
                });
		    });

后端

控制器

Jfinal使用COS组件进行上传,由于getFile方法没有直接传路径得到File,所以要封装方法在控制器中

    /**
     * 改造getFile方法,避免NPE错误
     * @return
     */
    protected UploadFile getChunkFile(String uploadPath) {
		List<UploadFile> uploadFiles = getFiles(uploadPath);
		return uploadFiles.size() > 0 ? uploadFiles.get(0) : null;
	}

通过三个方法进行文件处理:

  • uploadFile:获取已上传的分片,通过JDK的renameTo方法进行位置移动及重命名(保证分片唯一性)
  • checkChunk:检查分片,对比上传位置的分片及大小判断分片是否完全上传成功
  • mergeChunk:合并分片,遍历分片文件夹,进行合并及文件夹删除
    /**
	 * 上传文件
	 */
	public void uploadFile() {
		//必须要getFile后才能使用getPara方法
		UploadFile upload = getChunkFile(user.getId());
		try {
			Integer chunk = getParaToInt("chunk");
			String name = getPara("name");
			Long size = getParaToLong("size");
			
			File file = upload.getFile();
			File f = FileKit.renameChunk(file, chunk, name, size);
			file.renameTo(f);

		} catch (Exception e) {
			// TODO
		}
        renderNull();
	}
	
	/**
	 * 检查分块
	 */
	public void checkChunk() {
		String fileName = getPara("fileName");  
        Integer chunk = getParaToInt("chunk");
        Long chunkSize = getParaToLong("chunkSize");
        Long totalSize = getParaToLong("totalSize");
        
        File chunkFile = FileKit.getChunkFile(user.getId(), fileName, totalSize, chunk);
        
        // 检查文件是否存在,且大小是否一致
        if(chunkFile.exists() && chunkFile.length() == chunkSize){  
            // 上传过  
        	renderJson(YES);
        }else{  
            // 存在分片则删除
        	if(chunkFile.exists()) {
        		chunkFile.delete();
        	}
        	
        	renderJson(NO);
        }  
	}
	
	/**
	 * 合并分块
	 * @throws IOException 
	 */
	public void mergeChunk() throws IOException {
		String fileName = getPara("fileName");  
        Long totalSize = getParaToLong("totalSize");

        //读取目录里的所有文件  
        File dir = FileKit.getChunkFile(user.getId(), fileName, totalSize, 0).getParentFile();
        if(dir.exists()) {
        	File output = new File(dir.getParent(), fileName);
            FileKit.mergeChunk(dir, output);
            
    		// TODO 后续处理
        }

		renderNull();
	}

工具类FileKit

封装了三个主要方法:

  • getChunkFile:生成新的文件路径(upload/账号ID/文件名_大小)
  • renameChunk:重命名分片
  • mergeChunk:合并分片的封装
/**
     * 获取COS组件的分片
     * 
     * @param userId 用户ID
     * @param name 文件名
     * @param total 文件大小
     * @param chunk 分片序号
     * 
     * @return 分片文件
     */
    public static File getChunkFile(Long userId, String name, Long total, Integer chunk) {
		String body = null;
		int dot = name.lastIndexOf(".");
		if (dot != -1) {
			body = name.substring(0, dot);
		} else {
			body = name;
		}
    	
    	StringBuilder sb = new StringBuilder();
    	sb.append(Constant.SERVICE_UPLOAD_PATH);
    	sb.append(File.separator);
    	sb.append(userId);
    	sb.append(File.separator);
    	sb.append(body + "_" + total);
    	sb.append(File.separator);
    	sb.append(chunk);
    	
    	return new File(sb.toString());
    }
    
    /**
     * 重命名分片
     * @param f 文件
     * @return 文件
     */
    public static File renameChunk(File f) {
    	//默认第一个为0
    	f = new File(f.getParent(), "0");

    	//后续累加
		int count = 0;
		while (f.exists() && count < 9999) {
			count++;
			f = new File(f.getParent(), String.valueOf(count));
		}

		return f;
	}
    
    /**
     * 重命名分片
     * @param f 文件
     * @param chunk 分片序号
     * @param name 文件名
     * @param size 文件大小
     * @return 文件
     */
    public static File renameChunk(File f, Integer chunk, String name, Long size) {
    	//新的父文件
		String body = null;
		int dot = name.lastIndexOf(".");
		if (dot != -1) {
			body = name.substring(0, dot);
		} else {
			body = name;
		}
    	File parent = new File(f.getParent(), body + "_" + size);
    	
    	if (!parent.exists()) {
			if (!parent.mkdirs()) {
				return null;
			}
    	}
    	
    	return new File(parent, String.valueOf(chunk));
	}
    
    /**
     * 合并分片文件
     * @param dir 分片文件夹
     * @param output 目标文件
     * @throws IOException
     */
    @SuppressWarnings("resource")
	public static void mergeChunk(File dir, File output) throws IOException {
    	File[] fileArray = dir.listFiles(new FileFilter(){  
            //排除目录只要文件  
            @Override  
            public boolean accept(File pathname) {  
                if(pathname.isDirectory()){  
                    return false;
                }  
                return true;  
            }  
        });  
          
        //转成集合,便于排序  
        List<File> fileList = new ArrayList<File>(Arrays.asList(fileArray));  
        Collections.sort(fileList,new Comparator<File>() {  
            @Override  
            public int compare(File o1, File o2) {  
                if(Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())){  
                    return -1;  
                }  
                return 1;  
            }  
        });
        
        // 目标文件安排
        if(output.exists()) { 
        	output.delete();
        }
        output.createNewFile();  
        
        //输出流  
        FileChannel outChnnel = new FileOutputStream(output).getChannel();  
        //合并  
        FileChannel inChannel;  
        for(File file : fileList) {
            inChannel = new FileInputStream(file).getChannel();  
            inChannel.transferTo(0, inChannel.size(), outChnnel);  
            inChannel.close();
            file.delete();
        }
        outChnnel.close();
        
        //清除文件夹
        if(dir.isDirectory() && dir.exists()){  
        	dir.delete();  
        }
    }

注意

这种分片方式不适用于集群的情况,因为负载均衡会导致分片分散在集群中,这里建议单独抽取文件上传模块成为一个单例应用,保证分片一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值