From b9ca3339588920cdb088e5e21e0b1616ecdbff85 Mon Sep 17 00:00:00 2001
From: suyiiyii <suyiiyii@gmail.com>
Date: Tue, 3 Sep 2024 16:13:00 +0800
Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6?=
 =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=8F=8AS3=E9=9B=86?=
 =?UTF-8?q?=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

新版本中添加了文件上传功能,并集成了S3进行文件存储。这包括控制器、服务和工具类的更新,以及配置S3客户端的bean配置。
---
 pom.xml                                       |  14 +-
 .../top/suyiiyii/sims/common/S3Config.java    |  17 +++
 .../sims/controller/FileController.java       |  40 ++++++
 .../suyiiyii/sims/service/FileService.java    |  18 +++
 .../top/suyiiyii/sims/utils/S3Client.java     | 123 ++++++++++++++++++
 src/main/resources/application.yaml           |  15 ++-
 6 files changed, 225 insertions(+), 2 deletions(-)
 create mode 100644 src/main/java/top/suyiiyii/sims/common/S3Config.java
 create mode 100644 src/main/java/top/suyiiyii/sims/controller/FileController.java
 create mode 100644 src/main/java/top/suyiiyii/sims/service/FileService.java
 create mode 100644 src/main/java/top/suyiiyii/sims/utils/S3Client.java

diff --git a/pom.xml b/pom.xml
index 4374986..7611098 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,16 +95,28 @@
             <artifactId>spring-restdocs-mockmvc</artifactId>
             <scope>test</scope>
         </dependency>
+        <!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui -->
         <dependency>
             <groupId>org.springdoc</groupId>
             <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
-            <version>2.3.0</version>
+            <version>2.6.0</version>
         </dependency>
+
         <dependency>
             <groupId>org.xerial</groupId>
             <artifactId>sqlite-jdbc</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-s3</artifactId>
