什么是OSS:
数据以对象(Object)的形式存储在OSS的存储空间(Bucket )中。如果要使用OSS存储数据,您需要先创建Bucket,并指定Bucket的地域、访问权限、存储类型等属性。创建Bucket后,您可以将数据以Object的形式上传到Bucket,并指定Object的文件名(Key)作为其唯一标识。
分片上传:
在上传大文件(超过5 GB)到OSS的过程中,如果出现网络中断、程序异常退出等问题导致文件上传失败,您需要使用分片上传的方式上传大文件。分片上传通过将待上传的大文件分成多个较小的碎片(Part),充分利用网络带宽和服务器资源并发上传多个Part,加快上传完成时间,并在Part上传完成之后调用CompleteMultipartUpload接口将这些Part组合成一个完整的Object。
断点续传上传:
在上传大文件(超过5 GB)到OSS的过程中,如果出现网络中断、程序异常退出等问题导致文件上传失败,甚至重试多次仍无法完成上传,您需要使用断点续传上传的方式。断点续传上传将需要上传的大文件分成多个较小的分片并发上传,加速上传完成时间。如果上传过程中,某一分片上传失败,再次上传时会从Checkpoint文件记录的断点继续上传,无需重新上传所有分片。上传完成后,所有分片将合并成完整的文件。
一.验证服务端是否已有该文件、该文件是否已上传部分块?
点击上传文件→选择文件后→读取文件信息→计算MD5值→发送给服务器→查看服务器返回信息
计算MD5:
import SparkMD5 from 'spark-md5';this.calculateMD5(files.raw, this.sliceSize).then(async (md5) => {console.log(md5)}).catch((error) => {this.$message.error(error);});// md5 计算calculateMD5(file, chunkSize) {return new Promise((resolve, reject) => {const chunks = Math.ceil(file.size / chunkSize);let currentChunk = 0;const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();fileReader.onload = (e) => {spark.append(e.target.result); // 更新 MD5 值currentChunk++;if (currentChunk < chunks) {this.loadNextChunk(fileReader, currentChunk, chunkSize, file);} else {const md5 = spark.end(); // 计算最终 MD5 值resolve(md5);}};fileReader.onerror = function (e) {reject(e);};this.loadNextChunk(fileReader, currentChunk, chunkSize, file);});},// md5 计算loadNextChunk(fileReader, currentChunk, chunkSize, file) {const start = currentChunk * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);fileReader.readAsArrayBuffer(chunk);},
二 文件切块
sliceSize: 1024 * 1024 * 2, // 分片上传-每片5MBfileBlockArr: [],_this.fileBlockArr = [];let blob = file.raw;const { size: fileSize, name: fileName } = blob;//计算文件切片总数,Math.ceil向上取整const totalSlice = Math.ceil(fileSize / _this.sliceSize);_this.chunktotle = totalSlice;for (let i = 0; i < totalSlice; i++) {let start = i * _this.sliceSize;let end = Math.min(fileSize, start + _this.sliceSize);let chunkBlob = blob.slice(start, end); // 直接使用slice方法分片Blob对象_this.fileBlockArr.push(chunkBlob);}
三 调用初始化接口
调用初始化接口,拿到部分参数后进行块上传
const requestList = [];_this.fileBlockArr.forEach((item, index) => {if (index + 1 >= sliceNo) {const fn = () => {let data = new FormData();data.append('file', item);data.append('fileSlicesNum', totalSlice);data.append('sliceNo', index + 1);data.append('fileMD5', md5);data.append('resourceKey', _this.resourceKey);data.append('ossSlicesId', _this.ossSlicesId);data.append('fileSize', fileSize);return axios({url: process.env.VUE_APP_BASE_API + '/common/partUpload',method: 'post',data: data,headers: {Authorization: 'Bearer ' + getToken(),'Content-Type': 'multipart/form-data',},}).then((res) => {if (res.data.code === 200) {if (_this.percentCount === 0) {// 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值_this.percentCount = 100 / _this.fileBlockArr.length;}// _this.percent += _this.percentCount; // 改变进度_this.percent = res.data.progressPercent;_this.fileBlockArr.splice(index, 1); // 一旦上传成功就删除这一个 chunk,方便断点续传}}).catch((err) => {return Promise.reject(err);});};requestList.push(fn);}});let i = 0; // 记录发送的请求个数
四 上传与合并
let i = 0; // 记录发送的请求个数const complete = () => {let completedData = {ossSlicesId: _this.ossSlicesId,resourceKey: _this.resourceKey,fileMD5: _this.md5,};completePart(completedData).then((response) => {if (response.code == 200) {_this.form.versionUrl = response.data;}}).catch((error) => {console.log('completePart-error:', error);});};const send = async () => {if (i >= requestList.length) {complete();return;}await requestList[i]();i++;send();};send(); // 发送请求
全览
data(){return {sliceSize: 1024 * 1024 * 2, // 分片上传-每片5MBchunktotle: 0,percentCount: 0,percent: 0,md5: '',fileBlockArr: [],resourceKey: undefined,ossSlicesId: undefined,fileName: undefined,}},methods:{uploadChange(files, fileList) {this.fileName = files.name;this.calculateMD5(files.raw, this.sliceSize).then(async (md5) => {this.md5 = md5;// 分片验证let checkData = {fileMD5: this.md5,};continueUpload(checkData).then((response) => {if (response.code === 200) {if (response.data !== null &&response.data.sliceNo < response.data.fileSlicesNum) {//已存在,但未上传完成 取已上传到的块信息this.resourceKey = response.data.resourceKey;this.ossSlicesId = response.data.ossSlicesId;let sliceNo = response.data.sliceNo;blockUpload(files, sliceNo + 1);} else if (response.data !== null &&response.data.sliceNo == response.data.fileSlicesNum) {// 上传完成待合并let completedData = {ossSlicesId: response.data.ossSlicesId,resourceKey: response.data.resourceKey,fileMD5: _this.md5,};mergePart(completedData);} else {// 未上传partInit({fileName: files.name,storageDirectory: 'internalApp',}).then((response) => {if (response.code === 200) {this.resourceKey = response.data.resourceKey;this.ossSlicesId = response.data.ossSlicesId;blockUpload(files, 0);}});}}}).catch((error) => {console.log('continueUpload-error:', error);});function mergePart(completedData) {completePart(completedData).then((response) => {if (response.code == 200) {let fileData = {name: _this.fileName,url: response.data,};_this.fileList.push(fileData);_this.form.versionUrl = response.data;}}).catch((error) => {console.log('completePart-error:', error);});}let _this = this;function blockUpload(file, sliceNo) {_this.fileBlockArr = [];let blob = file.raw;const { size: fileSize, name: fileName } = blob;//计算文件切片总数,Math.ceil向上取整const totalSlice = Math.ceil(fileSize / _this.sliceSize);_this.chunktotle = totalSlice;for (let i = 0; i < totalSlice; i++) {let start = i * _this.sliceSize;let end = Math.min(fileSize, start + _this.sliceSize);let chunkBlob = blob.slice(start, end); // 直接使用slice方法分片Blob对象_this.fileBlockArr.push(chunkBlob);}const requestList = [];_this.fileBlockArr.forEach((item, index) => {if (index + 1 >= sliceNo) {const fn = () => {let data = new FormData();data.append('file', item);data.append('fileSlicesNum', totalSlice);data.append('sliceNo', index + 1);data.append('fileMD5', md5);data.append('resourceKey', _this.resourceKey);data.append('ossSlicesId', _this.ossSlicesId);data.append('fileSize', fileSize);return axios({url: process.env.VUE_APP_BASE_API + '/common/partUpload',method: 'post',data: data,headers: {Authorization: 'Bearer ' + getToken(),'Content-Type': 'multipart/form-data',},}).then((res) => {if (res.data.code === 200) {if (_this.percentCount === 0) {// 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值_this.percentCount = 100 / _this.fileBlockArr.length;}// _this.percent += _this.percentCount; // 改变进度_this.percent = res.data.progressPercent;_this.fileBlockArr.splice(index, 1); // 一旦上传成功就删除这一个 chunk,方便断点续传}}).catch((err) => {return Promise.reject(err);});};requestList.push(fn);}});let i = 0; // 记录发送的请求个数const complete = () => {let completedData = {ossSlicesId: _this.ossSlicesId,resourceKey: _this.resourceKey,fileMD5: _this.md5,};mergePart(completedData);};const send = async () => {if (i >= requestList.length) {complete();return;}await requestList[i]();i++;send();};send(); // 发送请求}}).catch((error) => {this.$message.error(error);});},// md5 计算calculateMD5(file, chunkSize) {return new Promise((resolve, reject) => {const chunks = Math.ceil(file.size / chunkSize);let currentChunk = 0;const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();fileReader.onload = (e) => {spark.append(e.target.result); // 更新 MD5 值currentChunk++;if (currentChunk < chunks) {this.loadNextChunk(fileReader, currentChunk, chunkSize, file);} else {const md5 = spark.end(); // 计算最终 MD5 值resolve(md5);}};fileReader.onerror = function (e) {reject(e);};this.loadNextChunk(fileReader, currentChunk, chunkSize, file);});},// md5 计算loadNextChunk(fileReader, currentChunk, chunkSize, file) {const start = currentChunk * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);fileReader.readAsArrayBuffer(chunk);},
}