前端Demo(html文件)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片上传测试</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script>
</head>
<body>
<h2>分片上传视频</h2>
<input type="file" id="fileInput" />
<button onclick="handleUpload()">上传视频</button>
<div id="progress-container" style="margin-top: 20px; width: 100%; background-color: #f3f3f3; height: 20px; border-radius: 5px; overflow: hidden;">
<div id="progress-bar" style="width: 0%; height: 100%; background-color: #4caf50;"></div>
<span id="progress-text">0%</span>
</div>
<script>
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片5MB
const uploadProgress = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
// 处理文件上传
async function handleUpload() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择一个文件!');
return;
}
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let chunkIndex = 0;
// 上传文件的每个分片
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end); // 获取文件片段
// 计算分片的 MD5
const md5 = await calculateChunkMD5(chunk);
// 创建FormData并上传分片
const formData = new FormData();
formData.append('videoFile', chunk, file.name);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
formData.append('md5', md5); // 添加 MD5 值
try {
await uploadChunk(formData, chunkIndex + 1, totalChunks);
updateProgress((chunkIndex + 1) / totalChunks);
} catch (error) {
console.error('分片上传失败: ', error);
alert('上传失败,请检查控制台日志。');
break;
}
}
alert('视频上传成功!');
}
// 上传单个分片
async function uploadChunk(formData, chunkIndex, totalChunks) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:7081/admin/api/courses/uploadVideo', true);
xhr.onload = function () {
if (xhr.status === 200) {
console.log(`分片 ${chunkIndex}/${totalChunks} 上传成功`);
resolve();
} else {
console.error(`分片 ${chunkIndex}/${totalChunks} 上传失败, 状态码: `, xhr.status);
console.error('响应: ', xhr.responseText); // 打印服务器响应
reject(new Error(`上传失败,状态码: ${xhr.status}`));
}
};
xhr.onerror = function () {
console.error(`分片 ${chunkIndex}/${totalChunks} 上传时发生错误`);
reject(new Error('网络错误,无法连接服务器'));
};
xhr.send(formData);
});
}
// 计算每个分片的 MD5
async function calculateChunkMD5(chunk) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const spark = new SparkMD5.ArrayBuffer();
spark.append(e.target.result);
const md5 = spark.end();
resolve(md5);
};
reader.readAsArrayBuffer(chunk);
});
}
// 更新进度条
function updateProgress(progress) {
const percent = Math.round(progress * 100);
uploadProgress.style.width = percent + '%';
progressText.innerText = percent + '%';
}
</script>
</body>
</html>
后端代码
引入依赖,这里我用的阿里云的OSS的客户端(虽然back桶是在腾讯云上,但是好像也可以用。为什么没用腾讯云,是因为之前版本腾讯云的依赖更新了,报错appid懒得去看解决,就暂时用的阿里云的)。
业务主体代码
@PostMapping("/uploadVideo")
@ResponseBody
public Result<String> uploadVideoChunk(@RequestPart MultipartFile videoFile, // 视频文件
@RequestParam("chunkIndex") int chunkIndex, // 分片索引
@RequestParam("totalChunks") int totalChunks, // 分片总数
@RequestParam("fileName") String fileName, // 文件名
@RequestParam("md5") String md5, // 文件MD5
HttpServletRequest request) {
try {
// Step 1: 计算接收到的分片的 MD5,并与前端提供的 MD5 进行比较
try (InputStream inputStream = videoFile.getInputStream()) {
String calculatedMd5 = DigestUtils.md5Hex(inputStream);
// 比较前端发送的 MD5 与后端计算的 MD5
if (!calculatedMd5.equals(md5)) {
return Result.error("MD5 校验失败,分片数据不一致");
}
}
// Step 2: 获取OSS配置,使用缓存并处理多线程安全
Set set = cacheUtil.get(CacheNameConst.OSS, "OSS", Set.class);
if (set == null) {
synchronized (this) { // 加锁
// 从数据库中获取配置信息并放入缓存
set = setService.getOne(new LambdaQueryWrapper<Set>().eq(Set::getKey, "OSS"));
if (set == null) {
cacheUtil.put(CacheNameConst.OSS, "OSS", "数据库没有数据");
} else {
// 将配置信息放入缓存
cacheUtil.put(CacheNameConst.OSS, "OSS", set);
}
}
}
if (set == null) {
return Result.error("未配置OSS");
}
OssSetDTO ossSetDTO = JSONUtil.toBean(set.getValues(), OssSetDTO.class);
TXOssSetDTO dto = ossSetDTO.getTxOssSetDTO();
// Step 3: 定义临时存储目录,用于保存分片文件
String tempDirPath = System.getProperty("java.io.tmpdir") + "/upload_chunks/";
File tempDir = new File(tempDirPath);
if (!tempDir.exists()) {
tempDir.mkdirs(); // 创建临时存储目录
}
// Step 4: 临时存储每个分片的文件
String chunkFileName = tempDirPath + fileName + ".part" + chunkIndex;
File chunkFile = new File(chunkFileName);
videoFile.transferTo(chunkFile); // 保存分片文件
// 将文件名字转换成二进制
String fileNameBytes = convertStringToBytes(fileName);
// Step 5: 检查是否所有分片都已上传完毕
if (allChunksUploaded(tempDirPath, fileName, totalChunks)) {
// 合并分片
String mergedFilePath = tempDirPath + fileName;
// 合并分片
mergeChunks(tempDirPath, fileName, totalChunks, mergedFilePath);
String name = fileNameBytes + ".mp4";
// 上传合并后的文件到OSS
String objectName = "videos/" + name;
OSS ossClient = new OSSClientBuilder().build(dto.getEndpoint(), dto.getSecretId(), dto.getSecretKey());
try (FileInputStream inputStream = new FileInputStream(new File(mergedFilePath))) {
ossClient.putObject(dto.getBucketName(), objectName, inputStream);
log.info("视频上传成功");
// 清理临时文件
deleteTempFiles(tempDirPath, fileName, totalChunks);
deleteTempFiles(tempDirPath, fileName + ".mp4", totalChunks);
return Result.data(dto.getPrefix() + "/" + objectName); // 返回视频URL
} catch (OSSException oe) {
log.error("OSSException: " + oe.getErrorMessage());
return Result.error(201, "请检查OSS配置");
} catch (ClientException ce) {
log.error("ClientException: " + ce.getMessage());
return Result.error(201, "请检查OSS配置");
}
}
return Result.data("分片" + chunkIndex + "上传成功");
} catch (IOException e) {
log.error("Error calculating MD5", e);
return Result.error("MD5 计算错误,分片上传失败");
} catch (Exception e) {
log.error("上传视频分片时发生错误: " + e.getMessage(), e);
return Result.error("视频分片上传失败");
}
}

