超大文件下载,支持断点续传
- 前言
- 一、背景
- 二、代码实现
- 1.后端
- 2.前端
- 创建工具类 download.js
- localforage 相关方法
- 3.效果展示
- localforage数据展示
- 下载进度展示
- 三、总结
前言
好记性不如烂笔头,站在岸上学不会游泳。这次分享一是为了记录下忙碌几天的成果,方便以后查阅;二是避免别人走弯路,给别人方便就是给自己方便。
一、背景
客户下载超大文件(1G以上)时,下载异常断开,分析了下有以下原因:
针对上述问题,找到一个合理的解决方案 分片下载,既可不超限亦不需要保持长连接,如果出现网络波动断开连接,亦可断点续传。多的不说,直接上干货。
二、代码实现
1.后端
创建下载工具类DownloadUtil.java
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; public class DownloadUtil { /** * 每个分片大小 50M */ private final static int BUFFER_SIZE = 1024 * 1024 * 50; public static void downloadByBlock(File file, HttpServletRequest request, HttpServletResponse response) throws IOException { // 获取文件名 String fileName = file.getName(); // 获取文件大小 long fileSize = file.length(); // 设置返回头 response.setContentType("application/octect-stream;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8")); response.setHeader("Accept-Ranges", "bytes"); // 判断是否包含Range String rangeHeader = request.getHeader("Range"); if (rangeHeader == null) { // 返回文件大小信息 response.setHeader("Content-Length", String.valueOf(fileSize)); response.setHeader("Buffer-Size", String.valueOf(BUFFER_SIZE)); response.setHeader("Access-Control-Expose-Headers", "Buffer-Size"); } else { // 分片下载 long start = 0; long end = fileSize - 1; String[] range = rangeHeader.split("=")[1].split("-"); if (range.length == 1) { start = Long.parseLong(range[0]); end = fileSize - 1; } else { start = Long.parseLong(range[0]); end = Long.parseLong(range[1]); } long contentLength = end - start + 1; // 返回头里存放每次读取的开始和结束字节 response.setHeader("Content-Length", String.valueOf(contentLength)); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize); InputStream in = new FileInputStream(file); OutputStream out = response.getOutputStream(); // 跳到第start字节 in.skip(start); byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = -1; long bytesWritten = 0; while ((bytesRead = in.read(buffer)) != -1) { if (bytesWritten + bytesRead > contentLength) { out.write(buffer, 0, (int) (contentLength - bytesWritten)); break; } else { out.write(buffer, 0, bytesRead); bytesWritten += bytesRead; } } in.close(); out.close(); } } }
后端代码借鉴其他博友 来源
2.前端
前端使用vue脚手架项目作为实例,也可用其他方式实现;
当前实例需要用到的技术:
localforage:localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验,最最重要的一点是通过它可以将数据持久化至本地磁盘
streamsaver:用于实现在Web浏览器中直接将大文件流式传输到用户设备的功能
创建工具类 download.js
import axios from 'axios'; import localForage from 'localforage' import streamSaver from 'streamsaver' import store from "@/store/index" // 基础数据库 存放其他文件的数据库实例名 const baseDataBaseName = "localforageInstance"; // 数据库进度数据主键 const progressKey = "progress"; /** * @description 分片下载 * @param fileName 文件名 * @param url 下载地址 * @param dataBaseName 数据库名,用于存放分片数据 每个文件唯一 * @param progress 下载进度 type:continue 续传 again重新下载 cancel取消 */ export async function downloadByBlock(fileName, url, dataBaseName, progress) { if(progress.type){ await controlFile(fileName, progress.type, dataBaseName) } // 创建基础数据库 用于保存文件信息 const baseDataBase = createInstance(baseDataBaseName); // 判断数据库中是否已存在该文件的下载任务 let isError = false; await getData(dataBaseName, baseDataBase, async(res) => { if(res && !progress.type){ alert("已存在下载进度!"); isError = true; } }) if(isError){ return; } // 创建数据库 用于存储每个文件的分片数据 const dataBase = createInstance(dataBaseName); // 获取文件大小 const response = await axios.head(url); // 文件大小 注意转为数字 const fileSize = +response.headers['content-length']; // 分片大小 注意转为数字 const chunkSize = +response.headers['buffer-size']; // 开始节点 let start = 0; // 结束节点 let end = chunkSize -1; if(fileSize
localforage 相关方法
/** * @description 创建数据库实例 每个实例对象都有独立的数据库 不会影响其他实例 * @param dataBasse 数据库名 */ function createInstance(dataBase){ return localForage.createInstance({ name: dataBase }); } /** * @description 保存数据 * @param name 键名 * @param value 数据 * @param storeName 仓库/数据库实例 */ async function saveData(name, value, storeName){ await storeName.setItem(name,value).then(()=>{ // success }).catch(err=>{ // err }) } /** * @description 获取数据 * @param name 键名 * @param storeName 仓库/数据库实例 * @param callback 回调函数 */ async function getData(name, storeName, callback){ await storeName.getItem(name).then((val)=>{ // success callback(val); }).catch(err=>{ // err callback(false); }) } /** * @description 移除数据 * @param name 键名 * @param storeName 仓库/数据库实例 */ async function removeData(name, storeName){ await storeName.removeItem(name).then(()=>{ // success }).catch(err=>{ // err }) } /** * @description 删除数据库实例 * @param dataBasse 数据库名 */ async function dropDataBase(dataBase){ await localForage.dropInstance({ name: dataBase }).then(()=>{ // success }).catch(err=>{ // err }) }
3.效果展示
localforage数据展示
F12打开管理器,application -> indexedDB
下载进度展示
三、总结
以上就是所有核心代码,当前功能涉及到下载、暂停、续传、取消,也可动态展示下载进度,因为是纯手工敲的,所以可能会有个别错误,见谅。其他细枝末节的代码就不在这陈述,再就是功能代码之前赶得比较急,也没怎么仔细整理,有些地方可能有更优的解法,欢迎大家评论区讨论