Java-断点下载

x33g5p2x  于2022-05-28 转载在 Java  
字(9.5k)|赞(0)|评价(0)|浏览(368)

介绍

当下载一个很大的文件时,如果下载到一半暂停,如果继续下载呢?断点下载就是解决这个问题的。
具体原理:
利用indexedDb,将下载的数据存储到用户的本地中,这样用户就算是关电脑那么下次下载还是从上次的位置开始的

  1. 先去看看本地缓存中是否存在这个文件的分片数据,如果存在那么就接着上一个分片继续下载(起始位置)
  2. 下载前先去后端拿文件的大小,然后计算分多少次下载(n/(1024*1024*10)) (结束位置)
  3. 每次下载的数据放入一个Blob中,然后存储到本地indexedDB
  4. 当全部下载完毕后,将所有本地缓存的分片全部合并,然后给用户

有很多人说必须使用content-length、Accept-Ranges、Content-Range还有Range。 但是这只是一个前后端的约定而已,所有没必须非要遵守,只要你和后端约定好怎么拿取数据就行

难点都在前端:

  1. 怎么存储
  2. 怎么计算下载多少次
  3. 怎么获取最后下载的分片是什么
  4. 怎么判断下载完成了
  5. 怎么保证下载的分片都是完整的
  6. 下载后怎么合并然后给用户

效果

