赞
踩
后端:SpringBoot+JDK17
前端:JavaScript+spark+md5.min.js
一、依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>uploadDemo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>uploadDemo</name> <description>uploadDemo</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
二、业务代码
@RestController
public class UploadController{
//上传路径
public static final String UPLOAD_PATH = "D:\\upload";
@RequestMapping("/upload")
public ResponseEntity<Map<String,String>> upload(@RequestParam MultipartFile file) throws IOException {
File dstFile = new File(UPLOAD_PATH,String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));
file.transferTo(dstFile);
return ResponseEntity.ok(Map.of("path",dstFile.getAbsolutePath()));
}
}
三、前端显示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>upload</title> </head> <body> upload <form enctype="multipart/form-data"> <input type="file" name="fileInput" id="fileInput"> <input type="button" value="上传" onclick="uploadFile()"> </form> 上传结果 <span id="uploadResult"></span> <script> var uploadResult=document.getElementById("uploadResult") function uploadFile() { var fileInput = document.getElementById('fileInput'); var file = fileInput.files[0]; if (!file) return; // 没有选择文件 var xhr = new XMLHttpRequest(); // 处理上传进度 xhr.upload.onprogress = function(event) { var percent = 100 * event.loaded / event.total; uploadResult.innerHTML='上传进度:' + percent + '%'; }; // 当上传完成时调用 xhr.onload = function() { if (xhr.status === 200) { uploadResult.innerHTML='上传成功'+ xhr.responseText; } } xhr.onerror = function() { uploadResult.innerHTML='上传失败'; } // 发送请求 xhr.open('POST', '/upload', true); var formData = new FormData(); formData.append('file', file); xhr.send(formData); } </script> </body> </html>
【注意事项】
在上传过程会报文件大小限制错误,主要有三个参数需要设置:
org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (46302921) exceeds the configured maximum (10485760)
需在springboot的application.properties 或者application.yml中添加:
max-file-size
max-request-size
默认大小分别是1M和10M,因此需要重新设定
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
如果使用nginx报 413状态码413 Request Entity Too Large,Nginx默认最大上传1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置项:client_max_body_size 1024m
一、前端分片
计算文件MD5值用了spark-md5这个库
因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>分片上传</title> <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script> </head> <body> 分片上传 <form enctype="multipart/form-data"> <input type="file" name="fileInput" id="fileInput"> <input type="button" value="计算文件MD5" onclick="calculateFileMD5()"> <input type="button" value="上传" onclick="uploadFile()"> <input type="button" value="检测文件完整性" onclick="checkFile()"> </form> <p> 文件MD5: <span id="fileMd5"></span> </p> <p> 上传结果: <span id="uploadResult"></span> </p> <p> 检测文件完整性: <span id="checkFileRes"></span> </p> <script> //每片的大小 var chunkSize = 1 * 1024 * 1024; var uploadResult = document.getElementById("uploadResult") var fileMd5Span = document.getElementById("fileMd5") var checkFileRes = document.getElementById("checkFileRes") var fileMd5; function calculateFileMD5(){ var fileInput = document.getElementById('fileInput'); var file = fileInput.files[0]; getFileMd5(file).then((md5) => { console.info(md5) fileMd5=md5; fileMd5Span.innerHTML=md5; }) } function uploadFile() { var fileInput = document.getElementById('fileInput'); var file = fileInput.files[0]; if (!file) return; if (!fileMd5) return; //获取到文件 let fileArr = this.sliceFile(file); //保存文件名称 let fileName = file.name; fileArr.forEach((e, i) => { //创建formdata对象 let data = new FormData(); data.append("totalNumber", fileArr.length) data.append("chunkSize", chunkSize) data.append("chunkNumber", i) data.append("md5", fileMd5) data.append("file", new File([e],fileName)); upload(data); }) } /** * 计算文件md5值 */ function getFileMd5(file) { return new Promise((resolve, reject) => { let fileReader = new FileReader() fileReader.onload = function (event) { let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result) resolve(fileMd5) } fileReader.readAsArrayBuffer(file) }) } function upload(data) { var xhr = new XMLHttpRequest(); // 当上传完成时调用 xhr.onload = function () { if (xhr.status === 200) { uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ; } } xhr.onerror = function () { uploadResult.innerHTML = '上传失败'; } // 发送请求 xhr.open('POST', '/uploadBig', true); xhr.send(data); } function checkFile() { var xhr = new XMLHttpRequest(); // 当上传完成时调用 xhr.onload = function () { if (xhr.status === 200) { checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText; } } xhr.onerror = function () { checkFileRes.innerHTML = '检测文件完整性失败'; } // 发送请求 xhr.open('POST', '/checkFile', true); let data = new FormData(); data.append("md5", fileMd5) xhr.send(data); } function sliceFile(file) { const chunks = []; let start = 0; let end; while (start < file.size) { end = Math.min(start + chunkSize, file.size); chunks.push(file.slice(start, end)); start = end; } return chunks; } </script> </body> </html>
二、后端
两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5
FileChannel fileChannel = randomAccessFile.getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkNumber * chunkSize, fileData.length);
mappedByteBuffer.put(fileData);
@RestController public class UploadController { public static final String UPLOAD_PATH = "D:\\upload\\"; /** * @param chunkSize 每个分片大小 * @param chunkNumber 当前分片 * @param md5 文件总MD5 * @param file 当前分片文件数据 * @return * @throws IOException */ @RequestMapping("/uploadBig") public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException { //文件存放位置 String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename())); //上传分片信息存放位置 String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5); //第一次创建分片记录文件 //创建目录 File dir = new File(dstFile).getParentFile(); if (!dir.exists()) { dir.mkdir(); //所有分片状态设置为0 byte[] bytes = new byte[totalNumber]; Files.write(Path.of(confFile), bytes); } //随机分片写入文件 try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw"); RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw"); InputStream inputStream = file.getInputStream()) { //定位到该分片的偏移量(可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题) randomAccessFile.seek(chunkNumber * chunkSize); //写入该分片数据大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的 randomAccessFile.write(inputStream.readAllBytes()); //定位到当前分片状态位置 randomAccessConfFile.seek(chunkNumber); //设置当前分片上传状态为1 randomAccessConfFile.write(1); } return ResponseEntity.ok(Map.of("path", dstFile)); } /** * 获取文件分片状态,检测文件MD5合法性 * * @param md5 * @return * @throws Exception */ @RequestMapping("/checkFile") public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception { String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5); Path path = Path.of(uploadPath); //MD5目录不存在文件从未上传过 if (!Files.exists(path.getParent())) { return ResponseEntity.ok(Map.of("msg", "文件未上传")); } //判断文件是否上传成功 StringBuilder stringBuilder = new StringBuilder(); byte[] bytes = Files.readAllBytes(path); for (byte b : bytes) { stringBuilder.append(String.valueOf(b)); } //所有分片上传完成计算文件MD5 if (!stringBuilder.toString().contains("0")) { File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5)); File[] files = file.listFiles(); String filePath = ""; for (File f : files) { //计算文件MD5是否相等 if (!f.getName().contains("conf")) { filePath = f.getAbsolutePath(); try (InputStream inputStream = new FileInputStream(f)) { String md5pwd = DigestUtils.md5DigestAsHex(inputStream); if (!md5pwd.equalsIgnoreCase(md5)) { return ResponseEntity.ok(Map.of("msg", "文件上传失败")); } } } } return ResponseEntity.ok(Map.of("path", filePath)); } else { //文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传 return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString())); } } }
用/checkFile接口,文件里如果有未完成上传的分片,接口返回chunks字段对就的位置值为0,前端将未上传的分片继续上传,完成后再调用/checkFile就完成了断点续传
只要修改前端代码流程就好了,比如张三上传了一个文件,然后李四又上传了同样内容的文件,同一文件的MD5值可以认为是一样的(虽然会存在不同文件的MD5一样,不过概率很小,可以认为MD5一样文件就是一样)李四调用/checkFile接口后,后端直接返回了李四上传的文件路径,李四就完成了秒传。大部分云盘秒传的思路应该也是这样,只不过计算文件HASH算法更为复杂,返回给用户文件路径也更为安全,要防止被别人算出文件路径了
===============================================
在Vue.js 中,如果网络请求使用 axios ,并且使用了 ElementUI 库,那么一般来说,文件上传有两种不同的实现方案:
一、后端提供一个文件上传的接口
SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/"); @PostMapping("/import") public RespBean importData(MultipartFile file,HttpServletRequest req) throws IOException{ //路径+当前时间 String format = sdf.format(new Date()); String realPath = req.getServletContext().getRealPath("/upload")+format; File folder = new File(realPath); if(!folder.exists()){ folder.mkdirs();//如果文件夹为空,则创建一个文件夹 } //上传的文件按照日期进行归类,使用 UUID 给文件重命名 String oldName = file.getPriginalFilename(); String newName = UUID.randomUUID().toString()+oldName.substring(oldName.lastIndexOf(".")); file.transferTo(new File(folder,newName)); String url = req.getSchem()+"://"+req.getServerName()+":"+req.getServerPort()+"/upload"+format+newName; System.out.println(url); return RespBean.ok("上传成功!"); }
方式一:Ajax上传
首先提供一个文件导入 input 组件,再来一个导入按钮,在导入按钮的事件中来完成导入的逻辑
<input type="file" ref="myfile">
<el-button @click="importData" type="success" size="mini" icon="el-icon-upload2">导入数据</el-button>
文件上传注意两点,1. 请求方法为 post,2. 设置 Content-Type 为 multipart/form-data
importData(){ //首先利用 Vue 中的 $refs 查找到存放文件的元素 let myfile = this.$refs.myfile; //ype 为 file 的 input 元素内部有一个 files 数组,里边存放了所有选择的 file,由于文件上传时,文件可以多选, //因此这里拿到的 files 对象是一个数组 let files = myfile.files; let file = files[0]; //构造一个 FormData ,用来存放上传的数据,FormData 不可以像 Java 中的 StringBuffer 使用链式配置 var formData = new FormData(); formData.append("file",file); this.uploadFileRequest("/system/basic/jl/import",formData).then(resp=>{ if(resp){ console.log(resp); } }) }
封装的上传方法
export const uploadFileRequest = (url,params) => {
return axios({
methos:'post',
url:'${base}${url}',
data:params,
headers:{
'Content-Type':'multipart/form-data'
}
});
}
方式二:Upload组件上传
如果使用 Upload ,则需要引入 ElementUI,所以一般建议,如果使用了 ElementUI 做 UI 控件的话,则可以考虑使用 Upload 组件来实现文件上传,如果没有使用 ElementUI 的话,则不建议使用 Upload 组件,至于其他的 UI 控件,各自都有自己的文件上传组件,具体使用可以参考各自文档。
<el-upload style="display:inline"
:show-file-list="false" <!--show-file-list 表示是否展示上传文件列表,默认为true,这里设置为不展示-->
:on-success="onSuccess"<!--on-success 和 on-error 分别表示上传成功和失败时候的回调,可以在这两个方法中,给用户一个相应的提示,如果有进度条,还需要在这两个方法中关闭进度条-->
:on-error="onError"
:before-upload="beforeUpload" <!--before-upload 表示上传之前的回调,可以在该方法中,做一些准备工作,例如展示一个进度条给用户 -->
action="/system/basic/jl/import"> <!--action 指文件上传地址-->
<!--上传按钮的点击状态和图标都设置为变量 ,在文件上传过程中,修改上传按钮的点击状态为不可点击,同时修改图标为一个正在加载的图标 loading-->
<!--上传的文本也设为变量,默认上传 button 的文本是 数据导入 ,当开始上传后,将找个 button 上的文本修改为 正在导入-->
<el-button size="mini" type="success":disabled="!enabledUploadBtn":icon="uploadBtnIcon">
{{btnText}}
</el-button>
</el-upload>
onSuccess(response,file,fileList){
this.enabledUploadBtn = true;
this.uploadBtnIcon = 'el-icon-upload2';
this.btnTest = '数据导入';
},
onError(err,file,fileList){
this.enabledUploadBtn = true;
this.uploadBtnIcon = 'el-icon-upload2';
this.btnTest = '数据导入';
},
beforeUpload(file){
this.enabledUploadBtn = false;
this.uploadBtnIcon = 'el-icon-loading';
this.btnTest = '正在导入';
}
在文件开始上传时,修改上传按钮为不可点击,同时修改上传按钮的图标和文本。
文件上传成功或者失败时,修改上传按钮的状态为可以点击,同时恢复上传按钮的图标和文本。
两种对比:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。