课外知识----断点续传
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,等信息)

浙公网安备 33010602011771号