mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 22:21:09 +08:00
Merge pull request #22 from nagisa77/codex/improve-tencent-cloud-cos-upload
Implement async COS upload
This commit is contained in:
5
pom.xml
5
pom.xml
@@ -66,6 +66,11 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.qcloud</groupId>
|
||||
<artifactId>cos_api</artifactId>
|
||||
<version>5.6.247</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> upload(byte[] data, String filename);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user