最近用到了Vue + Spring Boot來完成文件上傳的操做,踩了一些坑,對比了一些Vue的組件,發現了一個很好用的組件——Vue-Simple-Uploaderhtml
再說說爲何選用這個組件,對比vue-ant-design和element-ui的上傳組件,它能作到更多的事情,好比:前端
因爲需求中須要用到斷點續傳,因此選用了這個組件,下面我會從最基礎的上傳開始提及:vue
Vue代碼:java
<uploader :options="uploadOptions1" :autoStart="true" class="uploader-app" > <uploader-unsupport></uploader-unsupport> <uploader-drop> <uploader-btn style="margin-right:20px;" :attrs="attrs">選擇文件</uploader-btn> <uploader-btn :attrs="attrs" directory>選擇文件夾</uploader-btn> </uploader-drop> <uploader-list></uploader-list> </uploader>
該組件默認支持多文件上傳,這裏咱們從官方demo中粘貼過來這段代碼,而後在uploadOption1
中配置上傳的路徑便可,其中uploader-btn 中設置directory屬性便可選擇文件夾進行上傳。git
uploadOption1:github
uploadOptions1: { target: "//localhost:18080/api/upload/single",//上傳的接口 testChunks: false, //是否開啓服務器分片校驗 fileParameterName: "file",//默認的文件參數名 headers: {}, query() {}, categaryMap: { //用於限制上傳的類型 image: ["gif", "jpg", "jpeg", "png", "bmp"] } }
在後臺的接口的編寫,咱們爲了方便,定義了一個chunk類用於接收組件默認傳輸的一些後面方便分塊斷點續傳的參數:redis
Chunk類spring
@Data public class Chunk implements Serializable { private static final long serialVersionUID = 7073871700302406420L; private Long id; /** * 當前文件塊,從1開始 */ private Integer chunkNumber; /** * 分塊大小 */ private Long chunkSize; /** * 當前分塊大小 */ private Long currentChunkSize; /** * 總大小 */ private Long totalSize; /** * 文件標識 */ private String identifier; /** * 文件名 */ private String filename; /** * 相對路徑 */ private String relativePath; /** * 總塊數 */ private Integer totalChunks; /** * 文件類型 */ private String type; /** * 要上傳的文件 */ private MultipartFile file; }
在編寫接口的時候,咱們直接使用這個類做爲參數去接收vue-simple-uploader傳來的參數便可,注意這裏要使用POST來接收喲~sql
接口方法:數據庫
@PostMapping("single") public void singleUpload(Chunk chunk) { // 獲取傳來的文件 MultipartFile file = chunk.getFile(); // 獲取文件名 String filename = chunk.getFilename(); try { // 獲取文件的內容 byte[] bytes = file.getBytes(); // SINGLE_UPLOADER是我定義的一個路徑常量,這裏的意思是,若是不存在該目錄,則去建立 if (!Files.isWritable(Paths.get(SINGLE_FOLDER))) { Files.createDirectories(Paths.get(SINGLE_FOLDER)); } // 獲取上傳文件的路徑 Path path = Paths.get(SINGLE_FOLDER,filename); // 將字節寫入該文件 Files.write(path, bytes); } catch (IOException e) { e.printStackTrace(); } }
這裏須要注意一點,若是文件過大的話,Spring Boot後臺會報錯
org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.
這時須要在application.yml
中配置servlet的最大接收文件大小(默認大小是1MB和10MB)
spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB
下面咱們啓動項目,選擇須要上傳的文件就能夠看到效果了~ 是否是很方便~ 可是一樣的事情其他的組件基本上也能夠作到,之因此選擇這個,更多的是由於它能夠支持斷點分塊上傳,實現上傳過程當中斷網,再次聯網的話能夠從斷點位置開始繼續秒傳~下面咱們來看看斷點續傳是怎麼玩的。
先說一下分塊斷點續傳的大概原理,咱們在組件能夠配置分塊的大小,大於該值的文件會被分割成若干塊兒去上傳,同時將該分塊的chunkNumber
保存到數據庫(Mysql
or Redis
,這裏我選擇的是Redis
)
組件上傳的時候會攜帶一個identifier
的參數(這裏我採用的是默認的值,你也能夠經過生成md5的方式來從新賦值參數),將identifier
做爲Redis
的key,設置hashKey爲」chunkNumber「
,value是由每次上傳的chunkNumber
組成的一個Set
集合。
在將uploadOption
中的testChunk
的值設置爲true
以後,該組件會先發一個get請求,獲取到已經上傳的chunkNumber集合,而後在checkChunkUploadedByResponse
方法中判斷是否存在該片斷來進行跳過,發送post請求上傳分塊的文件。
每次上傳片斷的時候,service層返回當前的集合大小,並與參數中的totalChunks進行對比,若是發現相等,就返回一個狀態值,來控制前端發出merge
請求,將剛剛上傳的分塊合爲一個文件,至此文件的斷點分塊上傳就完成了。
<img style="width:600px;height:800px" src="https://img2018.cnblogs.com/blog/1528535/201905/1528535-20190513092325534-4536492.jpg" align=center />
下面是對應的代碼~
Vue代碼:
<uploader :options="uploadOptions2" :autoStart="true" :files="files" @file-added="onFileAdded2" @file-success="onFileSuccess2" @file-progress="onFileProgress2" @file-error="onFileError2" > <uploader-unsupport></uploader-unsupport> <uploader-drop> <uploader-btn :attrs="attrs">分塊上傳</uploader-btn> </uploader-drop> <uploader-list></uploader-list> </uploader>
校驗是否上傳過的代碼
uploadOptions2: { target: "//localhost:18080/api/upload/chunk", chunkSize: 1 * 1024 * 1024, testChunks: true, checkChunkUploadedByResponse: function(chunk, message) { let objMessage = JSON.parse(message); // 獲取當前的上傳塊的集合 let chunkNumbers = objMessage.chunkNumbers; // 判斷當前的塊是否被該集合包含,從而斷定是否須要跳過 return (chunkNumbers || []).indexOf(chunk.offset + 1) >= 0; }, headers: {}, query() {}, categaryMap: { image: ["gif", "jpg", "jpeg", "png", "bmp"], zip: ["zip"], document: ["csv"] } }
上傳後成功的處理,判斷狀態來進行merge操做
onFileSuccess2(rootFile, file, response, chunk) { let res = JSON.parse(response); // 後臺報錯 if (res.code == 1) { return; } // 須要合併 if (res.code == 205) { // 發送merge請求,參數爲identifier和filename,這個要注意須要和後臺的Chunk類中的參數名對應,不然會接收不到~ const formData = new FormData(); formData.append("identifier", file.uniqueIdentifier); formData.append("filename", file.name); merge(formData).then(response => {}); } },
斷定是否存在的代碼,注意這裏的是GET請求!!!
@GetMapping("chunk") public Map<String, Object> checkChunks(Chunk chunk) { return uploadService.checkChunkExits(chunk); } @Override public Map<String, Object> checkChunkExits(Chunk chunk) { Map<String, Object> res = new HashMap<>(); String identifier = chunk.getIdentifier(); if (redisDao.existsKey(identifier)) { Set<Integer> chunkNumbers = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList"); res.put("chunkNumbers",chunkNumbers); } return res; }
保存分塊,並保存數據到Redis的代碼。這裏的是POST請求!!!
@PostMapping("chunk") public Map<String, Object> saveChunk(Chunk chunk) { // 這裏的操做和保存單段落的基本是一致的~ MultipartFile file = chunk.getFile(); Integer chunkNumber = chunk.getChunkNumber(); String identifier = chunk.getIdentifier(); byte[] bytes; try { bytes = file.getBytes(); // 這裏的不一樣之處在於這裏進行了一個保存分塊時將文件名的按照-chunkNumber的進行保存 Path path = Paths.get(generatePath(CHUNK_FOLDER, chunk)); Files.write(path, bytes); } catch (IOException e) { e.printStackTrace(); } // 這裏進行的是保存到redis,並返回集合的大小的操做 Integer chunks = uploadService.saveChunk(chunkNumber, identifier); Map<String, Object> result = new HashMap<>(); // 若是集合的大小和totalChunks相等,斷定分塊已經上傳完畢,進行merge操做 if (chunks.equals(chunk.getTotalChunks())) { result.put("message","上傳成功!"); result.put("code", 205); } return result; } /** * 生成分塊的文件路徑 */ private static String generatePath(String uploadFolder, Chunk chunk) { StringBuilder sb = new StringBuilder(); // 拼接上傳的路徑 sb.append(uploadFolder).append(File.separator).append(chunk.getIdentifier()); //判斷uploadFolder/identifier 路徑是否存在,不存在則建立 if (!Files.isWritable(Paths.get(sb.toString()))) { try { Files.createDirectories(Paths.get(sb.toString())); } catch (IOException e) { log.error(e.getMessage(), e); } } // 返回以 - 隔離的分塊文件,後面跟的chunkNumber方便後面進行排序進行merge return sb.append(File.separator) .append(chunk.getFilename()) .append("-") .append(chunk.getChunkNumber()).toString(); } /** * 保存信息到Redis */ public Integer saveChunk(Integer chunkNumber, String identifier) { // 獲取目前的chunkList Set<Integer> oldChunkNumber = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList"); // 若是獲取爲空,則新建Set集合,並將當前分塊的chunkNumber加入後存到Redis if (Objects.isNull(oldChunkNumber)) { Set<Integer> newChunkNumber = new HashSet<>(); newChunkNumber.add(chunkNumber); redisDao.hmSet(identifier, "chunkNumberList", newChunkNumber); // 返回集合的大小 return newChunkNumber.size(); } else { // 若是不爲空,將當前分塊的chunkNumber加到當前的chunkList中,並存入Redis oldChunkNumber.add(chunkNumber); redisDao.hmSet(identifier, "chunkNumberList", oldChunkNumber); // 返回集合的大小 return oldChunkNumber.size(); } }
合併的後臺代碼:
@PostMapping("merge") public void mergeChunks(Chunk chunk) { String fileName = chunk.getFilename(); uploadService.mergeFile(fileName,CHUNK_FOLDER + File.separator + chunk.getIdentifier()); } @Override public void mergeFile(String fileName, String chunkFolder) { try { // 若是合併後的路徑不存在,則新建 if (!Files.isWritable(Paths.get(mergeFolder))) { Files.createDirectories(Paths.get(mergeFolder)); } // 合併的文件名 String target = mergeFolder + File.separator + fileName; // 建立文件 Files.createFile(Paths.get(target)); // 遍歷分塊的文件夾,並進行過濾和排序後以追加的方式寫入到合併後的文件 Files.list(Paths.get(chunkFolder)) //過濾帶有"-"的文件 .filter(path -> path.getFileName().toString().contains("-")) //按照從小到大進行排序 .sorted((o1, o2) -> { String p1 = o1.getFileName().toString(); String p2 = o2.getFileName().toString(); int i1 = p1.lastIndexOf("-"); int i2 = p2.lastIndexOf("-"); return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1))); }) .forEach(path -> { try { //以追加的形式寫入文件 Files.write(Paths.get(target), Files.readAllBytes(path), StandardOpenOption.APPEND); //合併後刪除該塊 Files.delete(path); } catch (IOException e) { e.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } }
至此,咱們的斷點續傳就完美結束了,完整的代碼我已經上傳到gayhub~,歡迎star fork pr~(後面還會把博文也上傳到gayhub喲~)
最近因爲家庭+工做忙昏了頭,鴿了這麼久非常抱歉,從這周開始恢復更新,同時本人在準備往大數據轉型,後續會出一系列的Java轉型大數據的學習筆記,包括Java基礎系列的深刻解讀和重寫,同時Spring Boot系列還會一直保持連載,不過可能不會每週都更,我會把目前使用Spring Boot中遇到的問題和坑寫一寫,謝謝一直支持個人粉絲們
原文出處:https://www.cnblogs.com/viyoung/p/10854863.html