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; } }