+            <version>1.12.706</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/top/suyiiyii/sims/common/S3Config.java b/src/main/java/top/suyiiyii/sims/common/S3Config.java
new file mode 100644
index 0000000..900618a
--- /dev/null
+++ b/src/main/java/top/suyiiyii/sims/common/S3Config.java
@@ -0,0 +1,17 @@
+package top.suyiiyii.sims.common;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import top.suyiiyii.sims.utils.S3Client;
+
+@Configuration
+public class S3Config {
+    @Bean
+    public static S3Client Config(@Value("${S3.ENDPOINT}") String endpoint,
+                                  @Value("${S3.ACCESS_KEY}") String accessKey,
+                                  @Value("${S3.SECRET_KEY}") String secretKey,
+                                  @Value("${S3.BUCKET}") String bucket) {
+        return new S3Client(endpoint, accessKey, secretKey, bucket);
+    }
+}
diff --git a/src/main/java/top/suyiiyii/sims/controller/FileController.java b/src/main/java/top/suyiiyii/sims/controller/FileController.java
new file mode 100644
index 0000000..d66f458
--- /dev/null
+++ b/src/main/java/top/suyiiyii/sims/controller/FileController.java
@@ -0,0 +1,40 @@
+package top.suyiiyii.sims.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import top.suyiiyii.sims.common.AuthAccess;
+import top.suyiiyii.sims.service.FileService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@Slf4j
+@RestController
+public class FileController {
+    @Autowired
+    FileService fileService;
+
+    @AuthAccess(allowRoles = {"user"})
+    @Operation(summary = "上传文件", description = "使用form-data格式\nfile:文件\nfilename: 文件名\n返回可访问的路径")
+    @PostMapping("/upload")
+    public String uploadFile(
+            @Parameter String filename,
+            @RequestBody(content = @Content(mediaType = "multipart/form-data",
+                    schema = @Schema(type = "string", format = "binary"))) MultipartFile file) {
+        try (InputStream in = file.getInputStream()) {
+            log.info("文件上传,文件名:{},描述:{}", file.getOriginalFilename(), filename);
+            return fileService.uploadFile(in, filename);
+        } catch (IOException e) {
+            log.warn("文件上传失败", e);
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/main/java/top/suyiiyii/sims/service/FileService.java b/src/main/java/top/suyiiyii/sims/service/FileService.java
new file mode 100644
index 0000000..f632a56
--- /dev/null
+++ b/src/main/java/top/suyiiyii/sims/service/FileService.java
@@ -0,0 +1,18 @@
+package top.suyiiyii.sims.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import top.suyiiyii.sims.utils.S3Client;
+
+import java.io.InputStream;
+
+@Service
+public class FileService {
+    @Autowired
+    private S3Client s3Client;
+
+    public String uploadFile(InputStream input,String fileName) {
+        String extension = fileName.substring(fileName.lastIndexOf("."));
+        return s3Client.uploadFile(input, extension);
+    }
+}
diff --git a/src/main/java/top/suyiiyii/sims/utils/S3Client.java b/src/main/java/top/suyiiyii/sims/utils/S3Client.java
new file mode 100644
index 0000000..837f121
--- /dev/null
+++ b/src/main/java/top/suyiiyii/sims/utils/S3Client.java
@@ -0,0 +1,123 @@
+package top.suyiiyii.sims.utils;
+
+
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.Protocol;
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.services.s3.S3ClientOptions;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.S3Object;
+import org.apache.commons.io.IOUtils;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.UUID;
+
+
+public class S3Client {
+    private final String endpoint;
+    private final String bucket;
+    private final AmazonS3 s3client;
+
+    public S3Client(String endpoint, String accessKey, String secretKey, String bucket) {
+        this.endpoint = endpoint;
+        this.bucket = bucket;
+        URL endpointUrl;
+        try {
+            endpointUrl = new URL(endpoint);
+        } catch (MalformedURLException e) {
+            throw new RuntimeException(e);
+        }
+        String protocol = endpointUrl.getProtocol();
+        int port = endpointUrl.getPort() == -1 ? endpointUrl.getDefaultPort() : endpointUrl.getPort();
+        ClientConfiguration clientConfig = new ClientConfiguration();
+        clientConfig.setSignerOverride("S3SignerType");
+        clientConfig.setProtocol(Protocol.valueOf(protocol.toUpperCase()));
+        // 禁用证书检查,避免https自签证书校验失败
+        System.setProperty("com.amazonaws.sdk.disableCertChecking", "true");
+        // 屏蔽 AWS 的 MD5 校验,避免校验导致的下载抛出异常问题
+        System.setProperty("com.amazonaws.services.s3.disableGetObjectMD5Validation", "true");
+        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
+        // 创建 S3Client 实例
+        AmazonS3 s3client = new AmazonS3Client(awsCredentials, clientConfig);
+        s3client.setEndpoint(endpointUrl.getHost() + ":" + port);
+        s3client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build());
+        this.s3client = s3client;
+    }
+
+
+    public boolean bucketExists(String bucket) {
+        try {
+            return s3client.doesBucketExist(bucket);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    public boolean existObject(String bucket, String objectId) {
+        try {
+            return s3client.doesObjectExist(bucket, objectId);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    public InputStream download(String bucket, String objectId) {
+        try {
+            S3Object o = s3client.getObject(bucket, objectId);
+            return o.getObjectContent();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public void download(String bucket, String objectId, OutputStream out) {
+        S3Object o = s3client.getObject(bucket, objectId);
+        try (InputStream in = o.getObjectContent()) {
+            IOUtils.copyLarge(in, out);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void upload(String bucket, String objectId, InputStream input) {
+        try {
+            // 创建文件上传的元数据
+            ObjectMetadata meta = new ObjectMetadata();
+            // 设置文件上传长度
+            meta.setContentLength(input.available());
+            // 上传
+            s3client.putObject(bucket, objectId, input, meta);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public String uploadFile(InputStream input) {
+        String objectID = UUID.randomUUID().toString();
+        upload(bucket, objectID, input);
+        return endpoint + "/" + bucket + "/" + objectID;
+    }
+
+    /**
+     * 接收文件流,自动使用随机uuid命名并保留扩展名
+     * 返回公网上可以直接访问的URL
+     *
+     * @param input         文件流
+     * @param extensionName 扩展名
+     * @return 文件的URL
+     */
+    public String uploadFile(InputStream input, String extensionName) {
+        String objectID = UUID.randomUUID() + extensionName;
+        upload(bucket, objectID, input);
+        return endpoint + "/" + bucket + "/" + objectID;
+    }
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 4b7599a..17c0cca 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -2,6 +2,10 @@
 spring:
   profiles:
     active: prod
+  servlet:
+    multipart:
+      max-file-size: 100MB
+      max-request-size: 100MB
   datasource:
     url: ${DATASOURCE_URL}
     username: ${DATASOURCE_USERNAME}
@@ -14,4 +18,13 @@ auto-table:
   model-package: top.suyiiyii.sims.entity
 
 jwt:
-  secret: ${JWT_SECRET}
\ No newline at end of file
+  secret: ${JWT_SECRET}
+
+S3:
+  ENDPOINT: ${S3_ENDPOINT}
+  BUCKET: ${S3_BUCKET}
+  ACCESS_KEY: ${S3_ACCESS_KEY}
+  SECRET_KEY: ${S3_SECRET_KEY}
+
+springdoc:
+  default-support-form-data: true
\ No newline at end of file

From d353304f3a8e7681d6a07fdb991d5d3bb7e1cce1 Mon Sep 17 00:00:00 2001
From: suyiiyii <suyiiyii@gmail.com>
Date: Tue, 3 Sep 2024 16:34:35 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E5=9C=A8=E6=B5=8B=E8=AF=95=E4=B8=ADmock=20?=
 =?UTF-8?q?S3Client=EF=BC=8C=E9=81=BF=E5=85=8D=E6=8A=A5=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                                   | 6 ++++++
 src/main/resources/application.yaml                       | 3 ---
 .../java/top/suyiiyii/sims/service/RbacServiceTest.java   | 8 ++++++++
 3 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7611098..df911c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,6 +117,12 @@
             <artifactId>commons-io</artifactId>
             <version>2.11.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>4.0.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 17c0cca..d54a8ca 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -25,6 +25,3 @@ S3:
   BUCKET: ${S3_BUCKET}
   ACCESS_KEY: ${S3_ACCESS_KEY}
   SECRET_KEY: ${S3_SECRET_KEY}
-
-springdoc:
-  default-support-form-data: true
\ No newline at end of file
diff --git a/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java b/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java
index b06cec6..f46b2ae 100644
--- a/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java
+++ b/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java
@@ -1,10 +1,14 @@
 package top.suyiiyii.sims.service;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.test.context.ActiveProfiles;
 import top.suyiiyii.sims.entity.Role;
+import top.suyiiyii.sims.utils.S3Client;
 
 import java.util.List;
 
@@ -12,11 +16,15 @@ import static org.junit.jupiter.api.Assertions.*;
 
 @SpringBootTest
 @ActiveProfiles("test")
+@ExtendWith(MockitoExtension.class)
 class RbacServiceTest {
 
     @Autowired
     private RbacService rbacService;
 
+    @MockBean
+    private S3Client s3Client;
+
     @Test
     void addRoleWithUserId() {
         int userId = 1; // mock userId