http協議本身對上傳文件大 小沒有限制,但是客戶的網絡環境質量、電腦硬件環境等參差不齊,如果一個大文件快上傳完了網斷了,電斷了沒 有上傳完成,需要客戶重新上傳,這是致命的,所以對於大文件上傳的要求最基本的是斷點續傳。
什么是斷點續傳:斷點續傳指的是在下載或上傳時,將下載或上傳任務(一個文件或一個壓縮包)人為的划分為幾個 部分,每一個部分采用一個線程進行上傳或下載,如果碰到網絡故障,可以從已經上傳或下載的部分開始繼續上傳 下載未完成的部分,而沒有必要從頭開始上傳下載,斷點續傳可以提高節省操作時間,提高用戶體驗性。
上傳流程如下:
1、上傳前先把文件分成塊
2、一塊一塊的上傳,上傳中斷后重新上傳,已上傳的分塊則不用再上傳
3、各分塊上傳完成最后合並文件 文件下載則同理。
實體類
package com.xuecheng.framework.domain.media;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* @Author: mrt.
* @Description:
* @Date:Created in 2018/1/24 10:04.
* @Modified By:
*/
@Data
@ToString
@Document(collection = "media_file")
public class MediaFile {
/*
文件id、名稱、大小、文件類型、文件狀態(未上傳、上傳完成、上傳失敗)、上傳時間、視頻處理方式、視頻處理狀態、hls_m3u8,hls_ts_list、課程視頻信息(課程id、章節id)
*/
@Id
//文件id
private String fileId;
//文件名稱
private String fileName;
//文件原始名稱
private String fileOriginalName;
//文件路徑
private String filePath;
//文件url
private String fileUrl;
//文件類型
private String fileType;
//mimetype
private String mimeType;
//文件大小
private Long fileSize;
//文件狀態
private String fileStatus;
//上傳時間
private Date uploadTime;
//處理狀態
private String processStatus;
//hls處理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
//tag標簽用於查詢
private String tag;
}
controller類
package com.xuecheng.manage_media.controller;
import com.xuecheng.api.media.MediaUploadControllerApi;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.service.MediaUploadService;
import jdk.management.resource.ResourceRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* Created by Administrator on 2020/6/6 0006.
*/
@RestController
@RequestMapping("media/upload")
public class MediaUploadController implements MediaUploadControllerApi {
@Autowired
private MediaUploadService uploadService;
@Override
@PostMapping("/register")
public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimeType, String fileExt) {
return uploadService.register( fileMd5, fileName, fileSize, mimeType, fileExt);
}
@Override
@PostMapping("/checkChunk")
public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
return uploadService.checkChunk( fileMd5, chunk, chunkSize);
}
@Override
public ResponseResult uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
return uploadService.uploadChunk( file, fileMd5, chunk);
}
@Override
public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimeType, String fileExt) {
return uploadService.mergeChunks( fileMd5, fileName, fileSize, mimeType, fileExt);
}
}
service類
package com.xuecheng.manage_media.service;
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.dao.MediaFileMapper;
import jdk.management.resource.ResourceRequest;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
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.security.MessageDigest;
import java.util.*;
/**
* Created by Administrator on 2020/6/6 0006.
*/
@Service
public class MediaUploadService {
@Autowired
private MediaFileMapper mediaFileMapper;
@Value("${xc-service-manage-media.upload-location}")
String uploadLocation;
/**
* 文件上傳前的注冊,檢查文件是否存在
* 根據文件MD5獲取文件路徑
* 規則:
* 一級目錄:md5的第一個字符
* 二級目錄:md5的第二個字符
* 三級目錄:md5
* 文件名:md5 + 文件擴展名
* @param fileMd5
* @param fileName
* @param fileSize
* @param mimeType
* @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、檢查文件在數據庫中是否有 上傳記錄
MediaFile mediaFile = mediaFileMapper.findByFileId(fileMd5);
if(exists && null != mediaFile){
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
}
//文件不存在時作一些准備工作,檢查文件所在目錄是否存在,如果不存在創建
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
fileFolder.mkdirs();
}
return new ResponseResult(CommonCode.SUCCESS);
}
/**
* 檢查分塊文件是否存在
* @param fileMd5
* @param chunk
* @param chunkSize
* @return
*/
public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
//得到分塊文件的所在目錄
String chunkFileFolderPath = this.getFileFolderPath(fileMd5) + "chunk" + File.separator;
//得到塊文件
File chunkFile = new File(chunkFileFolderPath + chunk);
if(chunkFile.exists()){
return new CheckChunkResult(CommonCode.SUCCESS,true);
}
return new CheckChunkResult(CommonCode.SUCCESS,false);
}
/**
* 上傳分塊
* @param file
* @param fileMd5
* @param chunk
* @return
*/
public ResponseResult uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
//檢查分塊目錄,如果不存在則要自動創建
String chunkFileFolderPath = this.getFileFolderPath(fileMd5) + "chunk" + File.separator;
//得到塊目錄
File chunkFile = new File(chunkFileFolderPath);
if(!chunkFile.exists()){
chunkFile.mkdirs();
}
//得到上傳文件的輸入流
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunk));
IOUtils.copy(inputStream,outputStream);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new ResponseResult(CommonCode.SUCCESS);
}
/**
* 合並塊文件
* @param fileMd5
* @param fileName
* @param fileSize
* @param mimeType
* @param fileExt
* @return
*/
public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimeType, String fileExt) {
//合並所有文件
//得到分塊文件的目錄
String chunkFileFolderPath = this.getFileFolderPath(fileMd5) + "chunk" + File.separator;
File chunkFile = new File(chunkFileFolderPath);
File[] files = chunkFile.listFiles();
//創建一個合並文件
String filePath = this.getFilePath(fileMd5, fileExt);
File mergeFile = new File(filePath);
//執行合並
mergeFile = this.mergeFile(Arrays.asList(files),mergeFile);
if(null == mergeFile){
ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
}
//檢驗文件的md5值是否與前端傳入的md5一致
boolean checkResult = this.checkFileMd5(mergeFile,fileMd5);
if(!checkResult){
ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
}
//將文件的信息寫入數據庫
MediaFile mediaFile = new MediaFile();
mediaFile.setFileId(fileMd5);
mediaFile.setFileName(fileMd5+"."+fileExt);
mediaFile.setFileOriginalName(fileName);
//文件路徑保存相對路徑
mediaFile.setFilePath(getFileFolderRelativePath(fileMd5,fileExt));
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimeType);
mediaFile.setFileType(fileExt);
//狀態為上傳成功
mediaFile.setFileStatus("301002");
mediaFileMapper.insert(mediaFile);
return new ResponseResult(CommonCode.SUCCESS);
}
/**獲取文件所述目錄路徑*/
private String getFileFolderPath(String fileMd5){
return uploadLocation + fileMd5.substring(0,1)+ File.separator+fileMd5.substring(1,2)+File.separator
+ fileMd5 +File.separator;
}
private String getFilePath(String fileMd5, String fileExt){
return uploadLocation + fileMd5.substring(0,1)+ File.separator+fileMd5.substring(1,2)+File.separator
+ fileMd5 +File.separator + fileMd5+"."+fileExt;
}
//得到文件目錄相對路徑,路徑中去掉根目錄
private String getFileFolderRelativePath(String fileMd5,String fileExt){
String filePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" +
fileMd5 + "/";
return filePath;
}
//合並文件
private File mergeFile(List<File> chunkFiles,File mergeFile){
try {
if(mergeFile.exists()){
mergeFile.delete();
}
mergeFile.createNewFile();
//對塊文件進行排序
Collections.sort(chunkFiles, 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_w = new RandomAccessFile(mergeFile,"rw");
byte[] bytes = new byte[1024];
for (File chunkFile:chunkFiles){
RandomAccessFile raf_r = new RandomAccessFile(chunkFile,"r");
int len = -1;
while ((len = raf_r.read(bytes)) != -1){
raf_w.write(bytes,0,len);
}
raf_r.close();
}
raf_w.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return null;
}
//校驗文件
private boolean checkFileMd5(File mergeFile,String md5){
try {
FileInputStream inputStream = new FileInputStream(mergeFile);
String md5Hex = DigestUtils.md5Hex(inputStream);
if(md5.equalsIgnoreCase(md5Hex)){
return true;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
