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

一、前提说明

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

二、应用场景

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

三、概念

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

六、日志结果

此时日志打印如下:

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

相关推荐
路在脚下@5 分钟前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
森屿Serien8 分钟前
Spring Boot常用注解
java·spring boot·后端
苹果醋31 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
菠萝蚊鸭2 小时前
Dhatim FastExcel 读写 Excel 文件
java·excel·fastexcel
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
007php0072 小时前
Go语言zero项目部署后启动失败问题分析与解决
java·服务器·网络·python·golang·php·ai编程
∝请叫*我简单先生2 小时前
java如何使用poi-tl在word模板里渲染多张图片
java·后端·poi-tl
ssr——ssss2 小时前
SSM-期末项目 - 基于SSM的宠物信息管理系统
java·ssm
一棵星3 小时前
Java模拟Mqtt客户端连接Mqtt Broker
java·开发语言