• 大文件批量上传断点续传文件秒传


    接上篇文章 java 超大文件分片上传 在其基础上继续实现 断点续传和文件秒传功能

    在上篇中,我们可以使用 file. slice 方法对文件进行分片,可以从后台读到当前文件已经上传的大小,就可以知道从哪里开始切片,断点续传的原理就是基于这个的。

    前端计算文件的 md5 ,后台数据库查询一遍(前提是把 md5 存储了,计算文件 md5 也是需要消耗时间的)即可知道是否有相同文件,这是实现文件秒传的方法。

    可能存在的问题:

    • 有两个人同时在上传同一个文件,但前一个人还没有上传完成,此时第二个文件认为是新文件不能秒传
    • 此时获取文件原数据时需要将文件信息保存起来,重点是要保存 md5 ,保证一个文件的 md5 保计算一次
    • 获取断点文件时,真实的文件上传位置应该是从文件系统中读出来的

    根据需求说明,后台应该存在四个接口,获取文件信息(包含是否可以秒传),获取断点文件列表,分片上传接口,文件完整性验证

    全部源码位置 : https://gitee.com/sanri/example/tree/master/test-mvc

    /**
         * 加载断点文件列表
         * @return
         */
    @GetMapping("/breakPointFiles")
    public List<FileInfoPo> breakPointFiles(){
        List<FileInfoPo> fileInfoPos = fileMetaDataRepository.breakPointFiles();
        return fileInfoPos;
    }
    
    /**
         * 获取文件元数据,判断文件是否可以秒传
         * @param originFileName
         * @param fileSize
         * @param md5
         * @return
         * @throws URISyntaxException
         */
    @GetMapping("/fileMetaData")
    public FileMetaData fileMetaData(String originFileName, Long fileSize, String md5) throws URISyntaxException, MalformedURLException {
        FileMetaData similarFile = bigFileStorage.checkSimilarFile(originFileName,fileSize, md5);
        if(similarFile != null){
            similarFile.setSecUpload(true);
    
            // 如果文件名不一致,则创建链接文件
            if(!similarFile.getOriginFileName() .equals(originFileName)) {
                bigFileStorage.createSimilarLink(similarFile);
            }
            return similarFile;
        }
    
        //获取文件相关信息
        String baseName = FilenameUtils.getBaseName(originFileName);
        String extension = FilenameUtils.getExtension(originFileName);
    
        String finalFileName = bigFileStorage.rename(baseName, fileSize);
        if(StringUtils.isNotEmpty(extension)){
            finalFileName += ("."+extension);
        }
    
        URI relativePath = bigFileStorage.relativePath(finalFileName);
    
        //如果没有相似文件,则要创建记录到数据库中,为后面断点续传做准备
        FileInfoPo fileInfoPo = new FileInfoPo();
        fileInfoPo.setName(originFileName);
        fileInfoPo.setType(extension);
        fileInfoPo.setUploaded(0);
        fileInfoPo.setSize(fileSize);
        fileInfoPo.setRelativePath(relativePath.toString());
        fileInfoPo.setMd5(md5);
        fileMetaDataRepository.insert(fileInfoPo);
    
        URI absoluteURI = bigFileStorage.absolutePath(relativePath);
        FileMetaData fileMetaData = new FileMetaData(originFileName, finalFileName, fileSize, relativePath.toString(), absoluteURI.toString());
        fileMetaData.setMd5(md5);
        fileMetaData.setFileType(extension);
        return fileMetaData;
    }
    
    /**
         * 获取当前文件已经上传的大小,用于断点续传
         * @return
         */
    @GetMapping("/filePosition")
    public long filePosition(String relativePath) throws IOException, URISyntaxException {
        return bigFileStorage.filePosition(relativePath);
    }
    
    /**
         * 上传分段
         * @param multipartFile
         * @return
         */
    @PostMapping("/uploadPart")
    public long uploadPart(@RequestParam("file") MultipartFile multipartFile, String relativePath) throws IOException, URISyntaxException {
        bigFileStorage.uploadPart(multipartFile,relativePath);
        return bigFileStorage.filePosition(relativePath);
    }
    
    /**
         * 检查文件是否完整
         * @param relativePath
         * @param fileSize
         * @param md5
         * @return
         */
    @GetMapping("/checkIntegrity")
    public void checkIntegrity(String relativePath,Long fileSize,String fileName) throws IOException, URISyntaxException {
        long filePosition = bigFileStorage.filePosition(relativePath);
        Assert.isTrue(filePosition == fileSize ,"大文件上传失败,文件大小不完整 "+filePosition+" != "+fileSize);
        String targetMd5 = bigFileStorage.md5(relativePath);
        FileInfoPo fileInfoPo = fileMetaDataRepository.selectByPrimaryKey(fileName);
        String md5 = fileInfoPo.getMd5();
        Assert.isTrue(targetMd5.equals(md5),"大文件上传失败,文件损坏 "+targetMd5+" != "+md5);
        //如果文件上传成功,更新文件上传大小
        fileMetaDataRepository.updateFilePosition(fileName,filePosition);
    }
    

    重要的处理部分其实还是前端,下面看前端的代码,需要使用到一个计算 md5 值的库 spark-md5.js

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>大文件批量上传,支持断点续传,文件秒传</title>
        <style>
            .upload-item{
                padding: 15px 10px;
                list-style-type: none;
    
                display: flex;
                flex-direction: row;
                margin-bottom: 10px;
                border: 1px dotted lightgray;
                 1000px;
    
                position: relative;
            }
            .upload-item:before{
                content: ' ';
                background-color: lightblue;
                 0px;
                position: absolute;
                left: 0;
                top: 0;
                bottom: 0;
                z-index: -1;
            }
            .upload-item span{
                display: block;
                margin-left: 20px;
            }
            .upload-item>.file-name{
                 200px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .upload-item>.upload-process{
                 50px;
                text-align: left;
            }
            .upload-item>.upload-status{
                 100px;
                text-align: center;
            }
    
            table{
                 100%;
                border-collapse: collapse;
                position: fixed;
                bottom: 200px;
                border: 1px solid whitesmoke;
            }
        </style>
    </head>
    <body>
        <div class="file-uploads">
            <input type="file" multiple id="file" />
            <button id="startUpload">开始上传</button>
            <ul id="uploadfiles">
    
            </ul>
    
            <table class="" style="" id="table"  >
                <thead>
                    <tr>
                        <td>文件名</td>
                        <td>文件大小</td>
                        <td>已上传大小</td>
                        <td>相对路径</td>
                        <td>md5</td>
                    </tr>
                </thead>
                <tbody></tbody>
            </table>
        </div>
    <!--    <script src="jquery-1.8.3.min.js"></script>-->
        <script src="jquery1.11.1.min.js"></script>
        <script src="spark-md5.min.js"></script>
    
        <script>
            const root = '';
            
            const breakPointFiles = root + '/breakPointFiles';      // 获取断点文件列表
            const fileMetaData = root + '/fileMetaData';            // 新上传文件元数据,secUpload 属性用于判断是否可以秒传
            const uploadPart = root +'/uploadPart';                 // 分片上传,每片的上传接口
            const checkIntegrity = root + '/checkIntegrity';        // 检查文件完整性
            const fileInfoPos = root + '/fileInfoPos';              // 获取系统中所有已经上传的文件(调试)
            
            const shardSize = 1024 * 1024 * 2;                      // 分片上传,每片大小 2M 
            const chunkSize = 1024 * 1024 * 4;                      // md5 计算每段大小 4M
            const statusInfoMap = {'0':'待上传','1':'正在计算','2':'正在上传','3':'上传成功','4':'上传失败','5':'暂停上传','6':'文件检查'};
    
            let uploadFiles = {};       //用于存储当前需要上传的文件列表 fileName=>fileInfo
    
            $(function () {
                // 用于调试 begin 加载系统中已经上传过的文件列表
                $.ajax({
                    type:'get',
                    url:fileInfoPos,
                    dataType:'json',
                    success:function (res) {
                        let htmlCodes = [];
    
                        for(let i=0;i<res.length;i++){
                            htmlCodes.push('<tr>');
                            htmlCodes.push('<td>'+res[i].name+'</td>');
                            htmlCodes.push('<td>'+res[i].size+'</td>');
                            htmlCodes.push('<td>'+res[i].uploaded+'</td>');
                            htmlCodes.push('<td>'+res[i].relativePath+'</td>');
                            htmlCodes.push('<td>'+res[i].md5+'</td>');
                            htmlCodes.push('</tr>')
                        }
                       $('table').append(htmlCodes.join(''))
                    }
                })
                // 用于调试 end
    
                // 事件绑定
                $('#file').change(changeFiles);                                             // 选择文件列表事件
                $('#startUpload').click(beginUpload);                                       // 开始上传
                $('#uploadfiles').on('change','input[type=file]',breakPointFileChange);     // 断点文件选择事件
    
                // 初始化时加载断点文件 
                (function () {
                    $.ajax({
                        type:'get',
                        url:breakPointFiles,
                        dataType:'json',
                        success:function (files) {
                            if(files && files.length > 0){
                                for (let i=0;i<files.length;i++){
                                    let fileId = id();
                                    let process = parseFloat((files[i].uploaded / files[i].size ) * 100).toFixed(2);
                                    $('#uploadfiles').append(templateUploadItem(fileId,files[i],process,5,'断点续传',i+1));
                                    uploadFiles[fileId] = {fileInfo:files[i],status:5};
                                }
                            }
                        }
                    })
                })(window);
    
                /**
                 * 文件重新选择事件
                 * @param e
                 */
                function changeFiles(e) {
                    // 检测文件列表是否符合要求,默认都符合
                    if(this.files.length == 0){return ;}
    
                    // 先把文件信息追加上去,不做检查也不上传
                    for (let i = 0; i < this.files.length; i++) {
                        let file = this.files[i];
                        let fileId = id();
                        $('#uploadfiles').append(templateUploadItem(fileId,file,0,0,''));
                        uploadFiles[fileId] = {file:file,status:0};
                    }
    
                }
    
    
                /**
                 * 断点文件选择文件事件
                */
                function breakPointFileChange(e) {
                    let fileId = $(e.target).closest('li').attr('fileId');
                    if(this.files.length > 0){
                        uploadFiles[fileId].file = this.files[0];
                    }
                }
    
                /**
                 * 开始上传
                 */
                function beginUpload() {
                    // 先对每一个文件进行检查,除断点文件不需要检查外
                    // console.log(uploadFiles);
                    for(let fileId in uploadFiles){
                        // 如果断点文件没有 file 信息,直接失败
                        if(uploadFiles[fileId].status == 5 && !uploadFiles[fileId].file){
                            //断点文件一定有 fileInfo
                            let fileInfo = uploadFiles[fileId].fileInfo;
                            let $li = $('#uploadfiles').find('li[fileId='+fileId+']');
                            $li.children('.upload-status').text('上传失败');fileInfo.status = 4;
                            $li.children('.tips').text('无文件信息');
                            continue;
                        }
                        if(uploadFiles[fileId].status == 5){
                            //如果断点文件有 file 信息,则可以直接断点续传了
                            let $li = $('#uploadfiles').find('li[fileId='+fileId+']');
                            $li.children('.upload-status').text('正在上传');uploadFiles[fileId].status = 2;
                            startUpload(uploadFiles[fileId],$li);
                            continue;
                        }
                        //其它待上传的文件,先后台检查文件信息,再上传
                        if(uploadFiles[fileId].status  == 0){
                            let $li = $('#uploadfiles').find('li[fileId='+fileId+']');
                            uploadFiles[fileId].status = 1; $li.children('.upload-status').text('正在计算')     //正在计算
                            checkFileItem(uploadFiles[fileId].file,function (res) {
                                if(res.message && res.message == 'fail'){
                                    $li.children('.upload-status').text(res.returnCode ||  '上传出错');uploadFiles[fileId].status = 4;
                                }else{
                                    uploadFiles[fileId].fileInfo = res;
                                    if(res.secUpload){
                                        $li.children('.upload-status').text('文件秒传');uploadFiles[fileId].status = 3;
                                        $li.children('.upload-process').text('100 %');
                                    }else{
                                        $li.children('.upload-status').text('正在上传');uploadFiles[fileId].status = 2;
                                        startUpload(uploadFiles[fileId],$li);
                                    }
                                }
                            });
                        }
                    }
    
                    /**
                     * 计算 md5 值,请求后台查看是否可秒传
                     */
                    function checkFileItem(file,callback) {
                        md5Hex(file,function (md5) {
                            $.ajax({
                                type:'get',
                                async:false,
                                url:fileMetaData,
                                data:{originFileName:file.name,fileSize:file.size,md5:md5},
                                dataType:'json',
                                success:callback
                            });
                        });
    
                    }
    
                    /**
                     * 开始正式上传单个文件
                     * */
                    function startUpload(uploadFile,$li) {
                        let file = uploadFile.file;
                        let offset = uploadFile.fileInfo.uploaded || 0;
                        let shardCount =Math.ceil((file.size - offset )/shardSize);
                        for(var i=0;i<shardCount;i++){
                            var start = i * shardSize + offset;
                            var end = Math.min(file.size,start + shardSize );//在file.size和start+shardSize中取最小值,避免切片越界
                            var filePart = file.slice(start,end);
                            var formData = new FormData();
                            formData.append("file",filePart,uploadFile.fileInfo.name || uploadFile.fileInfo.originFileName);
                            formData.append('relativePath',uploadFile.fileInfo.relativePath);
    
                            $.ajax({
                                async:false,
                                url: uploadPart,
                                cache: false,
                                type: "POST",
                                data: formData,
                                dateType: 'json',
                                processData: false,
                                contentType: false,
                                success:function (uploaded) {
                                    //进度计算
                                    let process = parseFloat((uploaded / file.size) * 100).toFixed(2);
                                    console.log(file.name+'|'+process);
                                    $li.find('.upload-process').text(process + '%');
    
                                    // 视觉进度
                                    // $('.upload-item').append("<style>.upload-item::before{ "+(process * 1000)+ "% }</style>");
    
                                    if(uploaded == file.size){
                                        // 上传完成后,检查文件完整性
                                        $li.children('.upload-status').text('文件检查');
                                        $.ajax({
                                            type:'get',
                                            async:false,
                                            url:checkIntegrity,
                                            data:{fileName:uploadFile.fileInfo.name || uploadFile.fileInfo.originFileName,fileSize:uploaded,relativePath:uploadFile.fileInfo.relativePath},
                                            success:function (res) {
                                                if(res.message != 'fail'){
                                                    $li.children('.upload-status').text('上传成功');
                                                }else{
                                                    $li.children('.upload-status').text('上传失败');
                                                    $li.children('.tips').text(res.returnCode);
                                                }
                                            }
                                        })
                                    }
                                }
                            });
                        }
                    }
                }
    
                /**
                 * 创建模板 html 上传文件项
                 * @param fileName
                 * @param process
                 * @param status
                 * @param tips
                 * @returns {string}
                 */
                function templateUploadItem(fileId,fileInfo,process,status,tips,breakPoint) {
                    let htmlCodes = [];
                    htmlCodes.push('<li class="upload-item" fileId="'+fileId+'">');
                    htmlCodes.push('<span class="file-name">'+(fileInfo.name || fileInfo.originFileName)+'</span>');
                    htmlCodes.push('<span class="file-size">'+(fileInfo.size)+'</span>');
                    htmlCodes.push('<span class="upload-process">'+process+' %</span>');
                    htmlCodes.push('<span class="upload-status" >'+statusInfoMap[status+'']+'</span>');
                    htmlCodes.push('<span class="tips">'+tips+'</span>');
                    if(breakPoint){
                        htmlCodes.push('<input type="file" name="file"  style="margin-left: 10px;"/>');
                    }
                    htmlCodes.push('</li>');
                    return htmlCodes.join('');
                }
    
                /**
                 * 计算 md5 值(同步计算)
                 * @param file
                 */
                function md5Hex(file,callback) {
                    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
                        chunks = Math.ceil(file.size / chunkSize),
                        currentChunk = 0,
    
                        spark = new SparkMD5.ArrayBuffer(),
                        fileReader = new FileReader();
    
                    fileReader.onload = function (e) {
                        spark.append(e.target.result);                   // Append array buffer
                        currentChunk++;
                        if (currentChunk < chunks) {
                            loadNext();
                        } else {
                            let hash = spark.end();
                            callback(hash);
                        }
                    }
    
                    fileReader.onerror = function () {
                        console.warn('md5 计算时出错');
                    };
    
                    function loadNext(){
                        var start = currentChunk * chunkSize,
                            end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
    
                        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
                    }
    
                    loadNext();
                }
    
                function id() {
                    return Math.floor(Math.random() * 1000);
                }
            });
            
        </script>
    </body>
    </html>
    

    源码位置: https://gitee.com/sanri/example/tree/master/test-mvc

    一点小推广

    创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。

    Excel 通用导入导出,支持 Excel 公式
    博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
    gitee:https://gitee.com/sanri/sanri-excel-poi

    使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具
    博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
    gitee:https://gitee.com/sanri/sanri-tools-maven

  • 相关阅读:
    慎用const_cast
    python中string的操作函数
    C++ Const总结
    python dict sorted 排序
    "没有找到MSVCP80D.dll,因此这个应用程序未能启动。重新安装应用程序...
    提高你开发效率的十五个 Visual Studio 使用技巧
    一些 python 插件 编译安装的注意事项
    html 制作表格 合并 样式
    将一个表的数据更新到另一个表中
    屏蔽web页面的右键,但不屏蔽输入框中的右键
  • 原文地址:https://www.cnblogs.com/sanri1993/p/11967689.html
Copyright © 2020-2023  润新知