diff --git a/pom.xml b/pom.xml
index 1c31f0b94..15ee53e6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,11 @@
lombok
true
+
+ com.qcloud
+ cos_api
+ 5.6.247
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/com/openisle/controller/UserController.java b/src/main/java/com/openisle/controller/UserController.java
index ba19c5dbd..d1f351f6a 100644
--- a/src/main/java/com/openisle/controller/UserController.java
+++ b/src/main/java/com/openisle/controller/UserController.java
@@ -29,7 +29,7 @@ public class UserController {
@PostMapping("/me/avatar")
public ResponseEntity> uploadAvatar(@RequestParam("file") MultipartFile file,
Authentication auth) throws IOException {
- String url = imageUploader.upload(file.getBytes(), file.getOriginalFilename());
+ String url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
userService.updateAvatar(auth.getName(), url);
return ResponseEntity.ok(Map.of("url", url));
}
diff --git a/src/main/java/com/openisle/service/CosImageUploader.java b/src/main/java/com/openisle/service/CosImageUploader.java
index c55bfab59..2142fd30d 100644
--- a/src/main/java/com/openisle/service/CosImageUploader.java
+++ b/src/main/java/com/openisle/service/CosImageUploader.java
@@ -1,24 +1,66 @@
package com.openisle.service;
+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.model.ObjectMetadata;
+import com.qcloud.cos.model.PutObjectRequest;
+import com.qcloud.cos.region.Region;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
+import java.io.ByteArrayInputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
/**
* ImageUploader implementation using Tencent Cloud COS.
- * For simplicity this demo just returns a URL composed of the base URL and file name.
*/
@Service
public class CosImageUploader extends ImageUploader {
+ private final COSClient cosClient;
+ private final String bucketName;
private final String baseUrl;
+ private final ExecutorService executor = Executors.newFixedThreadPool(2,
+ new CustomizableThreadFactory("cos-upload-"));
- public CosImageUploader(@Value("${cos.base-url:https://example.com}") String baseUrl) {
+ @org.springframework.beans.factory.annotation.Autowired
+ public CosImageUploader(
+ @Value("${cos.secret-id:}") String secretId,
+ @Value("${cos.secret-key:}") String secretKey,
+ @Value("${cos.region:ap-guangzhou}") String region,
+ @Value("${cos.bucket-name:}") String bucketName,
+ @Value("${cos.base-url:https://example.com}") String baseUrl) {
+ COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
+ ClientConfig config = new ClientConfig(new Region(region));
+ this.cosClient = new COSClient(cred, config);
+ this.bucketName = bucketName;
+ this.baseUrl = baseUrl;
+ }
+
+ // for tests
+ CosImageUploader(COSClient cosClient, String bucketName, String baseUrl) {
+ this.cosClient = cosClient;
+ this.bucketName = bucketName;
this.baseUrl = baseUrl;
}
@Override
- public String upload(byte[] data, String filename) {
- // In a real implementation you would call COS SDK here
- return baseUrl + "/" + filename;
+ public CompletableFuture upload(byte[] data, String filename) {
+ return CompletableFuture.supplyAsync(() -> {
+ ObjectMetadata meta = new ObjectMetadata();
+ meta.setContentLength(data.length);
+ PutObjectRequest req = new PutObjectRequest(
+ bucketName,
+ filename,
+ new ByteArrayInputStream(data),
+ meta);
+ cosClient.putObject(req);
+ return baseUrl + "/" + filename;
+ }, executor);
}
}
diff --git a/src/main/java/com/openisle/service/ImageUploader.java b/src/main/java/com/openisle/service/ImageUploader.java
index d6cc8f944..6f67ff917 100644
--- a/src/main/java/com/openisle/service/ImageUploader.java
+++ b/src/main/java/com/openisle/service/ImageUploader.java
@@ -10,5 +10,10 @@ public abstract class ImageUploader {
* @param filename name of the file
* @return accessible URL of the uploaded file
*/
- public abstract String upload(byte[] data, String filename);
+ /**
+ * Upload an image asynchronously and return a future of its accessible URL.
+ * Implementations should complete the future exceptionally on failures so
+ * callers can react accordingly.
+ */
+ public abstract java.util.concurrent.CompletableFuture upload(byte[] data, String filename);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 95e062f5e..b59558ad2 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -5,6 +5,10 @@ spring.jpa.hibernate.ddl-auto=update
resend.api.key=${RESEND_API_KEY:}
cos.base-url=${COS_BASE_URL:https://example.com}
+cos.secret-id=${COS_SECRET_ID:}
+cos.secret-key=${COS_SECRET_KEY:}
+cos.region=${COS_REGION:ap-guangzhou}
+cos.bucket-name=${COS_BUCKET_NAME:}
app.jwt.secret=${JWT_SECRET:ChangeThisSecretKeyForJwt}
app.jwt.expiration=${JWT_EXPIRATION:86400000}
diff --git a/src/test/java/com/openisle/controller/UserControllerTest.java b/src/test/java/com/openisle/controller/UserControllerTest.java
index e75c85ded..4e789776a 100644
--- a/src/test/java/com/openisle/controller/UserControllerTest.java
+++ b/src/test/java/com/openisle/controller/UserControllerTest.java
@@ -49,7 +49,7 @@ class UserControllerTest {
@Test
void uploadAvatar() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "a.png", MediaType.IMAGE_PNG_VALUE, "img".getBytes());
- Mockito.when(imageUploader.upload(any(), eq("a.png"))).thenReturn("http://img/a.png");
+ Mockito.when(imageUploader.upload(any(), eq("a.png"))).thenReturn(java.util.concurrent.CompletableFuture.completedFuture("http://img/a.png"));
mockMvc.perform(multipart("/api/users/me/avatar").file(file).principal(new UsernamePasswordAuthenticationToken("alice","p")))
.andExpect(status().isOk())
diff --git a/src/test/java/com/openisle/service/CosImageUploaderTest.java b/src/test/java/com/openisle/service/CosImageUploaderTest.java
index 7ac38d807..cba91f9b2 100644
--- a/src/test/java/com/openisle/service/CosImageUploaderTest.java
+++ b/src/test/java/com/openisle/service/CosImageUploaderTest.java
@@ -1,14 +1,21 @@
package com.openisle.service;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.model.PutObjectRequest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
class CosImageUploaderTest {
@Test
void uploadReturnsUrl() {
- CosImageUploader uploader = new CosImageUploader("http://cos.example.com");
- String url = uploader.upload("data".getBytes(), "img.png");
+ COSClient client = mock(COSClient.class);
+ CosImageUploader uploader = new CosImageUploader(client, "bucket", "http://cos.example.com");
+
+ String url = uploader.upload("data".getBytes(), "img.png").join();
+
+ verify(client).putObject(any(PutObjectRequest.class));
assertEquals("http://cos.example.com/img.png", url);
}
}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index fa5a6a264..0ff8e0dfa 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -6,6 +6,10 @@ spring.jpa.hibernate.ddl-auto=create-drop
resend.api.key=dummy
cos.base-url=http://test.example.com
+cos.secret-id=dummy
+cos.secret-key=dummy
+cos.region=ap-guangzhou
+cos.bucket-name=testbucket
app.jwt.secret=TestSecret
app.jwt.expiration=3600000