最近用到了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