BigFileUpload

目录
背景介绍
项目介绍
使用说明 
获取代码
需要知识点
启动项目
项目示范
核心讲解
功能分析 
分块上传
秒传功能
断点续传
总结

背景介绍
这个项目是在朋友的一次面试中,面试人提出了一个问题. 
我有一个100M的文件,然后我的宽带只有10M,我应该如何处理用户上传的文件? 
根据这个问题,我小试牛刀,写了这个项目.

期间查阅了资料,借鉴了Fourwen的项目的前端框架和md写法.

再次感谢.

项目介绍
项目采用如下:

上层: Java, JDK8, Tomcat8,
服务端: Jsp, 原生
前端: webuploader, bootstrap, jquery
来进行开发,

针对文件的上传,一般可以考虑的功能点有

断点续传 在断网或者在暂停的情况下,能够在上传断点中继续上传。

分块上传 也是断点续传的基础之一,把大文件通过前端分块,然后后台在组在一起。

文件秒传 服务中已经有人上传过文件,其他人再上传这个文件直接记录并放回成功。

其他功能 下面这些功能归类到其他,是因为它们基本都是通过WebUploader来实现的,很简单。

- 多线程上传 多个线程上传不同的块文件。 
- 文件进度显示 显示文件的上传完成情况。 
 

使用说明

获取代码
GitHub:https://github.com/ck-wizard/BigFileUpload
不会经常更新,下一步会做一个集合公司内部网址的项目.

需要知识点
项目使用nio来进行文件的读取和创建
使用原生web来开发,不使用任何框架
使用Apache提供的fileupload来实现上传数据的获取
使用Apache提供的codec来实现md5加密
并发的理解

启动项目

项目示范

功能分析
分块上传可以说是我们整个项目的基础,像断点续传、暂停这些都是需要用到分块。 
分块这块相对来说比较简单。前端是采用了webuploader,分块等基础功能已经封装起来,使用方便。 
借助webUpload提供给我们的文件API,前端就显得异常简单。

var uploader = WebUploader.create({
        // swf文件路径
         swf: '${ctx}/webuploader-0.1.5/Uploader.swf',        // 文件接收服务端。
         server: '${ctx}/upload.do',
         //文件上传请求的参数表,每次发送都会发送此对象中的参数
         formData: {
             md5: ''
         },        // 选择文件的按钮。可选。
         // 内部根据当前运行是创建,可能是input元素,也可能是flash.
         pick: '#picker',        // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
         resize: false,        chunked: true, // 分块
         chunkSize: 1 * 1024 * 1024, // 字节 1M分块
         threads: 3, //开启线程
         auto: false,        // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
         disableGlobalDnd: true,
         fileNumLimit: 1024,
         fileSizeLimit: 200 * 1024 * 1024,    // 200 M
         fileSingleSizeLimit: 100 * 1024 * 1024    // 100 M
     });



上传的文件会被发送到upload.do这个Controller,在里面的逻辑如下:

判断是文件上传请求,如果是继续,否则退出
使用fileupload jar包解析request请求上传的基础信息
使用FileUploadBean包装上传的基础信息.
拼装父目录,校验是否存在
    4.1 不存在就创建
    4.2 存在就进入检验
    4.2.1 检查md5值是否匹配, 应该建立数据库,存储文件信息才是更快 更好的解决办法.
    4.2.2 若匹配直接返回成功.
    4.2.3 若不成功,删除源文件再次读取

写入该分片数据到指定目录
    写入规则如下:
    // 0.读取上传文件到数组
    // 1.写到本地
    // 1.记录分片数,检查分片数
    // 2.当对应的md5读取数量达到对应的文件后,合并文件
    // 3.删除临时文件

完成
功能分析

分块上传
分块上传可以说是我们整个项目的基础,像断点续传、暂停这些都是需要用到分块。 
分块这块相对来说比较简单。前端是采用了webuploader,分块等基础功能已经封装起来,使用方便。 
借助webUpload提供给我们的文件API,前端就显得异常简单。

     

var uploader = WebUploader.create({
            // swf文件路径
             swf: '${ctx}/webuploader-0.1.5/Uploader.swf',            // 文件接收服务端。
             server: '${ctx}/upload.do',
             //文件上传请求的参数表,每次发送都会发送此对象中的参数
             formData: {
                 md5: ''
             },            // 选择文件的按钮。可选。
             // 内部根据当前运行是创建,可能是input元素,也可能是flash.
             pick: '#picker',            // 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
             resize: false,            chunked: true, // 分块
             chunkSize: 1 * 1024 * 1024, // 字节 1M分块
             threads: 3, //开启线程
             auto: false,            // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
             disableGlobalDnd: true,
             fileNumLimit: 1024,
             fileSizeLimit: 200 * 1024 * 1024,    // 200 M
             fileSingleSizeLimit: 100 * 1024 * 1024    // 100 M
         });


