目录
前置说明
目前没弄前端,搁置后续再说。前端若打算使用element-ui的el-upload改造分片上传组件的,推荐这篇文章。
获取文件分片
后端自测使用的分片可以通过ChunkFile来获取。
public class ChunkFile {
private static final String PATH = "D:/file/test/";
private static final String FILE_NAME = "稻香-周杰伦";
private static final String FILE_EXTENSION = ".mp4";
private static final Integer CHUNK_SIZE = 10485760; // 10MB
public static void main(String[] args) throws Exception {
File file = new File(PATH, FILE_NAME + FILE_EXTENSION);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
long size = FileUtil.size(file);
// 分片数
int chunkNum = (int) Math.ceil((double) size / CHUNK_SIZE);
byte[] bytes;
// 循环进行文件读取并写入数据至分片文件
for (int i = 0; i < chunkNum; i++) {
// 文件当前偏移量
long pointer = randomAccessFile.getFilePointer();
int len = i == chunkNum - 1 ? (int) (size - pointer) : CHUNK_SIZE;
bytes = new byte[len];
randomAccessFile.read(bytes, 0, len);
FileUtil.writeBytes(bytes, new File(PATH, i + FILE_EXTENSION));
}
}
}
测试文件大小14.5MB,CHUNK_SIZE分片大小设置为10MB。通过RandomAccessFile进行数据读取,通过hutool的FileUtil工具类进行数据写入,得到下面的0.mp4和1.mp4两个分片文件。
项目流程简述
该测试项目实现的功能:文件普通上传、文件分片上传、断点续传、秒传。文件普通上传不谈,这里展开分片逻辑进行讨论。
第一步:前端在进行大文件上传之前,先通过MD5算法(MD5不是加密算法)获取大文件的MD5值( 每个文件的MD5都不一样),将MD5值作为参数调用后端秒传检测接口——目的就是在文件上传前判断文件是否已经上传。此步好处->
好处一:如果文件已经上传过,则不用再上传,文件地址复用即可;
好处二:如果大文件是分片上传的,并且因为某些原因,只上传了部分分片,则接口返回已上传的分片信息,前端可以根据信息判断哪些分片还没有上传,只上传还没有上传的分片即可(断点续传)。
第二步:根据第一步的接口响应信息,判断是否需要进行文件上传。若要上传,调用文件上传接口进行文件或分片文件的上传。(每个分片文件也都需要其MD5值进行校验,分片文件合并在后端自动进行)
关键代码解读
有文件上传记录sys_file和分片记录sys_chunk_record两张表,在下面的文字中分表叫做A表和B表。
一、秒传检测代码
public UploadResultVo fastUploadCheck(Boolean isChunk, String md5) {
UploadResultVo vo = new UploadResultVo();
// 文件表查找
QueryWrapper<SysFile> wrapper = new QueryWrapper<SysFile>()
.eq("file_md5", md5);
List<SysFile> sysFileList = sysFileMapper.selectList(wrapper);
if (sysFileList.size() > 0) {
String kid = sysFileList.get(0).getKid();
vo.setUploaded(true).setFileKid(kid);
}
if (!vo.getUploaded() && isChunk) {
// 分片记录表查询
QueryWrapper<SysChunkRecord> chunkWrapper = new QueryWrapper<SysChunkRecord>()
.eq("file_md5", md5);
List<SysChunkRecord> sysChunkRecordList = sysChunkRecordMapper.selectList(chunkWrapper);
List<Integer> chunkNum = new ArrayList<>();
if (CollectionUtil.isNotEmpty(sysChunkRecordList)) {
chunkNum = sysChunkRecordList.stream().map(SysChunkRecord::getChunk).collect(Collectors.toList());
}
vo.setChunkNum(chunkNum);
}
return vo;
}
isChunk和md5都是秒传检测接口请求传递的参数,含义分别是——是否分片和文件md5值。
isChunk为false:只查询A表,selectList()方法如果有数据返回,则说明之前有上传过相同的文件。设置返回参数——uploaded为true,fileKid为文件记录主键。(这里其实使用mp的selectOne()方法即可,不用使用selectList(),因为表已经做了md5字段的唯一约束)
isChunk为true:查询A表的逻辑不变,另还查询了B表。此处的代码对应上面描述的好处二——告诉前端已经上传成功了哪些分片。
二、分片文件上传代码
public UploadResultVo chunkUpload(MultipartFileParam param) {
UploadResultVo vo = new UploadResultVo();
// 因为表设置了唯一约束,在插入数据前先判断数据是否已经存在
SysFile sf = queryByMd5(param.getMd5());
if (ObjectUtil.isNotEmpty(sf)) {
return vo.setUploaded(true).setFileKid(sf.getKid());
}
SysChunkRecord scr = queryByChunkMd5(param.getChunkMd5());
if (ObjectUtil.isNotEmpty(scr)) {
return vo.setUploaded(false);
}
File file = buildUploadFile(param);
MultipartFile multipartFile = param.getFile();
try {
// 分片文件md5校验
checkMd5(multipartFile.getInputStream(), param.getChunkMd5());
// 分片数据写入文件
RandomAccessFile accessFile = new RandomAccessFile(file, "rw");
if (accessFile.length() == 0) {
accessFile.setLength(param.getTotalSize());
}
FileChannel channel = accessFile.getChannel();
int position = (param.getChunk() - 1) * fileConfig.getChunkSize();
MappedByteBuffer map = channel.map(FileChannel.MapMode.READ_WRITE, position, multipartFile.getSize());
map.put(multipartFile.getBytes());
cleanBuffer(map);
channel.close();
accessFile.close();
} catch (IOException e) {
e.printStackTrace();
}
// 分片记录入库
saveSysChunkRecord(file, param);
// 检测是否为最后一块分片
QueryWrapper<SysChunkRecord> wrapper1 = new QueryWrapper<SysChunkRecord>()
.eq("file_md5", param.getMd5());
Integer count = sysChunkRecordMapper.selectCount(wrapper1);
if (count.equals(param.getTotalChunk())) {
try {
// 文件md5检验
checkMd5(new FileInputStream(file), param.getMd5());
// 文件上传记录入库
String kid = saveSysFile(file, param);
return vo.setUploaded(true).setFileKid(kid);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
// 清除分片记录
cleanChunkData(param.getMd5());
}
}
return vo.setUploaded(false);
}
MultipartFileParam是对文件上传参数的封装,有是否为分片上传、文件、文件名、md5值、分片数等参数。
先使用md5值查询A表和B表,检查是否已经上传过;然后对当前分片的chunkMd5进行校验,校验通过则通过RandomAccessFile进行数据的写入;写入完成后当前分片记录入库;最后判断当前分片是否是最后一块分片,如果是,则校验完整文件的md5,并完成文件记录的入库和分片记录的删除。
注:buildUploadFile()方法——创建文件上传目录和上传文件。
表设计SQL
CREATE TABLE `sys_file` (
`kid` varchar(36) NOT NULL COMMENT 'kid',
`file_name` varchar(255) NOT NULL COMMENT '文件名称',
`extension` varchar(255) DEFAULT NULL COMMENT '文件扩展名',
`file_size` bigint DEFAULT NULL COMMENT '文件大小',
`file_path` varchar(255) NOT NULL COMMENT '文件存放路径',
`file_md5` varchar(255) NOT NULL COMMENT '文件md5',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`kid`),
UNIQUE KEY `idx_file_md5` (`file_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='附件表';
CREATE TABLE `sys_chunk_record` (
`kid` varchar(36) NOT NULL COMMENT 'kid',
`chunk_file_name` varchar(255) NOT NULL COMMENT '文件名称',
`chunk_file_path` varchar(255) NOT NULL COMMENT '文件存放路径',
`file_md5` varchar(255) NOT NULL COMMENT '文件md5',
`chunk_file_md5` varchar(255) NOT NULL COMMENT '文件md5',
`chunk` int NOT NULL COMMENT '第几块分片',
`total_chunk` int NOT NULL COMMENT '总分片数量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`kid`),
UNIQUE KEY `idx_chunk_file_md5` (`chunk_file_md5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='附件表';
接口测试
测试工具:Apifox,测试时间:2022/10/21
先上传测试文件的第二块分片,sys_chunk_record表中保存了该分片上传记录;
此时调用秒传校验接口,可看见返回的分片数据;
然后上传测试文件的第一块分片,sys_chunk_record表中保存了该分片上传记录。
因为该分片为最后一块分片,sys_file表中会保存文件上传记录并清除sys_chunk_record表中的分片记录。
上传目录使用年月日格式,文件名是传递的参数fileName。
注:测试工具自测时,md5值的获取方式——打开cmd,使用certutil -hashfile命令。如:
测试项目获取地址
——代码结构
https://gitee.com/dlqx/springboot-code-book中的bigfile-up-down文件夹。