文件分片上传
1前端
<input
type="file"
accept=".mp4"
ref="videoInput"
@change="handleVideoChange"
style="display: none;">
2生成hash
// 根据整个文件的文件名和大小组合的字符串生成hash值,大概率确定文件的唯一性
fhash(file) {
// console.log("哈希字段: ", file.name+file.size.toString());
return new Promise(resolve => {
const spark = new SparkMD5();
spark.append(file.name+file.size.toString());
resolve(spark.end());
})
},
3生成切片
// 生成切片
createChunks(file) {
const result = [];
for (let i = 0; i < file.size; i += this.chunkSize) {
result.push(file.slice(i, i + this.chunkSize));
}
return result;
},
4查询未上传切片的hash,断点续传
// 获取当前还没上传的序号 断点续传
async askCurrentChunk(hash) {
return await this.$get("/video/ask-chunk", {
params: { hash: hash },
headers: { Authorization: "Bearer " + localStorage.getItem("teri_token") }
});
},
5上传分片
// 上传分片
async uploadChunk(formData) {
return await this.$post("/video/upload-chunk", formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: "Bearer " + localStorage.getItem("teri_token"),
}
})
},
6上传文件
async upload() {
const chunks = this.createChunks(this.selectedVideo);
// 向服务器查询还没上传的下一个分片序号
const result = await this.askCurrentChunk(this.hash);
this.current = result.data.data;
// 逐个上传分片
for (this.current; this.current < chunks.length; this.current++) {
const chunk = chunks[this.current];
const formData = new FormData();
formData.append('chunk', chunk); // 将当前分片作为单独的文件上传
formData.append('hash', this.hash);
formData.append('index', this.current); // 传递分片索引
// 发送分片到服务器
try {
const res = await this.uploadChunk(formData);
if (res.data.code !== 200) {
// ElMessage.error("分片上传失败");
this.isFailed = true;
this.isPause = true;
}
} catch {
// ElMessage.error("分片上传失败");
this.isFailed = true;
this.isPause = true;
return;
}
// 暂停上传
if (this.isPause) {
// 取消上传彻底删除已上传分片
if (this.isCancel) {
await this.cancelUpload(this.hash);
this.isCancel = false;
}
return;
}
this.progress = Math.round(((this.current + 1) / chunks.length) * 100); // 实时改进度条
}
this.progress = 100; // 上传完成再次确认为100%
},
后端
1查询hash
/**
* 获取视频下一个还没上传的分片序号
* @param hash 视频的hash值
* @return CustomResponse对象
*/
public CustomResponse askCurrentChunk(String hash) {
CustomResponse customResponse = new CustomResponse();
// 查询本地
// 获取分片文件的存储目录
File chunkDir = new File(CHUNK_DIRECTORY);
// 获取存储在目录中的所有分片文件
File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(hash + "-"));
// 返回还没上传的分片序号
if (chunkFiles == null) {
customResponse.setData(0);
} else {
customResponse.setData(chunkFiles.length);
}
// 查询OSS当前存在的分片数量,即前端要上传的分片序号,建议分布式系统才使用OSS存储分片,单体系统本地存储分片效率更高
// int counts = ossUploadUtil.countFiles("chunk/", hash + "-");
// customResponse.setData(counts);
return customResponse;
}
2上传分片
/**
* 上传单个视频分片,当前上传到阿里云对象存储
* @param chunk 分片文件
* @param hash 视频的hash值
* @param index 当前分片的序号
* @return CustomResponse对象
* @throws IOException
*/
@Override
public CustomResponse uploadChunk(MultipartFile chunk, String hash, Integer index) throws IOException {
CustomResponse customResponse = new CustomResponse();
// 构建分片文件名
String chunkFileName = hash + "-" + index;
// 存储到本地
// 构建分片文件的完整路径
String chunkFilePath = Paths.get(CHUNK_DIRECTORY, chunkFileName).toString();
// 检查是否已经存在相同的分片文件
File chunkFile = new File(chunkFilePath);
if (chunkFile.exists()) {
log.warn("分片 " + chunkFilePath + " 已存在");
customResponse.setCode(500);
customResponse.setMessage("已存在分片文件");
return customResponse;
}
// 保存分片文件到指定目录
chunk.transferTo(chunkFile);
// 存储到OSS,建议分布式系统才使用OSS存储分片,单体系统本地存储分片效率更高
// try {
// boolean flag = ossUploadUtil.uploadChunk(chunk, chunkFileName);
// if (!flag) {
// log.warn("分片 " + chunkFileName + " 已存在");
// customResponse.setCode(500);
// customResponse.setMessage("已存在分片文件");
// }
// } catch (IOException ioe) {
// log.error("读取分片文件数据流时出错了");
// }
// 返回成功响应
return customResponse;
}
3合并
/**
* 合并分片并将投稿信息写入数据库
* @param vui 存放投稿信息的 VideoUploadInfo 对象
*/
@Transactional
public void mergeChunks(VideoUploadInfoDTO vui) throws IOException {
String url; // 视频最终的URL
// 合并到本地
// // 获取分片文件的存储目录
// File chunkDir = new File(CHUNK_DIRECTORY);
// // 获取当前时间戳
// long timestamp = System.currentTimeMillis();
// // 构建最终文件名,将时间戳加到文件名开头
// String finalFileName = timestamp + vui.getHash() + ".mp4";
// // 构建最终文件的完整路径
// String finalFilePath = Paths.get(VIDEO_DIRECTORY, finalFileName).toString();
// // 创建最终文件
// File finalFile = new File(finalFilePath);
// // 获取所有对应分片文件
// File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(vui.getHash() + "-"));
// if (chunkFiles != null && chunkFiles.length > 0) {
// // 使用流操作对文件名进行排序,防止出现先合并 10 再合并 2
// List<File> sortedChunkFiles = Arrays.stream(chunkFiles)
// .sorted(Comparator.comparingInt(file -> Integer.parseInt(file.getName().split("-")[1])))
// .collect(Collectors.toList());
// try {
System.out.println("正在合并视频");
// // 合并分片文件
// for (File chunkFile : sortedChunkFiles) {
// byte[] chunkBytes = FileUtils.readFileToByteArray(chunkFile);
// FileUtils.writeByteArrayToFile(finalFile, chunkBytes, true);
// chunkFile.delete(); // 删除已合并的分片文件
// }
System.out.println("合并完成!");
// // 获取绝对路径,仅限本地服务器
// url = finalFile.getAbsolutePath();
System.out.println(url);
// } catch (IOException e) {
// // 处理合并失败的情况 重新入队等
// log.error("合并视频失败");
// throw e;
// }
// } else {
// // 没有找到分片文件 发通知用户投稿失败
// log.error("未找到分片文件 " + vui.getHash());
// return;
// }
// 合并到OSS,并返回URL地址
url = ossUtil.appendUploadVideo(vui.getHash());
if (url == null) {
return;
}
// 存入数据库
Date now = new Date();
Video video = new Video(
null,
vui.getUid(),
vui.getTitle(),
vui.getType(),
vui.getAuth(),
vui.getDuration(),
vui.getMcId(),
vui.getScId(),
vui.getTags(),
vui.getDescr(),
vui.getCoverUrl(),
url,
0,
now,
null
);
videoMapper.insert(video);
VideoStats videoStats = new VideoStats(video.getVid(),0,0,0,0,0,0,0,0);
videoStatsMapper.insert(videoStats);
esUtil.addVideo(video);
CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("video:" + video.getVid(), video), taskExecutor);
CompletableFuture.runAsync(() -> redisUtil.addMember("video_status:0", video.getVid()), taskExecutor);
CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("videoStats:" + video.getVid(), videoStats), taskExecutor);
// 其他逻辑 (发送消息通知写库成功)
}
4oss追加上传
/**
* 将本地的视频分片文件追加合并上传到OSS
* @param hash 视频的hash值,用于检索对应分片
* @return 视频在OSS的URL地址
* @throws IOException
*/
public String appendUploadVideo(@NonNull String hash) throws IOException {
// 生成文件名
String uuid = System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "");
String fileName = uuid + ".mp4";
// 完整路径名
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()).replace("-", "");
String filePathName = date + "/video/" + fileName;
ObjectMetadata meta = new ObjectMetadata();
// 设置内容类型为MP4视频
meta.setContentType("video/mp4");
int chunkIndex = 0;
long position = 0; // 追加位置
while (true) {
File chunkFile = new File(CHUNK_DIRECTORY + hash + "-" + chunkIndex);
if (!chunkFile.exists()) {
if (chunkIndex == 0) {
log.error("没找到任何相关分片文件");
return null;
}
break;
}
// 读取分片数据
FileInputStream fis = new FileInputStream(chunkFile);
byte[] buffer = new byte[(int) chunkFile.length()];
fis.read(buffer);
fis.close();
// 追加上传分片数据
try {
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(OSS_BUCKET, filePathName, new ByteArrayInputStream(buffer), meta);
appendObjectRequest.setPosition(position);
AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
position = appendObjectResult.getNextPosition();
} catch (OSSException oe) {
log.error("OSS出错了:" + oe.getErrorMessage());
throw oe;
} catch (ClientException ce) {
log.error("OSS连接出错了:" + ce.getMessage());
throw ce;
}
chunkFile.delete(); // 上传完后删除分片
chunkIndex++;
}
return OSS_BUCKET_URL + filePathName;
}