前端代码

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <h1>html5大文件断点下载传</h1>
  11. <div id="progressBar"></div>
  12. <Button id="but">下载</Button>
  13. <Button id="stop">暂停</Button>
  14. <script type="module">
  15. import FileSliceDownload from '/src/file/FileSliceDownload.js'
  16. let downloadUrl = "http://localhost:7003/fileslice/dwnloadsFIleSlice"
  17. let fileSizeUrl = "http://localhost:7003/fileslice/fIleSliceDownloadSize"
  18. let fileName = "Downloads.zip"
  19. let but = document.querySelector("#but")
  20. let stop = document.querySelector("#stop")
  21. let fileSliceDownload = new FileSliceDownload(downloadUrl, fileSizeUrl);
  22. fileSliceDownload.addProgress("#progressBar")
  23. but.addEventListener("click", function () {
  24. fileSliceDownload.startDownload(fileName)
  25. })
  26. stop.addEventListener("click", function () {
  27. fileSliceDownload.stop()
  28. })
  29. </script>
  30. </body>
  31. </html>
  1. class BlobUtls{
  2. // blob转文件并下载
  3. static downloadFileByBlob(blob, fileName = "file") {
  4. let blobUrl = window.URL.createObjectURL(blob)
  5. let link = document.createElement('a')
  6. link.download = fileName || 'defaultName'
  7. link.style.display = 'none'
  8. link.href = blobUrl
  9. // 触发点击
  10. document.body.appendChild(link)
  11. link.click()
  12. // 移除
  13. document.body.removeChild(link)
  14. }
  15. }
  16. export default BlobUtls;
  1. //导包要从项目全路径开始,也就是最顶部
  2. import BlobUtls from '/web-js/src/blob/BlobUtls.js'
  3. //导包
  4. class FileSliceDownload{
  5. #m1=1024*1024*10 //1mb 每次下载多少
  6. #db //indexedDB库对象
  7. #downloadUrl // 下载文件的地址
  8. #fileSizeUrl // 获取文件大小的url
  9. #fileSiez=0 //下载的文件大小
  10. #fileName // 下载的文件名称
  11. #databaseName="dbDownload"; //默认库名称
  12. #tableDadaName="tableDada" //用于存储数据的表
  13. #tableInfoName="tableInfo" //用于存储信息的表
  14. #fIleReadCount=0 //文件读取次数
  15. #fIleStartReadCount=0//文件起始的位置
  16. #barId = "bar"; //进度条id
  17. #progressId = "progress";//进度数值ID
  18. #percent=0 //百分比
  19. #checkDownloadInterval=null; //检测下载是否完成定时器
  20. #mergeInterval=null;//检测是否满足合并分片要求
  21. #stop=false; //是否结束
  22. //下载地址
  23. constructor(downloadUrl,fileSizeUrl) {
  24. this.check()
  25. this.#downloadUrl=downloadUrl;
  26. this.#fileSizeUrl=fileSizeUrl;
  27. }
  28. check(){
  29. let indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB ;
  30. if(!indexedDB){
  31. alert('不支持');
  32. }
  33. }
  34. //初始化
  35. #init(fileName){
  36. return new Promise((resolve,reject)=>{
  37. this.#fileName=fileName;
  38. this.#percent=0;
  39. this.#stop=false;
  40. const request = window.indexedDB.open(this.#databaseName, 1)
  41. request.onupgradeneeded = (e) => {
  42. const db = e.target.result
  43. if (!db.objectStoreNames.contains(this.#tableDadaName)) {
  44. db.createObjectStore(this.#tableDadaName, { keyPath: 'serial',autoIncrement:false })
  45. db.createObjectStore(this.#tableInfoName, { keyPath: 'primary',autoIncrement:false })
  46. }
  47. }
  48. request.onsuccess = e => {
  49. this.#db = e.target.result
  50. resolve()
  51. }
  52. })
  53. }
  54. #getFileSize(){
  55. return new Promise((resolve,reject)=>{
  56. let ref=this;
  57. var xhr = new XMLHttpRequest();
  58. //同步
  59. xhr.open("GET", this.#fileSizeUrl+"/"+this.#fileName,false)
  60. xhr.send()
  61. if (xhr.readyState === 4 && xhr.status === 200) {
  62. let ret = JSON.parse(xhr.response)
  63. if (ret.code === 20000) {
  64. ref.#fileSiez=ret.data
  65. }
  66. resolve()
  67. }
  68. })
  69. }
  70. #getTransactionDadaStore(){
  71. let transaction = this.#db.transaction([this.#tableDadaName], 'readwrite')
  72. let store = transaction.objectStore(this.#tableDadaName)
  73. return store;
  74. }
  75. #getTransactionInfoStore(){
  76. let transaction = this.#db.transaction([this.#tableInfoName], 'readwrite')
  77. let store = transaction.objectStore(this.#tableInfoName)
  78. return store;
  79. }
  80. #setBlob(begin,end,i,last){
  81. return new Promise((resolve,reject)=>{
  82. var xhr = new XMLHttpRequest();
  83. xhr.open("GET", this.#downloadUrl+"/"+this.#fileName+"/"+begin+"/"+end+"/"+last)
  84. xhr.responseType="blob" // 只支持异步,默认使用 text 作为默认值。
  85. xhr.send()
  86. xhr.onload = ()=> {
  87. if (xhr.status === 200) {
  88. let store= this.#getTransactionDadaStore()
  89. let obj={serial:i,blob:xhr.response}
  90. //添加分片到用户本地的库中
  91. store.add(obj)
  92. let store2= this.#getTransactionInfoStore()
  93. //记录下载了多少个分片了
  94. store2.put({primary:"count",count:i})
  95. //调整进度条
  96. let percent1= Math.ceil( (i/this.#fIleReadCount)*100)
  97. if(this.#percent<percent1){
  98. this.#percent=percent1;
  99. }
  100. this.#dynamicProgress()
  101. resolve()
  102. }
  103. }
  104. })
  105. }
  106. #mergeCallback(){
  107. // 读取全部字节到blob里,处理合并
  108. let arrayBlobs = [];
  109. let store1 = this.#getTransactionDadaStore()
  110. //按顺序找到全部的分片
  111. for (let i = 0; i <this.#fIleReadCount; i++) {
  112. let result= store1.get(IDBKeyRange.only(i))
  113. result.onsuccess=(data)=>{
  114. arrayBlobs.push(data.target.result.blob)
  115. }
  116. }
  117. //分片合并下载
  118. this.#mergeInterval= setInterval(()=> {
  119. if(arrayBlobs.length===this.#fIleReadCount){
  120. clearInterval(this.#mergeInterval);
  121. //多个Blob进行合并
  122. let fileBlob = new Blob(arrayBlobs);//合并后的数组转成⼀个Blob对象。
  123. BlobUtls.downloadFileByBlob(fileBlob,this.#fileName)
  124. //下载完毕后清除数据
  125. this. #clear()
  126. }
  127. },200)
  128. }
  129. #clear(){
  130. let store2 = this.#getTransactionDadaStore()
  131. let store3 = this.#getTransactionInfoStore()
  132. store2.clear() //清除本地全下载的数据
  133. store3.delete("count")//记录清除
  134. this.#fIleStartReadCount=0 //起始位置
  135. this.#db=null;
  136. this.#fileName=null;
  137. this.#fileSiez=0;
  138. this.#fIleReadCount=0 //文件读取次数
  139. this.#fIleStartReadCount=0//文件起始的位置
  140. }
  141. //检测是否有分片在本地
  142. #checkSliceDoesIsExist(){
  143. return new Promise((resolve,reject)=>{
  144. let store1 = this.#getTransactionInfoStore()
  145. let result= store1.get(IDBKeyRange.only("count"))
  146. result.onsuccess=(data)=>{
  147. let count= data.target.result?.count
  148. if(count){
  149. //防止因为网络的原因导致分片损坏,所以不要最后一个分片
  150. this.#fIleStartReadCount=count-1;
  151. }
  152. resolve();
  153. }
  154. })
  155. }
  156. /**
  157. * 样式可以进行修改
  158. * @param {*} progressId 需要将进度条添加到那个元素下面
  159. */
  160. addProgress (progressSelect) {
  161. let bar = document.createElement("div")
  162. bar.setAttribute("id", this.#barId);
  163. let num = document.createElement("div")
  164. num.setAttribute("id", this.#progressId);
  165. num.innerText = "0%"
  166. bar.appendChild(num);
  167. document.querySelector(progressSelect).appendChild(bar)
  168. }
  169. #dynamicProgress(){
  170. //调整进度
  171. let bar = document.getElementById(this.#barId)
  172. let progressEl = document.getElementById(this.#progressId)
  173. bar.style.width = this.#percent + '%';
  174. bar.style.backgroundColor = 'red';
  175. progressEl.innerHTML = this.#percent + '%'
  176. }
  177. stop(){
  178. this.#stop=true;
  179. }
  180. startDownload(fileName){
  181. //同步代码块
  182. ;(async ()=>{
  183. //初始化
  184. await this.#init(fileName)
  185. //自动调整分片,如果本地以下载了那么从上一次继续下载
  186. await this.#checkSliceDoesIsExist()
  187. //拿到文件的大小
  188. await this.#getFileSize()
  189. let begin=0; //开始读取的字节
  190. let end=this.#m1; // 结束读取的字节
  191. let last=false; //是否是最后一次读取
  192. this.#fIleReadCount= Math.ceil( this.#fileSiez/this.#m1)
  193. for (let i = this.#fIleStartReadCount; i < this.#fIleReadCount; i++) {
  194. if(this.#stop){
  195. return
  196. }
  197. begin=i*this.#m1;
  198. end=begin+this.#m1
  199. if(i===this.#fIleReadCount-1){
  200. last=true;
  201. }
  202. //添加分片
  203. await this.#setBlob(begin,end,i,last)
  204. }
  205. //定时检测存下载的分片数量是否够了
  206. this.#checkDownloadInterval= setInterval(()=> {
  207. let store = this.#getTransactionDadaStore()
  208. let result = store.count()
  209. result.onsuccess = (data) => {
  210. if (data.target.result === this.#fIleReadCount) {
  211. clearInterval(this.#checkDownloadInterval);
  212. //如果分片够了那么进行合并下载
  213. this.#mergeCallback()
  214. }
  215. }
  216. },200)
  217. })()
  218. }
  219. }
  220. export default FileSliceDownload;

后端代码

  1. package com.controller.commontools.fileDownload;
  2. import com.application.Result;
  3. import com.container.ArrayByteUtil;
  4. import com.file.FileWebDownLoad;
  5. import com.file.ReadWriteFileUtils;
  6. import com.path.ResourceFileUtil;
  7. import org.springframework.web.bind.annotation.*;
  8. import javax.servlet.http.HttpServletResponse;
  9. import java.io.BufferedOutputStream;
  10. import java.io.File;
  11. import java.io.OutputStream;
  12. import java.net.URLEncoder;
  13. @RestController
  14. @RequestMapping("/fileslice")
  15. public class FIleSliceDownloadController {
  16. private final String uploaddir="uploads"+ File.separator+"real"+File.separator;//实际文件目录
  17. // 获取文件的大小
  18. @GetMapping("/fIleSliceDownloadSize/{fileName}")
  19. public Result getFIleSliceDownloadSize(@PathVariable String fileName){
  20. String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName;
  21. File file= new File(absoluteFilePath);
  22. if(file.exists()&&file.isFile()){
  23. return Result.Ok(file.length(),Long.class);
  24. }
  25. return Result.Error();
  26. }
  27. /**
  28. * 分段下载文件
  29. * @param fileName 文件名称
  30. * @param begin 从文件什么位置开始读取
  31. * @param end 到什么位置结束
  32. * @param last 是否是最后一次读取
  33. * @param response
  34. */
  35. @GetMapping("/dwnloadsFIleSlice/{fileName}/{begin}/{end}/{last}")
  36. public void dwnloadsFIleSlice(@PathVariable String fileName, @PathVariable long begin, @PathVariable long end, @PathVariable boolean last, HttpServletResponse response){
  37. String absoluteFilePath = ResourceFileUtil.getAbsoluteFilePathAndCreate(uploaddir)+File.separator+fileName;
  38. File file= new File(absoluteFilePath);
  39. try(OutputStream toClient = new BufferedOutputStream(response.getOutputStream())) {
  40. long readSize = end - begin;
  41. //读取文件的指定字节
  42. byte[] bytes = new byte[(int)readSize];
  43. ReadWriteFileUtils.randomAccessFileRead(file.getAbsolutePath(),(int)begin,bytes);
  44. if(readSize<=file.length()||last){
  45. bytes=ArrayByteUtil.getActualBytes(bytes); //去掉多余的
  46. }
  47. response.setContentType("application/octet-stream");
  48. response.addHeader("Content-Length", String.valueOf(bytes.length));
  49. response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));
  50. toClient.write(bytes);
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. }

点赞 -收藏-关注-便于以后复习和收到最新内容有其他问题在评论区讨论-或者私信我-收到会在第一时间回复如有侵权,请私信联系我感谢,配合,希望我的努力对你有帮助^_^

相关文章

最新文章

更多