课外知识----断点续传

1 文件不分块

客户端会记录当前服务器的已经上传文件的大小,等到下载网络连接成功,给服务器发生我已经上传文件的大小,并继续发生字节

服务器接收已经上传文件的大小,用这个大小来将移动文件的cursor,进行append

 

2 文件分块

客户端首先将需要上传的文件分块,网络恢复正常,已经上传的文件分块不在上传,上传失败的文件分块重新上传

服务器将接受到的文件分块进行合并

 

 

Web客户端实现断点上传功能

WebUploader

官网:fexteam.gz01.bdysite.com/webuploader/

 http://fex.baidu.com/webuploader/

web

<template>
  <div><br/>
    操作步骤:<br/>
    1、点击“选择文件”,选择要上传的文件<br/>
    2、点击“开始上传”,开始上传文件<br/>
    3、如需重新上传请重复上边的步骤。<br/><br/>

    <div id="uploader" class="wu-example">
      <div class="btns" style="float:left;padding-right: 20px">
        <div id="picker">选择文件</div>
      </div>
      <div id="ctlBtn" class="webuploader-pick" @click="upload()">开始上传</div>

    </div>
    <!--用来存放文件信息-->
    <div id="thelist" class="uploader-list">
      <div v-if="uploadFile.id" :id='uploadFile.id'><span>{{uploadFile.name}}</span> <span class='percentage'>{{percentage}}%</span>
      </div>
    </div>
  </div>
</template>
<script>
    import $ from '../../../../static/plugins/jquery/dist/jquery.js'
    import webuploader from '../../../../static/plugins/webuploader/dist/webuploader.js'
    import '../../../../static/css/webuploader/webuploader.css'

    export default {
        data() {
            return {
                uploader: {},
                uploadFile: {},
                percentage: 0,
                fileMd5: ''
            }
        },
        methods: {
            //开始上传
            upload() {
                if (this.uploadFile && this.uploadFile.id) {
                    this.uploader.upload(this.uploadFile.id);
                } else {
                    alert("请选择文件");
                }
            }
        },
        mounted() {
//      var fileMd5;
//      var uploadFile;
            WebUploader.Uploader.register({
                    "before-send-file": "beforeSendFile",
                    "before-send": "beforeSend",
                    "after-send-file": "afterSendFile"
                }, {
                    beforeSendFile: function (file) {
                        // 创建一个deffered,用于通知是否完成操作
                        var deferred = WebUploader.Deferred();
                        // 计算文件的唯一标识,用于断点续传
                        (new WebUploader.Uploader()).md5File(file, 0, 100 * 1024 * 1024)
                            .then(function (val) {

                                this.fileMd5 = val;
                                this.uploadFile = file;
//                alert(this.fileMd5 )
                                //向服务端请求注册上传文件
                                $.ajax(
                                    {
                                        type: "POST",
                                        url: "/api/media/upload/register",
                                        data: {
                                            // 文件唯一表示
                                            fileMd5: this.fileMd5,
                                            fileName: file.name,
                                            fileSize: file.size,
                                            mimetype: file.type,
                                            fileExt: file.ext
                                        },
                                        dataType: "json",
                                        success: function (response) {
                                            if (response.success) {
                                                //alert('上传文件注册成功开始上传');
                                                deferred.resolve();
                                            } else {
                                                alert(response.message);
                                                deferred.reject();
                                            }
                                        }
                                    }
                                );
                            }.bind(this));

                        return deferred.promise();
                    }.bind(this),
                    beforeSend: function (block) {
                        var deferred = WebUploader.Deferred();
                        // 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的
                        $.ajax(
                            {
                                type: "POST",
                                url: "/api/media/upload/checkchunk",
                                data: {
                                    // 文件唯一表示
                                    fileMd5: this.fileMd5,
                                    // 当前分块下标
                                    chunk: block.chunk,
                                    // 当前分块大小
                                    chunkSize: block.end - block.start
                                },
                                dataType: "json",
                                success: function (response) {
                                    if (response.fileExist) {
                                        // 分块存在,跳过该分块
                                        deferred.reject();
                                    } else {
                                        // 分块不存在或不完整,重新发送
                                        deferred.resolve();
                                    }
                                }
                            }
                        );
                        //构建fileMd5参数,上传分块时带上fileMd5
                        this.uploader.options.formData.fileMd5 = this.fileMd5;
                        this.uploader.options.formData.chunk = block.chunk;
                        return deferred.promise();
                    }.bind(this),
                    afterSendFile: function (file) {
                        // 合并分块
                        $.ajax(
                            {
                                type: "POST",
                                url: "/api/media/upload/mergechunks",
                                data: {
                                    fileMd5: this.fileMd5,
                                    fileName: file.name,
                                    fileSize: file.size,
                                    mimetype: file.type,
                                    fileExt: file.ext
                                },
                                success: function (response) {
                                    //在这里解析合并成功结果
                                    if (response && response.success) {
                                        alert("上传成功")
                                    } else {
                                        alert("上传失败")
                                    }
                                }
                            }
                        );
                    }.bind(this)
                }
            );
            // 创建uploader对象,配置参数
            this.uploader = WebUploader.create(
                {
                    swf: "/static/plugins/webuploader/dist/Uploader.swf",//上传文件的flash文件,浏览器不支持h5时启动flash
                    server: "/api/media/upload/uploadchunk",//上传分块的服务端地址,注意跨域问题
                    fileVal: "file",//文件上传域的name
                    pick: "#picker",//指定选择文件的按钮容器
                    auto: false,//手动触发上传
                    disableGlobalDnd: true,//禁掉整个页面的拖拽功能
                    chunked: true,// 是否分块上传
                    chunkSize: 1 * 1024 * 1024, // 分块大小(默认5M)
                    threads: 3, // 开启多个线程(默认3个)
                    prepareNextFile: true// 允许在文件传输时提前把下一个文件准备好
                }
            );

            // 将文件添加到队列
            this.uploader.on("fileQueued", function (file) {
                    this.uploadFile = file;
                    this.percentage = 0;

                }.bind(this)
            );
            //选择文件后触发
            this.uploader.on("beforeFileQueued", function (file) {
//     this.uploader.removeFile(file)
                //重置uploader
                this.uploader.reset()
                this.percentage = 0;
            }.bind(this));

            // 监控上传进度
            // percentage:代表上传文件的百分比
            this.uploader.on("uploadProgress", function (file, percentage) {
                this.percentage = Math.ceil(percentage * 100);
            }.bind(this));
            //上传失败触发
            this.uploader.on("uploadError", function (file, reason) {
                console.log(reason)
                alert("上传文件失败");
            });
            //上传成功触发
            this.uploader.on("uploadSuccess", function (file, response) {
                console.log(response)
//        alert("上传文件成功!");
            });
            //每个分块上传请求后触发
            this.uploader.on('uploadAccept', function (file, response) {
                if (!(response && response.success)) {//分块上传失败,返回false
                    return false;
                }
            });
        }
    }
