背景:
前端上傳音視頻文件過(guò)大大于100MB。討論后決定采用oss分片上傳。
業(yè)務(wù)流程:
前端先調(diào)用一次初始化接口拿到本次分片任務(wù)的唯一分片id。前端負(fù)責(zé)分片,傳參:總片數(shù)、第幾片,唯一分片id等數(shù)據(jù),這些需要傳給后臺(tái),后臺(tái)才能夠以此判斷。下面是demo:
導(dǎo)maven包:注意需要3以上的版本
<!-- 阿里云對(duì)象存儲(chǔ)服務(wù) -->
<dependency>
? ? <groupId>com.aliyun.oss</groupId>
? ? <artifactId>aliyun-sdk-oss</artifactId>
? ? <version>3.15.1</version>
</dependency>
配置:
####################################### 阿里云對(duì)象存儲(chǔ)配置 #######################################
oss.endpoint.ext = oss-cn-zhangjiakou.aliyuncs.com
oss.endpoint.internal = oss-cn-zhangjiakou.aliyuncs.com
oss.accessKeyId = LTAI4FcG6N5FEUt38DQdHGE1
oss.accessKeySecret = KEBGjLMlLLvMLMi2FQ1GRZ825rUywh
oss.bucketName = dev-iot-services-public
業(yè)務(wù)實(shí)現(xiàn):使用了ossUtil工具類(lèi)
/**
? ? * 我們先初始化拿到分片唯一ID,返回給前端
? ? * @param param
? ? * @return
? ? */
? ? @ApiOperation("oss初始化分片")
? ? @PostMapping("/initTest")
? ? public UmsAdminLoginLogDO testInitControl(@RequestBody UmsAdminLoginLogDO param) {
? ? ? ? //分片上傳
? ? ? ? UmsAdminLoginLogDO result = new UmsAdminLoginLogDO();
? ? ? ? // 生成任務(wù)id
? ? ? ? String taskId = UUID.randomUUID().toString().replaceAll("-", "");
? ? ? ? result.setTaskId(taskId);
? ? ? ? //生成任務(wù)名稱,建議使用各種ID拼接
? ? ? ? String taskKey = param.getFileName() + taskId;
? ? ? ? // 請(qǐng)求阿里云oss獲取分片唯一ID
? ? ? ? String ossSlicesId = ossUtil.getUploadId(taskKey);
? ? ? ? result.setOssSlicesId(ossSlicesId);
? ? ? ? //每一片的大小
? ? ? ? result.setMinSliceSize("100k");
? ? ? ? redisUtil.set(ossSlicesId,result);
? ? ? ? return result;
? ? }
分片上傳:
/**
* 有些必傳的參數(shù)比如分片id,總片數(shù),第幾片,文件流數(shù)據(jù)源
* @param param
* @throws Exception
*/
@ApiOperation("oss分片上傳")
@PostMapping("/uploadTest")
public void testControl(@RequestBody UmsAdminLoginLogDO param) throws Exception {
? ? //必須求出redis中的PartETags,在分片合成文件中需要以此為依據(jù),合并文件返回最終地址
? ? UmsAdminLoginLogDO redisParam = (UmsAdminLoginLogDO) redisUtil.get(param.getOssSlicesId());
? ? if (redisParam !=null) {
? ? ? ? param.setPartETags(redisParam.getPartETags());
? ? }
? ? int sliceNo = param.getSliceNo();
? ? int fileSlicesNum = param.getFileSlicesNum();
? ? String ossSlicesId = param.getOssSlicesId();
? ? //字節(jié)流轉(zhuǎn)換
? ? InputStream inputStream = new ByteArrayInputStream(param.getContent());
? ? Map<Integer, PartETag> partETags = param.getPartETags();
? ? //分片上傳
? ? try {
? ? ? ? //每次上傳分片之后,OSS的返回結(jié)果會(huì)包含一個(gè)PartETag
? ? ? ? PartETag partETag = ossUtil.partUploadFile(param.getFileName(), inputStream, ossSlicesId,
? ? ? ? ? ? ? ? param.getFileMD5(), param.getSliceNo(), param.getContent().length);
? ? ? ? partETags.put(param.getSliceNo(), partETag);
? ? ? ? //分片編號(hào)等于總片數(shù)的時(shí)候合并文件,如果符合條件則合并文件,否則繼續(xù)等待
? ? ? ? if (fileSlicesNum==sliceNo) {
? ? ? ? ? ? //合并文件,注意:partETags必須是所有分片的所以必須存入redis,然后取出放入集合
? ? ? ? ? ? String url = ossUtil.completePartUploadFile(param.getFileName(), ossSlicesId,
? ? ? ? ? ? ? ? ? ? new ArrayList<>(partETags.values()));
? ? ? ? ? ? //oss地址返回后存入并清除redis
? ? ? ? ? ? param.setFileUrl(url);
? ? ? ? ? ? redisUtil.del(ossSlicesId);
? ? ? ? }else {
? ? ? ? ? ? redisUtil.set(param.getOssSlicesId(), param);
? ? ? ? }
? ? } catch (Exception e) {
? ? ? ? throw new Exception(ErrorCodeEnum.SYSTEM_ERROR.getMsg());
? ? }
}
工具類(lèi):
package com.macro.mall.tiny.demo.utils;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author zhangtonghao
* @create 2022-08-30 16:26
*/
@Component
public class OSSUtil {
? ? private static Logger logger = LoggerFactory.getLogger(OSSUtil.class);
? ? // private OSSClient ossClient;
? ? @Value("${oss.endpoint.ext}")
? ? private String endpoint;
? ? @Value("${oss.endpoint.internal}")
? ? private String internalEndpoint;
? ? @Value("${oss.accessKeyId}")
? ? private String accessKeyId;
? ? @Value("${oss.accessKeySecret}")
? ? private String accessKeySecret;
? ? @Value("${oss.bucketName}")
? ? private String bucketName;
? ? private OSS ossClient;
? ? @PostConstruct
? ? public void init() {
? ? ? ? ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
? ? }
? ? /**
? ? * 分塊上傳完成獲取結(jié)果
? ? */
? ? public String completePartUploadFile(String fileKey, String uploadId, List<PartETag> partETags) {
? ? ? ? CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketName, fileKey, uploadId,
? ? ? ? ? ? ? ? partETags);
? ? ? ? ossClient.completeMultipartUpload(request);
? ? ? ? String downLoadUrl = getDownloadUrl(fileKey, bucketName);
? ? ? ? logger.debug("-------------- 文件的下載URL ------------" + downLoadUrl);
? ? ? ? return downLoadUrl;
? ? }
? ? /**
? ? *
? ? * @param fileKey? 文件名稱
? ? * @param is? 文件流數(shù)據(jù)
? ? * @param uploadId oss唯一分片id
? ? * @param fileMd5 文件的md5值(非必傳)
? ? * @param partNum? 第幾片
? ? * @param partSize 總片數(shù)
? ? * @return
? ? */
? ? public PartETag partUploadFile(String fileKey, InputStream is, String uploadId, String fileMd5, int partNum,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? long partSize) {
? ? ? ? UploadPartRequest uploadPartRequest = new UploadPartRequest();
? ? ? ? uploadPartRequest.setBucketName(bucketName);
? ? ? ? uploadPartRequest.setUploadId(uploadId);
? ? ? ? uploadPartRequest.setPartNumber(partNum);
? ? ? ? uploadPartRequest.setPartSize(partSize);
? ? ? ? uploadPartRequest.setInputStream(is);
? ? ? ? uploadPartRequest.setKey(fileKey);
? ? ? ? uploadPartRequest.setMd5Digest(fileMd5);
? ? ? ? UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
? ? ? ? return uploadPartResult.getPartETag();
? ? }
? ? /**
? ? * 分塊上傳完成獲取結(jié)果
? ? */
? ? public String getUploadId(String fileKey) {
? ? ? ? InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, fileKey);
? ? ? ? // 初始化分片
? ? ? ? InitiateMultipartUploadResult unrest = ossClient.initiateMultipartUpload(request);
? ? ? ? // 返回uploadId,它是分片上傳事件的唯一標(biāo)識(shí),您可以根據(jù)這個(gè)ID來(lái)發(fā)起相關(guān)的操作,如取消分片上傳、查詢分片上傳等。
? ? ? ? String uploadId = unrest.getUploadId();
? ? ? ? return uploadId;
? ? }
? ? /**
? ? * 獲取bucket文件的下載鏈接
? ? *
? ? * @param pathFile? 首字母不帶/的路徑和文件
? ? * @param bucketName
? ? * @return 上報(bào)返回null, 成功返回地址
? ? */
? ? public String getDownloadUrl(String pathFile, String bucketName) {
? ? ? ? if (bucketName == null || "".equals(bucketName)) {
? ? ? ? ? ? bucketName = bucketName;
? ? ? ? }
? ? ? ? StringBuffer url = new StringBuffer();
? ? ? ? url.append("http://").append(bucketName).append(endpoint).append("/");
? ? ? ? if (pathFile != null && !"".equals(pathFile)) {
? ? ? ? ? ? url.append(pathFile);
? ? ? ? }
? ? ? ? return url.toString();
? ? }
? ? /**
? ? * 上傳文件到阿里云,并生成url
? ? *
? ? * @param filedir (key)文件名(不包括后綴)
? ? * @param in? ? ? 文件字節(jié)流
? ? * @return String 生成的文件url
? ? */
? ? public String uploadToAliyun(String filedir, InputStream in, String fileName, boolean isRandomName) {
? ? ? ? String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
? ? ? ? if (isRandomName) {
? ? ? ? ? ? fileName = UUIDGenerator.generateCommonUUID() + "." + suffix;
? ? ? ? }
? ? ? ? logger.debug("------------>文件名稱為:? " + fileName);
? ? ? ? OSSClient ossClient = new OSSClient(internalEndpoint, accessKeyId, accessKeySecret);
? ? ? ? String url = null;
? ? ? ? try {
? ? ? ? ? ? // 創(chuàng)建上傳Object的Metadata
? ? ? ? ? ? ObjectMetadata objectMetadata = new ObjectMetadata();
? ? ? ? ? ? objectMetadata.setContentLength(in.available());
? ? ? ? ? ? objectMetadata.setCacheControl("no-cache");// 設(shè)置Cache-Control請(qǐng)求頭,表示用戶指定的HTTP請(qǐng)求/回復(fù)鏈的緩存行為:不經(jīng)過(guò)本地緩存
? ? ? ? ? ? objectMetadata.setHeader("Pragma", "no-cache");// 設(shè)置頁(yè)面不緩存
? ? ? ? ? ? objectMetadata.setContentType(getcontentType(suffix));
? ? ? ? ? ? objectMetadata.setContentDisposition("inline;filename=" + fileName);
? ? ? ? ? ? // 上傳文件
? ? ? ? ? ? ossClient.putObject(bucketName, filedir + "/" + fileName, in, objectMetadata);
? ? ? ? ? ? url = buildUrl(filedir + "/" + fileName);
? ? ? ? } catch (IOException e) {
? ? ? ? ? ? logger.error("error", e);
? ? ? ? } finally {
? ? ? ? ? ? ossClient.shutdown();
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? if (in != null) {
? ? ? ? ? ? ? ? ? ? in.close();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } catch (IOException e) {
? ? ? ? ? ? ? ? logger.error("error", e);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return url;
? ? }
? ? private String buildUrl(String fileDir) {
? ? ? ? StringBuffer url = new StringBuffer();
? ? ? ? if (org.apache.commons.lang3.StringUtils.isEmpty(bucketName)) {
? ? ? ? ? ? logger.error("bucketName為空");
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? if (org.apache.commons.lang3.StringUtils.isEmpty(endpoint)) {
? ? ? ? ? ? logger.error("endpoint為空");
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? if (StringUtils.isEmpty(endpoint)) {
? ? ? ? ? ? logger.error("上傳文件目錄為空");
? ? ? ? ? ? return null;
? ? ? ? }
? ? ? ? url.append("https://").append(bucketName).append(".").append(endpoint).append("/").append(fileDir);
? ? ? ? return url.toString();
? ? }
? ? /**
? ? * 刪除圖片
? ? *
? ? * @param key
? ? */
? ? public void deletePicture(String key) {
? ? ? ? OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
? ? ? ? ossClient.deleteObject(bucketName, key);
? ? ? ? ossClient.shutdown();
? ? }
? ? /**
? ? * Description: 判斷OSS服務(wù)文件上傳時(shí)文件的contentType
? ? *
? ? * @param suffix 文件后綴
? ? * @return String HTTP Content-type
? ? */
? ? public String getcontentType(String suffix) {
? ? ? ? if (suffix.equalsIgnoreCase("bmp")) {
? ? ? ? ? ? return "image/bmp";
? ? ? ? } else if (suffix.equalsIgnoreCase("gif")) {
? ? ? ? ? ? return "image/gif";
? ? ? ? } else if (suffix.equalsIgnoreCase("jpeg") || suffix.equalsIgnoreCase("jpg")) {
? ? ? ? ? ? return "image/jpeg";
? ? ? ? } else if (suffix.equalsIgnoreCase("png")) {
? ? ? ? ? ? return "image/png";
? ? ? ? } else if (suffix.equalsIgnoreCase("html")) {
? ? ? ? ? ? return "text/html";
? ? ? ? } else if (suffix.equalsIgnoreCase("txt")) {
? ? ? ? ? ? return "text/plain";
? ? ? ? } else if (suffix.equalsIgnoreCase("vsd")) {
? ? ? ? ? ? return "application/vnd.visio";
? ? ? ? } else if (suffix.equalsIgnoreCase("pptx") || suffix.equalsIgnoreCase("ppt")) {
? ? ? ? ? ? return "application/vnd.ms-powerpoint";
? ? ? ? } else if (suffix.equalsIgnoreCase("docx") || suffix.equalsIgnoreCase("doc")) {
? ? ? ? ? ? return "application/msword";
? ? ? ? } else if (suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx")) {
? ? ? ? ? ? return "application/vnd.ms-excel";
? ? ? ? } else if (suffix.equalsIgnoreCase("xml")) {
? ? ? ? ? ? return "text/xml";
? ? ? ? } else if (suffix.equalsIgnoreCase("mp3")) {
? ? ? ? ? ? return "audio/mp3";
? ? ? ? } else if (suffix.equalsIgnoreCase("amr")) {
? ? ? ? ? ? return "audio/amr";
? ? ? ? } else if (suffix.equalsIgnoreCase("pdf")) {
? ? ? ? ? ? return "application/pdf";
? ? ? ? } else {
? ? ? ? ? ? return "text/plain";
? ? ? ? }
? ? }
}
實(shí)體類(lèi)對(duì)象:
package com.macro.mall.tiny.demo.model.po.mall;
import com.aliyun.oss.model.PartETag;
import lombok.Data;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author zhangtonghao
* @create 2022-04-06 15:08
*/
@Data
public class UmsAdminLoginLogDO {
? ? /**
? ? * 初始化任務(wù)id
? ? */
? ? private String taskId;
? ? /**
? ? * 上傳文件類(lèi)型
? ? */
? ? private String fileType;
? ? /**
? ? * 文件總片數(shù)
? ? */
? ? private Integer fileSlicesNum;
? ? /**
? ? * 分片編號(hào)(1-10000有序的編號(hào),越大的編號(hào)位置越靠后)
? ? */
? ? private Integer sliceNo;
? ? /**
? ? * 本次請(qǐng)求文件的md5值
? ? */
? ? private String fileMD5;
? ? /**
? ? *文件流數(shù)據(jù)
? ? */
? ? private byte[] content;
? ? /**
? ? * 文件名稱
? ? */
? ? private String fileName;
? ? /**
? ? * oss初始化分片id
? ? */
? ? private String ossSlicesId;
? ? /**
? ? * 最小分片大?。ǚ制蟼魇浅詈笠黄猓渌募坏眯∮谠撝担?/p>
? ? */
? ? private String minSliceSize;
? ? Map<Integer, PartETag> partETags = new HashMap<>(16);
}
文件流數(shù)據(jù):content,可以換成file等類(lèi)型,最后轉(zhuǎn)換成oss所需文件流即可,合格的程序員應(yīng)當(dāng)學(xué)會(huì)靈活應(yīng)變相關(guān)代碼,哈哈哈。
結(jié)語(yǔ):其實(shí)分片上傳和普通的上傳只是多了一個(gè)合并文件的步驟,其他的都是差不多;因?yàn)檠芯繒r(shí)間較短,還有些資料沒(méi)有查出,比如PartETag這代表含義等。有需要補(bǔ)充的歡迎在下面補(bǔ)充。
創(chuàng)作不易,如果這篇文章對(duì)你有用,請(qǐng)點(diǎn)個(gè)贊謝謝?(?ω?)?!