服务器先创建一个md5文件夹,然后按照上传的文件名进行一套规范命名,写入到一个临时文件中. 
然后记录这个临时文件.

// 规范命名
     String fileName = param.getName();
     String uploadDirPath = finalDirPath + param.getMd5();
     String tempFileName = fileName + "_" + param.getChunk() + "_tmp";
     Path tmpDir = Paths.get(uploadDirPath);
     // 写入临时文件
     Path path = Paths.get(uploadDirPath, tempFileName);
     byte[] fileData = FileUtils.read(param.getFile(), 2048);
     Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
     FileUtils.authorizationAll(path);
     // 记录
     FileBean fileBean;
     if(fileMap.containsKey(param.getMd5())) {
         fileBean = fileMap.get(param.getMd5());
     } else {
         fileBean = new FileBean(param.getName(), param.getChunks(), param.getMd5());
         fileMap.put(param.getMd5(), fileBean);
     }
     fileBean.setChunk(param.getChunk());



然后当文件分片都上传完成后,在把分片合并为一个文件,并且删除所有临时文件.

 

if(fileBean.isLoadComplate()) {
     // 合并文件..
     Path realFile = Paths.get(uploadDirPath, fileBean.getName());
     realFile = Files.createFile(realFile);
     // 设置权限
     FileUtils.authorizationAll(realFile);
     for(int i = 0 ; i < fileBean.getChunks(); i++) {
         // 获取每个分片
         tempFileName = fileName + "_" + i + "_tmp";
         Path itemPath = Paths.get(uploadDirPath, tempFileName);
         byte[] bytes = Files.readAllBytes(itemPath);
         Files.write(realFile, bytes, StandardOpenOption.APPEND);
         //写完后删除掉临时文件.
         Files.delete(itemPath);
     }
     logger.info("合并文件{}成功", fileName);
     }


 

秒传功能
上传文件是若发现父目录已经创建,并且目录下有上传的文件名,那么进行md5比较,若相同,直接返回,若不相同,删除目录文件,重新上传.

if (!Files.exists(tmpDir)) {
         Files.createDirectory(tmpDir);
     } else {
         // 文件夹已存在
         // 1.检查是否有文件,有进入2, 没有进3
         Path localPath = Paths.get(uploadDirPath, fileName);        // 2.检查md5值是否匹配, 应该建立数据库,存储文件信息才是更快 更好的解决办法.
         // 2.1.若匹配直接返回成功.
         // 2.2 若不成功,删除源文件再次读取
         if(Files.exists(localPath)) {
             String nowMd5 = DigestUtils.md5Hex(Files.newInputStream(localPath, StandardOpenOption.READ));
             if(StringUtils.equals(param.getMd5(), nowMd5)) {
                 // 比较相等,那么直接返回成功.
                 logger.info("已检测到重复文件{},并且比较md5相等,已直接返回", fileName);
                 return;
             } else {
                 // 删除
                 logger.info("已经存在的文件的md5不匹配上传上来的文件的md5,删除后重新下载");
                 Files.delete(localPath);
             }
         }
         // 3. 直接写入到具体目录下.
     }

断点续传
断点续传,就是在文件上传的过程中发生了中断,人为因素(暂停)或者不可抗力(断网或者网络差)导致了文件上传到一半失败了。然后在环境恢复的时候,重新上传该文件,而不至于是从新开始上传的。

文件上传时,获取分片大小,同服务器目录存储的分片大小进行比较,若一直,直接返回成功.

//写入该分片数据
     Path path = Paths.get(uploadDirPath, tempFileName);
     //文件上传时,获取是否有分片,如果有直接返回.
     if(!Files.exists(path)) {
         // 不存在
         byte[] fileData = FileUtils.read(param.getFile(), 2048);
         try {
             Files.write(path, fileData, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
         } catch (IOException e) {
             // 删除上传的文件
             Files.delete(path);
             throw e;
         }
         FileUtils.authorizationAll(path);
     } else {
         return;
     }


 

总结
选择使用原生是为了锻炼自己不要忘记基础,前前后后写了3天,复习了不少文件相关的操作,并且对lambda表达式和流 
有了进一步了解,还是很满足的.

在并发的情况下进行文件上传,在使用一个实例的成员变量进行存储的时候,在方法上面使用synchronized或代码段加synchronized 
或Lock或使用AtomInteger去进行并发操作,都没能达到正确统计的目的.最后使用ConcurrentHashMap才完成了正确的计数.

由此看来,多线程环境下,我还是个小菜鸟啊. 努力加油了.