</script>
<style scoped>


</style>

server

package com.xuecheng.manage_media.service;

import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;

/**
 * @author Administrator
 * @version 1.0
 **/
@Service
public class MediaUploadService {
    @Autowired
    MediaFileRepository mediaFileRepository;

    @Value("${xc-service-manage-media.upload-location}")
    String upload_location;
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    String routingkey_media_video;

    @Autowired
    RabbitTemplate rabbitTemplate;

    //得到文件所属目录路径
    private String getFileFolderPath(String fileMd5){
        return  upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";
    }
    //得到文件的路径
    private String getFilePath(String fileMd5,String fileExt){
        return upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" + fileMd5 + "." +fileExt;
    }

    //得到块文件所属目录路径
    private String getChunkFileFolderPath(String fileMd5){
        return  upload_location + fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/chunk/";
    }
    /**
     * 文件上传前的注册,检查文件是否存在
     * 根据文件md5得到文件路径
     * 规则:
     * 一级目录:md5的第一个字符
     * 二级目录:md5的第二个字符
     * 三级目录:md5
     * 文件名:md5+文件扩展名
     * @param fileMd5 文件md5值
     * @param fileExt 文件扩展名
     * @return 文件路径
     */
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {

        //1  检查文件在磁盘上是否存在
        //文件所属目录的路径
        String fileFolderPath = this.getFileFolderPath(fileMd5);
        //文件的路径
        String filePath =this.getFilePath(fileMd5,fileExt);
        File file = new File(filePath);
        //文件是否存在
        boolean exists = file.exists();

        //2 检查文件信息在mongodb中是否存在
        Optional<MediaFile > optional = mediaFileRepository.findById(fileMd5);
        if(exists && optional.isPresent()){
            //文件存在
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        //文件不存在时作一些准备工作,检查文件所在目录是否存在,如果不存在则创建
        File fileFolder = new File(fileFolderPath);
        if(!fileFolder.exists()){
            fileFolder.mkdirs();
        }

        return new ResponseResult(CommonCode.SUCCESS);
    }

    //分块检查(前端会将文件进行分块,每一个块都会进行checkchunk())
    /**
     *
     * @param fileMd5 文件md5
     * @param chunk 块的下标
     * @param chunkSize 块的大小
     * @return
     */
    public CheckChunkResult checkchunk(String fileMd5, Integer chunk, Integer chunkSize) {
        //检查分块文件是否存在
        //得到分块文件的所在目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        //块文件
        File chunkFile = new File(chunkFileFolderPath + chunk);
        if(chunkFile.exists()){
            //块文件存在
            return new CheckChunkResult(CommonCode.SUCCESS,true);
        }else{
            //块文件不存在
            return new CheckChunkResult(CommonCode.SUCCESS,false);
        }
    }
    //上传分块
    public ResponseResult uploadchunk(MultipartFile file, String fileMd5, Integer chunk) {
        //检查分块目录,如果不存在则要自动创建
        //得到分块目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        //得到分块文件路径
        String chunkFilePath = chunkFileFolderPath + chunk;

        File chunkFileFolder = new File(chunkFileFolderPath);
        //如果不存在则要自动创建
        if(!chunkFileFolder.exists()){
            chunkFileFolder.mkdirs();
        }
        //得到上传文件的输入流
        InputStream inputStream = null;
        FileOutputStream outputStream  =null;
        try {
            inputStream = file.getInputStream();
            outputStream = new FileOutputStream(new File(chunkFilePath));
            IOUtils.copy(inputStream,outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return new ResponseResult(CommonCode.SUCCESS);

    }

    //合并文件
    public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {

        //1、合并所有分块
        //得到分块文件的属目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        //分块文件列表
        File[] files = chunkFileFolder.listFiles();
        List<File> fileList = Arrays.asList(files);

        //创建一个合并文件
        String filePath = this.getFilePath(fileMd5, fileExt);
        File mergeFile = new File(filePath);

        //执行合并
        mergeFile = this.mergeFile(fileList, mergeFile);
        if(mergeFile == null){
            //合并文件失败
            ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
        }

        //2、校验文件的md5值是否和前端传入的md5一到
        boolean checkFileMd5 = this.checkFileMd5(mergeFile, fileMd5);
        if(!checkFileMd5){
            //校验文件失败
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }
        //3、将文件的信息写入mongodb
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileOriginalName(fileName);
        mediaFile.setFileName(fileMd5 + "." +fileExt);
        //文件路径保存相对路径
        String filePath1 = fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/";
        mediaFile.setFilePath(filePath1);
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);
        //状态为上传成功
        mediaFile.setFileStatus("301002");
        mediaFileRepository.save(mediaFile);
        //向MQ发送视频处理消息
        sendProcessVideoMsg(mediaFile.getFileId());
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 发送视频处理消息
     * @param mediaId 文件id
     * @return
     */
    public ResponseResult sendProcessVideoMsg(String mediaId){

        //查询数据库mediaFile
        Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
        if(!optional.isPresent()){
            ExceptionCast.cast(CommonCode.FAIL);
        }
        //构建消息内容
        Map<String,String> map = new HashMap<>();
        map.put("mediaId",mediaId);
        String jsonString = JSON.toJSONString(map);
        //向MQ发送视频处理消息
        try {
            rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,jsonString);
        } catch (AmqpException e) {
            e.printStackTrace();
            return new ResponseResult(CommonCode.FAIL);
        }

        return new ResponseResult(CommonCode.SUCCESS);
    }

    //校验文件
    private boolean checkFileMd5(File mergeFile,String md5){

        try {
            //创建文件输入流
            FileInputStream inputStream = new FileInputStream(mergeFile);
            //得到文件的md5
            String md5Hex = DigestUtils.md5Hex(inputStream);

            //和传入的md5比较
            if(md5.equalsIgnoreCase(md5Hex)){
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return false;

    }
    //合并文件
    private File mergeFile(List<File> chunkFileList, File mergeFile) {
        try {
            //如果合并文件已存在则删除,否则创建新文件
            if (mergeFile.exists()) {
                mergeFile.delete();
            } else {
                //创建一个新文件
                mergeFile.createNewFile();
            }
            //对块文件进行排序
            Collections.sort(chunkFileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if(Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName())){
                        return 1;
                    }
                    return -1;

                }
            });
            //创建一个写对象
            RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");
            byte[] b = new byte[1024];
            for(File chunkFile:chunkFileList){
                RandomAccessFile raf_read = new RandomAccessFile(chunkFile,"r");
                int len = -1;
                while ((len = raf_read.read(b))!=-1){
                    raf_write.write(b,0,len);
                }
                raf_read.close();
            }
            raf_write.close();
            return mergeFile;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

当文件上传到服务器后,我们需要使用ffmpeg来将视频进行处理,处理完成之后需要写入数据库(视频的id是前端传入的md5,视频的名字,视频的url,等信息)

 

 

 

 

 

 

posted @ 2019-12-05 20:28  小名的同学  阅读(361)  评论(0编辑  收藏  举报