先記錄下,后面有時(shí)間再去實(shí)現(xiàn)
(已實(shí)現(xiàn),可參考
- SpringBoot+vue文件上傳&下載&預(yù)覽&大文件分片上傳&文件上傳進(jìn)度
- SpringBoot+vue 大文件分片下載)
可參考鏈接:vue上傳大文件/視頻前后端(java)代碼
前端 + 后端 實(shí)現(xiàn)分片上傳(斷點(diǎn)續(xù)傳/極速秒傳)
前端slice分片上傳,后端用表記錄分片索引和分片大小和分片總數(shù),當(dāng)接受完最后一個(gè)分片(分片索引等于分片總數(shù),分片索引從1開始),就合并分片成完成的文件。前端需要遞歸上傳,并顯示加載動(dòng)畫和根據(jù)分片完成數(shù)量顯示進(jìn)度條
臨時(shí)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"><!-- 隱藏這個(gè)原生的上傳文件按鈕 -->
<button type="button" id="btn">觸發(fā)上傳</button></button><!-- 使用它來觸發(fā)選擇圖片動(dòng)作 -->
</form>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
<script>
/* 監(jiān)聽選擇圖片的事件 */
document.querySelector('#file').onchange = (e)=>{
console.log('改變了');
console.log(this); // 這里的this變成了Window, 因?yàn)閷懗闪思^函數(shù)。
console.dir(e.target);
// 選擇了一個(gè)文件,所以數(shù)組只有一個(gè)元素
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 (中國(guó)標(biāo)準(zhǔn)時(shí)間), webkitRelativePath: '', size: 242914, …}
upload(e.target.files[0])
document.querySelector('#file').value = '' // 讓下次即使選擇同一個(gè)文件仍能觸發(fā)onchange事件
}
function upload(file) {
console.log(file instanceof Blob); // true, 而Blob中有個(gè)slice方法,可以對(duì)文件進(jìn)行分片
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))
// 攜帶數(shù)據(jù)請(qǐng)求后臺(tái)
$.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('添加失敗');
}
}
});
}
/* 點(diǎn)的是#btn,但是我們要觸發(fā)#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請(qǐng)按實(shí)際情況填寫。
String endpoint = "https://oss-cn-shenzhen.aliyuncs.com";
// 阿里云賬號(hào)AccessKey擁有所有API的訪問權(quán)限,風(fēng)險(xiǎn)很高。強(qiáng)烈建議您創(chuàng)建并使用RAM用戶進(jìn)行API訪問或日常運(yùn)維,請(qǐng)登錄RAM控制臺(tái)創(chuàng)建RAM用戶。
String accessKeyId = "xxx";
String accessKeySecret = "yyy";
// 填寫B(tài)ucket名稱,例如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]; // 緩沖數(shù)組5k
while (true) {
// 找到上次讀取的位置
raFile.seek(accLen);
boolean finish = false;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 當(dāng)前讀取累積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;
}
}
// 發(fā)起追加請(qǐng)求
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大文件計(jì)算
javascript實(shí)現(xiàn)
參考:SpringBoot大文件上傳–前端計(jì)算文件的MD5文章來源:http://www.zghlxwxcb.cn/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庫(kù) library:https://github.com/satazor/SparkMD5
//監(jiān)聽文本框變化
document.getElementById("file").addEventListener("change", function() {
//聲明必要的變量
chunks=0;
currentChunk=0;
var fileReader = new FileReader();//一個(gè)用來讀取文件的對(duì)象
//文件分割方法(注意兼容性)
blobSlice = File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice,
file = document.getElementById("file").files[0],
//文件每塊分割2M,計(jì)算分割詳情
chunkSize = 2097152,
chunks = Math.ceil(file.size / chunkSize),//文件分成了幾塊
currentChunk = 0,//當(dāng)前處理的第幾塊
spark = new SparkMD5();//創(chuàng)建md5對(duì)象(基于SparkMD5)
//每塊文件讀取完畢之后的處理
fileReader.onload = function(e) {
console.log("讀取文件", currentChunk + 1, "/", chunks);
//每塊交由sparkMD5進(jìn)行計(jì)算
spark.appendBinary(e.target.result);
currentChunk++;
//如果文件處理完成計(jì)算MD5,如果還有分片繼續(xù)處理
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)每次執(zhí)行到blobSlice的時(shí)候就會(huì)跳轉(zhuǎn)到blobSlice定義的地方,可以理解為一個(gè)循環(huán)
}
loadNext();
});
</script>
</html>
java實(shí)現(xiàn)
參考:詳解JAVA中獲取文件MD5值的四種方法
須引入commons-codec包文章來源地址http://www.zghlxwxcb.cn/news/detail-416585.html
String s = DigestUtils.md5Hex(new FileInputStream(new File("D:\\documents\\尚硅谷谷粒學(xué)院項(xiàng)目視頻教程\\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>點(diǎn)擊上傳</em></div>
<div class="el-upload__tip" slot="tip">大小不超過 200M 的視頻</div>
</el-upload>
<!-- 進(jìn)度顯示 -->
<div class="progress-box">
<span>上傳進(jìn)度:{{ 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 ? '暫停' : '繼續(xù)'
}
},
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
// 獲取文件并轉(zhuǎn)成 ArrayBuffer 對(duì)象
const fileObj = file.raw
let buffer
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}
// 將文件按固定大?。?M)進(jìn)行切片,注意此處同時(shí)聲明了多個(gè)常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的數(shù)組
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 計(jì)算總共多個(gè)切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后綴名
this.preName = getUUID() //生成文件名前綴
this.fileName = this.preName+'.'+suffix //文件名
// 生成切片,這里后端要求傳遞的參數(shù)為字節(jié)數(shù)據(jù)塊(chunk)和每個(gè)數(shù)據(jù)塊的文件名(fileName)
let curChunk = 0 // 切片時(shí)的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${this.preName}_${i}.${suffix}` // 文件名規(guī)則按照 filename_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
this.chunkList = chunkList // sendRequest 要用到
this.sendRequest()
},
// 發(fā)送請(qǐng)求
sendRequest() {
const requestList = [] // 請(qǐng)求集合
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) { // 避免上傳成功后會(huì)刪除切片改變 chunkList 的長(zhǎng)度影響到 percentCount 的值
this.percentCount = 100 / this.chunkList.length
}
if (this.percent >= 100) {
this.percent = 100;
}else {
this.percent += this.percentCount // 改變進(jìn)度
}
if (this.percent >= 100) {
this.percent = 100;
}
this.chunkList.splice(index, 1) // 一旦上傳成功就刪除這一個(gè) chunk,方便斷點(diǎn)續(xù)傳
}else{
this.$mseeage({
type: "error",
message: response.data.message
})
return
}
})
}
requestList.push(fn)
})
let i = 0 // 記錄發(fā)送的請(qǐng)求個(gè)數(shù)
// 文件切片全部發(fā)送完畢后,需要請(qǐng)求 '/merge' 接口,把文件名傳遞給服務(wù)器
const complete = () => {
axios({
url: 'http://localhost/api/merge',
method: 'get',
params: {filename: this.fileName },
timeout: 60000
}).then(response => {
if (response.data.errcode === 0) { // 請(qǐng)求發(fā)送成功
// this.videoUrl = res.data.path
console.log(response.data)
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 發(fā)送完畢
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 發(fā)送請(qǐng)求
this.emitInput(this.fileName)
},
// 按下暫停按鈕
handleClickBtn() {
this.upload = !this.upload
// 如果不暫停則繼續(xù)上傳
if (this.upload) this.sendRequest()
},
// 將 File 對(duì)象轉(zhuǎn)為 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('轉(zhuǎn)換文件格式發(fā)生錯(cuò)誤'))
}
})
}
}
}
</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) {
// 用于存儲(chǔ)文件分片的文件夾
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(); // 刪除已經(jīng)合并的文件
}
randomAccessFileWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
return new Result();
}
到了這里,關(guān)于前端 + 后端 實(shí)現(xiàn)分片上傳(斷點(diǎn)續(xù)傳/極速秒傳)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!