背景:
前端上傳音視訊檔案過大大于100MB。讨論後決定采用oss分片上傳。
業務流程:
前端先調用一次初始化接口拿到本次分片任務的唯一分片id。前端負責分片,傳參:總片數、第幾片,唯一分片id等資料,這些需要傳給背景,背景才能夠以此判斷。下面是demo:
導maven包:注意需要3以上的版本
<!-- 阿裡雲對象存儲服務 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
配置:
####################################### 阿裡雲對象存儲配置 #######################################
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
業務實作:使用了ossUtil工具類
/**
* 我們先初始化拿到分片唯一ID,傳回給前端
* @param param
* @return
*/
@ApiOperation("oss初始化分片")
@PostMapping("/initTest")
public UmsAdminLoginLogDO testInitControl(@RequestBody UmsAdminLoginLogDO param) {
//分片上傳
UmsAdminLoginLogDO result = new UmsAdminLoginLogDO();
// 生成任務id
String taskId = UUID.randomUUID().toString().replaceAll("-", "");
result.setTaskId(taskId);
//生成任務名稱,建議使用各種ID拼接
String taskKey = param.getFileName() + taskId;
// 請求阿裡雲oss擷取分片唯一ID
String ossSlicesId = ossUtil.getUploadId(taskKey);
result.setOssSlicesId(ossSlicesId);
//每一片的大小
result.setMinSliceSize("100k");
redisUtil.set(ossSlicesId,result);
return result;
}
分片上傳:
/**
* 有些必傳的參數比如分片id,總片數,第幾片,檔案流資料源
* @param param
* @throws Exception
*/
@ApiOperation("oss分片上傳")
@PostMapping("/uploadTest")
public void testControl(@RequestBody UmsAdminLoginLogDO param) throws Exception {
//必須求出redis中的PartETags,在分片合成檔案中需要以此為依據,合并檔案傳回最終位址
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();
//位元組流轉換
InputStream inputStream = new ByteArrayInputStream(param.getContent());
Map<Integer, PartETag> partETags = param.getPartETags();
//分片上傳
try {
//每次上傳分片之後,OSS的傳回結果會包含一個PartETag
PartETag partETag = ossUtil.partUploadFile(param.getFileName(), inputStream, ossSlicesId,
param.getFileMD5(), param.getSliceNo(), param.getContent().length);
partETags.put(param.getSliceNo(), partETag);
//分片編号等于總片數的時候合并檔案,如果符合條件則合并檔案,否則繼續等待
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());
}
}
工具類:
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);
}
/**
* 分塊上傳完成擷取結果
*/
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 檔案流資料
* @param uploadId oss唯一分片id
* @param fileMd5 檔案的md5值(非必傳)
* @param partNum 第幾片
* @param partSize 總片數
* @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();
}
/**
* 分塊上傳完成擷取結果
*/
public String getUploadId(String fileKey) {
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, fileKey);
// 初始化分片
InitiateMultipartUploadResult unrest = ossClient.initiateMultipartUpload(request);
// 傳回uploadId,它是分片上傳事件的唯一辨別,您可以根據這個ID來發起相關的操作,如取消分片上傳、查詢分片上傳等。
String uploadId = unrest.getUploadId();
return uploadId;
}
/**
* 擷取bucket檔案的下載下傳連結
*
* @param pathFile 首字母不帶/的路徑和檔案
* @param bucketName
* @return 上報傳回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 檔案位元組流
* @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 {
// 建立上傳Object的Metadata
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(in.available());
objectMetadata.setCacheControl("no-cache");// 設定Cache-Control請求頭,表示使用者指定的HTTP請求/回複鍊的緩存行為:不經過本地緩存
objectMetadata.setHeader("Pragma", "no-cache");// 設定頁面不緩存
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服務檔案上傳時檔案的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";
}
}
}
實體類對象:
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 {
/**
* 初始化任務id
*/
private String taskId;
/**
* 上傳檔案類型
*/
private String fileType;
/**
* 檔案總片數
*/
private Integer fileSlicesNum;
/**
* 分片編号(1-10000有序的編号,越大的編号位置越靠後)
*/
private Integer sliceNo;
/**
* 本次請求檔案的md5值
*/
private String fileMD5;
/**
*檔案流資料
*/
private byte[] content;
/**
* 檔案名稱
*/
private String fileName;
/**
* oss初始化分片id
*/
private String ossSlicesId;
/**
* 最小分片大小(分片上傳是除最後一片外,其他檔案不得小于該值)
*/
private String minSliceSize;
Map<Integer, PartETag> partETags = new HashMap<>(16);
}
檔案流資料:content,可以換成file等類型,最後轉換成oss所需檔案流即可,合格的程式員應當學會靈活應變相關代碼,哈哈哈。
結語:其實分片上傳和普通的上傳隻是多了一個合并檔案的步驟,其他的都是差不多;因為研究時間較短,還有些資料沒有查出,比如PartETag這代表含義等。有需要補充的歡迎在下面補充。
創作不易,如果這篇文章對你有用,請點個贊謝謝♪(・ω・)ノ!