|
@@ -0,0 +1,366 @@
|
|
|
|
|
+package com.poyee.util;
|
|
|
|
|
+
|
|
|
|
|
+import com.poyee.config.TencentCosConfig;
|
|
|
|
|
+import com.qcloud.cos.COSClient;
|
|
|
|
|
+import com.qcloud.cos.ClientConfig;
|
|
|
|
|
+import com.qcloud.cos.auth.BasicCOSCredentials;
|
|
|
|
|
+import com.qcloud.cos.auth.COSCredentials;
|
|
|
|
|
+import com.qcloud.cos.auth.COSSigner;
|
|
|
|
|
+import com.qcloud.cos.model.ObjectMetadata;
|
|
|
|
|
+import com.qcloud.cos.model.PutObjectRequest;
|
|
|
|
|
+import com.qcloud.cos.model.PutObjectResult;
|
|
|
|
|
+import com.qcloud.cos.region.Region;
|
|
|
|
|
+import com.qcloud.cos.utils.Jackson;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.apache.commons.codec.binary.Base64;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 腾讯云COS工具类
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author: zheng
|
|
|
|
|
+ * @date: 2026/01/26
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+public class TencentCosUtil {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private TencentCosConfig tencentCosConfig;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取COS客户端
|
|
|
|
|
+ */
|
|
|
|
|
+ private COSClient getCosClient() {
|
|
|
|
|
+ // 1 初始化用户身份信息(secretId, secretKey)
|
|
|
|
|
+ COSCredentials cred = new BasicCOSCredentials(tencentCosConfig.getSecretId(), tencentCosConfig.getSecretKey());
|
|
|
|
|
+ // 2 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
|
|
|
|
|
+ ClientConfig clientConfig = new ClientConfig(new Region(tencentCosConfig.getRegion()));
|
|
|
|
|
+ // 3 生成cos客户端
|
|
|
|
|
+ return new COSClient(cred, clientConfig);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param file 文件
|
|
|
|
|
+ * @return 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public String uploadFile(MultipartFile file) throws IOException {
|
|
|
|
|
+ return uploadFile(file, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param file 文件
|
|
|
|
|
+ * @param folder 文件夹名称,可为空
|
|
|
|
|
+ * @return 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public String uploadFile(MultipartFile file, String folder) throws IOException {
|
|
|
|
|
+ // 检查文件大小
|
|
|
|
|
+ if (file.getSize() > tencentCosConfig.getMaxFileSize()) {
|
|
|
|
|
+ throw new IOException("文件大小超过限制,最大允许" + (tencentCosConfig.getMaxFileSize() / 1024 / 1024) + "MB");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查文件类型
|
|
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
|
|
+ if (originalFilename == null) {
|
|
|
|
|
+ throw new IOException("文件名不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String fileExtension = getFileExtension(originalFilename);
|
|
|
|
|
+ if (!isAllowedFileType(fileExtension)) {
|
|
|
|
|
+ throw new IOException("不支持的文件类型:" + fileExtension);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生成文件路径
|
|
|
|
|
+ String fileName = generateFileName(fileExtension);
|
|
|
|
|
+ String filePath = tencentCosConfig.getPrefix();
|
|
|
|
|
+ if (folder != null && !folder.isEmpty()) {
|
|
|
|
|
+ filePath += folder + "/";
|
|
|
|
|
+ }
|
|
|
|
|
+ filePath += fileName;
|
|
|
|
|
+
|
|
|
|
|
+ COSClient cosClient = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ cosClient = getCosClient();
|
|
|
|
|
+ // 设置文件元数据
|
|
|
|
|
+ ObjectMetadata metadata = new ObjectMetadata();
|
|
|
|
|
+ metadata.setContentLength(file.getSize());
|
|
|
|
|
+ metadata.setContentType(file.getContentType());
|
|
|
|
|
+
|
|
|
|
|
+ // 上传文件
|
|
|
|
|
+ PutObjectRequest putObjectRequest = new PutObjectRequest(
|
|
|
|
|
+ tencentCosConfig.getBucketName(), filePath, file.getInputStream(), metadata);
|
|
|
|
|
+ PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
|
|
|
|
+
|
|
|
|
|
+ if (putObjectResult.getETag() != null) {
|
|
|
|
|
+ // 返回文件访问路径
|
|
|
|
|
+ return getFileUrl(filePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new IOException("文件上传失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (cosClient != null) {
|
|
|
|
|
+ cosClient.shutdown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件流
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param inputStream 文件流
|
|
|
|
|
+ * @param fileName 文件名
|
|
|
|
|
+ * @return 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public String uploadFile(InputStream inputStream, String fileName) throws IOException {
|
|
|
|
|
+ return uploadFile(inputStream, fileName, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件流
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param inputStream 文件流
|
|
|
|
|
+ * @param fileName 文件名
|
|
|
|
|
+ * @param folder 文件夹名称,可为空
|
|
|
|
|
+ * @return 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public String uploadFile(InputStream inputStream, String fileName, String folder) throws IOException {
|
|
|
|
|
+ // 检查文件类型
|
|
|
|
|
+ String fileExtension = getFileExtension(fileName);
|
|
|
|
|
+ if (!isAllowedFileType(fileExtension)) {
|
|
|
|
|
+ throw new IOException("不支持的文件类型:" + fileExtension);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 生成文件路径
|
|
|
|
|
+ String newFileName = generateFileName(fileExtension);
|
|
|
|
|
+ String filePath = tencentCosConfig.getPrefix();
|
|
|
|
|
+ if (folder != null && !folder.isEmpty()) {
|
|
|
|
|
+ filePath += folder + "/";
|
|
|
|
|
+ }
|
|
|
|
|
+ filePath += newFileName;
|
|
|
|
|
+
|
|
|
|
|
+ COSClient cosClient = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ cosClient = getCosClient();
|
|
|
|
|
+ // 设置文件元数据
|
|
|
|
|
+ ObjectMetadata metadata = new ObjectMetadata();
|
|
|
|
|
+ metadata.setContentType(getContentType(fileExtension));
|
|
|
|
|
+
|
|
|
|
|
+ // 上传文件
|
|
|
|
|
+ PutObjectRequest putObjectRequest = new PutObjectRequest(
|
|
|
|
|
+ tencentCosConfig.getBucketName(), filePath, inputStream, metadata);
|
|
|
|
|
+ PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
|
|
|
|
+
|
|
|
|
|
+ if (putObjectResult.getETag() != null) {
|
|
|
|
|
+ // 返回文件访问路径
|
|
|
|
|
+ return getFileUrl(filePath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new IOException("文件上传失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (cosClient != null) {
|
|
|
|
|
+ cosClient.shutdown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除文件
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileUrl 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ public void deleteFile(String fileUrl) {
|
|
|
|
|
+ if (fileUrl == null || fileUrl.isEmpty()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从URL中提取文件路径
|
|
|
|
|
+ String filePath = getFilePathFromUrl(fileUrl);
|
|
|
|
|
+ if (filePath == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ COSClient cosClient = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ cosClient = getCosClient();
|
|
|
|
|
+ cosClient.deleteObject(tencentCosConfig.getBucketName(), filePath);
|
|
|
|
|
+ log.info("删除文件成功: {}", fileUrl);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("删除文件失败: {}", fileUrl, e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (cosClient != null) {
|
|
|
|
|
+ cosClient.shutdown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成文件名
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileExtension 文件扩展名
|
|
|
|
|
+ * @return 文件名
|
|
|
|
|
+ */
|
|
|
|
|
+ private String generateFileName(String fileExtension) {
|
|
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
|
|
+ String dateStr = sdf.format(new Date());
|
|
|
|
|
+ String uuid = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ return dateStr + "/" + uuid + "." + fileExtension;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取文件扩展名
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileName 文件名
|
|
|
|
|
+ * @return 文件扩展名
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFileExtension(String fileName) {
|
|
|
|
|
+ if (fileName == null || fileName.isEmpty()) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ int dotIndex = fileName.lastIndexOf('.');
|
|
|
|
|
+ if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ return fileName.substring(dotIndex + 1).toLowerCase();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查文件类型是否允许
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileExtension 文件扩展名
|
|
|
|
|
+ * @return 是否允许
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isAllowedFileType(String fileExtension) {
|
|
|
|
|
+ if (fileExtension == null || fileExtension.isEmpty()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String type : tencentCosConfig.getAllowFileTypes()) {
|
|
|
|
|
+ if (type.equalsIgnoreCase(fileExtension)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据文件扩展名获取Content-Type
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileExtension 文件扩展名
|
|
|
|
|
+ * @return Content-Type
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getContentType(String fileExtension) {
|
|
|
|
|
+ switch (fileExtension.toLowerCase()) {
|
|
|
|
|
+ case "jpg":
|
|
|
|
|
+ case "jpeg":
|
|
|
|
|
+ return "image/jpeg";
|
|
|
|
|
+ case "png":
|
|
|
|
|
+ return "image/png";
|
|
|
|
|
+ case "gif":
|
|
|
|
|
+ return "image/gif";
|
|
|
|
|
+ case "bmp":
|
|
|
|
|
+ return "image/bmp";
|
|
|
|
|
+ case "webp":
|
|
|
|
|
+ return "image/webp";
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "application/octet-stream";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取文件访问路径
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param filePath 文件路径
|
|
|
|
|
+ * @return 文件访问路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFileUrl(String filePath) {
|
|
|
|
|
+ // ranking-1309648802.cos.ap-shanghai.myqcloud.com
|
|
|
|
|
+ return "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com" + filePath;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从URL中提取文件路径
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileUrl 文件URL
|
|
|
|
|
+ * @return 文件路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFilePathFromUrl(String fileUrl) {
|
|
|
|
|
+ if (fileUrl == null || fileUrl.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 如果是自定义域名
|
|
|
|
|
+ if (tencentCosConfig.getDomain() != null && !tencentCosConfig.getDomain().isEmpty() && fileUrl.startsWith(tencentCosConfig.getDomain())) {
|
|
|
|
|
+ return fileUrl.substring(tencentCosConfig.getDomain().length() + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是默认域名
|
|
|
|
|
+ String defaultDomain = "https://" + tencentCosConfig.getBucketName() + ".cos." + tencentCosConfig.getRegion() + ".myqcloud.com/";
|
|
|
|
|
+ if (fileUrl.startsWith(defaultDomain)) {
|
|
|
|
|
+ return fileUrl.substring(defaultDomain.length());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("从URL中提取文件路径失败: {}", fileUrl, e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ public String postObjectUpload(String filename) {
|
|
|
|
|
+ String bucketName = tencentCosConfig.getBucketName();
|
|
|
|
|
+// String endpoint = "cos.ap-shanghai.myqcloud.com";
|
|
|
|
|
+ String key = filename;
|
|
|
|
|
+// String contentType = "image/jpeg";
|
|
|
|
|
+ String secretId = tencentCosConfig.getSecretId();
|
|
|
|
|
+ String secretKey = tencentCosConfig.getSecretKey();
|
|
|
|
|
+ long startTimestamp = System.currentTimeMillis() / 1000;
|
|
|
|
|
+ long endTimestamp = startTimestamp + 10 * 60;
|
|
|
|
|
+ String endTimestampStr = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").
|
|
|
|
|
+ format(endTimestamp * 1000);
|
|
|
|
|
+ String keyTime = startTimestamp + ";" + endTimestamp;
|
|
|
|
|
+ // 设置表单的body字段值
|
|
|
|
|
+ Map<String, String> formFields = new HashMap<>();
|
|
|
|
|
+ formFields.put("q-sign-algorithm", "sha1");
|
|
|
|
|
+ formFields.put("key", key);
|
|
|
|
|
+ formFields.put("q-ak", secretId);
|
|
|
|
|
+ formFields.put("q-key-time", keyTime);
|
|
|
|
|
+ // 构造policy,参考文档: https://cloud.tencent.com/document/product/436/14690
|
|
|
|
|
+ String policy = "{\n" +
|
|
|
|
|
+ " \"expiration\": \"" + endTimestampStr + "\",\n" +
|
|
|
|
|
+ " \"conditions\": [\n" +
|
|
|
|
|
+ " { \"bucket\": \"" + bucketName + "\" },\n" +
|
|
|
|
|
+ " { \"q-sign-algorithm\": \"sha1\" },\n" +
|
|
|
|
|
+ " { \"q-ak\": \"" + secretId + "\" },\n" +
|
|
|
|
|
+ " { \"q-sign-time\":\"" + keyTime + "\" }\n" +
|
|
|
|
|
+ " ]\n" +
|
|
|
|
|
+ "}";
|
|
|
|
|
+ // policy需要base64后算放入表单中
|
|
|
|
|
+ String encodedPolicy = new String(Base64.encodeBase64(policy.getBytes()));
|
|
|
|
|
+ // 设置policy
|
|
|
|
|
+ formFields.put("policy", encodedPolicy);
|
|
|
|
|
+ // 根据编码后的policy和secretKey计算签名
|
|
|
|
|
+ COSSigner cosSigner = new COSSigner();
|
|
|
|
|
+ String signature = cosSigner.buildPostObjectSignature(secretKey,keyTime, policy);
|
|
|
|
|
+ // 设置签名
|
|
|
|
|
+ formFields.put("q-signature", signature);
|
|
|
|
|
+ log.info("表单参数:{}",Jackson.toJsonPrettyString(formFields));
|
|
|
|
|
+ // 根据以上表单参数,构造最开始的body部分
|
|
|
|
|
+ return Jackson.toJsonPrettyString(formFields);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|