主要思想是前端做大文件的MD5计算和大文件的切分,后台做小文件的合并和MD5的校验,直接上代码。
前端VUE:
<template><el-uploadref="upload"class="upload-demo"action="#":http-request="handleUpload":on-change="handleFileChange":before-upload="handleBeforeUpload":auto-upload="true":multiple="false":file-list="fileList"><el-buttonslot="trigger"size="small"type="primary">选取文件</el-button><!-- <el-button style="margin-left: 10px;" size="small" type="success" @click="handleUpload">上传文件</el-button> --></el-upload></template><script>importSparkMD5from'spark-md5'importaxiosfrom"axios"; import { getKey,completeUpload} from"@/api/fileupload"; exportdefault { data() { return { percent: 0, fileList: [], chunkSize: 6*1024*1024, // 分片大小为2MB } }, methods: { handleFileChange(file) { this.fileList= [file] }, handleBeforeUpload(file) { // 根据文件大小判断是否需要分片if (file.size>this.chunkSize) { file.chunked=truefile.percent=0 } returntrue }, handleUpload(params) { constfile=params.fileletstartTime=newDate().getTime() if (file.chunked) { constloading=this.$loading({ lock: true, text: '大文件正在计算M5,请稍后', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); constreaderTotal=newFileReader() readerTotal.readAsArrayBuffer(file) readerTotal.onload= () => { constsparkTotal=newSparkMD5.ArrayBuffer() sparkTotal.append(readerTotal.result) constmd5=sparkTotal.end() letquery= {name:md5} getKey(query).then(resKey=>{ consttotalChunks=Math.ceil(file.size/this.chunkSize) letcurrentChunk=0constconfig= { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: (progressEvent) => { this.percent=Math.round((currentChunk+1) /totalChunks*100) loading.setText('大文件正在分片上传,请稍后'+this.percent+'%') }, }; constreader=newFileReader() constspark=newSparkMD5.ArrayBuffer() constuploadChunk= () => { conststart=currentChunk*this.chunkSizeconstend=Math.min(file.size, start+this.chunkSize) reader.readAsArrayBuffer(file.slice(start, end)) reader.onload= () => { spark.append(reader.result) constchunk=newBlob([reader.result]) chunk.file=filechunk.currentChunk=currentChunkchunk.totalChunks=totalChunkschunk.sparkMD5=spark.end() constformData=newFormData() formData.append('chunk', chunk) formData.append('filename', file.name) formData.append('totalChunks', totalChunks) formData.append('key', resKey.data) formData.append('currentChunk', currentChunk) axios.post(process.env.VUE_APP_BASE_API+"app/fileupload/file/upload/chunk", formData, config) .then(response=> { if (currentChunk<totalChunks-1) { currentChunk++uploadChunk() } else { letdata= {key:resKey.data,fileName:file.name} completeUpload(data).then(res=>{ this.fileList= [] loading.close(); this.percent=0this.$message.success('上传成功') letendTime=newDate().getTime() console.log('Chunk Cost:'+endTime-startTime) }) } }) .catch(error=> { this.$message.error(error.message) }) } } uploadChunk() }); } } else { constloading=this.$loading({ lock: true, text: '正在上传,请稍后', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); constself=this; constfile=params.fileconstformData=newFormData(); formData.append(self.upload_name, file); constconfig= { headers: { "Content-Type": "multipart/form-data" }, onUploadProgress: (progressEvent) => { this.percent= ((progressEvent.loaded/progressEvent.total) *100) |0; loading.setText('正在上传,请稍后'+this.percent+'%') }, }; axios .post( process.env.VUE_APP_BASE_API+"app/fileupload/file/upload", formData, config ) .then((res) => { loading.close(); this.$message.success('上传成功') this.percent=0letendTime=newDate().getTime() console.log('Common Cost:'+endTime-startTime) }) .catch((err) => { console.log(err); }); } } } } </script>
api/fileupload.js
importrequestfrom'@/utils/request'exportfunctiongetKey(query) { returnrequest({ url: '/app/fileupload/getKey', method: 'get', params: query }) } exportfunctioncompleteUpload(data) { returnrequest({ url: '/app/fileupload/completeUpload', method: 'post', data }) }
utils/request.js
importaxiosfrom'axios'import { MessageBox, Message } from'element-ui'importstorefrom'@/store'import { getToken } from'@/utils/auth'importrouterfrom'@/router'axios.defaults.headers.common['Content-Type'] ='application/json;charset=UTF-8'axios.defaults.headers.common['X-Requested-With'] ='XMLHttpRequest'axios.defaults.withCredentials=true// create an axios instanceconstservice=axios.create({ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url// withCredentials: true, // send cookies when cross-domain requeststimeout: 50000// request timeout}) // request interceptorservice.interceptors.request.use( config=> { // do something before request is sentif (store.getters.token) { // let each request carry token// ['X-Token'] is a custom headers key// please modify it according to the actual situationconfig.headers['X-Token'] =getToken() } returnconfig }, error=> { // do something with request errorconsole.log(error) // for debugreturnPromise.reject(error) } ) // response interceptorservice.interceptors.response.use( /** * If you want to get http information such as headers or status * Please return response => response *//** * Determine the request status by custom code * Here is just an example * You can also judge the status by HTTP Status Code */response=> { constres=response.dataif(res.ret===200||res.ret===404){ returnres; } // if the custom code is not 20000, it is judged as an error.if (res.code!==20000) { // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;// if (res.code === 50008 || res.code === 50012 || res.code === 50014) {if (res.code===50014) { // to re-loginMessageBox.confirm('您已经登出了,您可以取消继续待在此页面或者重新登录', '登出确认', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { store.dispatch('user/resetToken').then(() => { if (res.data===2) { router.push(`/login`) } else { router.push(`/member/login`) } // location.reload()// const type = this.$store.getters.type// if (type === 1) {// this.$router.push(`/member/login?redirect=${this.$route.fullPath}`)// } else {// this.$router.push(`/login?redirect=${this.$route.fullPath}`)// } }) }) } else { Message({ message: res.message||'Error', type: 'error', duration: 5*1000 }) } returnPromise.reject(newError(res.message||'Error')) } else { returnres } }, error=> { Message({ message: '网络异常', type: 'error', duration: 5*1000 }) returnPromise.reject(error) } ) exportdefaultservice
自行安装axios,spark-md5库,下面就是后端代码
importjakarta.servlet.http.HttpServletRequest; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.web.bind.annotation.*; importorg.springframework.web.multipart.MultipartFile; importorg.springframework.web.multipart.MultipartHttpServletRequest; importjava.io.*; importjava.nio.channels.FileChannel; importjava.nio.file.Files; importjava.nio.file.Paths; importjava.nio.file.Path; importjava.text.SimpleDateFormat; importjava.util.*; importjava.util.stream.Collectors; "/fileupload") (publicclassFileUploadController { "${file.dir}") (privateStringfiledir; privateMessageUtilsmessageUtils; value="/getKey") (publicResponseDatagetKey(Stringname) throwsException{ ResponseDatarest=newResponseData(); rest.setRet(20000); rest.setMsg("success"); Stringkey=UUID.randomUUID().toString(); rest.setData(key); AppConstants.fileKeyMd5Map.put(key,name); AppConstants.fileKeyChunkMap.put(key,newArrayList<>()); returnrest; } value="/completeUpload") (publicResponseDatacompleteUpload(FileCompleteUploadDtofileCompleteUploadDto,HttpServletRequestrequest) throwsException{ ResponseDatarest=newResponseData(); rest.setRet(20000); rest.setMsg("success"); Stringkey=fileCompleteUploadDto.getKey(); Stringmd5=AppConstants.fileKeyMd5Map.get(key); //按照顺序合并文件List<FileChunkObjDto>fileChunkObjDtoList=AppConstants.fileKeyChunkMap.get(key); List<FileChunkObjDto>newfileChunkObjDtoList=fileChunkObjDtoList.stream().sorted(Comparator.comparing(FileChunkObjDto::getIndex)).collect(Collectors.toList()); StringfileExt=fileCompleteUploadDto.getFileName().substring(fileCompleteUploadDto.getFileName().lastIndexOf(".") +1).toLowerCase(); StringlocalHost=UrlUtils.getLocalRealIp(); Stringport=String.valueOf(request.getLocalPort()); SimpleDateFormatdf=newSimpleDateFormat("yyyyMMddHHmmss"); StringnewFileName=df.format(newDate()) +"_"+localHost+"_"+port+"_"+newRandom().nextInt(1000) +"."+fileExt; FilenewFile=newFile(tifiledir+newFileName); FileChannelresultfileChannel=newFileOutputStream(newFile).getChannel(); for(FileChunkObjDtofileChunkObjDto:newfileChunkObjDtoList){ FileChannelfileChannel=newFileInputStream(tifiledir+fileChunkObjDto.getFileName()).getChannel(); resultfileChannel.transferFrom(fileChannel,resultfileChannel.size(),fileChannel.size()); fileChannel.close(); } resultfileChannel.close(); //校验md5// System.out.println("a md5:"+md5);// System.out.println("b md5:"+ FileUtils.getMD5(tifiledir+newFileName));for(FileChunkObjDtofileChunkObjDto:newfileChunkObjDtoList){ try { PathsourcePath=Paths.get(tifiledir+fileChunkObjDto.getFileName()); Files.delete(sourcePath); }catch (Exceptione){ } } //删除keyAppConstants.fileKeyMd5Map.remove(key); AppConstants.fileKeyChunkMap.remove(key); returnrest; } value="/file/upload/chunk") (publicResponseDatauploadChunkFile(Map<String,String>map, HttpServletRequestrequest) throwsException{ ResponseDatarest=newResponseData(); rest.setRet(20000); rest.setMsg("success"); MultipartHttpServletRequestmultipartRequest= (MultipartHttpServletRequest) request; Map<String, MultipartFile>fileMap=multipartRequest.getFileMap(); Stringkey=map.get("key"); StringcurrentChunk=map.get("currentChunk"); List<FileChunkObjDto>fileChunkObjDtoList=AppConstants.fileKeyChunkMap.get(key); FileChunkObjDtofileChunkObjDto=newFileChunkObjDto(); fileChunkObjDto.setIndex(Integer.parseInt(currentChunk)); StringctxPath=filedir; //创建文件夹Filefile=newFile(ctxPath); if (!file.exists()) { file.mkdirs(); } StringlocalHost=UrlUtils.getLocalRealIp(); Stringport=String.valueOf(request.getLocalPort()); SimpleDateFormatdf=newSimpleDateFormat("yyyyMMddHHmmss"); for (Map.Entry<String, MultipartFile>entity : fileMap.entrySet()) { // 上传文件名MultipartFilemf=entity.getValue(); StringnewFileName=df.format(newDate()) +"_"+localHost+"_"+port+"_"+newRandom().nextInt(1000); fileChunkObjDto.setFileName(newFileName); fileChunkObjDtoList.add(fileChunkObjDto); StringfilePath=ctxPath+newFileName; FileuploadFile=newFile(filePath); InputStreamin=null; FileOutputStreamout=null; try { byteb[] =newbyte[1024*1024]; inti=0; in=mf.getInputStream(); out=newFileOutputStream(uploadFile); while ((i=in.read(b)) !=-1) { //写出读入的字节数组,每次从0到所读入的位置,不会多写out.write(b, 0, i); } } catch (IOExceptione) { e.printStackTrace(); } finally { try { //关闭流if (in!=null) { in.close(); } if (out!=null) { out.close(); } } catch (IOExceptione) { thrownewRuntimeException(e); } } } returnrest; } value="/file/upload") (publicResponseDatauploadFile(Map<String,String>map, HttpServletRequestrequest) throwsException{ ResponseDatarest=newResponseData(); rest.setRet(20000); rest.setMsg("success"); MultipartHttpServletRequestmultipartRequest= (MultipartHttpServletRequest) request; Map<String, MultipartFile>fileMap=multipartRequest.getFileMap(); StringctxPath=filedir; //创建文件夹Filefile=newFile(ctxPath); if (!file.exists()) { file.mkdirs(); } StringfileName=null; StringlocalHost=UrlUtils.getLocalRealIp(); Stringport=String.valueOf(request.getLocalPort()); SimpleDateFormatdf=newSimpleDateFormat("yyyyMMddHHmmss"); for (Map.Entry<String, MultipartFile>entity : fileMap.entrySet()) { // 上传文件名MultipartFilemf=entity.getValue(); fileName=mf.getOriginalFilename(); StringfileExt=fileName.substring(fileName.lastIndexOf(".") +1).toLowerCase(); StringnewFileName=df.format(newDate()) +"_"+localHost+"_"+port+"_"+newRandom().nextInt(1000) +"."+fileExt; StringfilePath=ctxPath+newFileName; FileuploadFile=newFile(filePath); InputStreamin=null; FileOutputStreamout=null; try { byteb[] =newbyte[1024*1024]; inti=0; in=mf.getInputStream(); out=newFileOutputStream(uploadFile); while ((i=in.read(b)) !=-1) { //写出读入的字节数组,每次从0到所读入的位置,不会多写out.write(b, 0, i); } } catch (IOExceptione) { e.printStackTrace(); } finally { try { //关闭流if (in!=null) { in.close(); } if (out!=null) { out.close(); } } catch (IOExceptione) { thrownewRuntimeException(e); } } } returnrest; } }
AppConstants.java
publicstaticfinalMap<String,String>fileKeyMd5Map=newHashMap<>(){}; publicstaticfinalMap<String, List<FileChunkObjDto>>fileKeyChunkMap=newHashMap<>(){};
UrlUtils.java
publicclassUrlUtils { publicstaticintvalidateUrl(Stringurl){ URLu=null; HttpURLConnectionurlconn=null; intstate=0; do{ try { u=newURL(url); urlconn= (HttpURLConnection) u.openConnection(); //System.out.println(urlconn.);urlconn.setConnectTimeout(2000); state=urlconn.getResponseCode(); }catch(SocketTimeoutExceptione){ state=2; e.printStackTrace(); }catch (Exceptione) { e.printStackTrace(); break; } }while(false); returnstate; } publicstaticStringgetLocalRealIp(){ InetAddressaddr=null; try { addr=InetAddress.getLocalHost(); } catch (UnknownHostExceptione) { e.printStackTrace(); } byte[] ipAddr=addr.getAddress(); //String ipAddrStr = "";StringBuilderipAddrStr=newStringBuilder(); for (inti=0; i<ipAddr.length; i++) { if (i>0) { ipAddrStr.append("."); } ipAddrStr.append(ipAddr[i] &0xFF); } //System.out.println(ipAddrStr);returnipAddrStr.toString(); } /** * 获取当前网络ip * @param request * @return */publicstaticStringgetIpAddr(HttpServletRequestrequest){ StringipAddress=request.getHeader("x-forwarded-for"); if(ipAddress==null||ipAddress.length() ==0||"unknown".equalsIgnoreCase(ipAddress)) { ipAddress=request.getHeader("Proxy-Client-IP"); } if(ipAddress==null||ipAddress.length() ==0||"unknown".equalsIgnoreCase(ipAddress)) { ipAddress=request.getHeader("WL-Proxy-Client-IP"); } if(ipAddress==null||ipAddress.length() ==0||"unknown".equalsIgnoreCase(ipAddress)) { ipAddress=request.getRemoteAddr(); if(ipAddress.equals("127.0.0.1") ||ipAddress.equals("0:0:0:0:0:0:0:1")){ //根据网卡取本机配置的IP InetAddressinet=null; try { inet=InetAddress.getLocalHost(); } catch (UnknownHostExceptione) { e.printStackTrace(); } ipAddress=inet.getHostAddress(); } } //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if(ipAddress!=null&&ipAddress.length()>15){ //"***.***.***.***".length() = 15 if(ipAddress.indexOf(",")>0){ ipAddress=ipAddress.substring(0,ipAddress.indexOf(",")); } } returnipAddress; } publicstaticvoidmain(String []args){ System.out.println(getLocalRealIp()); } }
ResponseData.java
importcom.alibaba.fastjson.JSON; importcom.github.pagehelper.PageInfo; importorg.springframework.util.Assert; importjava.util.Iterator; importjava.util.LinkedHashMap; importjava.util.Map; /*** 目前vue前台解析返回格式是这样的*/publicclassResponseDataextendsLinkedHashMap<String, Object> { //设置返回标准publicstaticfinalStringRET="code";//返回代码publicstaticfinalStringMSG="message";//返回信息publicstaticfinalStringDATA="data";//其他内容publicstaticfinalStringTIMESTAMP="timestamp"; publicResponseData() { this.setRet(200).setMsg("请求成功").setTimestamp(System.currentTimeMillis()); } publicResponseData(intret, Stringmsg) { this.setRet(ret).setMsg(msg).setTimestamp(System.currentTimeMillis()); } publicResponseData(intret, Stringmsg, ObjectattributeValue) { this.setRet(ret).setMsg(msg).setTimestamp(System.currentTimeMillis()).setData(attributeValue); } publicResponseData(intret, Stringmsg, PageInfopageData) { this.setRet(ret).setMsg(msg).setTimestamp(System.currentTimeMillis()).setPageData(pageData); } publicResponseData(intret, Stringmsg, StringattributeName, ObjectattributeValue) { this.setRet(ret).setMsg(msg).setTimestamp(System.currentTimeMillis()).addAttribute(attributeName, attributeValue); } publicintgetRet() { return (Integer)super.get(RET); } publicResponseDatasetRet(intret) { this.put(RET, ret); returnthis; } publiclonggetTimestamp() { return (Long)super.get(TIMESTAMP); } publicResponseDatasetTimestamp(longtimestamp) { this.put(TIMESTAMP, timestamp); returnthis; } publicStringgetMsg() { return (String)super.get(MSG); } publicResponseDatasetMsg(Stringmsg) { this.put(MSG, msg); returnthis; } publicResponseDatasetData(ObjectattributeValue) { this.put(DATA, attributeValue); returnthis; } publicResponseDatasetPageData(PageInfopageinfo){ PageDatapageData=newPageData(pageinfo); this.put(DATA,pageData); returnthis; } publicResponseDataaddAttribute(StringattributeName, ObjectattributeValue) { Assert.notNull(attributeName, "Model attribute name must not be null"); this.put(attributeName, attributeValue); returnthis; } publicResponseDataaddAllAttributes(Map<String, ?>attributes) { if (attributes!=null) { this.putAll(attributes); } returnthis; } publicResponseDatamergeAttributes(Map<String, ?>attributes) { if (attributes!=null) { Iteratorvar2=attributes.entrySet().iterator(); while(var2.hasNext()) { Map.Entry<String,?>entry= (Map.Entry<String,?>)var2.next(); if (!this.containsKey(entry.getKey())) { this.put(entry.getKey(), entry.getValue()); } } } returnthis; } publicbooleancontainsAttribute(StringattributeName) { returnthis.containsKey(attributeName); } publicStringtoJsonString() { returnJSON.toJSONString(this); } }
FileChunkObjDto.java和FileCompleteUploadDto.java
publicclassFileChunkObjDto { privateIntegerindex; privateStringfileName; } publicclassFileCompleteUploadDto { privateStringkey; privateStringfileName; }
md5的比较校验可以自行做业务判断,缓存的处理可以使用redis做终端断点续传的长期存储改造。