手撸大文件上传:实现切片上传,断点上传和文件秒传的功能。

一、前提说明

此文章主要讲述后端服务代码和前后端实现思路部分,不涉及前端代码。

二、应用场景

上传视频等大文件的时候,调用服务器的上传接口,可能出现因为文件过大,连接时间超时导致的上传失败,如果文件太大了,可能出现上传一半网络异常,从而再次上传需要重新开始上传。为了解决这种场景问题,手写一个大文件上传实现切片上传,断点上传和文件秒传的功能。

三、概念

切片:切片上传是一种将大文件分割成多个小文件的方式,此时小文件就是切片。
断点上传:在文件分块的基础上,将每个小文件采用单独的线程进行上传\下载,如果碰到网络故障,可以从已经上传\下载的部分开始继续上传\下载未完成的部分,而没有必要从头开始上传\下载。
秒传:当文件上传时,文件资源标识存在时,文件不再重新上传,而直接返回文件URL。

四、思路

4.1 前端思路

  1. 获取大文件信息包含(文件名称,文件大小,格式类型
  2. 大文件分块可以利用强大的js库或者现成的组件进行分块处理。需要确定分块【chunk】的大小和分块的总数量【chunkChecksum】,然后为每一个分块指定一个索引值【chunkIndex】
  3. 为了实现秒传的功能,需要将文件的文件名称,文件大小,格式类型拼接然后进行MD5加密作为文件的唯一标识【fileId】
  4. 循环切块,将上面标红的信息作为参数传给接口,此时将接口返回的date,作为下一个需要传的切片索引,再次走接口,这是为了实现断点上传。
  5. 直到接口返回date是url地址为止,此时上传完成。
  6. 文件的暂停和继续上传由前端自行研究。

4.2 后端思路

redis使用redisTemplate.opsForList()的方式存贮切片,然后最后循环缓存合并,最好使用redisTemplate.opsForList().rightPush(key,value)存,并且设置过期时间防止上传一部分的放弃上传造成内存占用。redis使用自行百度。

  1. 获取到前端传的信息,利用redis暂存切片。
  2. 根据当前大文件的fileId,然后去数据库查询,如果存在,则直接返回当前大文件的地址,实现秒传功能
  3. 判断当前切片索引是否上传过,上传过则在判断如果已存在切片数量=总数量则直接合成切片,否则直接返回下一个需要上传的切片索引值,
  4. 当前切片没有上传过,则需要把当前文件暂时保存到redis中,并且返回下一个需要上传的切片索引值
  5. 判断是否切片上传完成,上传完成则合并切片形成大文件,否则直接进行下一个切片上传

五、接口实现过程

java 复制代码
	/**
	 * 上传大文件
	 *
	 * @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;
	}

六、日志结果

此时日志打印如下:

文件存储成功并且可以成功播放。

相关推荐
qfljg17 分钟前
OpenRewrite 实战指南
java
Sylvia33.22 分钟前
LOL实时数据接入深度解析:从WebSocket到完整数据模型
java·网络·python·websocket·网络协议
2601_962071576 小时前
【Java报错已解决】org.springframework.beans.factory.BeanCreationException
java·开发语言
zww89491116 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·eclipse
zww89491119 小时前
匿名树洞系统开发实战:从需求分析到部署指南
java·eclipse
黑马程序员毕设10 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设
木井巳10 小时前
【BFS/DFS 解决 FloodFill 算法】太平洋大西洋水流问题
java·算法·leetcode·深度优先·广度优先·宽度优先·推荐算法
白山编程大哥10 小时前
Java 集合算法:从排序、查找到底层原理的实战指南
java·python·算法
devpotato11 小时前
缓存与数据库更新顺序不一致问题
java·数据库·redis
xcl092511 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·spring boot·需求分析