赞
踩
Blob & File
spark-md5根据文件内容生成hash
大文件分片上传(批量并发,手动上传)vue组件封装-form组件
vue上传大文件/视频前后端(java)代码
springboot+vue自定义上传图片及视频
SpringBoot + VUE实现前台上传文件获取实时进度( 使用commons-fileupload设置上传监听器的实现)
springboot:实现文件上传下载实时进度条功能【附带源码】
vue + element-ui + springboot 实现文件下载进度条展现功能(里面有取消下载的功能实现和下载进度条)
vue+SpringBoot实现大文件分块上传、断点续传和秒传
SpringBoot+Vue.js前后端分离实现大文件分块上传github地址
Spring Boot+VUE分片上传大文件到OSS服务器解决方案
fastloader gitee地址
细说分片上传与极速秒传(SpringBoot+Vue实现)
【java】java实现大文件的分片上传与下载(springboot+vue3) 这个不错,后面可以详细看下,代码地址:https://gitee.com/zzhua195/big-file-upload
【视频流上传播放功能】前后端分离用springboot-vue简单实现视频流上传和播放功能【详细注释版本,包含前后端代码】
(前后端分离)SpringBoot+Vue实现视频播放
从文件加密到到视频文件进度条播放揭秘
Java后端实现视频分段渐进式播放
Spring Boot 大文件上传(断点上传)、服务端分片下载、客户端分片下载(断点下载)
SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放
前台
后台
前端控制台
后端控制台
<template> <div> 选择文件: <input type="file" ref="fileInputRef" @change="selectFile" multiple> <!-- 使用multiple属性,可选择多个文件 --> <br/> <img v-if="imgUrl" :src="imgUrl" alt="" style="width:54px;height:54px;"> <el-button v-if="imgUrl" type="primary" @click="uploadFile">上传</el-button> <hr/> </div> </template> <script> import axiosInstance from '@/utils/request.js' import axios from 'axios' export default { name: 'File', data() { return { imgUrl:'' } }, methods: { selectFile() { let file = this.$refs['fileInputRef'].files[0] console.log(file) // 上传前, 可以预览该图片 let blobUrl = URL.createObjectURL(file) this.imgUrl = blobUrl }, uploadFile() { // 因为可能选择多个文件, 所以这里是个数组 let file = this.$refs['fileInputRef'].files[0] let formData = new FormData() formData.append('mfile', file) // 必须和后端的参数名相同。(我们看到了, 其实就是把blob文件给了formData的一个key) formData.append("type", 'avatar') // 可以有下面2种方式, 来上传文件 /* axiosInstance .post('http://127.0.0.1:8083/file/uploadFile',formData, {headers: {'a':'b'}}) .then(res => { console.log('响应回来: ',res); }) */ axiosInstance({ // 这种传参方式, 在axios的index.d.ts中可以看到 url:'http://127.0.0.1:8083/file/uploadFile', method:'post', data: formData, // 直接将FormData作为data传输 headers: { 'a':'b' // 可携带自定义响应头 } }).then(res => { console.log('响应回来: ',res); }) console.log(this.$refs['fileInputRef'].value); // C:\fakepath\cfa86972-07a1-4527-8b8a-1991715ebbfe.png // 上传完文件后, 将value置为空, 以避免下次选择同样的图片而不会触发input file的change事件。 // (注意清空value后,将不能再从input file中获取file,而原先的file仍然能够使用) this.$refs['fileInputRef'].value = '' } } } </script> <style> </style>
@PostMapping("uploadFile") public Object uploadFile(@RequestPart("mfile")MultipartFile multipartFile,@RequestPart("type") String type) throws IOException { System.out.println(multipartFile.getClass()); System.out.println(type); // 源文件名 String originalFilename = multipartFile.getOriginalFilename(); // 内容类型 String contentType = multipartFile.getContentType(); // 文件是否为空(无内容) boolean empty = multipartFile.isEmpty(); // 文件大小 long size = multipartFile.getSize(); // 文件的字节数据 byte[] bytes = multipartFile.getBytes(); // 获取文件的字节输入流 InputStream inputStream = multipartFile.getInputStream(); // 将文件保存到指定路径下 multipartFile.transferTo(new File("d:/Projects/practice/test-springboot/src/main/resources/file/" + originalFilename)); System.out.println(originalFilename); System.out.println(contentType); System.out.println(empty); System.out.println(size); System.out.println(bytes.length); HashMap<String, Object> data = new HashMap<>(); data.put("data", "ok"); return data; }
<template> <div> <a href="http://127.0.0.1:8083/file/downloadFile?filename=头像a.png">avatar3.png</a> </div> </template> <script> import axiosInstance from '@/utils/request.js' import axios from 'axios' export default { name: 'File', data() { return { } }, methods: { } } </script> <style> </style>
@GetMapping("downloadFile") public void downloadFile(@RequestParam("filename") String filename) throws Exception { // 告知浏览器这是一个字节流,浏览器处理字节流的默认方式就是下载 // 意思是未知的应用程序文件,浏览器一般不会自动执行或询问执行。浏览器会像对待, // 设置了HTTP头Content-Disposition值为attachment的文件一样来对待这类文件,即浏览器会触发下载行为 response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE); // ,该响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者网页的一部分),还是以附件的形式下载并保存到本地。 response.setHeader(HttpHeaders.CONTENT_DISPOSITION,"attachment;fileName="+ URLEncoder.encode(filename, "UTF-8")); File file = new File("d:/Projects/practice/test-springboot/src/main/resources/file/" + filename); ServletOutputStream ros = response.getOutputStream(); FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[2 * 1024]; int len = 0; while ((len = fis.read(bytes)) != -1) { ros.write(bytes, 0, len); } ros.flush(); ros.close(); fis.close() }
<template> <div> <el-button type="success" @click="downloadFile">下载文件</el-button> </div> </template> <script> import axiosInstance from '@/utils/request.js' import axios from 'axios' export default { name: 'File', data() { return { } }, methods: { downloadFile() { let a = document.createElement('a') a.href = 'http://127.0.0.1:8083/file/downloadFile?filename=头像a.png' document.body.appendChild(a) a.style.display = 'none' a.click() document.body.removeChild(a) } } } </script> <style> </style>
<template> <div> <el-button type="success" @click="downloadFile">下载文件</el-button> </div> </template> <script> import axios from 'axios' export default { name: 'File', data() { return { } }, methods: { downloadFile() { axios({ // 使用原来的axios实例, 不能用封装的, 因为下面要直接拿响应的blob数据 url:'http://127.0.0.1:8083/file/downloadFile?filename=头像a.png', method:'get', headers: { 'a':'b' }, responseType: 'blob' // 这个可以在axios的index.d.ts中可以找到 }).then(response=>{ return response.data }).then(blob=>{ console.log(blob); let ablob = new Blob([blob]) let blobUrl = window.URL.createObjectURL(ablob) let tmpLink = document.createElement('a') tmpLink.style.display = 'none' tmpLink.href = blobUrl tmpLink.setAttribute('download','头像b.png') document.body.appendChild(tmpLink) tmpLink.click() document.body.removeChild(tmpLink) window.URL.revokeObjectURL(blobUrl) }) } } } </script> <style> </style>
直接在浏览器的地址栏输入,即可下载,同样用上面的地址即可:http://127.0.0.1:8083/file/downloadFile?filename=头像a.png
js使用:window.location.href='http://127.0.0.1:8083/file/downloadFile?filename=头像a.png'
能达到与上面一致的效果,并且当前页面不会跳转,地址栏也不会有变化。
<template> <div> <a href="http://127.0.0.1:8083/file/previewFile?filename=头像a.png">头像a.png</a> </div> </template> <script> import axios from 'axios' export default { name: 'File', data() { return { } }, methods: { } } </script> <style> </style>
设置好响应头即可
@GetMapping("previewFile") public void previewFile(@RequestParam("filename") String filename) throws Exception { // 可使用ServletContext 通过文件名获取 媒体资源类型 response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE); File file = new File("d:/Projects/practice/test-springboot/src/main/resources/file/" + filename); ServletOutputStream ros = response.getOutputStream(); // 可参考: StreamUtils FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[4 * 1024]; int len = 0; while ((len = fis.read(bytes)) != -1) { ros.write(bytes, 0, len); } ros.flush(); ros.close(); fis.close() }
在开始分片之前,先了解下md5加密,因为后面秒传需要用到,或者是其它场景需要标识到这个文件名或文件二进制内容。
定长的大整数
)。MD5将整个文件当作一个大文本信息,通过其不可逆的字符串变换算法,产生了这个唯一的MD5信息摘要
。安装spark-md5
npm install spark-md5 --save
对字符串操作
常规用法
// 16进制哈希
var hexHash = SparkMD5.hash('Hi there'); // d9385462d3deff78c352ebb3f941ce12
// 再次执行, 仍然是同样的值
var hexHash = SparkMD5.hash('Hi there'); // d9385462d3deff78c352ebb3f941ce12
// 感觉这个没事撒用(应该就是原始的二进制数据,然后这个二进制数据转成了字符串形式)
var rawHash = SparkMD5.hash('Hi there', true); // Ù8TbÓÞÿxÃRë³ùAÎ\x12
// 可以如下模拟以下上面这个过程,
var fr = new FileReader()
fr.read(new Blob([SparkMD5.hash('Hi there',true)]))
// 看如下,获取了跟上面一样的结果
console.log(fr.result) // Ù8TbÓÞÿxÃRë³ùAÎ\x12
进阶用法
var spark = new SparkMD5();
spark.append('Hi');
spark.append(' there');
// d9385462d3deff78c352ebb3f941ce12,这个跟上面一样
var hexHash = spark.end();
// Ԍ٠不知道是个什么玩意,跟上面直接调用SparkMD5.hash('Hi there', true);的结果不一样
var rawHash = spark.end(true);
对文件操作
对一个D:\documents\尚硅谷谷粒学院项目视频教程\项目资料.zip的1.18G的文件进行md5,获取的是:0efda58eb4bbb4ea4b69f9ac0d566075
,
下面的方法摘自:npmjs仓库的spark-md5,可以体会一下这个递归在js里的用法:给FileReader绑定load事件,根据分片信息获取分片数据,并使用FileReader去read这个数据,从而绑定的load事件的函数就会执行,当处理完这个分片数据后,然后去触发下一个分片,直到所有的分片都read了(那么上传分片的时候,也可以使用下面的递归这么玩)。
<template> <input type="file" ref="fileInputRef" @change="getMd5($event.target.files[0])" /> </template> export default { methods: { getMd5(file) { var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice, chunkSize = 10 * 1024 * 1024, chunks = Math.ceil(file.size / chunkSize), currentChunk = 0, spark = new SparkMD5.ArrayBuffer(), fileReader = new FileReader(); fileReader.onload = function (e) { console.log('read chunk nr', currentChunk + 1, 'of', chunks); spark.append(e.target.result); currentChunk++; if (currentChunk < chunks) { loadNext(); } else { console.log('finished loading'); console.info('computed hash', spark.end()); // Compute hash spark.destroy(); // 释放内存 } }; fileReader.onerror = function () { console.warn('oops, something went wrong.'); }; function loadNext() { var start = currentChunk * chunkSize, end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } loadNext(); }, } }
需要先导入依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>
对字符串操作
import org.apache.commons.codec.digest.DigestUtils;
public static void main(String[] args) {
String md5 = DigestUtils.md5Hex("Hi there");
// d9385462d3deff78c352ebb3f941ce12, 与前端的md5结果一致
System.out.println(md5);
System.out.println(md5.length());
}
对文件二进制数据内容操作
public static void main(String[] args) throws IOException {
String s = DigestUtils.md5Hex(new FileInputStream(new File("D:\\documents\\尚硅谷谷粒学院项目视频教程\\项目资料.zip")));
// 与前端计算结果一致
// 0efda58eb4bbb4ea4b69f9ac0d566075
System.out.println(s);
}
摘自风宇博客
public class FileUtils { /** * 获取文件md5值 * * @param inputStream 文件输入流 * @return {@link String} 文件md5值 */ public static String getMd5(InputStream inputStream) { try { MessageDigest md5 = MessageDigest.getInstance("md5"); byte[] buffer = new byte[8192]; int length; while ((length = inputStream.read(buffer)) != -1) { md5.update(buffer, 0, length); } return new String(Hex.encodeHex(md5.digest())); } catch (Exception e) { e.printStackTrace(); return null; } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * 得到文件扩展名 * * @param fileName 文件名称 * @return {@link String} 文件后缀 */ public static String getExtName(String fileName) { if (StringUtils.isBlank(fileName)) { return ""; } return fileName.substring(fileName.lastIndexOf(".")); } }
这里只是实现分片上传的功能。会存在传参可能不合理,应该让要根据文件内容来标识到这个文件。后面需要根据具体的设计来改代码。比如设计表记录文件的每一个上传分片的记录,这样就能直到当前文件上传到第几个分片了,加入上传过程中分片失败了,下次上传前,先查询下这个文件上传到第几个分片了,然后就从那个分片后面开始上传。当根据文件内容计算的md5值能够在后台查到的话,那就直接算作秒传。
<template> <div> <el-progress :text-inside="true" :stroke-width="26" :percentage="percentage" style="width: 350px;border-radius: 13px;border: 1px solid red;"></el-progress> <input type="file" ref="fileInputRef" /> <el-button @click="uploadFile">上传文件</el-button> </div> </template> <script> import axios from 'axios' export default { name: 'File', data() { return { // 进度条 percentage: 0 } }, methods: { async uploadFile() { const { files } = this.$refs['fileInputRef'] let file = files[0] console.log(file.name); let size = file.size console.log(size); // 3 - (0 1 2) let chunkSize = 10 * 1024 * 1024 // 1个分片 10M let start = 0 // 上传的开始位置 let index = 0 // 分片索引, 从0开始(0,1,2...) let totalFragmentCount = Math.ceil(size / chunkSize) // 总的分片数量 while (true) { let end; // 当前分片的结束位置(不包括,开区间) if (start + chunkSize > size) { // 如果加上了一个分片大小,超出了文件的大小, 那么结束位置就是文件大小 end = size } else { end = start + chunkSize // 如果加上了一个分片大小,没超出了文件的大小, 那么结束位置就是start加上分片大小 } // 对file分片,分片完后, 给分片一个名字, 这个名字可以在后台获取为分片文件的真实名字 let sfile = new File([file.slice(start, end)],`${file.name}-${index}`) // 上传完这个分片后, 再走下面的代码 await this.uploadFragmentFile(sfile, index, file.name, totalFragmentCount) index++ if (end == size) { // 检查是否传完了, 传完了的话, 就跳出循环 break } // 开始位置 start = end } console.log('发送合并文件请求'); this.mergeFragmentFile(file.name) }, // 上传分片文件(将切分的分片文件上传) uploadFragmentFile(sfile, index, realFilename, totalFragmentCount) { return new Promise((resolve, reject) => { let formData = new FormData() formData.append('sFile', sfile) formData.append('index', index) formData.append('realFilename', realFilename) console.log('sfile', sfile, index); axios({ url: 'http://localhost:8083/file/uploadSliceFile', method: 'post', data: formData, headers: { 'a': 'b' } }).then(res => { console.log(`上传第${index}个分片成功`); this.percentage = parseFloat(((index + 1) / totalFragmentCount * 100).toFixed(1)) resolve() }) }) }, // 合并分片文件(当所有分片上传成功之后, 发送合并分片的请求) mergeFragmentFile(realFilename) { axios({ url: 'http://localhost:8083/file/mergeFragmentFile', method: 'post', params: { realFilename }, headers: { 'a': 'b' } }).then(res => { console.log('合并成功'); }) } } } </script> <style></style>
@PostMapping("uploadSliceFile") public Object uploadSliceFile(@RequestParam("sFile")MultipartFile sFile,@RequestParam("realFilename") String realFilename, @RequestParam("index") Integer index) throws IOException { String md5 = DigestUtils.md5Hex(realFilename); System.out.println(realFilename); System.out.println(md5); System.out.println("分片名: " + sFile.getOriginalFilename()); File dir = new File("d:/Projects/practice/test-springboot/src/main/resources/file/fragment/" + md5); if (!dir.exists()) { dir.mkdirs(); } File sFileWithIndex = new File("d:/Projects/practice/test-springboot/src/main/resources/file/fragment/" + md5 + "/" + index); sFile.transferTo(sFileWithIndex); HashMap<String, Object> data = new HashMap<>(); data.put("data", "ok"); return data; } @PostMapping("mergeFragmentFile") public Object mergeFragmentFile(@RequestParam String realFilename) throws IOException { System.out.println("-------开始合并文件"); // 合并的文件 RandomAccessFile raf = new RandomAccessFile("d:/Projects/practice/test-springboot/src/main/resources/file/" + realFilename, "rw"); // 获取分片所在文件夹 String md5 = DigestUtils.md5Hex(realFilename); System.out.println(realFilename); System.out.println(md5); File file = new File("d:/Projects/practice/test-springboot/src/main/resources/file/fragment/" + md5); File[] files = file.listFiles(); int num = files.length; System.out.println(num); byte[] bytes = new byte[5 * 1024]; // 合并分片 for (int i = 0; i < num; i++) { File iFile = new File(file, String.valueOf(i)); // 将每一个分片文件包装为缓冲流 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(iFile)); int len = 0; // 将分片文件包装的流写入RandomAccessFile while ((len = bis.read(bytes)) != -1) { raf.write(bytes, 0, len); } bis.close(); } // 删除分片所在文件夹的分片文件 for (File tmpFile : files) { tmpFile.delete(); } // 删除分片所在文件夹 file.delete(); raf.close(); HashMap<String, Object> data = new HashMap<>(); data.put("data", "ok"); return data; }
上面vue实现的分片上传,有些问题
也就是不能从指定的分片开始上传
。<template> <div> <el-progress :text-inside="true" :stroke-width="26" :percentage="percentage" style="width: 350px;border-radius: 13px;border: 1px solid red;"></el-progress> <input type="file" ref="fileInputRef" /> <el-button @click="uploadFile">开始上传文件</el-button> <el-button @click="stopUpload">暂停上传</el-button> <el-button @click="countinueUpload">继续上传</el-button> </div> </template> <script> import axios from 'axios' export default { name: 'File', data() { return { // 进度条 percentage: 0, // 已上传完成的分片索引 index: -1, // 是否暂停上传 isStop: false } }, methods: { // 停止上传 stopUpload() { this.isStop = true }, // 继续上传 countinueUpload() { this.isStop = false this.uploadFileFromIndex(++this.index) }, // 上传 uploadFile() { this.uploadFileFromIndex(0) }, // 从第几个分片开始上传(index从0开始算,index=0算作第一个分片) uploadFileFromIndex(index) { let _this = this const { files } = this.$refs['fileInputRef'] let file = files[0] let chunkSize = 5 * 1024 * 1024 // 分片大小 10M let chunkTotalCount = Math.ceil(file.size / chunkSize) // 分片总数 // debugger uploadSliceFile(index) // 上传指定索引的分片文件 function uploadSliceFile(idx) { if (idx >= chunkTotalCount) { console.log('文件已上传完成...'); return } // 分片开始位置 let start = idx * chunkSize // 分片结束位置 let end = (start + chunkSize) > file.size ? file.size : start + chunkSize // 对文件分片 let sFile = new File([file.slice(start, end)], `${file.name}.${idx}`) let formData = new FormData() formData.append('sFile', sFile) formData.append('realFilename', file.name) formData.append('index', idx) axios({ url: 'http://localhost:8083/file/uploadSliceFile', method: 'post', data: formData, headers: { 'a': 'b' } }).then(res => { if (idx === chunkTotalCount - 1) { // 已经上传完了最后一个分片 console.log('上传完成'); // 记录已完成的分片索引 _this.index = idx _this.percentage = 100 // 发送合并文件请求 mergeFragmentFile(file.name) } else { // 上传完成指定索引的分片之后, 更新文件上传进度 _this.percentage = parseFloat(((idx + 1) / chunkTotalCount * 100).toFixed(1)) // 记录已完成的分片索引 _this.index = idx if (!_this.isStop) { // 如果没有点击暂停的话, 再上传下一个索引的分片 uploadSliceFile(++idx) } } }) } // 发送合并分片文件请求 function mergeFragmentFile(realFilename) { axios({ url: 'http://localhost:8083/file/mergeFragmentFile', method: 'post', params: { realFilename }, headers: { 'a': 'b' } }).then(res => { console.log('合并成功'); }) } } } } </script> <style></style>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。