Browse Source

!865 基础设施:前端直连上传文件
Merge pull request !865 from 疯狂的世界/develop

芋道源码 1 year ago
parent
commit
b2782e1d9a

+ 14 - 2
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/FileClient.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.framework.file.core.client;
 
+import cn.iocoder.yudao.framework.file.core.client.s3.FilePresignedUrlBO;
+
 /**
  * 文件客户端
  *
@@ -18,11 +20,11 @@ public interface FileClient {
      * 上传文件
      *
      * @param content 文件流
-     * @param path 相对路径
+     * @param path    相对路径
      * @return 完整路径,即 HTTP 访问地址
      * @throws Exception 上传文件时,抛出 Exception 异常
      */
-    String upload(byte[] content, String path, String type) throws  Exception;
+    String upload(byte[] content, String path, String type) throws Exception;
 
     /**
      * 删除文件
@@ -40,4 +42,14 @@ public interface FileClient {
      */
     byte[] getContent(String path) throws Exception;
 
+    /**
+     * 获得文件预签名地址
+     *
+     * @param fileName 文件名称
+     * @return 文件预签名地址
+     */
+    default FilePresignedUrlBO getPresignedObjectUrl(String fileName) throws Exception {
+        throw new UnsupportedOperationException("不支持的操作");
+    }
+
 }

+ 27 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/FilePresignedUrlBO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.file.core.client.s3;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 文件预签名地址 BO
+ *
+ * @author owen
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class FilePresignedUrlBO {
+
+    /**
+     * 文件上传 URL(用于上传)
+     */
+    private String uploadUrl;
+
+    /**
+     * 文件 URL(用于读取、下载等)
+     */
+    private String url;
+
+}

+ 17 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java

@@ -5,8 +5,10 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
 import io.minio.*;
+import io.minio.http.Method;
 
 import java.io.ByteArrayInputStream;
+import java.util.concurrent.TimeUnit;
 
 import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
 import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT;
@@ -117,4 +119,19 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
         return IoUtil.readBytes(response);
     }
 
+    @Override
+    public FilePresignedUrlBO getPresignedObjectUrl(String fileName) throws Exception {
+        String uploadUrl = client.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
+                .method(Method.PUT)
+                .bucket(config.getBucket())
+                .object(fileName)
+                /**
+                 * 过期时间(秒数)取值范围:1秒 ~ 7天
+                 * {@link GetPresignedObjectUrlArgs.Builder#validateExpiry(int)}
+                 */
+                .expiry(10, TimeUnit.MINUTES)
+                .build()
+        );
+        return new FilePresignedUrlBO(uploadUrl, config.getDomain() + "/" + fileName);
+    }
 }

+ 19 - 10
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java

@@ -8,14 +8,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileUploadReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.service.file.FileService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.annotation.security.PermitAll;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,12 +26,6 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.annotation.Resource;
-import jakarta.annotation.security.PermitAll;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
-
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 @Tag(name = "管理后台 - 文件存储")
@@ -50,6 +47,18 @@ public class FileController {
         return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
     }
 
+    @GetMapping("/presigned-url")
+    @Operation(summary = "获取文件预签名地址")
+    public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("fileName") String fileName) throws Exception {
+        return success(fileService.getFilePresignedUrl(fileName));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建文件")
+    public CommonResult<Long> createFile(@Valid @RequestBody FileCreateReqVO createReqVO) {
+        return success(fileService.createFile(createReqVO));
+    }
+
     @DeleteMapping("/delete")
     @Operation(summary = "删除文件")
     @Parameter(name = "id", description = "编号", required = true)
@@ -62,7 +71,7 @@ public class FileController {
     @GetMapping("/{configId}/get/**")
     @PermitAll
     @Operation(summary = "下载文件")
-    @Parameter(name = "configId", description = "配置编号",  required = true)
+    @Parameter(name = "configId", description = "配置编号", required = true)
     public void getFileContent(HttpServletRequest request,
                                HttpServletResponse response,
                                @PathVariable("configId") Long configId) throws Exception {

+ 33 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileCreateReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "管理后台 - 文件创建 Request VO")
+@Data
+public class FileCreateReqVO {
+
+    @NotNull(message = "文件配置编号不能为空")
+    @Schema(description = "文件配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
+    private Long configId;
+
+    @NotNull(message = "文件路径不能为空")
+    @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg")
+    private String path;
+
+    @NotNull(message = "原文件名不能为空")
+    @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao.jpg")
+    private String name;
+
+    @NotNull(message = "文件 URL不能为空")
+    @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
+    private String url;
+
+    @Schema(description = "文件MIME类型", example = "application/octet-stream")
+    private String type;
+
+    @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Integer size;
+
+}

+ 23 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "管理后台 - 文件预签名地址 Response VO")
+@Data
+public class FilePresignedUrlRespVO {
+
+    @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
+    private Long configId;
+
+    @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
+    private String uploadUrl;
+
+    @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
+    private String url;
+
+}

+ 22 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java

@@ -1,7 +1,9 @@
 package cn.iocoder.yudao.module.infra.service.file;
 
-import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 
 /**
@@ -22,13 +24,21 @@ public interface FileService {
     /**
      * 保存文件,并返回文件的访问路径
      *
-     * @param name 文件名称
-     * @param path 文件路径
+     * @param name    文件名称
+     * @param path    文件路径
      * @param content 文件内容
      * @return 文件路径
      */
     String createFile(String name, String path, byte[] content);
 
+    /**
+     * 创建文件
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createFile(FileCreateReqVO createReqVO);
+
     /**
      * 删除文件
      *
@@ -40,9 +50,17 @@ public interface FileService {
      * 获得文件内容
      *
      * @param configId 配置编号
-     * @param path 文件路径
+     * @param path     文件路径
      * @return 文件内容
      */
     byte[] getFileContent(Long configId, String path) throws Exception;
 
+    /**
+     * 生成文件预签名地址信息
+     *
+     * @param fileName 文件名称
+     * @return 预签名地址信息
+     */
+    FilePresignedUrlRespVO getFilePresignedUrl(String fileName) throws Exception;
+
 }

+ 21 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java

@@ -4,16 +4,19 @@ import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.file.core.client.FileClient;
+import cn.iocoder.yudao.framework.file.core.client.s3.FilePresignedUrlBO;
 import cn.iocoder.yudao.framework.file.core.utils.FileTypeUtils;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
+import jakarta.annotation.Resource;
 import lombok.SneakyThrows;
 import org.springframework.stereotype.Service;
 
-import jakarta.annotation.Resource;
-
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
 
@@ -66,6 +69,15 @@ public class FileServiceImpl implements FileService {
         return url;
     }
 
+    @Override
+    public Long createFile(FileCreateReqVO createReqVO) {
+        // 插入
+        FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
+        fileMapper.insert(file);
+        // 返回
+        return file.getId();
+    }
+
     @Override
     public void deleteFile(Long id) throws Exception {
         // 校验存在
@@ -95,4 +107,11 @@ public class FileServiceImpl implements FileService {
         return client.getContent(path);
     }
 
+    @Override
+    public FilePresignedUrlRespVO getFilePresignedUrl(String fileName) throws Exception {
+        FileClient fileClient = fileConfigService.getMasterFileClient();
+        FilePresignedUrlBO bo = fileClient.getPresignedObjectUrl(fileName);
+        return BeanUtils.toBean(bo, FilePresignedUrlRespVO.class, f -> f.setConfigId(fileClient.getId()));
+    }
+
 }