代码仅供参考,请根据项目实际需求调整。
阿里云OSS分片上传 官方文档 点击前往
后端(JAVA)代码
controller 代码
import com.aliyun.oss.model.PartETag;
import com.xxx.config.ErrorCode; // 只是个错误码而已就不贴了
import com.xxx.model.api.ResultBean;
import com.xxx.model.bean.upload.AliyunPartETag;
import com.xxx.service.AliIOssService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/upload")
public class UploadController {
@Autowired
private AliIOssService aliIOssService;
/**
* 删除已上传的文件
*
* @param fileName 文件名
* @return
*/
@PostMapping("/delete/file")
public ResultBean deleteFile(@RequestParam("fileName") String fileName) {
ResultBean ret = new ResultBean();
aliIOssService.deleteOssObject(fileName);
log.info("delete file:{}", fileName);
return ret;
}
/**
* 初始化分片上传
*
* @return fileName 生成的文件名
*/
@PostMapping("/part/init")
public ResultBean initUploadPart(@RequestParam("fileName") String fileName) {
ResultBean ret = new ResultBean();
String extensionName = getExtensionName(fileName);
ArrayList array = new ArrayList<String>();
array.add("mp4");
if (!array.contains(extensionName)) {
ret.setError(ErrorCode.PARA_IS_ERROR, "文件格式不正确");
return ret;
}
//修改原文件名
String name = System.currentTimeMillis() + "." + extensionName;
//初始化分片上传
String uploadId = aliIOssService.initPartUpload(name);
ret.addEntry("uploadId", uploadId);
ret.addEntry("fileName", name);
log.info("upload part init sourceFileName:{}, currentFileName:{}, uploadId:{}", fileName, name, uploadId);
return ret;
}
/**
* 上传分片
*
* @param file
* @param partNumber 当前文件为第几片
* @param uploadId uploadId它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
* @param fileName 文件名
* @param partSize 分片大小
* @return partETag 前端记得存起来,合并分片的时候要用
*/
@PostMapping("/part/{partNumber}")
public ResultBean UploadPart(MultipartFile file, @PathVariable("partNumber") Integer partNumber,
@RequestParam("uploadId") String uploadId, @RequestParam("name") String fileName,
@RequestParam("partSize") Long partSize) {
ResultBean ret = new ResultBean();
log.info("upload part start ,uploadId: {}, fileName:{}, partNumber:{}, partSize:{}", uploadId, fileName, partNumber, partSize);
PartETag partETag = aliIOssService.PartUploader(file, fileName, uploadId, partNumber, partSize);
Map map = new HashMap<String, Object>();
map.put("partNumber", partETag.getPartNumber());
map.put("partSize", partETag.getPartSize());
map.put("eTag", partETag.getETag());
//long 类型过长传(短点就没问题)到前端会出bug(这个bug必现),故转为String。bug 如下
//后端传出的值:8677056718873628943, 前端接收的值: 8677056718873629000
map.put("partCRC", partETag.getPartCRC().toString());
ret.addEntry("partETag", map);
return ret;
}
/**
* 合并分片
*
* @param uploadId uploadId它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
* @param fileName 文件名
* @param aliyunPartETags 需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
* @return url 文件url
*/
@PostMapping("/part/merge")
public ResultBean UploadPartMerge(@RequestParam("uploadId") String uploadId, @RequestParam("fileName") String fileName, @RequestBody List<AliyunPartETag> aliyunPartETags) {
ResultBean ret = new ResultBean();
List<PartETag> partETags = new ArrayList<PartETag>();
aliyunPartETags.forEach(item -> {
PartETag partETag = new PartETag(item.getPartNumber(), item.geteTag(), item.getPartSize(), item.getPartCRC());
partETags.add(partETag);
log.info("part merge message, Number:{}, ETag:{}, Size:{}, CRC:{}", item.getPartNumber(), item.geteTag(), item.getPartSize(), item.getPartCRC());
});
aliIOssService.PartMerge(fileName, uploadId, partETags);
ret.addEntry("url", aliIOssService.getImgUrl() + fileName);
return ret;
}
/**
* 取消分片上传 并删除所有已上传的分片
*
* @param fileName 文件名
* @param uploadId
* @return
*/
@PostMapping("/delete/part")
public ResultBean deletePart(@RequestParam("fileName") String fileName, @RequestParam("uploadId") String uploadId) {
ResultBean ret = new ResultBean();
aliIOssService.abortPartUpload(fileName, uploadId);
log.info("delete part fileName:{}, uploadId:{}", fileName, uploadId);
return ret;
}
private String getExtensionName(String fileName) {
if (!(null == fileName) || ("".equals(fileName))) {
int dot = fileName.lastIndexOf('.');
if ((dot > -1) && (dot < fileName.length() - 1)) {
return fileName.substring(dot + 1).trim();
}
}
return "";
}
}
AliIOssService代码
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Service
public class AliIOssService{
//改成你自己的
@Value("${oss.end_point}")
private String endpoint;
@Value("${oss.access_key}")
private String accessKey;
@Value("${oss.secret_key}")
private String secretKey;
@Value("${oss.bucket_name}")
private String bucketName;
public String getImgUrl() {
return imgUrl;
}
public void putOssObject(String objectName, byte[] bytes) {
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
ossClient.shutdown();
}
public void deleteOssObject(String objectName) {
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
try {
ossClient.deleteObject(bucketName, objectName);
} catch (Exception e) {
log.error("deleteOssObject, failed to delete object:{}, exception:{}.", objectName, e.getLocalizedMessage());
}
ossClient.shutdown();
return;
}
/**
* 初始分片上传
*
* @param objectName 文件名
* @return uploadId 它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
*/
public String initPartUpload(String objectName) {
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);
InitiateMultipartUploadResult result = ossClient.initiateMultipartUpload(request);
ossClient.shutdown();
// 返回uploadId,它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
return result.getUploadId();
}
/**
* 分片上传
*
* @param file 分片好的文件
* @param objectName 文件名
* @param uploadId
* @param partNumber 当前文件是第几片
* @param partSize 分片大小
* @return
*/
public PartETag PartUploader(MultipartFile file, String objectName, String uploadId, Integer partNumber, Long partSize) {
// 创建OSSClient实例。
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
PartETag partETag = null;
try {
// 创建UploadPartRequest,上传片块
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setBucketName(bucketName);
uploadPartRequest.setKey(objectName);
uploadPartRequest.setUploadId(uploadId);
uploadPartRequest.setInputStream(file.getInputStream());
// 设置分片大小。
uploadPartRequest.setPartSize(partSize);
// 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出这个范围,OSS将返回InvalidArgument的错误码。
uploadPartRequest.setPartNumber(partNumber);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
partETag = uploadPartResult.getPartETag();
log.info("part upload success, uploadId:{}, Number:{}, Size:{}, ETag:{}, CRC:{},", uploadId, partETag.getPartNumber(), partETag.getPartSize(), partETag.getETag(), partETag.getPartCRC());
} catch (Exception e) {
log.error("upload part error ,uploadId: {}, fileName:{}, partNumber:{}, partSize:{}, Exception:{}", uploadId, objectName, partNumber, partSize, e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return partETag;
}
/**
* 分片合并
*
* @param objectName 文件名
* @param uploadId
* @param partETags
*/
public void PartMerge(String objectName, String uploadId, List<PartETag> partETags) {
// 创建OSSClient实例。
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
// 获取已经上传的分片
/*ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, fileName, uploadId);
PartListing partListing = ossClient.listParts(listPartsRequest);
int partCount = partListing.getParts().size();
for (int i = 0; i < partCount; i++) {
PartSummary partSummary = partListing.getParts().get(i);
System.out.println("\tPart#" + partSummary.getPartNumber() + ", ETag=" + partSummary.getETag());
//PartETag partETag = new PartETag(partSummary.getPartNumber(), partSummary.getETag());
//partETags.add(partETag);
}*/
// 在执行该操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
CompleteMultipartUploadRequest completeMultipartUploadRequest =
new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);
ossClient.completeMultipartUpload(completeMultipartUploadRequest);
ossClient.shutdown();
//合并成功
log.info("part merge success fileName:{}", objectName);
}
/**
* 取消分片上传 并删除已上传的分片
*
* @param objectName 文件名
* @param uploadId
*/
public void abortPartUpload(String objectName, String uploadId) {
OSSClient ossClient = new OSSClient(endpoint, accessKey, secretKey);
// 取消分片上传,其中uploadId源自InitiateMultipartUpload。
AbortMultipartUploadRequest abortMultipartUploadRequest = new AbortMultipartUploadRequest(bucketName, objectName, uploadId);
ossClient.abortMultipartUpload(abortMultipartUploadRequest);
ossClient.shutdown();
}
}
ResultBean 代码
import java.util.HashMap;
import java.util.Map;
public class ResultBean {
public static final int SUCCESS = 0;
public static final int FAILED = 1;
private int retcode;
private String text;
private Map<String, Object> value = new HashMap<String, Object>();
public ResultBean() {
this.retcode = ResultBean.SUCCESS;
this.text = "";
}
public ResultBean(int code, String msg) {
this.retcode = code;
this.text = msg;
}
public static ResultBean success(){
return new ResultBean();
}
public static ResultBean failed(int code, String msg){
return new ResultBean(code, msg);
}
public int getRetcode() {
return retcode;
}
public void setRetcode(int retcode) {
this.retcode = retcode;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Map<String, Object> getValue() {
return value;
}
public void setValue(Map<String, Object> value) {
this.value = value;
}
public void addEntry(String key, Object object) {
this.value.put(key, object);
}
public void setError(int retcode, String text){
this.retcode = retcode;
this.text = text;
}
}
AliyunPartETag 代码
public class AliyunPartETag {
/**
* The result information for a part's upload in a multipart upload.
*/
private static final long serialVersionUID = 2471854027355307627L;
private int partNumber;
private String eTag;
private long partSize;
private Long partCRC;
public AliyunPartETag() {
}
/**
* Constructor
*
* @param partNumber Part number.
* @param eTag Part ETag.
*/
public AliyunPartETag(int partNumber, String eTag) {
this.partNumber = partNumber;
this.eTag = eTag;
}
/**
* Constructor
*
* @param partNumber Part number.
* @param eTag Part ETag.
* @param partSize Part Size.
* @param partCRC Part's CRC value.
*/
public AliyunPartETag(int partNumber, String eTag, long partSize, Long partCRC) {
this.partNumber = partNumber;
this.eTag = eTag;
this.partSize = partSize;
this.partCRC = partCRC;
}
/**
* Gets part number.
*
* @return Part number.
*/
public int getPartNumber() {
return partNumber;
}
/**
* Sets part number.
*
* @param partNumber Part number.
*/
public void setPartNumber(int partNumber) {
this.partNumber = partNumber;
}
public String geteTag() {
return eTag;
}
public void seteTag(String eTag) {
this.eTag = eTag;
}
public long getPartSize() {
return partSize;
}
public void setPartSize(long partSize) {
this.partSize = partSize;
}
public Long getPartCRC() {
return partCRC;
}
public void setPartCRC(Long partCRC) {
this.partCRC = partCRC;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((eTag == null) ? 0 : eTag.hashCode());
result = prime * result + partNumber;
return result;
}
}
前端(VUE)代码
注:前端用的是Element ui
<el-upload
ref="uploadVideo"
name="file"
list-type="mp4"
accept=".mp4"
action
:file-list="fileVideoList"
:http-request="handlePartUpload"
:on-remove="removeVideo"
:auto-upload="true"
:limit="1"
style="width: 400px"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">仅支持上传mp4文件(小于500M)</div>
</el-upload>
<div
v-show="updateLoading"
style="position: relative; font-size: 12px; margin-top: 10px; z-index: 10000; color:red"
>
在上传完成之前请勿刷新或关闭当前页面
</div>
## import 代码
import { partUpload } from '@/utils/partUpload' //代码在下面(改成自己的路径)
import {
fetchRemoveFile,
fetchRemovePart
} from '@/api/partUpload' //代码在下面(改成自己的路径)
## data 代码
updateLoading: false,
fileUploadData: null,
// 上传进度数据
uploadProgress: {
fileSize: 0, // 上传文件的总大小 单位:k
uploadSize: 0, // 已上传文件的大小 单位:k
status: 0 // 上传的状态 0 无上传文件,1 上传中, 2 上传成功,3 上传失败
},
## methods 代码
handlePartUpload(option) {
if (this.nullTxtFile) {
this.fileVideoList = []
return
}
this.updateLoading = true
const file = option.file
console.log(file, 'file')
this.uploadProgress.uploadSize = 0
this.uploadProgress.status = 0
partUpload({
file: file, // 视频实体
pieceSize: 2, // 每个分片的大小 MB
threadNumber: 2, // 上传线程数量,不建议大于 5,最好不要大于文件的总片数
init: data => { // 分片上传初始化 回调
this.fileUploadData = data
},
success: data => { // 分片上传成功回调
// option.onSuccess(data)
this.fileUploadData = data
// 获取后端生成的文件名
this.$set(this.temp, 'video', data.fileName)
this.fileVideoList.push({ name: data.fileName, url: data.fileUrl, status: 'success' })
this.$message.success('上传成功')
this.updateLoading = false
console.log('分片上传视频成功')
},
error: e => {
this.$message.error('上传失败,请重新上传')
console.log('分片上传视频失败')
this.updateLoading = false
},
progress: data => { // 上传进度回调
this.uploadProgress = data
option.onProgress({ percent: data.uploadSize / data.fileSize * 100 })// 更新进度条
// 视频上传失败
if (data.status == 3) {
option.onError('上传失败')
this.updateLoading = false
// 视频上传成功
} else if (data.status == 2) {
this.updateLoading = false
}
}
})
},
removeVideo(file, fileList) {
// 视频分片上传中
if (this.uploadProgress.status == 1) {
// 删除上传的分片
fetchRemovePart(this.fileUploadData.fileName, this.fileUploadData.uploadId).then(res => {
})
}
if (this.fileVideoList.length > 0 && this.fileVideoList[0].status == 'success') {
// 视频上传完成
if (this.uploadProgress.status == 2) {
// 删除文件
fetchRemoveFile(this.fileVideoList[0].name).then(res => {
})
}
}
this.updateLoading = false
this.temp.video = ''
this.fileVideoList = []
},
/utils/partUpload.js 代码
记得先导包 npm install js-md5 --save
import md5 from 'js-md5'
import { uploadPartInit, uploadPartStart, uploadPartMerge } from '@/api/partUpload'
/**
* 分片上传(多线程上传 使用Promise实现)
* @param file 要上传的文件
* @param pieceSize 每一片文件的大小 MB 为单位
* @threadNumber 上传的线程数 最大不建议超过 5 (线程数请勿大于总片数 否则 都只有会有一个线程上传)
* @param success 上传成功回调 返回 fileMD5: 文件的MD5值, fileName: 后端生成的文件名, fileUrl: 文件URL , uploadId:oss生成的 uploadId
* @param error 上传失败回调
* @param progress 文件分片上传进度回调 详见变量 progressData 上面的注释
* @param init 初始化分片上传成功回调 返回 fileName: 后端生成的文件名, fileMD5: 文件的MD5, uploadId:oss生成的 uploadId
*/
export const partUpload = ({ file, pieceSize = 2, threadNumber = 2, success, error, progress, init }) => {
/**
* 上传的进度和状态
* @fileSize 上传文件的总大小 单位:K
* @uploadSize 已经上传文件的大小 单位:K
* @status 上传的状态 1:上传中 , 2:上传成功 , 3:上传失败
*/
const progressData = {
fileSize: file.size,
uploadSize: 0,
status: 1
}
if (!file || !file.size) {
progressData.status = 3
progress && progress(progressData)
error && error('无效的文件')
return
}
// 上传过程中用到的变量
let fileMD5 = ''// 总文件列表
const partSize = pieceSize * 1024 * 1024 // 多少MB一片
const partCount = Math.ceil(file.size / partSize) // 总片数
let uploadId = '' // uploadId它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
let fileName = '' // 文件名
let successPartNumber = 0// 上传成功的分片数量
// 合并分片时返回给后端的数据
const partETagList = []
const threadList = [] // 多线程上传开始和结束的分片位置
for (let i = 0; i < threadNumber - 1; i++) {
// 分片开始位置 和 分片结束位置
threadList.push({ start: parseInt(partCount / threadNumber) * i, end: parseInt(partCount / threadNumber) * (i + 1) })
}
// 最后一个线程的分片开始位置 和 分片结束位置
threadList.push({ start: threadList[threadList.length - 1].end ? threadList[threadList.length - 1].end : 0, end: partCount })
// 获取md5
const readFileMD5 = () => {
progress && progress(progressData)
// 读取文件的md5
// console.log('获取文件的MD5值')
const fileRederInstance = new FileReader()
fileRederInstance.readAsBinaryString(file)
fileRederInstance.addEventListener('load', e => {
const fileBolb = e.target.result
fileMD5 = md5(fileBolb)
// 初始文件上传 获取 uploadId:
uploadPartInit({ 'md5': fileMD5, 'fileName': file.name }).then(res => {
// uploadId它是分片上传事件的唯一标识,可以根据这个ID来发起相关的操作,如取消分片上传、查询分片上传等。
uploadId = res.value.uploadId
// 后端生成的文件名
fileName = res.value.fileName
init && init({ fileName: fileName, fileMD5: fileMD5, uploadId: uploadId })
// console.log(`文件开始上传 uploadId:${uploadId}, fileName: ${fileName}`)
for (const thread of threadList) {
readChunkMD5(thread.start, thread.end)
}
}).catch((e) => {
progressData.status = 3
progress && progress(progressData)
error && error(e)
// console.log('初始化分片上传失败')
})
})
}
const getChunkInfo = (file, currentChunk, chunkSize) => {
const start = currentChunk * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
return { start, end, part: chunk }
}
// 针对每个文件进行chunk处理
async function readChunkMD5(start, end) {
// 针对单个文件进行chunk上传
for (let i = start; i < end; i++) {
const { part } = getChunkInfo(file, i, partSize)
// console.log('总片数' + partCount)
// console.log('分片后的数据---测试:' + i)
// console.log(part)
// 阻塞 等待此分片上传完成再上传下一片
await uploadChunk({ part: part, partNumber: i, partCount: partCount })
}
}
const uploadChunk = (chunkInfo) => {
return new Promise((resolve, reject) => {
// progressFun()
const fetchForm = new FormData()
fetchForm.append('partCount', chunkInfo.partCount)
fetchForm.append('partNumber', chunkInfo.partNumber + 1)
fetchForm.append('partSize', chunkInfo.part.size)
fetchForm.append('file', chunkInfo.part)
fetchForm.append('md5', fileMD5)
fetchForm.append('name', fileName)
fetchForm.append('uploadId', uploadId)
uploadPartStart(fetchForm, chunkInfo.partNumber + 1).then(res => {
// 上传成功分片数量
successPartNumber++
// 文件上传进度
progressData.uploadSize += chunkInfo.part.size
progress && progress(progressData)
// console.log('分片上传返回信息:' + res.value)
partETagList.push(res.value.partETag)
if (res.retcode == 0) {
// success && success(res)
// 当上传成功分片数 大于或等于 总分片数时
if (successPartNumber >= chunkInfo.partCount) {
// console.log('文件开始合并')
uploadPartMerge({
md5: fileMD5,
name: fileName,
uploadId: uploadId,
partETagList: partETagList
}).then(res => {
if (res.retcode == 0) {
progressData.status = 2
progress && progress(progressData)
// 上传成功回调
success && success({
fileMD5: fileMD5,
fileName: fileName,
fileUrl: res.value.url,
uploadId: uploadId
})
// console.log('文件合并成功')
} else {
error && error('文件合并失败')
// console.log(res.text)
}
}).catch((e) => {
progressData.status = 3
error && error(e)
progress && progress(progressData)
// console.log('文件合并错误')
})
}
} else {
error && error('上传失败')
progressData.status = 3
progress && progress(progressData)
// console.log(res.text)
}
resolve()
}).catch((e) => {
throw new Error('上传失败, 文件被移除')
})
})
}
readFileMD5() // 开始执行代码
}
/api/partUpload.js 代码
import request from '@/utils/request'
export function uploadPartInit(data) {
return request({
url: 'upload/part/init',
method: 'post',
params: data
})
}
export function uploadPartStart(data, partNumber) {
return request({
url: 'upload/part/' + partNumber,
method: 'post',
data
})
}
export function uploadPartMerge(data) {
return request({
url: 'upload/part/merge',
method: 'post',
params: {
'uploadId': data.uploadId,
'fileName': data.name
},
data: data.partETagList
})
}
export function fetchRemoveFile(fileName) {
return request({
url: '/upload/delete/file',
method: 'post',
params: {
fileName: fileName
}
})
}
export function fetchRemovePart(fileName, uploadId) {
return request({
url: '/upload/delete/part',
method: 'post',
params: {
fileName: fileName,
uploadId: uploadId
}
})
}
前端分片上传部分代码参考(没错ctrl C V工程师就是在下了) 点击前往
Q.E.D.