From 82bdc04b57d5f10ec5ee8e4ab8261b606bbfb95f Mon Sep 17 00:00:00 2001
From: smallchill <smallchill@163.com>
Date: Fri, 28 Feb 2025 00:08:40 +0800
Subject: [PATCH] =?UTF-8?q?:tada:=20=E5=A2=9E=E5=8A=A0=E8=85=BE=E8=AE=AF?=
 =?UTF-8?q?=E4=BA=91oss=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 blade-starter-oss/pom.xml                     |   6 +
 .../core/oss/TencentCosTemplate.java          | 280 ++++++++++++++++++
 .../oss/config/TencentCosConfiguration.java   |  81 +++++
 .../core/oss/props/OssProperties.java         |  10 +
 4 files changed, 377 insertions(+)
 create mode 100644 blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java
 create mode 100644 blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java

diff --git a/blade-starter-oss/pom.xml b/blade-starter-oss/pom.xml
index a0266ed..130d19c 100644
--- a/blade-starter-oss/pom.xml
+++ b/blade-starter-oss/pom.xml
@@ -41,6 +41,12 @@
             <artifactId>qiniu-java-sdk</artifactId>
             <version>7.9.4</version>
         </dependency>
+        <!--腾讯COS-->
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+            <version>5.6.147</version>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java
new file mode 100644
index 0000000..5d45b55
--- /dev/null
+++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/TencentCosTemplate.java
@@ -0,0 +1,280 @@
+/**
+ * Copyright (c) 2018-2099, yangkai.shen.
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springblade.core.oss;
+
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.model.CannedAccessControlList;
+import com.qcloud.cos.model.ObjectMetadata;
+import com.qcloud.cos.model.PutObjectResult;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import org.springblade.core.oss.model.BladeFile;
+import org.springblade.core.oss.model.OssFile;
+import org.springblade.core.oss.props.OssProperties;
+import org.springblade.core.oss.rule.OssRule;
+import org.springblade.core.tool.utils.StringPool;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * <p>
+ * 腾讯云 COS 操作
+ * </p>
+ *
+ * @author yangkai.shen
+ * @date Created in 2020/1/7 17:24
+ */
+@AllArgsConstructor
+public class TencentCosTemplate implements OssTemplate {
+	private final COSClient cosClient;
+	private final OssProperties ossProperties;
+	private final OssRule ossRule;
+
+	@Override
+	@SneakyThrows
+	public void makeBucket(String bucketName) {
+		if (!bucketExists(bucketName)) {
+			cosClient.createBucket(getBucketName(bucketName));
+			// TODO: 权限是否需要修改为私有,当前为 公有读、私有写
+			cosClient.setBucketAcl(getBucketName(bucketName), CannedAccessControlList.PublicRead);
+		}
+	}
+
+	@Override
+	@SneakyThrows
+	public void removeBucket(String bucketName) {
+		cosClient.deleteBucket(getBucketName(bucketName));
+	}
+
+	@Override
+	@SneakyThrows
+	public boolean bucketExists(String bucketName) {
+		return cosClient.doesBucketExist(getBucketName(bucketName));
+	}
+
+	@Override
+	@SneakyThrows
+	public void copyFile(String bucketName, String fileName, String destBucketName) {
+		cosClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public void copyFile(String bucketName, String fileName, String destBucketName, String destFileName) {
+		cosClient.copyObject(getBucketName(bucketName), fileName, getBucketName(destBucketName), destFileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public OssFile statFile(String fileName) {
+		return statFile(ossProperties.getBucketName(), fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public OssFile statFile(String bucketName, String fileName) {
+		ObjectMetadata stat = cosClient.getObjectMetadata(getBucketName(bucketName), fileName);
+		OssFile ossFile = new OssFile();
+		ossFile.setName(fileName);
+		ossFile.setLink(fileLink(ossFile.getName()));
+		ossFile.setHash(stat.getContentMD5());
+		ossFile.setLength(stat.getContentLength());
+		ossFile.setPutTime(stat.getLastModified());
+		ossFile.setContentType(stat.getContentType());
+		return ossFile;
+	}
+
+	@Override
+	@SneakyThrows
+	public String filePath(String fileName) {
+		return getOssHost().concat(StringPool.SLASH).concat(fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public String filePath(String bucketName, String fileName) {
+		return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public String fileLink(String fileName) {
+		return getOssHost().concat(StringPool.SLASH).concat(fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public String fileLink(String bucketName, String fileName) {
+		return getOssHost(bucketName).concat(StringPool.SLASH).concat(fileName);
+	}
+
+	/**
+	 * 文件对象
+	 *
+	 * @param file 上传文件类
+	 * @return
+	 */
+	@Override
+	@SneakyThrows
+	public BladeFile putFile(MultipartFile file) {
+		return putFile(ossProperties.getBucketName(), file.getOriginalFilename(), file);
+	}
+
+	/**
+	 * @param fileName 上传文件名
+	 * @param file     上传文件类
+	 * @return
+	 */
+	@Override
+	@SneakyThrows
+	public BladeFile putFile(String fileName, MultipartFile file) {
+		return putFile(ossProperties.getBucketName(), fileName, file);
+	}
+
+	@Override
+	@SneakyThrows
+	public BladeFile putFile(String bucketName, String fileName, MultipartFile file) {
+		return putFile(bucketName, fileName, file.getInputStream());
+	}
+
+	@Override
+	@SneakyThrows
+	public BladeFile putFile(String fileName, InputStream stream) {
+		return putFile(ossProperties.getBucketName(), fileName, stream);
+	}
+
+	@Override
+	@SneakyThrows
+	public BladeFile putFile(String bucketName, String fileName, InputStream stream) {
+		return put(bucketName, stream, fileName, false);
+	}
+
+	@SneakyThrows
+	public BladeFile put(String bucketName, InputStream stream, String key, boolean cover) {
+		makeBucket(bucketName);
+		String originalName = key;
+		key = getFileName(key);
+		ObjectMetadata objectMetadata = new ObjectMetadata();
+		// 覆盖上传
+		if (cover) {
+			cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata);
+		} else {
+			PutObjectResult response = cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata);
+			int retry = 0;
+			int retryCount = 5;
+			while (!StringUtils.hasText(response.getETag()) && retry < retryCount) {
+				response = cosClient.putObject(getBucketName(bucketName), key, stream, objectMetadata);
+				retry++;
+			}
+		}
+		BladeFile file = new BladeFile();
+		file.setOriginalName(originalName);
+		file.setName(key);
+		file.setDomain(getOssHost(bucketName));
+		file.setLink(fileLink(bucketName, key));
+		return file;
+	}
+
+	@Override
+	@SneakyThrows
+	public void removeFile(String fileName) {
+		cosClient.deleteObject(getBucketName(), fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public void removeFile(String bucketName, String fileName) {
+		cosClient.deleteObject(getBucketName(bucketName), fileName);
+	}
+
+	@Override
+	@SneakyThrows
+	public void removeFiles(List<String> fileNames) {
+		fileNames.forEach(this::removeFile);
+	}
+
+	@Override
+	@SneakyThrows
+	public void removeFiles(String bucketName, List<String> fileNames) {
+		fileNames.forEach(fileName -> removeFile(getBucketName(bucketName), fileName));
+	}
+
+	/**
+	 * 根据规则生成存储桶名称规则
+	 *
+	 * @return String
+	 */
+	private String getBucketName() {
+		return getBucketName(ossProperties.getBucketName());
+	}
+
+	/**
+	 * 根据规则生成存储桶名称规则
+	 *
+	 * @param bucketName 存储桶名称
+	 * @return String
+	 */
+	private String getBucketName(String bucketName) {
+		return ossRule.bucketName(bucketName).concat(StringPool.DASH).concat(ossProperties.getAppId());
+	}
+
+	/**
+	 * 根据规则生成文件名称规则
+	 *
+	 * @param originalFilename 原始文件名
+	 * @return string
+	 */
+	private String getFileName(String originalFilename) {
+		return ossRule.fileName(originalFilename);
+	}
+
+	/**
+	 * 获取域名
+	 *
+	 * @param bucketName 存储桶名称
+	 * @return String
+	 */
+	public String getOssHost(String bucketName) {
+		String prefix = getEndpoint().contains("https://") ? "https://" : "http://";
+		return prefix + cosClient.getClientConfig().getEndpointBuilder().buildGeneralApiEndpoint(getBucketName(bucketName));
+	}
+
+	/**
+	 * 获取域名
+	 *
+	 * @return String
+	 */
+	public String getOssHost() {
+		return getOssHost(ossProperties.getBucketName());
+	}
+
+	/**
+	 * 获取服务地址
+	 *
+	 * @return String
+	 */
+	public String getEndpoint() {
+		if (StringUtil.isBlank(ossProperties.getTransformEndpoint())) {
+			return ossProperties.getEndpoint();
+		}
+		return StringUtil.removeSuffix(ossProperties.getTransformEndpoint(), StringPool.SLASH);
+	}
+
+}
diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java
new file mode 100644
index 0000000..e47905f
--- /dev/null
+++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/config/TencentCosConfiguration.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2018-2099, yangkai.shen.
+ * <p>
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.gnu.org/licenses/lgpl.html
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springblade.core.oss.config;
+
+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.region.Region;
+import lombok.AllArgsConstructor;
+import org.springblade.core.oss.TencentCosTemplate;
+import org.springblade.core.oss.props.OssProperties;
+import org.springblade.core.oss.rule.OssRule;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * <p>
+ * 腾讯云 COS 自动装配
+ * </p>
+ *
+ * @author yangkai.shen
+ * @date Created in 2020/1/7 17:24
+ */
+@AllArgsConstructor
+@AutoConfiguration(after = OssConfiguration.class)
+@ConditionalOnClass({COSClient.class})
+@EnableConfigurationProperties(OssProperties.class)
+@ConditionalOnProperty(value = "oss.name", havingValue = "tencentcos")
+public class TencentCosConfiguration {
+
+	private final OssProperties ossProperties;
+	private final OssRule ossRule;
+
+
+	@Bean
+	@ConditionalOnMissingBean(COSClient.class)
+	public COSClient ossClient() {
+		// 初始化用户身份信息(secretId, secretKey)
+		COSCredentials credentials = new BasicCOSCredentials(ossProperties.getAccessKey(), ossProperties.getSecretKey());
+		// 设置 bucket 的区域, COS 地域的简称请参照 https://cloud.tencent.com/document/product/436/6224
+		Region region = new Region(ossProperties.getRegion());
+		// clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。
+		ClientConfig clientConfig = new ClientConfig(region);
+		// 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。
+		clientConfig.setMaxConnectionsCount(1024);
+		// 设置Socket层传输数据的超时时间,默认为50000毫秒。
+		clientConfig.setSocketTimeout(50000);
+		// 设置建立连接的超时时间,默认为50000毫秒。
+		clientConfig.setConnectionTimeout(50000);
+		// 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。
+		clientConfig.setConnectionRequestTimeout(1000);
+		return new COSClient(credentials, clientConfig);
+	}
+
+	@Bean
+	@ConditionalOnBean({COSClient.class})
+	@ConditionalOnMissingBean(TencentCosTemplate.class)
+	public TencentCosTemplate tencentCosTemplate(COSClient cosClient) {
+		return new TencentCosTemplate(cosClient, ossProperties, ossRule);
+	}
+
+}
diff --git a/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java b/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java
index d63c709..041aee9 100644
--- a/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java
+++ b/blade-starter-oss/src/main/java/org/springblade/core/oss/props/OssProperties.java
@@ -68,6 +68,16 @@ public class OssProperties {
 	 */
 	private String bucketName = "blade";
 
+	/**
+	 * 应用ID TencentCOS需要
+	 */
+	private String appId;
+
+	/**
+	 * 区域简称 TencentCOS/Amazon S3 需要
+	 */
+	private String region;
+
 	/**
 	 * 自定义属性
 	 */