分享一个大文件断点续传的工具类,实测效果很好。

package com.skyworth.file.util;

import cn.hutool.core.io.FileUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.skyworth.file.model.FileInfo;
import com.skyworth.file.model.Response;
import com.skyworth.file.model.UploadFileParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedList;
import java.util.List;

/**
 * @apiNote 大文件-断点续传工具类
 * @date 2022/10/9
 **/
@Slf4j
@Component
public class LocalUpload {

    /**
     * 秒传、断点的文件MD5验证
     * 根据文件路径获取要上传的文件夹下的 文件名.conf 文件
     * 通过判断 .conf 文件状态来验证
     */
    public static Response checkFileMd5(String fileMd5, String fileName, String confFilePath, String tmpFilePath) throws Exception {
        boolean isParamEmpty = StringUtils.isBlank(fileMd5) || StringUtils.isBlank(fileName) || StringUtils.isBlank(confFilePath) || StringUtils.isBlank(tmpFilePath);
        if (isParamEmpty) {
            throw new Exception("参数值为空");
        }
        //构建分片配置文件对象
        File confFile = new File(confFilePath + File.separatorChar + fileName + ".conf");
        //布尔值:上传的文件缓存对象是否存在
        boolean isTmpFileEmpty = new File(tmpFilePath + File.separatorChar + fileName + "_tmp").exists();
        if (confFile.exists() && isTmpFileEmpty) {
            byte[] completeList = FileUtils.readFileToByteArray(confFile);
            List<String> missChunkList = new LinkedList<>();
            for (int i = 0; i < completeList.length; i++) {
                if (completeList[i] != Byte.MAX_VALUE) {
                    missChunkList.add(Integer.toString(i));
                }
            }
            JSONArray jsonArray = JSON.parseArray(JSONObject.toJSONString(missChunkList));
            return Response.createOKResponse(HttpStatus.PARTIAL_CONTENT.value(), "文件已部分上传", jsonArray);
        }
        //布尔值:上传的文件对象是否存在
        boolean isFileEmpty = new File(tmpFilePath + File.separatorChar + fileName).exists();
        if (isFileEmpty && confFile.exists()) {
            return Response.createErrorResponse(HttpStatus.OK.value(), "文件已上传成功");
        }
        return Response.createErrorResponse(HttpStatus.NOT_FOUND.value(), "文件不存在");
    }


    /**
     * 文件分片、断点续传上传程序
     * 创建 文件名.conf 文件记录已上传分片信息
     * 使用 RandomAccessFile(随机访问文件) 类随机指定位置写入文件,类似于合成分片
     * 检验分片文件是否全部上传完成,重命名缓存文件
     */
    public static synchronized Response fragmentFileUploader(UploadFileParam param, String confFilePath, String filePath, long chunkSize, HttpServletRequest request) throws Exception {
        boolean isParamEmpty = StringUtils.isBlank(filePath) || StringUtils.isBlank(confFilePath) && param.getFile() == null;
        if (isParamEmpty) {
            throw new Exception("参数值为空");
        }
        //判断enctype属性是否为multipart/form-data
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        if (!isMultipart) {
            throw new IllegalArgumentException("上传内容不是有效的multipart/form-data类型.");
        }
        try {
            //分片配置文件
            File confFile = FileUtil.file(FileUtil.mkdir(confFilePath), String.format("%s.conf", param.getName()));
            RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
            //把该分段标记为 true 表示完成
            accessConfFile.setLength(param.getChunks());
            accessConfFile.seek(param.getChunk());
            accessConfFile.write(Byte.MAX_VALUE);
            accessConfFile.close();
            //_tmp的缓存文件对象
            File tmpFile = FileUtil.file(FileUtil.mkdir(filePath), String.format("%s_tmp", param.getName()));
            //随机位置写入文件
            RandomAccessFile accessTmpFile = new RandomAccessFile(tmpFile, "rw");
            long offset = chunkSize * param.getChunk();
            //定位到该分片的偏移量、写入该分片数据、释放
            accessTmpFile.seek(offset);
            accessTmpFile.write(param.getFile().getBytes());
            accessTmpFile.close();
            //检查是否全部分片都成功上传
            byte[] completeList = FileUtils.readFileToByteArray(confFile);
            byte isComplete = Byte.MAX_VALUE;
            for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
                // 与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
                isComplete = (byte) (isComplete & completeList[i]);
            }
            if (isComplete != Byte.MAX_VALUE) {
                return Response.createErrorResponse(HttpStatus.OK.value(), "文件上传成功");
            }
            boolean isSuccess = renameFile(tmpFile, param.getName());
            if (!isSuccess) {
                throw new Exception("文件重命名时失败");
            }
            //全部上传成功后构建文件对象
            FileInfo fileInfo = FileInfo.builder()
                    .hash(param.getMd5())
                    .name(param.getName())
                    .type(param.getFile().getContentType())
                    .path(tmpFile.getParent() + File.separatorChar + param.getName())
                    .createTime(System.currentTimeMillis())
                    .build();
            return Response.createOKResponse(HttpStatus.CREATED.value(), "文件上传完成", fileInfo);
        } catch (IOException e) {
            e.printStackTrace();
            return Response.createErrorResponse("文件上传失败");
        }
    }

    /**
     * 用于上传成功后重命名文件
     */
    private static boolean renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的tmp文件是否存在,是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            return false;
        }
        //修改tmp文件名-文件传输完成
        File newFile = new File(toBeRenamed.getParent() + File.separatorChar + toFileNewName);
        //return toBeRenamed.renameTo(newFile);
        //下面逻辑兼容两个客户端同时上传一个文件,如果不需要,可忽略
        if (!newFile.exists()) {
            return toBeRenamed.renameTo(newFile);
        }
        if (newFile.exists() && toBeRenamed.exists()) {
            toBeRenamed.delete();
        }
        return true;
    }
}

赞赏一下

大文件断点续传工具类-LMLPHP

10-20 15:06