1.效果图
2.前端html
<!DOCTYPE html>
<html>
<head></head>
<body>
<form>
<input type="file" id="fileInput" multiple>
<button type="button" onclick="upload()" >大文件分片上传</button>
</form>
<script>
function upload() {
var fileInput = document.getElementById('fileInput');
var fileName = document.getElementById("fileInput").files[0].name;
var files = fileInput.files;
var chunkSize = 1024 * 1024 * 10; // 每个块的大小为10MB
var totalChunks = Math.ceil(files[0].size / chunkSize); // 文件总块数
var currentChunk = 0; // 当前块数
console.log("当前文件:"+fileName+",大小:"+files[0].size+",分片大小:"+chunkSize+",分片数:"+totalChunks);
// 分片上传文件
function uploadChunk() {
var xhr = new XMLHttpRequest();
var formData = new FormData();
// 将当前块数和总块数添加到formData中
formData.append('current_slice_index', currentChunk);
formData.append('file_name', fileName);
formData.append('user_name', "15910761260");
// 计算当前块在文件中的偏移量和长度
var start = currentChunk * chunkSize;
var end = Math.min(files[0].size, start + chunkSize);
var chunk = files[0].slice(start, end);
// 添加当前块到formData中
formData.append('file', chunk);
// 发送分片到后端
xhr.open('POST', 'http://192.x.x.x:8060/file/fileInfo/uploadSlice');
xhr.send(formData);
xhr.onload = function(data) {
console.log('上传第'+ currentChunk +'个分片结果:'+xhr.responseText);
// 需要判断反馈结果,如果成功,才继续,否则再次上传失败部分
// 更新当前块数
currentChunk++;
// 如果还有未上传的块,则继续上传
if (currentChunk < totalChunks) {
uploadChunk();
} else {
// 所有块都上传完毕,进行文件合并
console.log("开始合并");
mergeChunks(fileName);
}
}
}
// 合并所有分片
function mergeChunks() {
var xhr = new XMLHttpRequest();
var formData = new FormData();
formData.append('user_name', "15910761260");
formData.append('total_slice_num', totalChunks);
formData.append('file_name', fileName);
formData.append('file_owner', 'MediaResource');
formData.append('order_code', 'MediaResource');
formData.append('folder', 'MediaResource');
xhr.open("POST", "http://192.x.x.x:8060/file/fileInfo/mergeSlice");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log("文件合并完成:", xhr.responseText);
} else {
console.error(xhr.responseText);
}
}
};
xhr.send(formData);
}
// 开始上传
uploadChunk();
}
</script>
</body>
</html>
2.java代码
上传分片代码
@ResponseBody
@PostMapping(value = "/uploadSlice")
@Operation(summary = "上传文件分片", description = "返回是否上传成功")
public MisResult<String> UploadSlice(UploadFileSliceInput uploadFileSliceInput,
@RequestParam(value = "file") MultipartFile multipartFile) throws Exception {
return fileInfoService.UploadSlice(uploadFileSliceInput, multipartFile.getBytes());
}
@Override
public MisResult<String> UploadSlice(UploadFileSliceInput uploadFileSliceInput, byte[] content) {
MisResult<String> result = new MisResult<>();
try {
//参数校验
ValidateUtil.ValidateThrowException(uploadFileSliceInput);
if (content == null || content.length == 0) {
result.Error(MisResultCode.PARAM_ERROR);
return result;
}
//判断分片是否已上传
String nameMd5 = DigestUtils.md5Hex(uploadFileSliceInput.getFile_name());
String redisKey = nameMd5 + "_" + uploadFileSliceInput.getUser_name();
String chunkStatus = (String) RedisUtil.HashGet(redisKey, uploadFileSliceInput.getCurrent_slice_index() + "");
if (StringUtils.hasLength(chunkStatus) && MisStatus.Finished.equals(chunkStatus)) {
result.Success("文件分片上传成功!");
return result;
}
//保存文件分片到临时文件夹
String absolutePath = FileConfig.Path + File.separator + "temp" + File.separator + "slice_" + nameMd5
+ "_" + uploadFileSliceInput.getCurrent_slice_index();
File saveFile = new File(absolutePath);
FileCopyUtils.copy(content, saveFile);
//记录上传状态
long timeout = RedisUtil.GetExpire(redisKey);
RedisUtil.HashSet(redisKey, uploadFileSliceInput.getCurrent_slice_index() + "", MisStatus.Finished);
result.Success("文件分片上传成功!");
if (timeout < 0) {
RedisUtil.SetExpire(redisKey, 60 * 60);//1个小时超时删除
}
} catch (Exception e) {
result.Error(MisResultCode.UNKNOWN_ERROR, "分片上传异常," + e.getMessage());
log.error("分片上传异常", e);
}
return result;
}
上传参数
package mis.dto.file.fileinfo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* description 分片上传文件dto
*/
@Schema(description = "分片上传文件dto")
@Data
@Accessors(chain = true)
public class UploadFileSliceInput {
@NotNull
@Schema(description = "上传手机号")
private String user_name;
@NotNull
@Schema(description = "文件名")
private String file_name;
@NotNull
@Schema(description = "当前分片序号")
private int current_slice_index;
}
合并分片代码
@ResponseBody
@PostMapping(value = "/mergeSlice")
@Operation(summary = "合并文件分片", description = "返回是否上传成功")
public MisResult<FileInfoOutput> MergeSlice(UploadFileSliceMerge uploadFileSliceMerge) throws Exception {
return fileInfoService.MergeSlice(uploadFileSliceMerge);
}
@Override
public MisResult<FileInfoOutput> MergeSlice(UploadFileSliceMerge uploadFileSliceMerge) throws Exception {
MisResult<FileInfoOutput> result = new MisResult<>();
//参数校验,非空项及上传分片数量是否正确
ValidateUtil.ValidateThrowException(uploadFileSliceMerge);
String nameMd5 = DigestUtils.md5Hex(uploadFileSliceMerge.getFile_name());
String redisKey = nameMd5 + "_" + uploadFileSliceMerge.getUser_name();
Map<Object, Object> sliceMap = RedisUtil.HashGetMap(redisKey);
if (sliceMap.size() != uploadFileSliceMerge.getTotal_slice_num()) {
result.Error(MisResultCode.MIS_ERR_NOTEXITS, "文件分片上传不完整," + uploadFileSliceMerge.getUser_name());
log.error(result.getMessage());
return result;
}
//根据分片文件序号排列
File tmpDir = new File(FileConfig.Path + File.separator + "temp");
File[] sliceFiles = tmpDir.listFiles((dir, name) -> name.contains("slice") &&
name.contains(nameMd5));
if (sliceFiles == null) {
result.Error(MisResultCode.MIS_ERR_NOTEXITS, "文件分片不存在!");
return result;
}
Arrays.sort(sliceFiles, (o1, o2) -> {
String o1Index = o1.getName().replace("_", "")
.replace("slice", "")
.replace(nameMd5, "");
String o2Index = o2.getName().replace("_", "")
.replace("slice", "")
.replace(nameMd5, "");
return Integer.parseInt(o1Index) - Integer.parseInt(o2Index);
});
//设置最终存储路径
String fileOwner = StringUtils.hasLength(uploadFileSliceMerge.getFile_owner()) ?
uploadFileSliceMerge.getFile_owner() : "unknown";//文件拥有者
String newFileName = UuidGenerator.generate() + "." + FileUtil.GetSuffix(uploadFileSliceMerge.getFile_name());//新文件名
String storageDir = FileConfig.Path + File.separator;
if (StringUtils.hasLength(uploadFileSliceMerge.getFolder())) {
storageDir += uploadFileSliceMerge.getFolder();//存储目录
} else {
storageDir += DateUnitl.ToString(null, "yyyyMM") + File.separator + fileOwner;//存储目录
}
if (!new File(storageDir).exists()) {
new File(storageDir).mkdirs();
}
String absolutePath = storageDir + File.separator + newFileName;//存储绝对路径
String filePath = absolutePath.replace(FileConfig.Path, "");//存储相对路径
//合并分片文件
FileChannel outChannel = null;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(absolutePath);
outChannel = outputStream.getChannel();
for (int i = 0; i < sliceFiles.length; i++) {
FileInputStream inputStream = new FileInputStream(sliceFiles[i]);
try (FileChannel inChannel = inputStream.getChannel()) {
inChannel.transferTo(0, inChannel.size(), outChannel);
FileUtil.Close(inChannel);
}
FileUtil.Close(inputStream);
}
} catch (Exception e) {
log.error("合并分片异常", e);
result.Error(MisResultCode.ACTION_ERROR, "合并分片异常," + uploadFileSliceMerge.getFile_name());
return result;
} finally {
FileUtil.Close(outChannel);
FileUtil.Close(outputStream);
}
//保存最终文件信息
File bigFile = new File(absolutePath);
String uuid = UuidGenerator.generate();
String downloadUrl = FileConfig.Access + "?fileInfoId=" + uuid;
FileInfo newFileInfo = new FileInfo(uuid)
.setFile_owner(fileOwner)
.setOrder_code(uploadFileSliceMerge.getOrder_code())
.setFile_name(uploadFileSliceMerge.getFile_name())
.setFile_path(filePath)
.setFile_absolute_path(absolutePath)
.setFile_size(bigFile.length())
.setFile_type(FileUtil.JudgeFileTypeByName(uploadFileSliceMerge.getFile_name()))
.setFile_status(MisStatus.Enable)
.setDownload_path(downloadUrl)
.setNote(MisServerConfig.App);
var savedFile = this.fileInfoRepository.save(newFileInfo);
//清理分片和缓存
for (File sliceFile : sliceFiles) {
sliceFile.delete();
}
RedisUtil.Delete(nameMd5 + "_" + uploadFileSliceMerge.getUser_name());
//返回文件对象
var modelMapper = new ModelMapper();
var fileInfoOutput = modelMapper.map(savedFile, FileInfoOutput.class);
fileInfoOutput.setPreview_path("/file/fileInfo/preview?fileInfoId=" + uuid);
result.Success(fileInfoOutput);
return result;
}
合并分片参数
package mis.dto.file.fileinfo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* description 合并分片参数dto
*/
@Schema(description = "分片上传文件dto")
@Data
@Accessors(chain = true)
public class UploadFileSliceMerge {
//合并参数
@NotNull
@Schema(description = "上传手机号")
private String user_name;
@NotNull
@Schema(description = "总分片数")
private double total_slice_num;
//文件参数
@NotNull
@Schema(description = "文件名")
private String file_name;//文件名
@Schema(description = "文件持有人")
private String file_owner;//文件持有人
@Schema(description = "单据编码")
private String order_code;//单据编码
@Schema(description = "文件夹,如果文件夹不为空,则使用传入的文件夹保存文件")
private String folder;//文件夹,如果文件夹不为空,则使用传入的文件夹保存文件
}