提示:以下是本篇文章正文内容,下面案例可供参考
一、简单的分片上传
针对第一个问题,如果文件过大,上传到一半断开了,若重新开始上传的话,会很消耗时间,并且你也并不知道距离上次断开时,已经上传到哪一部分了。因此我们应该先对大文件进行分片处理,防止上面提到的问题。
前端代码:
html复制代码文件上传示例
ps:以上代码使用了html+js完成,请求是使用了xhr来发送请求。其中的地址为自己本地的接口地址。由于平时测试并不需要真正上传大型文件,所以每个分片的大小定义为10KB,以此模拟大文件上传。
(相关资料图)
后端代码:
java复制代码@RestController@RequestMapping("/file")public class FileController { @Autowired private ResourceLoader resourceLoader; @Value("${}") private String uploadPath; private Map>chunksMap = new ConcurrentHashMap<>(); @PostMapping("/upload") public void upload(@RequestParam int currentChunk, @RequestParam int totalChunks, @RequestParam MultipartFile chunk,@RequestParam String fileName) throws IOException { // 将分片保存到临时文件夹中 String chunkName = () + "." + currentChunk; File chunkFile = new File(uploadPath, chunkName); (chunkFile); // 记录分片上传状态 ListchunkList = (fileName); if (chunkList == null) { chunkList = new ArrayList<>(totalChunks); (fileName, chunkList); } (chunkFile); } @PostMapping("/merge") public String merge(@RequestParam String fileName) throws IOException { // 获取所有分片,并按照分片的顺序将它们合并成一个文件 ListchunkList = (fileName); if (chunkList == null || () == 0) { throw new RuntimeException("分片不存在"); } File outputFile = new File(uploadPath, fileName); try (FileChannel outChannel = new FileOutputStream(outputFile).getChannel()) { for (int i = 0; i < (); i++) { try (FileChannel inChannel = new FileInputStream((i)).getChannel()) { (0, (), outChannel); } (i).delete(); // 删除分片 } } (fileName); // 删除记录 // 获取文件的访问URL Resource resource = ("file:" + uploadPath + fileName); //由于是本地文件,所以开头是"file",如果是服务器,请改成自己服务器前缀 return ().toString(); }}
ps: 使用一个map记录上传了哪些分片,这里将分片存在了本地的文件夹,等到分片都上传完成后合并并删除分片。用ConcurrentHashMap代替HashMap是因为它在多线程下是安全的。
以上只是一个简单的文件上传代码,但是只要在这上面另做修改就可以解决上面提到的问题。
二、解决问题
1. 怎么避免大量的硬盘读写
上面代码有一个弊端,就是将分片的内容存在了本地的文件夹里。而且在合并的时候判断上传是否完全也是从文件夹读取文件的。对磁盘的大量读写操作不仅速度慢,还会导致服务器崩溃,因此下面代码使用了redis来存储分片信息,避免对磁盘过多读写。(你也可以使用mysql或者其他中间件来存储信息,由于读写尽量不要在mysql,所以我使用了redis)。
2.目标文件过大,如果在上传过程中断开了怎么办
使用redis来存储分片内容,当断开后,文件信息还是存储在redis中,用户再次上传时,检测redis是否有该分片的内容,如果有则跳过。
3. 前端页面上传的文件数据与原文件数据不一致该如何发现
前端在调用上传接口时,先计算文件的校验和,然后将文件和校验和一并传给后端,后端对文件再计算一次校验和,两个校验和进行对比,如果相等,则说明数据一致,如果不一致则报错,让前端重新上传该片段。 js计算校验和代码:
js复制代码 // 计算文件的 SHA-256 校验和 function calculateHash(fileChunk) { return new Promise((resolve, reject) =>{ const blob = new Blob([fileChunk]); const reader = new FileReader(); (blob); = () =>{ const arrayBuffer = ; const crypto = || ; const digest = ("SHA-256", arrayBuffer); (hash =>{ const hashArray = (new Uint8Array(hash)); const hashHex = (b =>(16).padStart(2, "0")).join(""); resolve(hashHex); }); }; = () =>{ reject(new Error("Failed to calculate hash")); }; }); }
java复制代码 public static String calculateHash(byte[] fileChunk) throws Exception { MessageDigest md = ("SHA-256"); (fileChunk); byte[] hash = (); ByteBuffer byteBuffer = (hash); StringBuilder hexString = new StringBuilder(); while (()) { (("%02x", ())); } return (); }
注意点:
这里前端和后端计算校验和的算法一定要是一致的,不然得不到相同的结果。 在前端中使用了crypto对文件进行计算,需要引入相关的js。 你可以使用script引入也可以直接下载jshtml复制代码
crypto的下载地址 如果github打不开,可能需要使用npm下载了
4. 上传过程中如果断开了应该如何判断哪些分片没有上传
对redis检测哪个分片的下标不存在,若不存在则存入list,最后将list返回给前端
java复制代码boolean allChunksUploaded = true;ListmissingChunkIndexes = new ArrayList<>(); for (int i = 0; i < (); i++) { if (!((i))) { allChunksUploaded = false; (i); } } if (!allChunksUploaded) { return (_REQUEST).body(missingChunkIndexes); }
三、完整代码
1、引入依赖
xml复制代码 lettuce-core spring-boot-starter-data-redis
lettuce是一个Redis客户端,你也可以不引入,直接使用redisTemplat就行了
2、前端代码
html复制代码File Upload Demo
3、后端接口代码
java复制代码@RestController@RequestMapping("/file2")public class File2Controller { private static final String FILE_UPLOAD_PREFIX = "file_upload:"; @Autowired private ResourceLoader resourceLoader; @Value("${}") private String uploadPath; @Autowired private ThreadLocalredisConnectionThreadLocal; // @Autowired// private RedisTemplate redisTemplate; @PostMapping("/upload") public ResponseEntity>uploadFile(@RequestParam("chunk") MultipartFile chunk, @RequestParam("chunkIndex") Integer chunkIndex, @RequestParam("chunkSize") Integer chunkSize, @RequestParam("chunkChecksum") String chunkChecksum, @RequestParam("fileId") String fileId) throws Exception { if ((fileId) || (fileId)) { fileId = ().toString(); } String key = FILE_UPLOAD_PREFIX + fileId; byte[] chunkBytes = (); String actualChecksum = calculateHash(chunkBytes); if (!(actualChecksum)) { return (_REQUEST).body("Chunk checksum does not match"); }// if(!().hasKey(key,(chunkIndex))) {// ().put(key, (chunkIndex), chunkBytes);// } RedisConnection connection = (); Boolean flag = ((), (chunkIndex).getBytes()); if (flag==null || flag == false) { ((), (chunkIndex).getBytes(), chunkBytes); } return (fileId); } public static String calculateHash(byte[] fileChunk) throws Exception { MessageDigest md = ("SHA-256"); (fileChunk); byte[] hash = (); ByteBuffer byteBuffer = (hash); StringBuilder hexString = new StringBuilder(); while (()) { (("%02x", ())); } return (); } @PostMapping("/merge") public ResponseEntity>mergeFile(@RequestParam("fileId") String fileId, @RequestParam("fileName") String fileName) throws IOException { String key = FILE_UPLOAD_PREFIX + fileId; RedisConnection connection = (); try { MapchunkMap = (());// Map chunkMap = ().entries(key); if (()) { return (_FOUND).body("File not found"); } MaphashMap = new HashMap<>(); for(entry :()){ ((new String(())),()); } // 检测是否所有分片都上传了 boolean allChunksUploaded = true; ListmissingChunkIndexes = new ArrayList<>(); for (int i = 0; i < (); i++) { if (!((i))) { allChunksUploaded = false; (i); } } if (!allChunksUploaded) { return (_REQUEST).body(missingChunkIndexes); } File outputFile = new File(uploadPath, fileName); boolean flag = mergeChunks(hashMap, outputFile); Resource resource = ("file:" + uploadPath + fileName); if (flag == true) { (());// (key); return ().body(().toString()); } else { return (555).build(); } } catch (Exception e) { (); return (_SERVER_ERROR).body(()); } } private boolean mergeChunks(MapchunkMap, File destFile) { try (FileOutputStream outputStream = new FileOutputStream(destFile)) { // 将分片按照顺序合并 for (int i = 0; i < (); i++) { byte[] chunkBytes = ((i)); (chunkBytes); } return true; } catch (IOException e) { (); return false; } }}
4、redis配置
java复制代码@Configurationpublic class RedisConfig { @Value("${}") private String host; @Value("${}") private int port; @Value("${}") private String password; @Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); (host); (port); ((password)); return new LettuceConnectionFactory(config); } @Bean public ThreadLocalredisConnectionThreadLocal(RedisConnectionFactory redisConnectionFactory) { return (() ->()); }}
使用 redisConnectionThreadLocal 是为了避免多次建立连接,很耗时间
总结
以上就是该功能的完整代码。使用代码记得修改uploadPath,避免代码找不到目录路径。在代码最后,可以使用mysql对整个文件计算校验和,将校验和结果和文件名、文件大小、文件类型存入数据库中,在下次大文件上传前先判断是否存在。若存在就不要上传避免占用空间。
作者:NameGo 链接:/post/7222910781921837093