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


    一、前提说明

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

    二、应用场景

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

    三、概念

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

    五、接口实现过程

    	/**
    	 * 上传大文件
    	 *
    	 * @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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    六、日志结果

    此时日志打印如下:
    在这里插入图片描述
    文件存储成功并且可以成功播放。

  • 相关阅读:
    电脑重装系统后鼠标动不了该怎么解决
    手把手教你蜂鸟e203协处理器的扩展
    Oracle-触发器和程序包
    自动驾驶学习笔记(一)——Apollo平台
    正方形(Squares, ACM/ICPC World Finals 1990, UVa201)rust解法
    听GPT 讲Istio源代码--pilot
    Java学习 --- 面向对象三大特征之封装
    H5生成二维码
    springboot集成UidGenerator
    无涯教程-Android Online Test函数
  • 原文地址:https://blog.csdn.net/XuDream/article/details/133939755