再来看视频文件代码

这是本地缓存的

其他代码
// 检查是否所有分片都已上传完毕
private boolean allChunksUploaded(String tempDirPath, String fileName, int totalChunks) {
for (int i = 0; i < totalChunks; i++) {
File partFile = new File(tempDirPath + fileName + ".part" + i);
if (!partFile.exists()) {
return false;
}
}
return true;
}
// 合并分片
private void mergeChunks(String tempDirPath, String fileName, int totalChunks, String mergedFilePath) throws
IOException {
try (BufferedOutputStream mergedStream = new BufferedOutputStream(new FileOutputStream(mergedFilePath, true))) {
for (int i = 0; i < totalChunks; i++) {
File partFile = new File(tempDirPath + fileName + ".part" + i);
try (BufferedInputStream partStream = new BufferedInputStream(new FileInputStream(partFile))) {
byte[] buffer = new byte[32768]; // 大文件时这个就可以设置大些 一般是1024
int bytesRead;
while ((bytesRead = partStream.read(buffer)) != -1) {
// 将分片内容写入合并文件
mergedStream.write(buffer, 0, bytesRead);
}
}
}
}
}
// 删除临时文件
private void deleteTempFiles(String tempDirPath, String fileName, int totalChunks) {
for (int i = 0; i < totalChunks; i++) {
File partFile = new File(tempDirPath + fileName + ".part" + i);
if (partFile.exists()) {
partFile.delete();
}
}
File mergedFile = new File(tempDirPath + fileName);
if (mergedFile.exists()) {
mergedFile.delete();
}
}
// 将名字转换成二进制数组
// 这里是为了解决上传到COS上面,判断是新增视频还是将原来的视频覆盖掉做更新处理
public String convertStringToBytes(String fileName) {
try {
// 使用 UTF-8 字符编码将字符串转换为字节数组
return fileName.getBytes("UTF-8").toString();
} catch (UnsupportedEncodingException e) {
// 如果字符编码不支持,抛出异常
e.printStackTrace();
return null;
}
}
我们再调用这个接口,就可以使用分片上传这个功能了

看打印日志,ok,接口调用成功了。

555

被折叠的 条评论
为什么被折叠?



