Java断点下载
胡安民 人气:0介绍
当下载一个很大的文件时,如果下载到一半暂停,如果继续下载呢?断点下载就是解决这个问题的。
具体原理:
利用indexedDb,将下载的数据存储到用户的本地中,这样用户就算是关电脑那么下次下载还是从上次的位置开始的
- 先去看看本地缓存中是否存在这个文件的分片数据,如果存在那么就接着上一个分片继续下载(起始位置)
- 下载前先去后端拿文件的大小,然后计算分多少次下载(n/(1024*1024*10)) (结束位置)
- 每次下载的数据放入一个Blob中,然后存储到本地indexedDB
- 当全部下载完毕后,将所有本地缓存的分片全部合并,然后给用户
有很多人说必须使用content-length、Accept-Ranges、Content-Range还有Range。 但是这只是一个前后端的约定而已,所有没必须非要遵守,只要你和后端约定好怎么拿取数据就行
难点都在前端:
- 怎么存储
- 怎么计算下载多少次
- 怎么获取最后下载的分片是什么
- 怎么判断下载完成了
- 怎么保证下载的分片都是完整的
- 下载后怎么合并然后给用户
效果
前端代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>html5大文件断点下载传</h1> <div id="progressBar"></div> <Button id="but">下载</Button> <Button id="stop">暂停</Button> <script type="module"> import FileSliceDownload from '/src/file/FileSliceDownload.js' let downloadUrl = "http://localhost:7003/fileslice/dwnloadsFIleSlice" let fileSizeUrl = "http://localhost:7003/fileslice/fIleSliceDownloadSize" let fileName = "Downloads.zip" let but = document.querySelector("#but") let stop = document.querySelector("#stop") let fileSliceDownload = new FileSliceDownload(downloadUrl, fileSizeUrl); fileSliceDownload.addProgress("#progressBar") but.addEventListener("click", function () { fileSliceDownload.startDownload(fileName) }) stop.addEventListener("click", function () { fileSliceDownload.stop() }) </script> </body> </html>
class BlobUtls{ // blob转文件并下载 static downloadFileByBlob(blob, fileName = "file") { let blobUrl = window.URL.createObjectURL(blob) let link = document.createElement('a') link.download = fileName || 'defaultName' link.style.display = 'none' link.href = blobUrl // 触发点击 document.body.appendChild(link) link.click() // 移除 document.body.removeChild(link) } } export default BlobUtls;
//导包要从项目全路径开始,也就是最顶部 import BlobUtls from '/web-js/src/blob/BlobUtls.js' //导包 class FileSliceDownload{ #m1=1024*1024*10 //1mb 每次下载多少 #db //indexedDB库对象 #downloadUrl // 下载文件的地址 #fileSizeUrl // 获取文件大小的url #fileSiez=0 //下载的文件大小 #fileName // 下载的文件名称 #databaseName="dbDownload"; //默认库名称 #tableDadaName="tableDada" //用于存储数据的表 #tableInfoName="tableInfo" //用于存储信息的表 #fIleReadCount=0 //文件读取次数 #fIleStartReadCount=0//文件起始的位置 #barId = "bar"; //进度条id #progressId = "progress";//进度数值ID #percent=0 //百分比 #checkDownloadInterval=null; //检测下载是否完成定时器 #mergeInterval=null;//检测是否满足合并分片要求 #stop=false; //是否结束 //下载地址 constructor(downloadUrl,fileSizeUrl) { this.check() this.#downloadUrl=downloadUrl; this.#fileSizeUrl=fileSizeUrl; } check(){ let indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB ; if(!indexedDB){ alert('不支持'); } } //初始化 #init(fileName){ return new Promise((resolve,reject)=>{ this.#fileName=fileName; this.#percent=0; this.#stop=false; const request = window.indexedDB.open(this.#databaseName, 1) request.onupgradeneeded = (e) => { const db = e.target.result if (!db.objectStoreNames.contains(this.#tableDadaName)) { db.createObjectStore(this.#tableDadaName, { keyPath: 'serial',autoIncrement:false }) db.createObjectStore(this.#tableInfoName, { keyPath: 'primary',autoIncrement:false }) } } request.onsuccess = e => { this.#db = e.target.result resolve() } }) } #getFileSize(){ return new Promise((resolve,reject)=>{ let ref=this; var xhr = new XMLHttpRequest(); //同步 xhr.open("GET", this.#fileSizeUrl+"/"+this.#fileName,false) xhr.send() if (xhr.readyState === 4 && xhr.status === 200) { let ret = JSON.parse(xhr.response) if (ret.code === 20000) { ref.#fileSiez=ret.data } resolve() } }) } #getTransactionDadaStore(){ let transaction = this.#db.transaction([this.#tableDadaName], 'readwrite') let store = transaction.objectStore(this.#tableDadaName) return store; } #getTransactionInfoStore(){ let transaction = this.#db.transaction([this.#tableInfoName], 'readwrite') let store = transaction.objectStore(this.#tableInfoName) return store; } #setBlob(begin,end,i,last){ return new Promise((resolve,reject)=>{ var xhr = new XMLHttpRequest(); xhr.open("GET", this.#downloadUrl+"/"+this.#fileName+"/"+begin+"/"+end+"/"+last) xhr.responseType="blob" // 只支持异步,默认使用 text 作为默认值。 xhr.send() xhr.onload = ()=> { if (xhr.status === 200) { let store= this.#getTransactionDadaStore() let obj={serial:i,blob:xhr.response} //添加分片到用户本地的库中 store.add(obj) let store2= this.#getTransactionInfoStore() //记录下载了多少个分片了 store2.put({primary:"count",count:i}) //调整进度条 let percent1= Math.ceil( (i/this.#fIleReadCount)*100) if(this.#percent<percent1){ this.#percent=percent1; } this.#dynamicProgress() resolve() } } }) } #mergeCallback(){ // 读取全部字节到blob里,处理合并 let arrayBlobs = []; let store1 = this.#getTransactionDadaStore() //按顺序找到全部的分片 for (let i = 0; i <this.#fIleReadCount; i++) { let result= store1.get(IDBKeyRange.only(i)) result.onsuccess=(data)=>{ arrayBlobs.push(data.target.result.blob) } } //分片合并下载 this.#mergeInterval= setInterval(()=> { if(arrayBlobs.length===this.#fIleReadCount){ clearInterval(this.#mergeInterval); //多个Blob进行合并 let fileBlob = new Blob(arrayBlobs);//合并后的数组转成⼀个Blob对象。 BlobUtls.downloadFileByBlob(fileBlob,this.#fileName) //下载完毕后清除数据 this. #clear() } },200) } #clear(){ let store2 = this.#getTransactionDadaStore() let store3 = this.#getTransactionInfoStore() store2.clear() //清除本地全下载的数据 store3.delete("count")//记录清除 this.#fIleStartReadCount=0 //起始位置 this.#db=null; this.#fileName=null; this.#fileSiez=0; this.#fIleReadCount=0 //文件读取次数 this.#fIleStartReadCount=0//文件起始的位置 } //检测是否有分片在本地 #checkSliceDoesIsExist(){ return new Promise((resolve,reject)=>{ let store1 = this.#getTransactionInfoStore() let result= store1.get(IDBKeyRange.only("count")) result.onsuccess=(data)=>{ let count= data.target.result?.count if(count){ //防止因为网络的原因导致分片损坏,所以不要最后一个分片 this.#fIleStartReadCount=count-1; } resolve(); } }) } /** * 样式可以进行修改 * @param {*} progressId 需要将进度条添加到那个元素下面 */ addProgress (progressSelect) { let bar = document.createElement("div") bar.setAttribute("id", this.#barId); let num = document.createElement("div") num.setAttribute("id", this.#progressId); num.innerText = "0%" bar.appendChild(num); document.querySelector(progressSelect).appendChild(bar) } #dynamicProgress(){ //调整进度 let bar = document.getElementById(this.#barId) let progressEl = document.getElementById(this.#progressId) bar.style.width = this.#percent + '%'; bar.style.backgroundColor = 'red'; progressEl.innerHTML = this.#percent + '%' } stop(){ this.#stop=true; } startDownload(fileName){ //同步代码块 ;(async ()=>{ //初始化 await this.#init(fileName) //自动调整分片,如果本地以下载了那么从上一次继续下载 await this.#checkSliceDoesIsExist() //拿到文件的大小 await this.#getFileSize() let begin=0; //开始读取的字节 let end=this.#m1; // 结束读取的字节 let last=false; //是否是最后一次读取 this.#fIleReadCount= Math.ceil( this.#fileSiez/this.#m1) for (let i = this.#fIleStartReadCount; i < this.#fIleReadCount; i++) { if(this.#stop){ return } begin=i*this.#m1; end=begin+this.#m1 if(i===this.#fIleReadCount-1){ last=true; } //添加分片 await this.#setBlob(begin,end,i,last) } //定时检测存下载的分片数量是否够了 this.#checkDownloadInterval= setInterval(()=> { let store = this.#getTransactionDadaStore() let result = store.count() result.onsuccess = (data) => { if (data.target.result === this.#fIleReadCount) { clearInterval(this.#checkDownloadInterval); //如果分片够了那么进行合并下载 this.#mergeCallback() } } },200) })() } } export default FileSliceDownload;
后端代码
package com.controller.commontools.fileDownload; import com.application.Result; import com.container.ArrayByteUtil; import com.file.FileWebDownLoad; import com.file.ReadWriteFileUtils; import com.path.ResourceFileUtil; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.BufferedOutputStream; import java.io.File; import java.io.OutputStream; import java.net.URLEncoder; @RestController @RequestMapping("/fileslice") public class FIleSliceDownloadController { private final String uploaddir="uploads"+ File.separator+"real"+File.separator;//实际文件目录 // 获取文件的大小 @GetMapping("/fIleSliceDownloadSize/{fileName}") public Result getFIleSliceDownloadSize(@PathVariable String fileName){ String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName; File file= new File(absoluteFilePath); if(file.exists()&&file.isFile()){ return Result.Ok(file.length(),Long.class); } return Result.Error(); } /** * 分段下载文件 * @param fileName 文件名称 * @param begin 从文件什么位置开始读取 * @param end 到什么位置结束 * @param last 是否是最后一次读取 * @param response */ @GetMapping("/dwnloadsFIleSlice/{fileName}/{begin}/{end}/{last}") public void dwnloadsFIleSlice(@PathVariable String fileName, @PathVariable long begin, @PathVariable long end, @PathVariable boolean last, HttpServletResponse response){ String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName; File file= new File(absoluteFilePath); try(OutputStream toClient = new BufferedOutputStream(response.getOutputStream())) { long readSize = end - begin; //读取文件的指定字节 byte[] bytes = new byte[(int)readSize]; ReadWriteFileUtils.randomAccessFileRead(file.getAbsolutePath(),(int)begin,bytes); if(readSize<=file.length()||last){ bytes=ArrayByteUtil.getActualBytes(bytes); //去掉多余的 } response.setContentType("application/octet-stream"); response.addHeader("Content-Length", String.valueOf(bytes.length)); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8")); toClient.write(bytes); } catch (Exception e) { e.printStackTrace(); } } }
加载全部内容