先记录下,后面有时间再去实现
(已实现,可参考
- SpringBoot+vue文件上传&下载&预览&大文件分片上传&文件上传进度
- SpringBoot+vue 大文件分片下载)
可参考链接:vue上传大文件/视频前后端(java)代码
前端 + 后端 实现分片上传(断点续传/极速秒传)
前端slice分片上传,后端用表记录分片索引和分片大小和分片总数,当接受完最后一个分片(分片索引等于分片总数,分片索引从1开始),就合并分片成完成的文件。前端需要递归上传,并显示加载动画和根据分片完成数量显示进度条
临时demo
<!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>
<form action="http://www.baidu.com/a">
<input type="file" type="hidden" id="file"><!-- 隐藏这个原生的上传文件按钮 -->
<button type="button" id="btn">触发上传</button></button><!-- 使用它来触发选择图片动作 -->
</form>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>
/* 监听选择图片的事件 */
document.querySelector('#file').onchange = (e)=>{
console.log('改变了');
console.log(this); // 这里的this变成了Window, 因为写成了箭头函数。
console.dir(e.target);
// 选择了一个文件,所以数组只有一个元素
console.log(e.target.files); // FileList {0: File, length: 1}
console.log(e.target.files[0]); // File {name: 'GIF 2023-4-1 18-14-01.gif', lastModified: 1680344051705, lastModifiedDate: Sat Apr 01 2023 18:14:11 GMT+0800 (中国标准时间), webkitRelativePath: '', size: 242914, …}
upload(e.target.files[0])
document.querySelector('#file').value = '' // 让下次即使选择同一个文件仍能触发onchange事件
}
function upload(file) {
console.log(file instanceof Blob); // true, 而Blob中有个slice方法,可以对文件进行分片
let formData = new FormData()
let shardSize = 10 * 1024 * 1024
let shardIndex = 1
let start = shardSize * shardIndex
let end = Math.min(file.size, start + shardSize)
console.log(start,end);
formData.append('mfile', file.slice(start,end))
// 携带数据请求后台
$.ajax({
url: 'http://127.0.0.1:8083/article/uploadImg',
type: 'POST',
data: formData,
contentType: false,
processData: false,
cache: false,
success: function (data) {
if (data.success) {
alert('添加成功');
} else {
alert('添加失败');
}
}
});
}
/* 点的是#btn,但是我们要触发#file文件上传 */
document.querySelector('#btn').onclick = function(){
document.querySelector('#file').click()
}
</script>
</html>
@PostMapping("uploadImg")
public Result uploadImg(@RequestParam("mfile") MultipartFile mfile) throws IOException {
String filename = mfile.getOriginalFilename();
mfile.transferTo(new File("D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\"+filename));
return Result.ok(filename);
}
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
oss 将前面的分片上传改为oss里的追加上传
public static void main(String[] args) throws IOException {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "xxx";
String accessKeySecret = "yyy";
// 填写Bucket名称,例如examplebucket。
String bucketName = "test-zzhua";
String objectName = "video/juc.mp4";
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ObjectMetadata meta = new ObjectMetadata();
meta.setObjectAcl(CannedAccessControlList.PublicRead);
RandomAccessFile raFile = new RandomAccessFile(new File("D:\\Projects\\vue-springboot\\src\\main\\resources\\static\\img\\juc.mp4"), "r");
long totalLen = raFile.length();
// 定义每次追加上传的大小 3M
long everyLen = 3 * 1024 * 1024;
long accLen = 0;
byte[] bytes = new byte[5 * 1024]; // 缓冲数组5k
while (true) {
// 找到上次读取的位置
raFile.seek(accLen);
boolean finish = false;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 当前读取累积3M, 或不够3M就读完了
int currLen = 0;
while (true) {
int readLen = raFile.read(bytes);
if (readLen == -1) {
finish = true;
break;
}
currLen += readLen;
baos.write(bytes, 0, readLen);
if (currLen >= everyLen) {
break;
}
}
// 发起追加请求
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucketName, objectName, bais,meta);
appendObjectRequest.setPosition(accLen);
ossClient.appendObject(appendObjectRequest);
if (finish) {
break;
}
accLen += currLen;
}
}
md5大文件计算
javascript实现
参考:SpringBoot大文件上传–前端计算文件的MD5文章来源:https://www.toymoban.com/news/detail-416585.html
<!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>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.js"></script>
</head>
<body>
<form id="from" method="post" action="/upload" enctype="multipart/form-data">
<table>
<tr>
<td>
<input id="md5" name="md5">
<input id="file" name="upload" type="file">
<input id="submit" type="submit" value="上传">
</td>
</tr>
</table>
</form>
</body>
<script>
//注意此方法引用了SparkMD5库 library:https://github.com/satazor/SparkMD5
//监听文本框变化
document.getElementById("file").addEventListener("change", function() {
//声明必要的变量
chunks=0;
currentChunk=0;
var fileReader = new FileReader();//一个用来读取文件的对象
//文件分割方法(注意兼容性)
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
file = document.getElementById("file").files[0],
//文件每块分割2M,计算分割详情
chunkSize = 2097152,
chunks = Math.ceil(file.size / chunkSize),//文件分成了几块
currentChunk = 0,//当前处理的第几块
spark = new SparkMD5();//创建md5对象(基于SparkMD5)
//每块文件读取完毕之后的处理
fileReader.onload = function(e) {
console.log("读取文件", currentChunk + 1, "/", chunks);
//每块交由sparkMD5进行计算
spark.appendBinary(e.target.result);
currentChunk++;
//如果文件处理完成计算MD5,如果还有分片继续处理
if (currentChunk < chunks) {
loadNext();
} else {
md5=spark.end();//最终的MD5
console.log("MD5:"+md5);
}
};
//处理单片文件的上传
function loadNext() {
var start = currentChunk * chunkSize,
end = start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsBinaryString(blobSlice.call(file, start, end));
//blobSlice.call(file, start, end)每次执行到blobSlice的时候就会跳转到blobSlice定义的地方,可以理解为一个循环
}
loadNext();
});
</script>
</html>
java实现
参考:详解JAVA中获取文件MD5值的四种方法
须引入commons-codec包文章来源地址https://www.toymoban.com/news/detail-416585.html
String s = DigestUtils.md5Hex(new FileInputStream(new File("D:\\documents\\尚硅谷谷粒学院项目视频教程\\6 - What If I Want to Move Faster.mp4")));
ystem.out.println(s);
vue上传大文件/视频前后端(java)代码
<template>
<div>
<!-- 上传组件 -->
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
</el-upload>
<!-- 进度显示 -->
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%</span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
</div>
</div>
</template>
<script>
import { getUUID } from '@/utils'
import axios from 'axios'
export default {
name: 'singleUpload',
props: {
value: String
},
filters: {
btnTextFilter(val) {
return val ? '暂停' : '继续'
}
},
data() {
return {
videoUrl: this.value,
percent: 0,
upload: true,
percentCount: 0,
suffix: '',
fileName: '',
preName: ''
}
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
async handleChange(file) {
if (!file) return
this.percent = 0
this.percentCount = 0
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw
let buffer
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}
// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名
this.preName = getUUID() //生成文件名前缀
this.fileName = this.preName+'.'+suffix //文件名
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${this.preName}_${i}.${suffix}` // 文件名规则按照 filename_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
this.chunkList = chunkList // sendRequest 要用到
this.sendRequest()
},
// 发送请求
sendRequest() {
const requestList = [] // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('filename', item.fileName)
return axios({
url: 'http://localhost/api/chunk',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: formData
}).then(response => {
if (response.data.errcode === 0) { // 成功
if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
this.percentCount = 100 / this.chunkList.length
}
if (this.percent >= 100) {
this.percent = 100;
}else {
this.percent += this.percentCount // 改变进度
}
if (this.percent >= 100) {
this.percent = 100;
}
this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
}else{
this.$mseeage({
type: "error",
message: response.data.message
})
return
}
})
}
requestList.push(fn)
})
let i = 0 // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件名传递给服务器
const complete = () => {
axios({
url: 'http://localhost/api/merge',
method: 'get',
params: {filename: this.fileName },
timeout: 60000
}).then(response => {
if (response.data.errcode === 0) { // 请求发送成功
// this.videoUrl = res.data.path
console.log(response.data)
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 发送完毕
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 发送请求
this.emitInput(this.fileName)
},
// 按下暂停按钮
handleClickBtn() {
this.upload = !this.upload
// 如果不暂停则继续上传
if (this.upload) this.sendRequest()
},
// 将 File 对象转为 ArrayBuffer
fileToBuffer(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.onload = e => {
resolve(e.target.result)
}
fr.readAsArrayBuffer(file)
fr.onerror = () => {
reject(new Error('转换文件格式发生错误'))
}
})
}
}
}
</script>
<style scoped "">
.progress-box {
box-sizing: border-box;
width: 360px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding: 8px 10px;
background-color: #ecf5ff;
font-size: 14px;
border-radius: 4px;
}
.videoShow{
width: 100%;
height:600px;
padding: 10px 0 50px;
position: relative;
}
#videoBox{
object-fit:fill;
border-radius: 8px;
display: inline-block;
vertical-align: baseline;
}
.video-img{
position: absolute;
top: 0;
bottom: 0;
width: 100%;
z-index: 999;
background-size:100%;
cursor:pointer;
}
.video-img img {
display:block;
width: 60px;
height: 60px;
position: relative;
top:260px;
left: 48%;
}
video:focus {
outline: -webkit-focus-ring-color auto 0px;
}
</style>
/**
* 获取uuid
*/
export function getUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
String dirPath = "D:\\video\\train"
@PostMapping("/chunk")
public Result upLoadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("filename") String fileName) {
// 用于存储文件分片的文件夹
File folder = new File(dirPath);
if (!folder.exists() && !folder.isDirectory())
folder.mkdirs();
// 文件分片的路径
String filePath = dirPath + fileName;
try {
File saveFile = new File(filePath);
// 写入文件中
//FileOutputStream fileOutputStream = new FileOutputStream(saveFile);
//fileOutputStream.write(chunk.getBytes());
//fileOutputStream.close();
chunk.transferTo(saveFile);
return new Result();
} catch (Exception e) {
e.printStackTrace();
}
return new Result();
}
@GetMapping("/merge")
public Result MergeChunk(@RequestParam("filename") String filename) {
String preName = filename.substring(0,filename.lastIndexOf("."));
// 文件分片所在的文件夹
File chunkFileFolder = new File(dirPath);
// 合并后的文件的路径
File mergeFile = new File(dirPath + filename);
// 得到文件分片所在的文件夹下的所有文件
File[] chunks = chunkFileFolder.listFiles();
System.out.println(chunks.length);
assert chunks != null;
// 排序
File[] files = Arrays.stream(chunks)
.filter(file -> file.getName().startsWith(preName))
.sorted(Comparator.comparing(o -> Integer.valueOf(o.getName().split("\\.")[0].split("_")[1])))
.toArray(File[]::new);
try {
// 合并文件
RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
byte[] bytes = new byte[1024];
for (File chunk : files) {
RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
int len;
while ((len = randomAccessFileReader.read(bytes)) != -1) {
randomAccessFileWriter.write(bytes, 0, len);
}
randomAccessFileReader.close();
System.out.println(chunk.getName());
chunk.delete(); // 删除已经合并的文件
}
randomAccessFileWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
return new Result();
}
到了这里,关于前端 + 后端 实现分片上传(断点续传/极速秒传)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!