此文章主要讲述后端服务代码和前后端实现思路部分,不涉及前端代码。
上传视频等大文件的时候,调用服务器的上传接口,可能出现因为文件过大,连接时间超时导致的上传失败,如果文件太大了,可能出现上传一半网络异常,从而再次上传需要重新开始上传。为了解决这种场景问题,手写一个大文件上传实现切片上传,断点上传和文件秒传的功能。
切片
:切片上传是一种将大文件分割成多个小文件的方式,此时小文件就是切片。
断点上传
:在文件分块的基础上,将每个小文件采用单独的线程进行上传\下载,如果碰到网络故障,可以从已经上传\下载的部分开始继续上传\下载未完成的部分,而没有必要从头开始上传\下载。
秒传
:当文件上传时,文件资源标识存在时,文件不再重新上传,而直接返回文件URL。
格式类型
)【chunk】
的大小和分块的总数量【chunkChecksum】
,然后为每一个分块指定一个索引值【chunkIndex】
。【fileId】
。redis使用redisTemplate.opsForList()的方式存贮切片,然后最后循环缓存合并,最好使用redisTemplate.opsForList().rightPush(key,value)存,并且设置过期时间防止上传一部分的放弃上传造成内存占用。redis使用自行百度。
fileId
,然后去数据库查询,如果存在,则直接返回当前大文件的地址,实现秒传功能已存在切片数量=总数量
则直接合成切片,否则直接返回下一个需要上传的切片索引值, /**
* 上传大文件
*
* @param chunk 切片文件
* @param chunkIndex 切片索引
* @param chunkChecksum 切片总数
* @param fileFormat 文件格式
* @param fileId 大文件标志(最好是让前端把文件的(名称+大小+类型)进行MD5加密)
* @return
* @throws Exception
*/
@Inside(value = false)
@PostMapping("/uploadBigFile")
public R uploadFile(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("chunkIndex") Integer chunkIndex,
@RequestParam("chunkChecksum") Integer chunkChecksum,
@RequestParam("fileFormat") String fileFormat,
@RequestParam("fileId") String fileId) throws Exception {
log.info("切片索引" + chunkIndex);
log.info("切片总数" + chunkChecksum);
String key = CommonConstants.FILE_KEY + fileId;
//STEP 获取当前大文件的id,然后去数据库查询,如果存在,则直接返回当前大文件的地址,实现秒传功能
Material material = materialService.getByFileId(fileId);
if (material != null) {
return R.ok(material.getUrl(), "上传成功,实现秒传");
}
//STEP 判断当前切片索引是否上传过,上传过则在判断如果已存在切片数量=总数量则直接合成切片,否则直接返回下一个需要上传的切片索引值,
Object chunkIndexRedis = redisTemplate.opsForList().index(key, chunkIndex);
if (chunkIndexRedis != null) {
Long chunkIndexRedisMax = redisTemplate.opsForList().size(key);
log.info("已经上传的切片数量" + chunkIndexRedisMax);
int chunkIndexRedisIntMax = chunkIndexRedisMax.intValue();
if (chunkIndexRedisIntMax == chunkChecksum) {
String merge = merge(key, fileFormat);
redisTemplate.delete(key);
return R.ok(merge, "上传完成,实现切片合并异常");
}
return R.ok(chunkIndexRedisMax + 1, "上传成功,实现断点功能");
}
//STEP 当前切片没有上传过,则需要把当前文件暂时保存到redis中,并且返回下一个需要上传的切片索引值
//分片文件大小
byte[] chunkBytes = chunk.getBytes();
redisTemplate.opsForList().rightPush(key, chunkBytes);
redisTemplate.expire(key, CommonConstants.FILE_KEY_OUT_TIME, TimeUnit.MINUTES);
//STEP 判断是否切片上传完成,上传完成则合并切片形成大文件,否则直接进行下一个切片上传
if (chunkIndex == chunkChecksum) {
String merge = merge(key, fileFormat);
redisTemplate.delete(key);
return R.ok(merge, "上传完成,实现全部切片上传");
}
return R.ok(chunkIndex + 1, "上传成功,实现当前切片上传");
}
/**
* 合并切片,把切片写入到文件夹中
*
* @param key redis中的key
* @param fileFormat 文件格式
* @return
* @throws FileNotFoundException
*/
public String merge(String key, String fileFormat) throws FileNotFoundException {
log.info("合并切片");
//文件名称随机生成(文件名+.+后缀)
String fileName = IdUtil.getSnowflake(0, 0).nextId() + "." + fileFormat;
//文件地址
String path = UpmsConstants.TOMCAT_PATH + "/video/";
//判断文件是否存在,不存在创建文件
File newFile = new File(path);
//如果文件夹不存在
if (!newFile.exists()) {
//创建文件夹
newFile.mkdir();
}
FileOutputStream outputStream = new FileOutputStream(path + fileName, true);//文件追加写入
FileInputStream fileInputStream = null;//分片文件
try {
List<byte[]> chunkList = redisTemplate.opsForList().range(key, 0, -1);
for (byte[] bytes : chunkList) {
outputStream.write(bytes);
}
} catch (IOException e) {
log.error("分片合并异常", e);
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
log.info("IO流关闭");
System.gc();
} catch (Exception e) {
log.error("IO流关闭", e);
}
}
log.info("合并分片结束");
return UpmsConstants.TOMCAT_Url + "video/" + fileName;
}
此时日志打印如下:
文件存储成功并且可以成功播放。