Java實現斷點下載功能的示例代碼
介紹
當下載一個很大的文件時,如果下載到一半暫停,如果繼續下載呢?斷點下載就是解決這個問題的。
具體原理:
利用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(); } } }
以上就是Java實現斷點下載功能的示例代碼的詳細內容,更多關於Java斷點下載的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 一文詳解如何根據後端返回的url下載json文件
- vue導出excel文件流中文亂碼問題及解決
- vue+django實現下載文件的示例
- 十分鐘封裝一個好用的axios步驟示例
- JS實現單個或多個文件批量下載的方法詳解