Java—大文件分片上传

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

}
原文地址:https://www.cnblogs.com/bao-bei/p/13058998.html