java實現(xiàn)文件分片上傳并且斷點續(xù)傳的示例代碼
一、簡單的分片上傳
針對第一個問題,如果文件過大,上傳到一半斷開了,若重新開始上傳的話,會很消耗時間,并且你也并不知道距離上次斷開時,已經(jīng)上傳到哪一部分了。因此我們應(yīng)該先對大文件進(jìn)行分片處理,防止上面提到的問題。
前端代碼:
<!DOCTYPE html> <html> <head> ? ? <title>文件上傳示例</title> ? ? <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> <form> ? ? <input type="file" id="fileInput" multiple> ? ? <button type="button" onclick="upload()" >上傳</button> </form> <script> ? ? function upload() { ? ? ? ? var fileInput = document.getElementById('fileInput'); ? ? ? ? var fileName = document.getElementById("fileInput").files[0].name; ? ? ? ? var files = fileInput.files; ? ? ? ? var chunkSize = 1024 * 10; // 每個塊的大小為10KB ? ? ? ? var totalChunks = Math.ceil(files[0].size / chunkSize); // 文件總塊數(shù) ? ? ? ? var currentChunk = 0; // 當(dāng)前塊數(shù) ? ? ? ? // 分片上傳文件 ? ? ? ? function uploadChunk() { ? ? ? ? ? ? var xhr = new XMLHttpRequest(); ? ? ? ? ? ? var formData = new FormData(); ? ? ? ? ? ? // 將當(dāng)前塊數(shù)和總塊數(shù)添加到formData中 ? ? ? ? ? ? formData.append('currentChunk', currentChunk); ? ? ? ? ? ? formData.append('totalChunks', totalChunks); ? ? ? ? ? ? formData.append('fileName',fileName); ? ? ? ? ? ? // 計算當(dāng)前塊在文件中的偏移量和長度 ? ? ? ? ? ? var start = currentChunk * chunkSize; ? ? ? ? ? ? var end = Math.min(files[0].size, start + chunkSize); ? ? ? ? ? ? var chunk = files[0].slice(start, end); ? ? ? ? ? ? // 添加當(dāng)前塊到formData中 ? ? ? ? ? ? formData.append('chunk', chunk); ? ? ? ? ? ? // 發(fā)送分片到后端 ? ? ? ? ? ? xhr.open('POST', '/file/upload'); ? ? ? ? ? ? xhr.send(formData); ? ? ? ? ? ? xhr.onload = function() { ? ? ? ? ? ? ? ? // 更新當(dāng)前塊數(shù) ? ? ? ? ? ? ? ? currentChunk++; ? ? ? ? ? ? ? ? // 如果還有未上傳的塊,則繼續(xù)上傳 ? ? ? ? ? ? ? ? if (currentChunk < totalChunks) { ? ? ? ? ? ? ? ? ? ? uploadChunk(); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? // 所有塊都上傳完畢,進(jìn)行文件合并 ? ? ? ? ? ? ? ? ? ? mergeChunks(fileName); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? // 合并所有分片 ? ? ? ? function mergeChunks() { ? ? ? ? ? ? var xhr = new XMLHttpRequest(); ? ? ? ? ? ? xhr.open("POST", "/file/merge", true); ? ? ? ? ? ? xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); ? ? ? ? ? ? xhr.onreadystatechange = function() { ? ? ? ? ? ? ? ? if (xhr.readyState === 4) { ? ? ? ? ? ? ? ? ? ? if (xhr.status === 200) { ? ? ? ? ? ? ? ? ? ? ? ? console.log("文件上傳完成:", xhr.responseText); ? ? ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? ? ? console.error(xhr.responseText); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? }; ? ? ? ? ? ? xhr.send("fileName=" + fileName); ? ? ? ? } ? ? ? ? // 開始上傳 ? ? ? ? uploadChunk(); ? ? } </script> </body> </html>
ps:以上代碼使用了html+js完成,請求是使用了xhr來發(fā)送請求。其中xhr.open的地址為自己本地的接口地址。由于平時測試并不需要真正上傳大型文件,所以每個分片的大小定義為10KB,以此模擬大文件上傳。
后端代碼:
@RestController @RequestMapping("/file") public class FileController { ? ? @Autowired ? ? private ResourceLoader resourceLoader; ? ? @Value("${my.config.savePath}") ? ? private String uploadPath; ? ? private Map<String, List<File>> chunksMap = new ConcurrentHashMap<>(); ? ? @PostMapping("/upload") ? ? public void upload(@RequestParam int currentChunk, @RequestParam int totalChunks, ? ? ? ? ? ? ? ? ? ? ? ?@RequestParam MultipartFile chunk,@RequestParam String fileName) throws IOException { ? ? ? ? // 將分片保存到臨時文件夾中 ? ? ? ? String chunkName = chunk.getOriginalFilename() + "." + currentChunk; ? ? ? ? File chunkFile = new File(uploadPath, chunkName); ? ? ? ? chunk.transferTo(chunkFile); ? ? ? ? // 記錄分片上傳狀態(tài) ? ? ? ? List<File> chunkList = chunksMap.get(fileName); ? ? ? ? if (chunkList == null) { ? ? ? ? ? ? chunkList = new ArrayList<>(totalChunks); ? ? ? ? ? ? chunksMap.put(fileName, chunkList); ? ? ? ? } ? ? ? ? chunkList.add(chunkFile); ? ? } ? ? @PostMapping("/merge") ? ? public String merge(@RequestParam String fileName) throws IOException { ? ? ? ? // 獲取所有分片,并按照分片的順序?qū)⑺鼈兒喜⒊梢粋€文件 ? ? ? ? List<File> chunkList = chunksMap.get(fileName); ? ? ? ? if (chunkList == null || chunkList.size() == 0) { ? ? ? ? ? ? throw new RuntimeException("分片不存在"); ? ? ? ? } ? ? ? ? File outputFile = new File(uploadPath, fileName); ? ? ? ? try (FileChannel outChannel = new FileOutputStream(outputFile).getChannel()) { ? ? ? ? ? ? for (int i = 0; i < chunkList.size(); i++) { ? ? ? ? ? ? ? ? try (FileChannel inChannel = new FileInputStream(chunkList.get(i)).getChannel()) { ? ? ? ? ? ? ? ? ? ? inChannel.transferTo(0, inChannel.size(), outChannel); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? chunkList.get(i).delete(); // 刪除分片 ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? chunksMap.remove(fileName); // 刪除記錄 ? ? ? ? // 獲取文件的訪問URL ? ? ? ? Resource resource =? ? ? ? ? ?? ??? ??? ?resourceLoader.getResource("file:" + uploadPath + fileName); //由于是本地文件,所以開頭是"file",如果是服務(wù)器,請改成自己服務(wù)器前綴 ? ? ? ? return resource.getURI().toString(); ? ? } }
ps: 使用一個map記錄上傳了哪些分片,這里將分片存在了本地的文件夾,等到分片都上傳完成后合并并刪除分片。用ConcurrentHashMap代替HashMap是因為它在多線程下是安全的。
以上只是一個簡單的文件上傳代碼,但是只要在這上面另做修改就可以解決上面提到的問題。
二、解決問題
1. 怎么避免大量的硬盤讀寫
上面代碼有一個弊端,就是將分片的內(nèi)容存在了本地的文件夾里。而且在合并的時候判斷上傳是否完全也是從文件夾讀取文件的。對磁盤的大量讀寫操作不僅速度慢,還會導(dǎo)致服務(wù)器崩潰,因此下面代碼使用了redis來存儲分片信息,避免對磁盤過多讀寫。(你也可以使用mysql或者其他中間件來存儲信息,由于讀寫盡量不要在mysql,所以我使用了redis)。
2.目標(biāo)文件過大,如果在上傳過程中斷開了怎么辦
使用redis來存儲分片內(nèi)容,當(dāng)斷開后,文件信息還是存儲在redis中,用戶再次上傳時,檢測redis是否有該分片的內(nèi)容,如果有則跳過。
3. 前端頁面上傳的文件數(shù)據(jù)與原文件數(shù)據(jù)不一致該如何發(fā)現(xiàn)
前端在調(diào)用上傳接口時,先計算文件的校驗和,然后將文件和校驗和一并傳給后端,后端對文件再計算一次校驗和,兩個校驗和進(jìn)行對比,如果相等,則說明數(shù)據(jù)一致,如果不一致則報錯,讓前端重新上傳該片段。
js計算校驗和代碼:
// 計算文件的 SHA-256 校驗和 ? ? function calculateHash(fileChunk) { ? ? ? ? return new Promise((resolve, reject) => { ? ? ? ? ? ? const blob = new Blob([fileChunk]); ? ? ? ? ? ? const reader = new FileReader(); ? ? ? ? ? ? reader.readAsArrayBuffer(blob); ? ? ? ? ? ? reader.onload = () => { ? ? ? ? ? ? ? ? const arrayBuffer = reader.result; ? ? ? ? ? ? ? ? const crypto = window.crypto || window.msCrypto; ? ? ? ? ? ? ? ? const digest = crypto.subtle.digest("SHA-256", arrayBuffer); ? ? ? ? ? ? ? ? digest.then(hash => { ? ? ? ? ? ? ? ? ? ? const hashArray = Array.from(new Uint8Array(hash)); ? ? ? ? ? ? ? ? ? ? const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); ? ? ? ? ? ? ? ? ? ? resolve(hashHex); ? ? ? ? ? ? ? ? }); ? ? ? ? ? ? }; ? ? ? ? ? ? reader.onerror = () => { ? ? ? ? ? ? ? ? reject(new Error('Failed to calculate hash')); ? ? ? ? ? ? }; ? ? ? ? }); ? ? }
? ? public static String calculateHash(byte[] fileChunk) throws Exception { ? ? ? ? MessageDigest md = MessageDigest.getInstance("SHA-256"); ? ? ? ? md.update(fileChunk); ? ? ? ? byte[] hash = md.digest(); ? ? ? ? ByteBuffer byteBuffer = ByteBuffer.wrap(hash); ? ? ? ? StringBuilder hexString = new StringBuilder(); ? ? ? ? while (byteBuffer.hasRemaining()) { ? ? ? ? ? ? hexString.append(String.format("%02x", byteBuffer.get())); ? ? ? ? } ? ? ? ? return hexString.toString(); ? ? }
注意點:
這里前端和后端計算校驗和的算法一定要是一致的,不然得不到相同的結(jié)果。
在前端中使用了crypto對文件進(jìn)行計算,需要引入相關(guān)的js。
你可以使用script引入也可以直接下載js
<script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script>
4. 上傳過程中如果斷開了應(yīng)該如何判斷哪些分片沒有上傳
對redis檢測哪個分片的下標(biāo)不存在,若不存在則存入list,最后將list返回給前端
boolean allChunksUploaded = true; List<Integer> missingChunkIndexes = new ArrayList<>(); for (int i = 0; i < hashMap.size(); i++) { if (!hashMap.containsKey(String.valueOf(i))) { allChunksUploaded = false; missingChunkIndexes.add(i); } } if (!allChunksUploaded) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(missingChunkIndexes); }
三、完整代碼
1、引入依賴
<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
lettuce是一個Redis客戶端,你也可以不引入,直接使用redisTemplat就行了
2、前端代碼
<!DOCTYPE html> <html lang="en"> <head> ? ? <meta charset="UTF-8"> ? ? <title>File Upload Demo</title> </head> <body> <input type="file" id="fileInput" multiple> <button type="button" onclick="uploadFile()" >上傳</button> <div id="progressBar"></div> <script src="https://cdn.bootcss.com/crypto-js/3.1.9-1/crypto-js.min.js"></script> <script> ? ? var fileId = ""; ? ? var fileName = null; ? ? var file; ? ? const chunkSize = 1024 * 10; // 每個分片的大小10KB ? ? async function uploadFile() { ? ? ? ? var fileInput = document.getElementById('fileInput'); ? ? ? ? file = fileInput.files[0]; ? ? ? ? fileName = document.getElementById("fileInput").files[0].name; ? ? ? ? // 分片上傳文件 ? ? ? ? const chunks = Math.ceil(file.size / chunkSize); ? ? ? ? for (let i = 0; i < chunks; i++) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? await uploadChunk(file, i); ? ? ? ? ? ? } catch (error) { ? ? ? ? ? ? ? ? console.error('Failed to upload chunk', i, error); ? ? ? ? ? ? ? ? // 如果上傳失敗,則嘗試恢復(fù)上傳 ? ? ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ? ? await uploadChunk(file, i); ? ? ? ? ? ? ? ? } catch (error) { ? ? ? ? ? ? ? ? ? ? console.error('Failed to resume upload', i, error); ? ? ? ? ? ? ? ? ? ? return; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? // 合并文件 ? ? ? ? try { ? ? ? ? ? ? const fileUrl = await mergeFile(); ? ? ? ? ? ? console.log('File URL:', fileUrl); ? ? ? ? } catch (error) { ? ? ? ? ? ? console.error('Failed to merge file', error); ? ? ? ? } ? ? } ? ? function uploadChunk(file, chunkIndex) { ? ? ? ? return new Promise((resolve, reject) => { ? ? ? ? ? ? let fileTemp = file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize); ? ? ? ? ? ? var myPromise = calculateHash(fileTemp); ? ? ? ? ? ? myPromise.then(result =>{ ? ? ? ? ? ? ? ? const formData = new FormData(); ? ? ? ? ? ? ? ? formData.append('chunk',fileTemp); ? ? ? ? ? ? ? ? formData.append('chunkIndex', chunkIndex); ? ? ? ? ? ? ? ? formData.append('chunkChecksum', result); ? ? ? ? ? ? ? ? formData.append('chunkSize', chunkSize); ? ? ? ? ? ? ? ? formData.append('fileId',fileId); ? ? ? ? ? ? ? ? const xhr = new XMLHttpRequest(); ? ? ? ? ? ? ? ? xhr.open('POST', '/hospital/file2/upload', true); ? ? ? ? ? ? ? ? xhr.onload = () => { ? ? ? ? ? ? ? ? ? ? if (xhr.status === 200) { ? ? ? ? ? ? ? ? ? ? ? ? resolve(xhr.response); ? ? ? ? ? ? ? ? ? ? ? ? fileId = xhr.responseText ? ? ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? ? ? reject(xhr.statusText); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? }; ? ? ? ? ? ? ? ? xhr.onerror = () => { ? ? ? ? ? ? ? ? ? ? reject(xhr.statusText); ? ? ? ? ? ? ? ? }; ? ? ? ? ? ? ? ? xhr.send(formData); ? ? ? ? ? ? }) ? ? ? ? }); ? ? } ? ? function mergeFile() { ? ? ? ? return new Promise((resolve, reject) => { ? ? ? ? ? ? const xhr = new XMLHttpRequest(); ? ? ? ? ? ? const formData = new FormData(); ? ? ? ? ? ? formData.append('fileId',fileId); ? ? ? ? ? ? formData.append('fileName',fileName); ? ? ? ? ? ? xhr.open('POST', '/hospital/file2/merge', true); ? ? ? ? ? ? xhr.onload = () => { ? ? ? ? ? ? ? ? if (xhr.status === 200) { ? ? ? ? ? ? ? ? ? ? resolve(xhr.response); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? reject(xhr.statusText); ? ? ? ? ? ? ? ? ? ? resume(xhr.response.replace(/\[|]/g,'').split(',')); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? }; ? ? ? ? ? ? xhr.onerror = () => { ? ? ? ? ? ? ? ? reject(xhr.statusText); ? ? ? ? ? ? }; ? ? ? ? ? ? xhr.send(formData); ? ? ? ? }); ? ? } ? ? async function resume(list){ ? ? ? ? for (let i = 0; i < list.length; i++) { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? await uploadChunk(file, i); ? ? ? ? ? ? } catch (error) { ? ? ? ? ? ? ? ? console.error('Failed to upload chunk', i, error); ? ? ? ? ? ? ? ? // 如果上傳失敗,則嘗試恢復(fù)上傳 ? ? ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ? ? await uploadChunk(file, i); ? ? ? ? ? ? ? ? } catch (error) { ? ? ? ? ? ? ? ? ? ? console.error('Failed to resume upload', i, error); ? ? ? ? ? ? ? ? ? ? return; ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? // 合并文件 ? ? ? ? try { ? ? ? ? ? ? const fileUrl = await mergeFile(); ? ? ? ? ? ? console.log('File URL:', fileUrl); ? ? ? ? } catch (error) { ? ? ? ? ? ? console.error('Failed to merge file', error); ? ? ? ? } ? ? } ? ? // 計算文件的 SHA-256 校驗和 ? ? function calculateHash(fileChunk) { ? ? ? ? return new Promise((resolve, reject) => { ? ? ? ? ? ? const blob = new Blob([fileChunk]); ? ? ? ? ? ? const reader = new FileReader(); ? ? ? ? ? ? reader.readAsArrayBuffer(blob); ? ? ? ? ? ? reader.onload = () => { ? ? ? ? ? ? ? ? const arrayBuffer = reader.result; ? ? ? ? ? ? ? ? const crypto = window.crypto || window.msCrypto; ? ? ? ? ? ? ? ? const digest = crypto.subtle.digest("SHA-256", arrayBuffer); ? ? ? ? ? ? ? ? digest.then(hash => { ? ? ? ? ? ? ? ? ? ? const hashArray = Array.from(new Uint8Array(hash)); ? ? ? ? ? ? ? ? ? ? const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); ? ? ? ? ? ? ? ? ? ? resolve(hashHex); ? ? ? ? ? ? ? ? }); ? ? ? ? ? ? }; ? ? ? ? ? ? reader.onerror = () => { ? ? ? ? ? ? ? ? reject(new Error('Failed to calculate hash')); ? ? ? ? ? ? }; ? ? ? ? }); ? ? } </script> </body> </html>
3、后端接口代碼
@RestController @RequestMapping("/file2") public class File2Controller { ? ? private static final String FILE_UPLOAD_PREFIX = "file_upload:"; ? ? @Autowired ? ? private ResourceLoader resourceLoader; ? ? @Value("${my.config.savePath}") ? ? private String uploadPath; ? ? @Autowired ? ? private ThreadLocal<RedisConnection> redisConnectionThreadLocal; // ? ?@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 (StringUtils.isBlank(fileId) || StringUtils.isEmpty(fileId)) { ? ? ? ? ? ? fileId = UUID.randomUUID().toString(); ? ? ? ? } ? ? ? ? String key = FILE_UPLOAD_PREFIX + fileId; ? ? ? ? byte[] chunkBytes = chunk.getBytes(); ? ? ? ? String actualChecksum = calculateHash(chunkBytes); ? ? ? ? if (!chunkChecksum.equals(actualChecksum)) { ? ? ? ? ? ? return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Chunk checksum does not match"); ? ? ? ? } // ? ? ? ?if(!redisTemplate.opsForHash().hasKey(key,String.valueOf(chunkIndex))) { // ? ? ? ? ? ?redisTemplate.opsForHash().put(key, String.valueOf(chunkIndex), chunkBytes); // ? ? ? ?} ? ? ? ? RedisConnection connection = redisConnectionThreadLocal.get(); ? ? ? ? Boolean flag = connection.hExists(key.getBytes(), String.valueOf(chunkIndex).getBytes()); ? ? ? ? if (flag==null || flag == false) { ? ? ? ? ? ? connection.hSet(key.getBytes(), String.valueOf(chunkIndex).getBytes(), chunkBytes); ? ? ? ? } ? ? ? ? return ResponseEntity.ok(fileId); ? ? } ? ? public static String calculateHash(byte[] fileChunk) throws Exception { ? ? ? ? MessageDigest md = MessageDigest.getInstance("SHA-256"); ? ? ? ? md.update(fileChunk); ? ? ? ? byte[] hash = md.digest(); ? ? ? ? ByteBuffer byteBuffer = ByteBuffer.wrap(hash); ? ? ? ? StringBuilder hexString = new StringBuilder(); ? ? ? ? while (byteBuffer.hasRemaining()) { ? ? ? ? ? ? hexString.append(String.format("%02x", byteBuffer.get())); ? ? ? ? } ? ? ? ? return hexString.toString(); ? ? } ? ? @PostMapping("/merge") ? ? public ResponseEntity<?> mergeFile(@RequestParam("fileId") String fileId, @RequestParam("fileName") String fileName) throws IOException { ? ? ? ? String key = FILE_UPLOAD_PREFIX + fileId; ? ? ? ? RedisConnection connection = redisConnectionThreadLocal.get(); ? ? ? ? try { ? ? ? ? ? ? Map<byte[], byte[]> chunkMap = connection.hGetAll(key.getBytes()); // ? ? ? ? ? ?Map chunkMap = redisTemplate.opsForHash().entries(key); ? ? ? ? ? ? if (chunkMap.isEmpty()) { ? ? ? ? ? ? ? ? return ResponseEntity.status(HttpStatus.NOT_FOUND).body("File not found"); ? ? ? ? ? ? } ? ? ? ? ? ? Map<String,byte[]> hashMap = new HashMap<>(); ? ? ? ? ? ? for(Map.Entry<byte[],byte[]> entry :chunkMap.entrySet()){ ? ? ? ? ? ? ? ? hashMap.put((new String(entry.getKey())),entry.getValue()); ? ? ? ? ? ? } ? ? ? ? ? ? // 檢測是否所有分片都上傳了 ? ? ? ? ? ? boolean allChunksUploaded = true; ? ? ? ? ? ? List<Integer> missingChunkIndexes = new ArrayList<>(); ? ? ? ? ? ? for (int i = 0; i < hashMap.size(); i++) { ? ? ? ? ? ? ? ? if (!hashMap.containsKey(String.valueOf(i))) { ? ? ? ? ? ? ? ? ? ? allChunksUploaded = false; ? ? ? ? ? ? ? ? ? ? missingChunkIndexes.add(i); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? if (!allChunksUploaded) { ? ? ? ? ? ? ? ? return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(missingChunkIndexes); ? ? ? ? ? ? } ? ? ? ? ? ? File outputFile = new File(uploadPath, fileName); ? ? ? ? ? ? boolean flag = mergeChunks(hashMap, outputFile); ? ? ? ? ? ? Resource resource = resourceLoader.getResource("file:" + uploadPath + fileName); ? ? ? ? ? ? if (flag == true) { ? ? ? ? ? ? ? ? connection.del(key.getBytes()); // ? ? ? ? ? ? ? ?redisTemplate.delete(key); ? ? ? ? ? ? ? ? return ResponseEntity.ok().body(resource.getURI().toString()); ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? return ResponseEntity.status(555).build(); ? ? ? ? ? ? } ? ? ? ? } catch (Exception e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); ? ? ? ? } ? ? } ? ? private boolean mergeChunks(Map<String, byte[]> chunkMap, File destFile) { ? ? ? ? try (FileOutputStream outputStream = new FileOutputStream(destFile)) { ? ? ? ? ? ? // 將分片按照順序合并 ? ? ? ? ? ? for (int i = 0; i < chunkMap.size(); i++) { ? ? ? ? ? ? ? ? byte[] chunkBytes = chunkMap.get(String.valueOf(i)); ? ? ? ? ? ? ? ? outputStream.write(chunkBytes); ? ? ? ? ? ? } ? ? ? ? ? ? return true; ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? return false; ? ? ? ? } ? ? } }
4、redis配置
@Configuration public class RedisConfig { ? ? @Value("${spring.redis.host}") ? ? private String host; ? ? @Value("${spring.redis.port}") ? ? private int port; ? ? @Value("${spring.redis.password}") ? ? private String password; ? ? @Bean ? ? public RedisConnectionFactory redisConnectionFactory() { ? ? ? ? RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); ? ? ? ? config.setHostName(host); ? ? ? ? config.setPort(port); ? ? ? ? config.setPassword(RedisPassword.of(password)); ? ? ? ? return new LettuceConnectionFactory(config); ? ? } ? ? @Bean ? ? public ThreadLocal<RedisConnection> redisConnectionThreadLocal(RedisConnectionFactory redisConnectionFactory) { ? ? ? ? return ThreadLocal.withInitial(() -> redisConnectionFactory.getConnection()); ? ? } }
使用 redisConnectionThreadLocal 是為了避免多次建立連接,很耗時間
總結(jié)
以上就是該功能的完整代碼。使用代碼記得修改uploadPath,避免代碼找不到目錄路徑。在代碼最后,可以使用mysql對整個文件計算校驗和,將校驗和結(jié)果和文件名、文件大小、文件類型存入數(shù)據(jù)庫中,在下次大文件上傳前先判斷是否存在。若存在就不要上傳避免占用空間。
到此這篇關(guān)于java實現(xiàn)文件分片上傳并且斷點續(xù)傳的示例代碼的文章就介紹到這了,更多相關(guān)java 文件分片上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot 整合druid數(shù)據(jù)庫密碼加密功能的實現(xiàn)代碼
這篇文章主要介紹了springboot 整合druid數(shù)據(jù)庫密碼加密功能的實現(xiàn)代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01MyBatis?handleResultSet結(jié)果集解析過程示例
這篇文章主要為大家介紹了MyBatis?handleResultSet結(jié)果集解析過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02springboot3.X版本集成mybatis遇到的問題及解決
在將SpringBoot3.X版本與MyBatis集成時,直接參考基于SpringBoot2.X的配置方法會導(dǎo)致各種報錯,尤其是無法注入mapper的bean問題,這主要是因為SpringBoot3.X版本需要搭配MyBatis3.0.3及以上版本才能正常工作,通過更新maven配置至MyBatis3.0.3版本,可以解決這一問題2024-09-09Logback與Log4j2日志框架性能對比與調(diào)優(yōu)方式
這篇文章主要介紹了Logback與Log4j2日志框架性能對比與調(diào)優(yōu)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12