Kaynağa Gözat

feat: 文件存储功能开发,新增ftp、sftp支持,修改minio文件存储实现;修改原本的大屏封面存储逻辑,从而适配文件存储类型的切换

hong.yang 1 yıl önce
ebeveyn
işleme
fc0f733079
25 değiştirilmiş dosya ile 1830 ekleme ve 317 silme
  1. 19 0
      DataRoom/dataroom-core/pom.xml
  2. 0 94
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/MinioConfig.java
  3. 15 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FileConfig.java
  4. 108 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FtpConfig.java
  5. 50 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/MinioConfig.java
  6. 116 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/SftpConfig.java
  7. 30 50
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/biz/component/service/impl/BizComponentServiceImpl.java
  8. 0 20
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/enums/FileUploadType.java
  9. 0 57
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/FileOperationStrategy.java
  10. 22 1
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/IDataRoomOssService.java
  11. 154 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomFtpFileServiceImpl.java
  12. 68 10
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomLocalFileServiceImpl.java
  13. 103 29
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomMinioServiceImpl.java
  14. 157 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomSftpFileServiceImpl.java
  15. 153 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpClientFactory.java
  16. 79 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpPoolServiceImpl.java
  17. 105 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpClientFactory.java
  18. 74 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpPoolService.java
  19. 33 43
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/manage/service/impl/DataRoomPageServiceImpl.java
  20. 242 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/FtpClientUtil.java
  21. 1 1
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/MinioFileInterface.java
  22. 56 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/PathUtils.java
  23. 242 0
      DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/SftpClientUtils.java
  24. 0 12
      DataRoom/dataroom-server/src/main/resources/application.yml
  25. 3 0
      DataRoom/pom.xml

+ 19 - 0
DataRoom/dataroom-core/pom.xml

@@ -72,5 +72,24 @@
             <artifactId>minio</artifactId>
             <version>${minio.version}</version>
         </dependency>
+        <!-- ftp 连接工具包 -->
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>${commons-net.version}</version>
+        </dependency>
+        <!-- 连接池化技术 -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-dbcp2</artifactId>
+            <version>${commons-dbcp2.version}</version>
+        </dependency>
+        <!-- sftp 连接依赖-->
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <version>${jsch.version}</version>
+        </dependency>
+
     </dependencies>
 </project>

+ 0 - 94
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/MinioConfig.java

@@ -1,94 +0,0 @@
-package com.gccloud.dataroom.core.config;
-
-import io.minio.MinioClient;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * Minio 配置信息
- *
- * @author Acechengui
- */
-@Configuration
-@ConfigurationProperties(prefix = "minio")
-public class MinioConfig
-{
-    /**
-     * 服务地址
-     */
-    private String url;
-
-    /**
-     * 用户名
-     */
-    private String accessKey;
-
-    /**
-     * 密码
-     */
-    private String secretKey;
-
-    /**
-     * 存储桶名称
-     */
-    private String bucketName;
-
-    public String getUrl()
-    {
-        return url;
-    }
-
-    public void setUrl(String url)
-    {
-        this.url = url;
-    }
-
-    public String getAccessKey()
-    {
-        return accessKey;
-    }
-
-    public void setAccessKey(String accessKey)
-    {
-        this.accessKey = accessKey;
-    }
-
-    public String getSecretKey()
-    {
-        return secretKey;
-    }
-
-    public void setSecretKey(String secretKey)
-    {
-        this.secretKey = secretKey;
-    }
-
-    public String getBucketName()
-    {
-        return bucketName;
-    }
-
-    public void setBucketName(String bucketName)
-    {
-        this.bucketName = bucketName;
-    }
-
-    @Bean
-    public MinioClient getMinioClient() {
-        if (StringUtils.isEmpty(url) || StringUtils.isEmpty(accessKey) || StringUtils.isEmpty(secretKey)) {
-            // 如果未配置Minio相关的配置项,则使用本地文件存储
-            // 或者返回一个默认的MinioClient实例,用于本地文件存储
-            return createDefaultMinioClient();
-        }
-        return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
-    }
-
-    private MinioClient createDefaultMinioClient() {
-        return MinioClient.builder()
-                .endpoint("http://minio.example.com")
-                .credentials("accessKey", "secretKey")
-                .build();
-    }
-}

+ 15 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FileConfig.java

@@ -30,4 +30,19 @@ public class FileConfig {
             "mp4", "mov", "mp3",
             "rar", "zip"
     );
+
+    /**
+     * ftp配置
+     */
+    private FtpConfig ftp;
+
+    /**
+     * sftp配置
+     */
+    private SftpConfig sftp;
+
+    /**
+     * minio配置
+     */
+    private MinioConfig minio;
 }

+ 108 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FtpConfig.java

@@ -0,0 +1,108 @@
+package com.gccloud.dataroom.core.config.bean;
+
+import lombok.Data;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/17 15:18
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "gc.starter.file.ftp")
+public class FtpConfig extends GenericObjectPoolConfig {
+
+    /**
+     * ftp服务器地址
+     */
+    private String host;
+
+    /**
+     * ftp服务器端口
+     */
+    private Integer port;
+
+    /**
+     * ftp服务器用户名
+     */
+    private String username;
+
+    /**
+     * ftp服务器密码
+     */
+    private String password;
+
+    /**
+     * 传输编码
+     */
+    String encoding = "utf-8";
+    /**
+     * 被动模式:在这种模式下,数据连接是由客户程序发起的
+     */
+    boolean passiveMode = true;
+    /**
+     * 连接超时时间
+     */
+    int clientTimeout = 30000;
+
+    /**
+     * 0=ASCII_FILE_TYPE(ASCII格式),1=EBCDIC_FILE_TYPE,2=LOCAL_FILE_TYPE(二进制文件)
+     */
+    int transferFileType = 2;
+    /**
+     * 重新连接时间
+     */
+    int retryTimes;
+    /**
+     * 缓存大小
+     */
+    int bufferSize = 1024;
+
+    /* 连接池配置 */
+
+    /**
+     * 最大连接数
+     */
+    int maxTotal = DEFAULT_MAX_TOTAL;
+    /**
+     * 最小空闲
+     */
+    int minIdle = DEFAULT_MIN_IDLE;
+    /**
+     * 最大空闲
+     */
+    int maxIdle = DEFAULT_MAX_IDLE;
+    /**
+     * 最大等待时间 10s
+     */
+    int maxWait = 10000;
+    /**
+     * 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
+     */
+    boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
+    /**
+     * 取对象时验证
+     */
+    boolean testOnBorrow = true ;
+    /**
+     * 回收验证
+     */
+    boolean testOnReturn = true;
+    /**
+     * 创建时验证
+     */
+    boolean testOnCreate = true;
+    /**
+     * 空闲验证
+     */
+    boolean testWhileIdle = true;
+    /**
+     * 后进先出
+     */
+    boolean lifo = DEFAULT_LIFO;
+
+
+}

+ 50 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/MinioConfig.java

@@ -0,0 +1,50 @@
+package com.gccloud.dataroom.core.config.bean;
+
+import com.gccloud.common.exception.GlobalException;
+import io.minio.MinioClient;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Minio 配置信息
+ *
+ * @author Acechengui
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "gc.starter.file.minio")
+public class MinioConfig
+{
+    /**
+     * 服务地址
+     */
+    private String url;
+
+    /**
+     * 用户名
+     */
+    private String accessKey;
+
+    /**
+     * 密码
+     */
+    private String secretKey;
+
+    /**
+     * 存储桶名称
+     */
+    private String bucketName;
+
+    @Bean
+    @ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "minio")
+    public MinioClient getMinioClient() {
+        if (StringUtils.isBlank(bucketName)) {
+            throw new GlobalException("Minio bucketName 不能为空");
+        }
+        return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
+    }
+}

+ 116 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/SftpConfig.java

@@ -0,0 +1,116 @@
+package com.gccloud.dataroom.core.config.bean;
+
+import lombok.Data;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/17 15:18
+ */
+@Configuration
+@ConfigurationProperties(prefix = "gc.starter.file.sftp")
+@Data
+public class SftpConfig extends GenericObjectPoolConfig {
+
+    /**
+     * ftp服务器地址
+     */
+    private String host;
+
+    /**
+     * ftp服务器端口
+     */
+    private Integer port;
+
+    /**
+     * ftp服务器用户名
+     */
+    private String username;
+
+    /**
+     * ftp服务器密码
+     */
+    private String password;
+
+    /**
+     * 私钥
+     */
+    private String privateKey;
+
+    /**
+     * 传输编码
+     */
+    String encoding = "utf-8";
+    /**
+     * 被动模式:在这种模式下,数据连接是由客户程序发起的
+     */
+    boolean passiveMode = true;
+    /**
+     * 连接超时时间
+     */
+    int clientTimeout = 30000;
+
+    /**
+     * 0=ASCII_FILE_TYPE(ASCII格式),1=EBCDIC_FILE_TYPE,2=LOCAL_FILE_TYPE(二进制文件)
+     */
+    int transferFileType = 2;
+    /**
+     * 重新连接时间
+     */
+    int retryTimes;
+
+    /**
+     * 缓存大小
+     */
+    int bufferSize = 1024;
+
+
+    /* 连接池配置 */
+
+
+    /**
+     * 最大连接数
+     */
+    int maxTotal = DEFAULT_MAX_TOTAL;
+    /**
+     * 最小空闲
+     */
+    int minIdle = DEFAULT_MIN_IDLE;
+    /**
+     * 最大空闲
+     */
+    int maxIdle = DEFAULT_MAX_IDLE;
+    /**
+     * 最大等待时间 10s
+     */
+    int maxWait = 10000;
+    /**
+     * 池对象耗尽之后是否阻塞,maxWait < 0 时一直等待
+     */
+    boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
+    /**
+     * 取对象时验证
+     */
+    boolean testOnBorrow = true ;
+    /**
+     * 回收验证
+     */
+    boolean testOnReturn = true;
+    /**
+     * 创建时验证
+     */
+    boolean testOnCreate = true;
+    /**
+     * 空闲验证
+     */
+    boolean testWhileIdle = true;
+    /**
+     * 后进先出
+     */
+    boolean lifo = DEFAULT_LIFO;
+
+
+}

+ 30 - 50
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/biz/component/service/impl/BizComponentServiceImpl.java

@@ -7,9 +7,12 @@ import com.gccloud.dataroom.core.module.biz.component.dao.DataRoomBizComponentDa
 import com.gccloud.dataroom.core.module.biz.component.dto.BizComponentSearchDTO;
 import com.gccloud.dataroom.core.module.biz.component.entity.BizComponentEntity;
 import com.gccloud.dataroom.core.module.biz.component.service.IBizComponentService;
+import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
 import com.gccloud.dataroom.core.utils.CodeGenerateUtils;
 import com.gccloud.common.exception.GlobalException;
 import com.gccloud.common.vo.PageVO;
+import com.gccloud.dataroom.core.utils.PathUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -17,9 +20,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.io.*;
 import java.util.Base64;
 import java.util.List;
 
@@ -35,6 +36,9 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
     @Resource
     private DataRoomConfig bigScreenConfig;
 
+    @Resource
+    private IDataRoomOssService ossService;
+
     @Override
     public PageVO<BizComponentEntity> getPage(BizComponentSearchDTO searchDTO) {
         LambdaQueryWrapper<BizComponentEntity> queryWrapper = new LambdaQueryWrapper<>();
@@ -93,10 +97,14 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
             urlPrefix += "/";
         }
         for (BizComponentEntity entity : list) {
-            if (StringUtils.isBlank(entity.getCoverPicture())) {
+            String coverPicture = entity.getCoverPicture();
+            if (StringUtils.isBlank(coverPicture)) {
                 continue;
             }
-            entity.setCoverPicture(urlPrefix + entity.getCoverPicture().replace("\\", "/"));
+            if (coverPicture.startsWith("/")) {
+                coverPicture = coverPicture.substring(1);
+            }
+            entity.setCoverPicture(urlPrefix + PathUtils.normalizePath(coverPicture));
         }
         return bizComponentEntity;
     }
@@ -133,13 +141,6 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
     }
 
 
-    /**
-     * 将base64字符串转为图片文件存储
-     *
-     * @param base64String
-     * @param fileName
-     * @return
-     */
     private String saveCoverPicture(String base64String, String fileName) {
         String fileUrl = "";
         if (StringUtils.isBlank(base64String)) {
@@ -150,29 +151,21 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
             base64String = base64String.substring(base64String.indexOf(",") + 1);
             // 解码base64字符串
             byte[] imageBytes = Base64.getDecoder().decode(base64String);
-            String basePath = bigScreenConfig.getFile().getBasePath();
-            // 不是/结尾,加上/
-            if (!basePath.endsWith("/") || !basePath.endsWith("\\")) {
-                basePath += File.separator;
-            }
-            // 检查目录是否存在,不存在则创建
-            File file = new File(basePath + "cover");
-            if (!file.exists()) {
-                file.mkdirs();
-            }
-            // 保存为图片文件
-            String filePath = basePath + "cover" + File.separator + fileName + ".png";
-            fileUrl = "cover" + File.separator + fileName + ".png";
-            FileOutputStream outputStream = new FileOutputStream(filePath);
-            outputStream.write(imageBytes);
-            outputStream.close();
+            InputStream inputStream = new ByteArrayInputStream(imageBytes);
+            DataRoomFileEntity fileEntity = new DataRoomFileEntity();
+            String filePath = "cover" + File.separator + fileName + ".png";
+            ossService.upload(inputStream, filePath, 0, fileEntity);
+            // 封面先不保存到资源库
+            // fileService.save(fileEntity);
             log.info("组业务件封面保存至:{}", filePath);
-        } catch (IOException e) {
+            fileUrl = fileEntity.getUrl();
+        } catch (Exception e) {
             log.error(ExceptionUtils.getStackTrace(e));
         }
         return fileUrl;
     }
 
+
     public static final String COPY_SUFFIX = "-副本";
 
     @Override
@@ -198,11 +191,11 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
             i++;
         }
         copyFrom.setCode(CodeGenerateUtils.generate("bizComponent"));
-        boolean copy = this.copyCoverPicture(oldCode, copyFrom.getCode());
-        if (!copy) {
+        String copyUrl = this.copyCoverPicture(oldCode, copyFrom.getCode());
+        if (StringUtils.isBlank(copyUrl)) {
             copyFrom.setCoverPicture(null);
         } else {
-            copyFrom.setCoverPicture("cover" + File.separator + copyFrom.getCode() + ".png");
+            copyFrom.setCoverPicture(copyUrl);
         }
         this.save(copyFrom);
         return copyFrom.getCode();
@@ -215,26 +208,13 @@ public class BizComponentServiceImpl extends ServiceImpl<DataRoomBizComponentDao
      * @param newFileName
      * @return
      */
-    private boolean copyCoverPicture(String oldFileName, String newFileName) {
+    private String copyCoverPicture(String oldFileName, String newFileName) {
         if (StringUtils.isBlank(oldFileName)) {
-            return false;
-        }
-        String basePath = bigScreenConfig.getFile().getBasePath() + File.separator;
-        String oldFile = basePath + "cover" + File.separator + oldFileName + ".png";
-        // 检查文件是否存在
-        File file = new File(oldFile);
-        if (!file.exists() || !file.isFile()) {
-            return false;
-        }
-        // 复制一份
-        String newFilePath = basePath + "cover" + File.separator + newFileName + ".png";
-        try {
-            FileUtils.copyFile(file, new File(newFilePath));
-            return true;
-        } catch (IOException e) {
-            log.error(ExceptionUtils.getStackTrace(e));
+            return "";
         }
-        return false;
+        String oldFile = "cover" + File.separator + oldFileName + ".png";
+        String newFilePath =  "cover" + File.separator + newFileName + ".png";
+        return ossService.copy(oldFile, newFilePath);
     }
 
     @Override

+ 0 - 20
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/enums/FileUploadType.java

@@ -1,20 +0,0 @@
-package com.gccloud.dataroom.core.module.file.enums;
-
-/**
- * @Description Description
- * @Author Acechengui
- * @Date Created in 2023-10-14
- */
-public enum FileUploadType {
-    LOCAL("local"),
-    MINIO("minio");
-
-    // 以上是枚举的成员,必须先定义,而且使用分号结束
-    private final String value;
-    FileUploadType(String value) {
-        this.value = value;
-    }
-    public String getValue() {
-        return value;
-    }
-}

+ 0 - 57
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/FileOperationStrategy.java

@@ -1,57 +0,0 @@
-package com.gccloud.dataroom.core.module.file.service;
-
-import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
-import com.gccloud.dataroom.core.module.file.enums.FileUploadType;
-import lombok.extern.slf4j.Slf4j;
-import org.jetbrains.annotations.NotNull;
-import org.springframework.context.annotation.Condition;
-import org.springframework.context.annotation.ConditionContext;
-import org.springframework.core.type.AnnotatedTypeMetadata;
-import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * 文件操作策略类
- * @author Acechengui
- */
-@Service
-@Slf4j
-public class FileOperationStrategy {
-    private final IDataRoomOssService dataRoomOssService;
-
-    public FileOperationStrategy(IDataRoomOssService  localFileService, IDataRoomOssService  minioFileService) {
-        this.dataRoomOssService = localFileService != null ? localFileService : minioFileService;
-    }
-
-    public static class LocalFileCondition implements Condition {
-        @Override
-        public boolean matches(ConditionContext context, @NotNull AnnotatedTypeMetadata metadata) {
-            String uploadType = context.getEnvironment().getProperty("gc.starter.file.uploadType");
-            return FileUploadType.LOCAL.getValue().equalsIgnoreCase(uploadType) ||
-                   !FileUploadType.MINIO.getValue().equalsIgnoreCase(uploadType);
-        }
-    }
-
-    public static class MinioFileCondition implements Condition {
-        @Override
-        public boolean matches(ConditionContext context, @NotNull AnnotatedTypeMetadata metadata) {
-            String uploadType = context.getEnvironment().getProperty("gc.starter.file.uploadType");
-            return FileUploadType.MINIO.getValue().equalsIgnoreCase(uploadType);
-        }
-    }
-
-    public DataRoomFileEntity upload(MultipartFile file, DataRoomFileEntity entity, HttpServletResponse response, HttpServletRequest request) {
-        return dataRoomOssService.upload(file, entity, response, request);
-    }
-
-    public void download(String fileId, HttpServletResponse response, HttpServletRequest request) {
-        dataRoomOssService.download(fileId, response, request);
-    }
-
-    public void delete(String fileId) {
-        dataRoomOssService.delete(fileId);
-    }
-}

+ 22 - 1
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/IDataRoomOssService.java

@@ -6,6 +6,7 @@ import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
 
 /**
  * 文件管理
@@ -14,7 +15,7 @@ import javax.servlet.http.HttpServletResponse;
  */
 public interface IDataRoomOssService {
     /**
-     * 上传文件
+     * 上传文件,文件名重新生成
      *
      * @param file
      * @param entity
@@ -24,6 +25,18 @@ public interface IDataRoomOssService {
      */
     DataRoomFileEntity upload(MultipartFile file, DataRoomFileEntity entity, HttpServletResponse response, HttpServletRequest request);
 
+
+    /**
+     * 上传文件, 保留传入的文件名
+     * @param inputStream
+     * @param fileName
+     * @param size
+     * @param entity
+     * @return
+     */
+    DataRoomFileEntity upload(InputStream inputStream, String fileName, long size, DataRoomFileEntity entity);
+
+
     /**
      * 下载文件
      *
@@ -39,4 +52,12 @@ public interface IDataRoomOssService {
      * @param fileId
      */
     void delete(String fileId);
+
+    /**
+     * 复制文件,目前用于封面复制
+     * @param sourcePath
+     * @param targetPath
+     * @return 返回复制后的文件路径
+     */
+    default String copy(String sourcePath, String targetPath) {return "";}
 }

+ 154 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomFtpFileServiceImpl.java

@@ -0,0 +1,154 @@
+package com.gccloud.dataroom.core.module.file.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.gccloud.common.exception.GlobalException;
+import com.gccloud.dataroom.core.config.DataRoomConfig;
+import com.gccloud.dataroom.core.config.bean.FileConfig;
+import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomFileService;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
+import com.gccloud.dataroom.core.utils.FtpClientUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+
+/**
+ * ftp文件管理实现类
+ * 将文件上传至ftp服务器,需要配置ftp服务器相关信息
+ * 由于该方案无法直接通过url访问文件,所以需要手动在对应的服务器上部署nginx等服务,将ftp服务器上的文件开放访问,然后将该服务地址配置到gc.starter.file.urlPrefix中
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/17 15:12
+ */
+@Service
+@Slf4j
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "ftp")
+public class DataRoomFtpFileServiceImpl implements IDataRoomOssService {
+
+    @Resource
+    private FtpClientUtil ftpUtil;
+    @Resource
+    private DataRoomConfig bigScreenConfig;
+    @Resource
+    private IDataRoomFileService sysFileService;
+
+
+    @Override
+    public DataRoomFileEntity upload(MultipartFile file, DataRoomFileEntity fileEntity, HttpServletResponse response, HttpServletRequest request) {
+        String originalFilename = file.getOriginalFilename();
+        // 提取文件后缀名
+        String extension = FilenameUtils.getExtension(originalFilename);
+        FileConfig fileConfig = bigScreenConfig.getFile();
+        if (!fileConfig.getAllowedFileExtensionName().contains("*") && !fileConfig.getAllowedFileExtensionName().contains(extension)) {
+            log.error("不支持 {} 文件类型",extension);
+            throw new GlobalException("不支持的文件类型");
+        }
+        // 重命名
+        String id = IdWorker.getIdStr();
+        String newFileName = id + "." + extension;
+        long size = file.getSize();
+        InputStream inputStream;
+        try {
+            inputStream = file.getInputStream();
+        } catch (IOException e) {
+            log.error("上传文件到FTP服务失败:获取文件流失败");
+            log.error(ExceptionUtils.getStackTrace(e));
+            throw new GlobalException("获取文件流失败");
+        }
+        return this.upload(inputStream, newFileName, size, fileEntity);
+    }
+
+
+    @Override
+    public DataRoomFileEntity upload(InputStream inputStream, String fileName, long size, DataRoomFileEntity fileEntity) {
+        // 提取文件后缀名
+        String extension = FilenameUtils.getExtension(fileName);
+        // 上传的目标路径
+        String basePath = bigScreenConfig.getFile().getBasePath();
+        // 上传文件到ftp
+        boolean upload = ftpUtil.upload(basePath, fileName, inputStream);
+        if (!upload) {
+            log.error("上传文件到ftp失败");
+            throw new GlobalException("上传文件到ftp失败");
+        }
+        fileEntity.setOriginalName(fileName);
+        fileEntity.setNewName(fileName);
+        fileEntity.setPath(basePath);
+        fileEntity.setSize(size);
+        fileEntity.setExtension(extension);
+        fileEntity.setUrl("/" + fileName);
+        return fileEntity;
+    }
+
+    @Override
+    public void download(String fileId, HttpServletResponse response, HttpServletRequest request) {
+        DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
+        if (fileEntity == null) {
+            response.setStatus(HttpStatus.NOT_FOUND.value());
+            log.error("下载的文件不存在");
+            return;
+        }
+        response.setContentType("application/x-msdownload");
+        response.setContentType("multipart/form-data");
+        // 不设置前端无法从header获取文件名
+        response.setHeader("Access-Control-Expose-Headers", "filename");
+        try {
+            response.setHeader("filename", URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
+            // 解决下载的文件不携带后缀
+            response.setHeader("Content-Disposition", "attachment;fileName="+URLEncoder.encode(fileEntity.getOriginalName(),"UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+        OutputStream outputStream;
+        try {
+            outputStream = response.getOutputStream();
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+            return;
+        }
+        boolean download = ftpUtil.download(fileEntity.getPath(), fileEntity.getNewName(), outputStream);
+        if (!download) {
+            log.error("下载文件失败");
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+        }
+        try {
+            outputStream.close();
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+        sysFileService.updateDownloadCount(1, fileId);
+    }
+
+    @Override
+    public void delete(String fileId) {
+        DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
+        if (fileEntity == null) {
+            log.error("删除的文件不存在");
+            return;
+        }
+        sysFileService.removeById(fileId);
+        // 删除ftp上的文件
+        ftpUtil.delete(fileEntity.getPath(), fileEntity.getNewName());
+    }
+
+    @Override
+    public String copy(String sourcePath, String targetPath) {
+        String basePath = bigScreenConfig.getFile().getBasePath() + File.separator;
+        boolean copySuccess = ftpUtil.copy(basePath + sourcePath, basePath + targetPath);
+        if (!copySuccess) {
+            return "";
+        }
+        return targetPath;
+    }
+}

+ 68 - 10
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomLocalFileServiceImpl.java

@@ -1,19 +1,18 @@
 package com.gccloud.dataroom.core.module.file.service.impl;
 
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.gccloud.common.exception.GlobalException;
 import com.gccloud.dataroom.core.config.DataRoomConfig;
 import com.gccloud.dataroom.core.config.bean.FileConfig;
 import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
-import com.gccloud.dataroom.core.module.file.service.FileOperationStrategy;
 import com.gccloud.dataroom.core.module.file.service.IDataRoomFileService;
 import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
-import com.gccloud.common.exception.GlobalException;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Conditional;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
@@ -21,19 +20,15 @@ import org.springframework.web.multipart.MultipartFile;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.net.URLEncoder;
 
 /**
  * 文件管理
  */
-@Conditional(FileOperationStrategy.LocalFileCondition.class)
-@Service("localFileService")
 @Slf4j
-@ConditionalOnProperty(prefix = "gc.starter.dataroom.component", name = "IDataRoomOssService", havingValue = "DataRoomLocalFileServiceImpl", matchIfMissing = true)
+@Service
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "local", matchIfMissing = true)
 public class DataRoomLocalFileServiceImpl implements IDataRoomOssService {
 
     @Resource
@@ -75,6 +70,44 @@ public class DataRoomLocalFileServiceImpl implements IDataRoomOssService {
         return fileEntity;
     }
 
+
+    @Override
+    public DataRoomFileEntity upload(InputStream inputStream, String fileName, long size, DataRoomFileEntity fileEntity) {
+        // 上传文件保存到的路径, 从配置文件获取
+        String basePath = bigScreenConfig.getFile().getBasePath();
+        // 提取文件后缀名
+        String extension = FilenameUtils.getExtension(fileName);
+        String destPath = basePath + File.separator + fileName;
+        try {
+            File dest = new File(destPath);
+            // 检查文件所在的目录是否存在,不存在则创建
+            String parent = dest.getParent();
+            File parentFile = new File(parent);
+            if (!parentFile.exists()) {
+                FileUtils.forceMkdir(parentFile);
+            }
+            FileOutputStream outputStream = new FileOutputStream(dest);
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            outputStream.close();
+            inputStream.close();
+        } catch (Exception e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+            log.error(String.format("文件 %s 存储到 %s 失败", fileName, destPath));
+            throw new GlobalException("文件上传失败");
+        }
+        fileEntity.setOriginalName(fileName);
+        fileEntity.setNewName(fileName);
+        fileEntity.setPath(basePath);
+        fileEntity.setSize(size);
+        fileEntity.setExtension(extension);
+        fileEntity.setUrl("/" + fileName);
+        return fileEntity;
+    }
+
     @Override
     public void download(String fileId, HttpServletResponse response, HttpServletRequest request) {
         DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
@@ -131,4 +164,29 @@ public class DataRoomLocalFileServiceImpl implements IDataRoomOssService {
         }
         file.delete();
     }
+
+    @Override
+    public String copy(String sourcePath, String targetPath) {
+        String basePath = bigScreenConfig.getFile().getBasePath() + File.separator;
+        File sourceFile = new File(basePath + sourcePath);
+        File targetFile = new File(basePath + targetPath);
+        // 检查源文件是否存在
+        if (!sourceFile.exists()) {
+            log.error("复制源文件不存在:{}", sourcePath);
+            return "";
+        }
+        // 检查源文件是否是文件夹
+        if (sourceFile.isDirectory()) {
+            log.error("源文件为文件夹:{},无法复制", sourcePath);
+            return "";
+        }
+        try {
+            FileUtils.copyFile(sourceFile, targetFile);
+        } catch (IOException e) {
+            log.error(String.format("文件 %s 复制到 %s 失败", sourcePath, targetPath));
+            log.error(ExceptionUtils.getStackTrace(e));
+            return "";
+        }
+        return targetPath;
+    }
 }

+ 103 - 29
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomMinioServiceImpl.java

@@ -3,21 +3,20 @@ package com.gccloud.dataroom.core.module.file.service.impl;
 import com.baomidou.mybatisplus.core.toolkit.IdWorker;
 import com.gccloud.common.exception.GlobalException;
 import com.gccloud.dataroom.core.config.DataRoomConfig;
-import com.gccloud.dataroom.core.config.MinioConfig;
 import com.gccloud.dataroom.core.config.bean.FileConfig;
+import com.gccloud.dataroom.core.config.bean.MinioConfig;
 import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
-import com.gccloud.dataroom.core.module.file.service.FileOperationStrategy;
 import com.gccloud.dataroom.core.module.file.service.IDataRoomFileService;
 import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
 import com.gccloud.dataroom.core.utils.MinioFileInterface;
-import io.minio.MinioClient;
-import io.minio.PutObjectArgs;
-import lombok.SneakyThrows;
+import com.gccloud.dataroom.core.utils.PathUtils;
+import io.minio.*;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.context.annotation.Conditional;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
@@ -26,6 +25,7 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
@@ -35,14 +35,11 @@ import java.time.format.DateTimeFormatter;
  *
  * @author Acechengui
  */
-@Service("minioFileService")
-@Conditional(FileOperationStrategy.MinioFileCondition.class)
 @Slf4j
+@Service
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "minio")
 public class DataRoomMinioServiceImpl implements IDataRoomOssService {
 
-    @Resource
-    private MinioConfig minioConfig;
-
     @Resource
     private MinioClient minioclient;
 
@@ -51,14 +48,13 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
 
     @Resource
     private IDataRoomFileService sysFileService;
+
     @Resource
     private MinioFileInterface minioFileInterface;
 
     /**
      * 上传文件
-     *
      */
-    @SneakyThrows
     @Override
     public DataRoomFileEntity upload(MultipartFile file, DataRoomFileEntity fileEntity, HttpServletResponse response, HttpServletRequest request) {
         String originalFilename = file.getOriginalFilename();
@@ -66,7 +62,7 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
         String extension = FilenameUtils.getExtension(originalFilename);
         FileConfig fileConfig = bigScreenConfig.getFile();
         if (!fileConfig.getAllowedFileExtensionName().contains("*") && !fileConfig.getAllowedFileExtensionName().contains(extension)) {
-            log.error("不支持 {} 文件类型",extension);
+            log.error("不支持 {} 文件类型", extension);
             throw new GlobalException("不支持的文件类型");
         }
         String module = request.getParameter("module");
@@ -77,31 +73,74 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
         // 重命名
         String newFileName = IdWorker.getIdStr() + "." + extension;
         // 组装路径:获取当前日期并格式化为"yyyy/mm/dd"格式的字符串
-        String filePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/" + newFileName;
-        InputStream inputStream = file.getInputStream();
-        PutObjectArgs args = PutObjectArgs.builder()
+        String basePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+        String filePath = basePath + "/" + newFileName;
+        MinioConfig minioConfig = bigScreenConfig.getFile().getMinio();
+        try (InputStream inputStream = file.getInputStream()) {
+            PutObjectArgs args = PutObjectArgs.builder()
                     .bucket(minioConfig.getBucketName())
                     .object(filePath)
                     .stream(inputStream, file.getSize(), -1)
                     .contentType(file.getContentType())
                     .build();
-        minioclient.putObject(args);
-
-        String url = minioConfig.getUrl() + "/" + minioConfig.getBucketName() + "/" + filePath;
+            minioclient.putObject(args);
+        } catch (Exception e) {
+            log.error("上传文件到Minio失败");
+            log.error(ExceptionUtils.getStackTrace(e));
+            throw new GlobalException("上传文件失败");
+        }
+        // 现在url存储文件的相对路径,对于minio来说,就是bucketName/文件名
+        String url = "/" + minioConfig.getBucketName() + "/" + filePath;
         fileEntity.setOriginalName(originalFilename);
         fileEntity.setNewName(newFileName);
         fileEntity.setPath(filePath);
         fileEntity.setSize(file.getSize());
         fileEntity.setExtension(extension);
         fileEntity.setUrl(url);
+        fileEntity.setModule(module);
+        fileEntity.setBucket(minioConfig.getBucketName());
         return fileEntity;
     }
 
+    @Override
+    public DataRoomFileEntity upload(InputStream inputStream, String fileName, long size, DataRoomFileEntity entity) {
+        fileName = PathUtils.normalizePath(fileName);
+        String extension = FilenameUtils.getExtension(fileName);
+        MinioConfig minioConfig = bigScreenConfig.getFile().getMinio();
+        long fileSize = size == 0 ? -1 : size;
+        // 使用minio的最小分片大小
+        long partSize = fileSize == -1 ? ObjectWriteArgs.MIN_MULTIPART_SIZE : -1;
+        try {
+            PutObjectArgs args = PutObjectArgs.builder()
+                    .bucket(minioConfig.getBucketName())
+                    .object(fileName)
+                    .stream(inputStream, fileSize, partSize)
+                    .contentType("image/png")
+                    .build();
+            minioclient.putObject(args);
+        } catch (Exception e) {
+            log.error("上传文件到Minio失败");
+            log.error(ExceptionUtils.getStackTrace(e));
+            throw new GlobalException("上传文件失败");
+        } finally {
+            IOUtils.closeQuietly(inputStream);
+        }
+        // 现在url存储文件的相对路径,对于minio来说,就是bucketName/文件名
+        String url = "/" + minioConfig.getBucketName() + "/" + fileName;
+        entity.setOriginalName(fileName);
+        entity.setNewName(fileName);
+        entity.setPath(fileName);
+        entity.setSize(fileSize);
+        entity.setExtension(extension);
+        entity.setUrl(url);
+        entity.setBucket(minioConfig.getBucketName());
+        return entity;
+    }
+
+
     /**
      * 下载文件
-     *
      */
-    @SneakyThrows
     @Override
     public void download(String fileId, HttpServletResponse response, HttpServletRequest request) {
         DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
@@ -114,9 +153,14 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
         response.setContentType("multipart/form-data");
         // 不设置前端无法从header获取文件名
         response.setHeader("Access-Control-Expose-Headers", "filename");
-        response.setHeader("filename", URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
-        // 解决下载的文件不携带后缀
-        response.setHeader("Content-Disposition", "attachment;fileName="+URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
+        try {
+            response.setHeader("filename", URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
+            // 解决下载的文件不携带后缀
+            response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            log.error("文件名编码失败");
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
         try {
             InputStream is = minioFileInterface.download(fileEntity.getPath());
             IOUtils.copy(is, response.getOutputStream());
@@ -124,7 +168,8 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
             response.getOutputStream().close();
         } catch (Exception e) {
             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
-            log.error(String.format("下载文件%s失败", fileEntity.getOriginalName()));
+            log.error("下载文件失败: {}", fileEntity.getPath());
+            log.error(ExceptionUtils.getStackTrace(e));
         } finally {
             sysFileService.updateDownloadCount(1, fileId);
         }
@@ -133,10 +178,39 @@ public class DataRoomMinioServiceImpl implements IDataRoomOssService {
     /**
      * 删除文件
      */
-    @SneakyThrows
     @Override
     public void delete(String fileId) {
-        String path = sysFileService.getById(fileId).getPath();
-        minioFileInterface.deleteObject(path.substring(path.indexOf(minioConfig.getBucketName())+minioConfig.getBucketName().length()+1));
+        DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
+        if (fileEntity == null) {
+            log.error("删除的文件不存在");
+            return;
+        }
+        sysFileService.removeById(fileId);
+        String path = fileEntity.getPath();
+        try {
+            minioFileInterface.deleteObject(path);
+        } catch (Exception e) {
+            log.error("删除Minio文件失败: {}", path);
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+    }
+
+    @Override
+    public String copy(String sourcePath, String targetPath) {
+        MinioConfig minioConfig = bigScreenConfig.getFile().getMinio();
+        CopySource source = CopySource.builder().bucket(minioConfig.getBucketName()).object(sourcePath).build();
+        CopyObjectArgs args = CopyObjectArgs.builder()
+                .bucket(minioConfig.getBucketName())
+                .object(PathUtils.normalizePath(targetPath))
+                .source(source)
+                .build();
+        try {
+            minioclient.copyObject(args);
+        } catch (Exception e) {
+            log.error("复制Minio文件失败: {}", sourcePath);
+            log.error(ExceptionUtils.getStackTrace(e));
+            return "";
+        }
+        return minioConfig.getBucketName() + "/" + targetPath;
     }
 }

+ 157 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomSftpFileServiceImpl.java

@@ -0,0 +1,157 @@
+package com.gccloud.dataroom.core.module.file.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import com.gccloud.common.exception.GlobalException;
+import com.gccloud.dataroom.core.config.DataRoomConfig;
+import com.gccloud.dataroom.core.config.bean.FileConfig;
+import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomFileService;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
+import com.gccloud.dataroom.core.utils.SftpClientUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+
+/**
+ * sftp文件管理实现类
+ * 将文件上传至sftp服务器,需要配置sftp服务器相关信息
+ * 由于该方案无法直接通过url访问文件,所以需要手动在对应的服务器上部署nginx等服务,将sftp服务器上的文件开放访问,然后将该服务地址配置到gc.starter.file.urlPrefix中
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/17 15:12
+ */
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "sftp")
+public class DataRoomSftpFileServiceImpl implements IDataRoomOssService {
+
+    @Resource
+    private SftpClientUtils sftpUtil;
+    @Resource
+    private DataRoomConfig bigScreenConfig;
+    @Resource
+    private IDataRoomFileService sysFileService;
+
+
+    @Override
+    public DataRoomFileEntity upload(MultipartFile file, DataRoomFileEntity fileEntity, HttpServletResponse response, HttpServletRequest request) {
+        String originalFilename = file.getOriginalFilename();
+        // 提取文件后缀名
+        String extension = FilenameUtils.getExtension(originalFilename);
+        FileConfig fileConfig = bigScreenConfig.getFile();
+        if (!fileConfig.getAllowedFileExtensionName().contains("*") && !fileConfig.getAllowedFileExtensionName().contains(extension)) {
+            log.error("不支持 {} 文件类型",extension);
+            throw new GlobalException("不支持的文件类型");
+        }
+        long size = file.getSize();
+        // 重命名
+        String id = IdWorker.getIdStr();
+        String newFileName = id + "." + extension;
+        InputStream inputStream;
+        try {
+            inputStream = file.getInputStream();
+        } catch (IOException e) {
+            log.error("上传文件到SFTP服务失败:获取文件流失败");
+            log.error(ExceptionUtils.getStackTrace(e));
+            throw new GlobalException("获取文件流失败");
+        }
+        this.upload(inputStream, newFileName, size, fileEntity);
+        return fileEntity;
+    }
+
+
+    @Override
+    public DataRoomFileEntity upload(InputStream inputStream, String fileName, long size, DataRoomFileEntity fileEntity) {
+        // 提取文件后缀名
+        String extension = FilenameUtils.getExtension(fileName);
+        // 上传的目标路径
+        String basePath = bigScreenConfig.getFile().getBasePath();
+        // 上传文件到sftp
+        boolean upload = sftpUtil.upload(basePath, fileName, inputStream);
+        if (!upload) {
+            log.error("上传文件到sftp失败");
+            throw new GlobalException("上传文件到sftp失败");
+        }
+        fileEntity.setOriginalName(fileName);
+        fileEntity.setNewName(fileName);
+        fileEntity.setPath(basePath);
+        fileEntity.setSize(size);
+        fileEntity.setExtension(extension);
+        fileEntity.setUrl("/" + fileName);
+        return fileEntity;
+    }
+
+    @Override
+    public void download(String fileId, HttpServletResponse response, HttpServletRequest request) {
+        DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
+        if (fileEntity == null) {
+            response.setStatus(HttpStatus.NOT_FOUND.value());
+            log.error("下载的文件不存在");
+            return;
+        }
+        response.setContentType("application/x-msdownload");
+        response.setContentType("multipart/form-data");
+        // 不设置前端无法从header获取文件名
+        response.setHeader("Access-Control-Expose-Headers", "filename");
+        try {
+            response.setHeader("filename", URLEncoder.encode(fileEntity.getOriginalName(), "UTF-8"));
+            // 解决下载的文件不携带后缀
+            response.setHeader("Content-Disposition", "attachment;fileName="+URLEncoder.encode(fileEntity.getOriginalName(),"UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+        OutputStream outputStream;
+        try {
+            outputStream = response.getOutputStream();
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+            return;
+        }
+        boolean download = sftpUtil.download(fileEntity.getPath(), fileEntity.getNewName(), outputStream);
+        if (!download) {
+            log.error("下载文件失败");
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+        }
+        try {
+            outputStream.close();
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+        sysFileService.updateDownloadCount(1, fileId);
+    }
+
+    @Override
+    public void delete(String fileId) {
+        DataRoomFileEntity fileEntity = sysFileService.getById(fileId);
+        if (fileEntity == null) {
+            log.error("删除的文件不存在");
+            return;
+        }
+        sysFileService.removeById(fileId);
+        // 删除sftp上的文件
+        sftpUtil.delete(fileEntity.getPath(), fileEntity.getNewName());
+    }
+
+
+    @Override
+    public String copy(String sourcePath, String targetPath) {
+        String basePath = bigScreenConfig.getFile().getBasePath() + File.separator;
+
+        boolean copySuccess = sftpUtil.copy(basePath + sourcePath, basePath + targetPath);
+        if (!copySuccess) {
+            return "";
+        }
+        return targetPath;
+    }
+}

+ 153 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpClientFactory.java

@@ -0,0 +1,153 @@
+package com.gccloud.dataroom.core.module.file.service.pool.ftp;
+
+import com.gccloud.dataroom.core.config.DataRoomConfig;
+import com.gccloud.dataroom.core.config.bean.FtpConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPReply;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.PooledObjectFactory;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * FtpClient 工厂连接对象
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 10:17
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "ftp")
+public class FtpClientFactory implements PooledObjectFactory<FTPClient> {
+    /**
+     * 注入 ftp 连接配置
+     */
+    @Resource
+    private DataRoomConfig config;
+
+    /**
+     * 创建连接到池中
+     *
+     * @return
+     * @throws Exception
+     */
+    @Override
+    public PooledObject<FTPClient> makeObject() throws Exception {
+        log.info("创建ftp连接");
+        FtpConfig ftp = config.getFile().getFtp();
+        FTPClient ftpClient = new FTPClient();
+        ftpClient.setConnectTimeout(ftp.getClientTimeout());
+        ftpClient.connect(ftp.getHost(), ftp.getPort());
+        int reply = ftpClient.getReplyCode();
+        if (!FTPReply.isPositiveCompletion(reply)) {
+            ftpClient.disconnect();
+            return null;
+        }
+        boolean success;
+        if (StringUtils.isBlank(ftp.getUsername())) {
+            success = ftpClient.login("anonymous", "anonymous");
+        } else {
+            success = ftpClient.login(ftp.getUsername(), ftp.getPassword());
+        }
+        if (!success) {
+            return null;
+        }
+        ftpClient.setFileType(ftp.getTransferFileType());
+        ftpClient.setBufferSize(1024);
+        ftpClient.setControlEncoding(ftp.getEncoding());
+        if (ftp.isPassiveMode()) {
+            ftpClient.enterLocalPassiveMode();
+        }
+        log.debug("创建ftp连接");
+        return new DefaultPooledObject<>(ftpClient);
+    }
+
+    /**
+     * 链接状态检查
+     *
+     * @param pool
+     * @return
+     */
+    @Override
+    public boolean validateObject(PooledObject<FTPClient> pool) {
+        FTPClient ftpClient = pool.getObject();
+        try {
+            return ftpClient != null && ftpClient.sendNoOp();
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 销毁连接,当连接池空闲数量达到上限时,调用此方法销毁连接
+     *
+     * @param pool
+     * @throws Exception
+     */
+    @Override
+    public void destroyObject(PooledObject<FTPClient> pool) throws Exception {
+        FTPClient ftpClient = pool.getObject();
+        if (ftpClient != null) {
+            try {
+                ftpClient.disconnect();
+                log.debug("销毁ftp连接");
+            } catch (Exception e) {
+                log.error("销毁FtpClient异常");
+                log.error(ExceptionUtils.getStackTrace(e));
+            }
+        }
+    }
+
+    /**
+     * 钝化连接
+     * 在连接被归还到连接池时,调用此方法
+     * @param p
+     * @throws Exception
+     */
+    @Override
+    public void passivateObject(PooledObject<FTPClient> p) throws Exception{
+        FTPClient ftpClient = p.getObject();
+        try {
+            ftpClient.changeWorkingDirectory(config.getFile().getBasePath());
+            // 钝化链接时,如果logout,下次再使用重新连接时间会长,所以先不做logout操作
+            /*
+             * ftpClient.logout();
+             * if (ftpClient.isConnected()) {
+             *     ftpClient.disconnect();
+             * }
+             */
+        } catch (Exception e) {
+            log.error("钝化FtpClient异常");
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+    }
+
+    /**
+     * 激活连接
+     * 在连接从连接池中取出时,调用此方法
+     * @param pool
+     * @throws Exception
+     */
+    @Override
+    public void activateObject(PooledObject<FTPClient> pool) throws Exception {
+        FtpConfig ftp = config.getFile().getFtp();
+        FTPClient ftpClient = pool.getObject();
+        if (!ftpClient.isConnected()) {
+            log.info("ftp连接已关闭,重新连接");
+            ftpClient.connect(ftp.getHost(),ftp.getPort());
+            ftpClient.login(ftp.getUsername(), ftp.getPassword());
+        }
+        ftpClient.setControlEncoding(ftp.getEncoding());
+        ftpClient.changeWorkingDirectory(config.getFile().getBasePath());
+        // 设置上传文件类型为二进制,否则将无法打开文件
+        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+    }
+
+}

+ 79 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpPoolServiceImpl.java

@@ -0,0 +1,79 @@
+package com.gccloud.dataroom.core.module.file.service.pool.ftp;
+
+import com.gccloud.dataroom.core.config.bean.FtpConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+/**
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 10:20
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "ftp")
+public class FtpPoolServiceImpl {
+
+    /**
+     * ftp 连接池生成
+     */
+    private GenericObjectPool<FTPClient> pool;
+
+    /**
+     * ftp 客户端配置文件
+     */
+    @Resource
+    private FtpConfig config;
+
+    /**
+     * ftp 客户端工厂
+     */
+    @Resource
+    private FtpClientFactory factory;
+
+    /**
+     * 初始化pool
+     */
+    @PostConstruct
+    private void initPool() {
+        log.info("初始化FTP连接池");
+        this.pool = new GenericObjectPool<FTPClient>(this.factory, this.config);
+    }
+
+    /**
+     * 获取ftpClient
+     */
+    public FTPClient borrowObject() {
+        log.info("获取 FTPClient");
+        if (this.pool != null) {
+            try {
+                return this.pool.borrowObject();
+            } catch (Exception e) {
+                log.error("获取 FTPClient 失败 ", e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 归还 ftpClient
+     */
+    public void returnObject(FTPClient ftpClient) {
+        if (this.pool == null || ftpClient == null) {
+            return;
+        }
+        try {
+            ftpClient.changeWorkingDirectory("/");
+        } catch (Exception e) {
+            log.error("FTPClient 重置目录失败 ", e);
+        }
+        this.pool.returnObject(ftpClient);
+    }
+
+}

+ 105 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpClientFactory.java

@@ -0,0 +1,105 @@
+package com.gccloud.dataroom.core.module.file.service.pool.sftp;
+
+import com.gccloud.dataroom.core.config.bean.SftpConfig;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.Session;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Properties;
+
+/**
+ * SFTP 工厂连接对象
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 15:17
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "sftp")
+public class SftpClientFactory extends BasePooledObjectFactory<ChannelSftp>{
+
+    @Resource
+    private SftpConfig config;
+
+    /**
+     * 新建对象
+     */
+    @Override
+    public ChannelSftp create() {
+        ChannelSftp channel = null;
+        try {
+            // 用户名密码不能为空
+            if (StringUtils.isBlank(config.getUsername()) || StringUtils.isBlank(config.getPassword())) {
+                log.error("SFTP用户名密码不能为空");
+                return null;
+            }
+            JSch jsch = new JSch();
+            // 设置私钥
+            if (StringUtils.isNotBlank(config.getPrivateKey())) {
+                jsch.addIdentity(config.getPrivateKey());
+            }
+            // jsch的session需要补充设置sshConfig.put("PreferredAuthentications", "publickey,keyboard-interactive,password")来跳过Kerberos认证,同样的HutoolSFTPUtil工具类里面也有这个问题
+            Session sshSession = jsch.getSession(config.getUsername(), config.getHost(), config.getPort());
+            sshSession.setPassword(config.getPassword());
+            Properties sshConfig = new Properties();
+            // “StrictHostKeyChecking”如果设置成“yes”,ssh就不会自动把计算机的密匙加入“$HOME/.ssh/known_hosts”文件,并且一旦计算机的密匙发生了变化,就拒绝连接。
+            sshConfig.put("StrictHostKeyChecking", "no");
+            sshSession.setConfig(sshConfig);
+            sshSession.connect();
+            channel = (ChannelSftp) sshSession.openChannel("sftp");
+            channel.connect();
+        } catch (Exception e) {
+            log.error("连接 sftp 失败,请检查配置");
+            log.error(ExceptionUtils.getStackTrace(e));
+        }
+        return channel;
+    }
+
+    /**
+     * 创建一个连接
+     *
+     * @param channelSftp
+     * @return
+     */
+    @Override
+    public PooledObject<ChannelSftp> wrap(ChannelSftp channelSftp) {
+        return new DefaultPooledObject<>(channelSftp);
+    }
+
+    /**
+     * 销毁一个连接
+     *
+     * @param p
+     */
+    @Override
+    public void destroyObject(PooledObject<ChannelSftp> p) {
+        ChannelSftp channelSftp = p.getObject();
+        channelSftp.disconnect();
+    }
+
+    @Override
+    public boolean validateObject(final PooledObject<ChannelSftp> p) {
+        final ChannelSftp channelSftp = p.getObject();
+        try {
+            if (channelSftp.isClosed()) {
+                return false;
+            }
+            channelSftp.cd("/");
+        } catch (Exception e) {
+            log.error("ChannelSftp 不可用");
+            log.error(ExceptionUtils.getStackTrace(e));
+            return false;
+        }
+        return true;
+    }
+
+}

+ 74 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpPoolService.java

@@ -0,0 +1,74 @@
+package com.gccloud.dataroom.core.module.file.service.pool.sftp;
+
+import com.gccloud.dataroom.core.config.bean.SftpConfig;
+import com.jcraft.jsch.ChannelSftp;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+/**
+ * Sftp 连接池服务类
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 15:21
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "sftp")
+public class SftpPoolService {
+
+    /**
+     * ftp 连接池生成
+     */
+    private GenericObjectPool<ChannelSftp> pool;
+
+    /**
+     * ftp 客户端配置文件
+     */
+    @Resource
+    private SftpConfig config;
+
+    /**
+     * ftp 客户端工厂
+     */
+    @Resource
+    private SftpClientFactory factory;
+
+    /**
+     * 初始化pool
+     */
+    @PostConstruct
+    private void initPool() {
+        log.info("初始化SFTP连接池");
+        this.pool = new GenericObjectPool<ChannelSftp>(this.factory, this.config);
+    }
+
+    /**
+     * 获取sftp
+     */
+    public ChannelSftp borrowObject() {
+        if (this.pool != null) {
+            try {
+                return this.pool.borrowObject();
+            } catch (Exception e) {
+                log.error("获取 ChannelSftp 失败", e);
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 归还 sftp
+     */
+    public void returnObject(ChannelSftp channelSftp) {
+        if (this.pool != null && channelSftp != null) {
+            this.pool.returnObject(channelSftp);
+        }
+    }
+
+}

+ 33 - 43
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/manage/service/impl/DataRoomPageServiceImpl.java

@@ -10,6 +10,9 @@ import com.gccloud.dataroom.core.module.basic.entity.PagePreviewEntity;
 import com.gccloud.dataroom.core.module.chart.bean.Chart;
 import com.gccloud.dataroom.core.module.chart.bean.Linkage;
 import com.gccloud.dataroom.core.module.chart.components.datasource.DataSetDataSource;
+import com.gccloud.dataroom.core.module.file.entity.DataRoomFileEntity;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomFileService;
+import com.gccloud.dataroom.core.module.file.service.IDataRoomOssService;
 import com.gccloud.dataroom.core.module.manage.dto.DataRoomPageDTO;
 import com.gccloud.dataroom.core.module.manage.dto.DataRoomSearchDTO;
 import com.gccloud.dataroom.core.module.manage.extend.DataRoomExtendClient;
@@ -23,6 +26,7 @@ import com.gccloud.common.exception.GlobalException;
 import com.gccloud.common.utils.AssertUtils;
 import com.gccloud.common.utils.BeanConvertUtils;
 import com.gccloud.common.vo.PageVO;
+import com.gccloud.dataroom.core.utils.PathUtils;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import lombok.extern.slf4j.Slf4j;
@@ -30,12 +34,13 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.tomcat.util.http.fileupload.FileItem;
 import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.commons.CommonsMultipartFile;
 
 import javax.annotation.Resource;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import java.io.*;
 import java.util.Base64;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +72,9 @@ public class DataRoomPageServiceImpl extends ServiceImpl<DataRoomPageDao, PageEn
     @Resource
     private IDataRoomPagePreviewService previewService;
 
+    @Resource
+    private IDataRoomOssService ossService;
+
     @Override
     public PageEntity getByCode(String code) {
         if (code.startsWith(IDataRoomPagePreviewService.PREVIEW_KEY)) {
@@ -124,24 +132,15 @@ public class DataRoomPageServiceImpl extends ServiceImpl<DataRoomPageDao, PageEn
             base64String = base64String.substring(base64String.indexOf(",") + 1);
             // 解码base64字符串
             byte[] imageBytes = Base64.getDecoder().decode(base64String);
-            String basePath = bigScreenConfig.getFile().getBasePath();
-            // 不是/结尾,加上/
-            if (!basePath.endsWith("/") || !basePath.endsWith("\\")) {
-                basePath += File.separator;
-            }
-            // 检查目录是否存在,不存在则创建
-            File file = new File(basePath + "cover");
-            if (!file.exists()) {
-                file.mkdirs();
-            }
-            // 保存为图片文件
-            String filePath = basePath + "cover" + File.separator + fileName + ".png";
-            fileUrl = "cover" + File.separator + fileName + ".png";
-            FileOutputStream outputStream = new FileOutputStream(filePath);
-            outputStream.write(imageBytes);
-            outputStream.close();
+            InputStream inputStream = new ByteArrayInputStream(imageBytes);
+            DataRoomFileEntity fileEntity = new DataRoomFileEntity();
+            String filePath = "cover" + File.separator + fileName + ".png";
+            ossService.upload(inputStream, filePath, 0, fileEntity);
+            // 封面先不保存到资源库
+            // fileService.save(fileEntity);
             log.info("大屏封面保存至:{}", filePath);
-        } catch (IOException e) {
+            fileUrl = fileEntity.getUrl();
+        } catch (Exception e) {
             log.error(ExceptionUtils.getStackTrace(e));
         }
         return fileUrl;
@@ -154,26 +153,13 @@ public class DataRoomPageServiceImpl extends ServiceImpl<DataRoomPageDao, PageEn
      * @param newFileName
      * @return
      */
-    private boolean copyCoverPicture(String oldFileName, String newFileName) {
+    private String copyCoverPicture(String oldFileName, String newFileName) {
         if (StringUtils.isBlank(oldFileName)) {
-            return false;
-        }
-        String basePath = bigScreenConfig.getFile().getBasePath() + File.separator;
-        String oldFile = basePath + "cover" + File.separator + oldFileName + ".png";
-        // 检查文件是否存在
-        File file = new File(oldFile);
-        if (!file.exists() || !file.isFile()) {
-            return false;
+            return "";
         }
-        // 复制一份
-        String newFilePath = basePath + "cover" + File.separator + newFileName + ".png";
-        try {
-            FileUtils.copyFile(file, new File(newFilePath));
-            return true;
-        } catch (IOException e) {
-            log.error(ExceptionUtils.getStackTrace(e));
-        }
-        return false;
+        String oldFile = "cover" + File.separator + oldFileName + ".png";
+        String newFilePath = "cover" + File.separator + newFileName + ".png";
+        return ossService.copy(oldFile, newFilePath);
     }
 
     @Override
@@ -265,10 +251,14 @@ public class DataRoomPageServiceImpl extends ServiceImpl<DataRoomPageDao, PageEn
             urlPrefix += "/";
         }
         for (PageEntity pageEntity : list) {
-            if (StringUtils.isBlank(pageEntity.getCoverPicture())) {
+            String coverPicture = pageEntity.getCoverPicture();
+            if (StringUtils.isBlank(coverPicture)) {
                 continue;
             }
-            pageEntity.setCoverPicture(urlPrefix + pageEntity.getCoverPicture().replace("\\", "/"));
+            if (coverPicture.startsWith("/")) {
+                coverPicture = coverPicture.substring(1);
+            }
+            pageEntity.setCoverPicture(urlPrefix + PathUtils.normalizePath(coverPicture));
         }
         return page;
     }
@@ -352,11 +342,11 @@ public class DataRoomPageServiceImpl extends ServiceImpl<DataRoomPageDao, PageEn
                 component.setComponentKey(newCode);
             }
         }
-        boolean copy = this.copyCoverPicture(oldCode, screenEntity.getCode());
-        if (!copy) {
+        String copyUrl = this.copyCoverPicture(oldCode, screenEntity.getCode());
+        if (StringUtils.isBlank(copyUrl)) {
             screenEntity.setCoverPicture(null);
         } else {
-            screenEntity.setCoverPicture("cover" + File.separator + screenEntity.getCode() + ".png");
+            screenEntity.setCoverPicture(copyUrl);
         }
         this.save(screenEntity);
         dataRoomExtendClient.afterAdd(screenEntity.getCode());

+ 242 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/FtpClientUtil.java

@@ -0,0 +1,242 @@
+package com.gccloud.dataroom.core.utils;
+
+
+import com.gccloud.dataroom.core.config.bean.FtpConfig;
+import com.gccloud.dataroom.core.module.file.service.pool.ftp.FtpPoolServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPFile;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * 2023/2/5 15:04
+ *
+ * @author HZ
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "ftp")
+public class FtpClientUtil {
+
+    @Resource
+    private FtpConfig config;
+
+    /**
+     * ftp 连接池
+     */
+    @Resource
+    private FtpPoolServiceImpl ftpPoolService;
+
+
+    /**
+     * 上传文件
+     *
+     * @param uploadPath 上传路径
+     * @param fileName   文件名
+     * @param input      文件输入流
+     * @return 上传结果
+     */
+    public boolean upload(String uploadPath, String fileName, InputStream input) {
+        String[] paths = PathUtils.handlePath(uploadPath, fileName);
+        uploadPath = paths[0];
+        fileName = paths[1];
+        boolean success = false;
+        FTPClient ftpClient = ftpPoolService.borrowObject();
+        try {
+            boolean changeSuccess = this.changeWorkingDirectory(uploadPath, ftpClient);
+            if (!changeSuccess) {
+                log.info("切换目录失败,目录:{}", uploadPath);
+                return false;
+            }
+            String workingDir = ftpClient.printWorkingDirectory();
+            log.info("当前工作目录:{}", workingDir);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.setControlEncoding("UTF-8");
+            ftpClient.setBufferSize(config.getBufferSize());
+            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+            ftpClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);
+            success = ftpClient.storeFile(fileName, input);
+            if (success) {
+                log.info("文件上传成功:{}", uploadPath + "/" + fileName);
+            } else {
+                log.info("文件上传失败:{}", uploadPath + "/" + fileName);
+            }
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            if (null != input){
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    log.error(ExceptionUtils.getStackTrace(e));
+                }
+            }
+            ftpPoolService.returnObject(ftpClient);
+        }
+        return success;
+    }
+
+
+    /**
+     * 下载文件到输出流
+     * @param ftpPath FTP服务器文件目录
+     * @param ftpFileName 文件名称
+     * @param outputStream 输出流
+     * @return 下载结果
+     */
+    public boolean download(String ftpPath, String ftpFileName, OutputStream outputStream) {
+        String[] paths = PathUtils.handlePath(ftpPath, ftpFileName);
+        ftpPath = paths[0] + "/";
+        ftpFileName = paths[1];
+        FTPClient ftpClient = ftpPoolService.borrowObject();
+        try {
+            // 检查目标文件是否存在
+            String finalFtpFileName = ftpFileName;
+            FTPFile[] ftpFiles = ftpClient.listFiles(ftpPath, file -> file.isFile() && file.getName().equals(finalFtpFileName));
+            if (ftpFiles == null || ftpFiles.length == 0) {
+                log.info("FTP服务器文件不存在:{}", ftpPath + ftpFileName);
+                return false;
+            }
+            ftpClient.setControlEncoding("UTF-8");
+            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.changeWorkingDirectory(ftpPath);
+            boolean success = ftpClient.retrieveFile(finalFtpFileName, outputStream);
+            if (!success) {
+                log.info("FTP文件下载失败:{}", ftpPath + ftpFileName);
+                return false;
+            }
+            log.info("FTP文件下载成功:{}", ftpPath + ftpFileName);
+            return true;
+        } catch (Exception e) {
+            log.info("FTP文件下载失败:{}", ftpPath + ftpFileName);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            try {
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (IOException e) {
+                log.error(ExceptionUtils.getStackTrace(e));
+            }
+            ftpPoolService.returnObject(ftpClient);
+        }
+        return false;
+    }
+
+
+
+    /**
+     * 从FTP服务器删除文件
+     * 存在文件的目录无法删除
+     *
+     * @param ftpPath  服务器文件存储路径
+     * @param fileName 服务器文件存储名称
+     * @return 删除结果
+     */
+    public boolean delete(String ftpPath, String fileName) {
+        String[] paths = PathUtils.handlePath(ftpPath, fileName);
+        ftpPath = paths[0] + "/";
+        fileName = paths[1];
+        FTPClient ftpClient = ftpPoolService.borrowObject();
+        try {
+            // 在 ftp 目录下获取文件名与 fileName 匹配的文件信息
+            String finalFileName = fileName;
+            FTPFile[] ftpFiles = ftpClient.listFiles(ftpPath, file -> file.isFile() && file.getName().equals(finalFileName));
+            if (ftpFiles == null || ftpFiles.length == 0) {
+                log.error("FTP服务器文件不存在:{},", ftpPath + fileName);
+                return false;
+            }
+            // 删除文件
+            boolean del;
+            ftpClient.changeWorkingDirectory(ftpPath);
+            del = ftpClient.deleteFile(finalFileName);
+            log.info(del ? "文件:{}删除成功" : "文件:{}删除失败", ftpPath + fileName);
+            return del;
+        } catch (IOException e) {
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            ftpPoolService.returnObject(ftpClient);
+        }
+        return false;
+    }
+
+    /**
+     * 复制文件,目前仅支持文件复制
+     * @param sourcePath
+     * @param targetPath
+     */
+    public boolean copy(String sourcePath, String targetPath) {
+        sourcePath = PathUtils.normalizePath(sourcePath);
+        targetPath = PathUtils.normalizePath(targetPath);
+        // 分割路径和文件名
+        String[] sourceSplit = sourcePath.split("/");
+        String[] targetSplit = targetPath.split("/");
+        sourcePath = sourcePath.substring(0, sourcePath.length() - sourceSplit[sourceSplit.length - 1].length());
+        targetPath = targetPath.substring(0, targetPath.length() - targetSplit[targetSplit.length - 1].length());
+        String sourceFileName = sourceSplit[sourceSplit.length - 1];
+        String targetFileName = targetSplit[targetSplit.length - 1];
+        FTPClient ftpClient = ftpPoolService.borrowObject();
+        try {
+            // 检查目标文件是否存在
+            FTPFile[] ftpFiles = ftpClient.listFiles(sourcePath, file -> file.isFile() && file.getName().equals(sourceFileName));
+            if (ftpFiles == null || ftpFiles.length == 0) {
+                log.error("FTP服务器文件不存在:{}", sourcePath + sourceFileName);
+                return false;
+            }
+            ftpClient.setControlEncoding("UTF-8");
+            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
+            ftpClient.enterLocalPassiveMode();
+            ftpClient.changeWorkingDirectory(sourcePath);
+            boolean success = ftpClient.retrieveFile(sourceFileName, new FileOutputStream(targetPath + targetFileName));
+            if (!success) {
+                log.error("FTP文件复制失败:{}", sourcePath + sourceFileName);
+                return false;
+            }
+            log.info("FTP文件复制成功:{}", targetPath + targetFileName);
+            return true;
+        } catch (Exception e) {
+            log.info("FTP文件复制失败:{}", sourcePath + sourceFileName);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            ftpPoolService.returnObject(ftpClient);
+        }
+        return false;
+    }
+
+    /**
+     * 改变当前目录
+     *
+     * @param workPath 新的当前工作目录
+     * @return
+     */
+    private boolean changeWorkingDirectory(String workPath, FTPClient ftpClient) throws IOException {
+        boolean success = ftpClient.changeWorkingDirectory(workPath);
+        if (!success) {
+            String[] dirs = workPath.split("/");
+            for (String str : dirs) {
+                if(StringUtils.isBlank(str)) {
+                    continue;
+                }
+                if (!ftpClient.changeWorkingDirectory(str)) {
+                    boolean makeDirectory = ftpClient.makeDirectory(str);
+                    log.info("创建目录:{}, 结果: {}", str, makeDirectory ? "成功" : "失败");
+                    success = ftpClient.changeWorkingDirectory(str);
+                }
+            }
+        }
+        return success;
+    }
+
+}
+

+ 1 - 1
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/MinioFileInterface.java

@@ -1,6 +1,6 @@
 package com.gccloud.dataroom.core.utils;
 
-import com.gccloud.dataroom.core.config.MinioConfig;
+import com.gccloud.dataroom.core.config.bean.MinioConfig;
 import io.minio.BucketExistsArgs;
 import io.minio.GetObjectArgs;
 import io.minio.GetPresignedObjectUrlArgs;

+ 56 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/PathUtils.java

@@ -0,0 +1,56 @@
+package com.gccloud.dataroom.core.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 17:29
+ */
+public class PathUtils {
+
+    /**
+     * 处理路径,如果路径中包含\,则替换为/,再检查替换后路径,是否包含连续的/,如果包含,则替换为单个/
+     * @param path
+     * @return
+     */
+    public static String normalizePath(String path) {
+        if (StringUtils.isBlank(path)) {
+            return path;
+        }
+        path = path.replace("\\", "/");
+        while (path.contains("//")) {
+            path = path.replace("//", "/");
+        }
+        return path;
+    }
+
+    /**
+     * 处理文件路径和文件名
+     * 文件名可能包含路径,需要将路径分离出来,转移到filePath下
+     * @param filePath
+     * @param fileName
+     * @return
+     */
+    public static String[] handlePath(String filePath, String fileName) {
+        filePath = normalizePath(filePath);
+        // 去除路径最后的/
+        if (filePath.endsWith("/")) {
+            filePath = filePath.substring(0, filePath.length() - 1);
+        }
+        fileName = normalizePath(fileName);
+        // 去除文件名前的/
+        if (fileName.startsWith("/")) {
+            fileName = fileName.substring(1);
+        }
+        // fileName可能包含路径,需要将路径分离出来,转移到filePath下
+        String[] split = fileName.split("/");
+        if (split.length > 1) {
+            String fileNameTemp = split[split.length - 1];
+            String filePathTemp = fileName.substring(0, fileName.length() - fileNameTemp.length());
+            filePath = filePath + "/" + filePathTemp;
+            fileName = fileNameTemp;
+        }
+        return new String[]{filePath, fileName};
+    }
+}

+ 242 - 0
DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/SftpClientUtils.java

@@ -0,0 +1,242 @@
+package com.gccloud.dataroom.core.utils;
+
+import com.gccloud.dataroom.core.config.bean.SftpConfig;
+import com.gccloud.dataroom.core.module.file.service.pool.sftp.SftpPoolService;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.SftpException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Sftp 客户端工具类
+ * @author hongyang
+ * @version 1.0
+ * @date 2023/10/18 15:28
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(prefix = "gc.starter.file", name = "type", havingValue = "sftp")
+public class SftpClientUtils {
+
+    @Resource
+    private SftpConfig config;
+
+    @Resource
+    private SftpPoolService sFtpPoolService;
+
+
+    /**
+     * 上传文件
+     * @param uploadPath
+     * @param fileName
+     * @param inputStream
+     * @return
+     */
+    public boolean upload(String uploadPath, String fileName, InputStream inputStream) {
+        ChannelSftp sftp = sFtpPoolService.borrowObject();
+
+        String[] paths = PathUtils.handlePath(uploadPath, fileName);
+        uploadPath = paths[0];
+        // TODO 检查目录是否存在,不存在则创建
+        if (!this.exist(uploadPath)) {
+            try {
+                sftp.mkdir(uploadPath);
+            } catch (SftpException e) {
+                log.error(ExceptionUtils.getStackTrace(e));
+                return false;
+            }
+        }
+        fileName = "/" + paths[1];
+        String filePath = uploadPath.concat(fileName);
+        try {
+            sftp.put(inputStream, filePath);
+            /**
+             * 权限
+             */
+            String permission = "755";
+            sftp.chmod(Integer.parseInt(permission, 8), filePath);
+            log.info("文件上传成功:{}", filePath);
+            return true;
+        } catch (SftpException e) {
+            log.error("文件上传失败:{}", filePath);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            if (null != inputStream){
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error(ExceptionUtils.getStackTrace(e));
+                }
+            }
+            sFtpPoolService.returnObject(sftp);
+        }
+        return false;
+    }
+
+    /**
+     * 下载文件
+     * @param sftpPath
+     * @param fileName
+     * @param outputStream
+     * @return
+     */
+    public boolean download(String sftpPath, String fileName, OutputStream outputStream) {
+        ChannelSftp sftp = sFtpPoolService.borrowObject();
+        String[] paths = PathUtils.handlePath(sftpPath, fileName);
+        sftpPath = paths[0];
+        fileName = "/" + paths[1];
+        String filePath = sftpPath.concat(fileName);
+        try (InputStream inputStream = sftp.get(filePath)){
+            // 将输入流的数据复制到输出流中
+            byte[] bytes = new byte[config.getBufferSize()];
+            int len;
+            while ((len = inputStream.read(bytes)) != -1) {
+                outputStream.write(bytes, 0, len);
+            }
+            log.info("文件下载成功:{}", filePath);
+            return true;
+        } catch (Exception e ) {
+            log.info("文件下载失败:{}", filePath);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            if (null != outputStream){
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    log.error(ExceptionUtils.getStackTrace(e));
+                }
+            }
+            sFtpPoolService.returnObject(sftp);
+        }
+        return false;
+    }
+
+    /**
+     * 从SFTP服务器删除文件
+     * 存在文件的目录无法删除
+     *
+     * @param sftpPath  服务器文件存储路径
+     * @param fileName 服务器文件存储名称
+     * @return 删除结果
+     */
+    public boolean delete(String sftpPath, String fileName) {
+        ChannelSftp sftp = sFtpPoolService.borrowObject();
+        String[] paths = PathUtils.handlePath(sftpPath, fileName);
+        sftpPath = paths[0];
+        fileName = "/" + paths[1];
+        String filePath = sftpPath.concat(fileName);
+        // 检查文件是否存在
+        if (!this.exist(filePath)) {
+            log.info("文件不存在:{}", filePath);
+            return true;
+        }
+        // 检查是否为文件夹
+        if (this.isDirectory(filePath)) {
+            log.info("该路径为文件夹:{},无法删除", filePath);
+            return false;
+        }
+        try {
+            sftp.rm(filePath);
+            log.info("文件删除成功:{}", filePath);
+            return true;
+        } catch (SftpException e) {
+            log.info("文件删除失败:{}", filePath);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            sFtpPoolService.returnObject(sftp);
+        }
+        return false;
+    }
+
+    /**
+     * 文件复制,目前只支持文件复制
+     * @param sourcePath
+     * @param targetPath
+     */
+    public boolean copy(String sourcePath, String targetPath) {
+        ChannelSftp sftp1 = sFtpPoolService.borrowObject();
+        ChannelSftp sftp2 = sFtpPoolService.borrowObject();
+        sourcePath = PathUtils.normalizePath(sourcePath);
+        targetPath = PathUtils.normalizePath(targetPath);
+        // 检查源文件是否存在
+        if (!this.exist(sourcePath)) {
+            log.error("复制源文件不存在:{}", sourcePath);
+            return false;
+        }
+        // 检查源文件是否为文件夹
+        if (this.isDirectory(sourcePath)) {
+            log.error("源文件为文件夹:{},无法复制", sourcePath);
+            return false;
+        }
+        // 复制
+        InputStream inputStream = null;
+        try {
+            inputStream = sftp1.get(sourcePath);
+            sftp2.put(inputStream, targetPath);
+            log.info("文件复制成功:{}", sourcePath);
+            return true;
+        } catch (SftpException e) {
+            log.error("文件复制失败:{}", sourcePath);
+            log.error(ExceptionUtils.getStackTrace(e));
+        } finally {
+            if (null != inputStream){
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error(ExceptionUtils.getStackTrace(e));
+                }
+            }
+            sFtpPoolService.returnObject(sftp1);
+            sFtpPoolService.returnObject(sftp2);
+        }
+        return false;
+    }
+
+    /**
+     * 判断SFTP上的path是否为文件夹
+     * 注:如果该路径不存在,那么会返回false
+     *
+     * @param path SFTP上的路径
+     * @return 判断结果
+     */
+    public boolean isDirectory(String path) {
+        ChannelSftp sftp = sFtpPoolService.borrowObject();
+        // 合法的错误id
+        // int legalErrorId = 4;
+        try {
+            sftp.cd(path);
+            return true;
+        } catch (SftpException e) {
+            // 如果 path不存在,那么报错信息为【No such file】,错误id为【2】
+            // 如果 path存在,但是不能cd进去,那么报错信息形如【Can't change directory: /files/sqljdbc4-3.0.jar】,错误id为【4】
+            return false;
+        } finally {
+            sFtpPoolService.returnObject(sftp);
+        }
+    }
+
+    /**
+     * 检查文件是否存在
+     * @param filePath
+     * @return
+     */
+    private boolean exist(String filePath) {
+        ChannelSftp sftp = sFtpPoolService.borrowObject();
+        try {
+            sftp.lstat(filePath);
+            return true;
+        } catch (SftpException e) {
+            return false;
+        } finally {
+            sFtpPoolService.returnObject(sftp);
+        }
+    }
+
+}

+ 0 - 12
DataRoom/dataroom-server/src/main/resources/application.yml

@@ -30,18 +30,6 @@ spring:
     resources:
       static-locations: classpath:/static/,classpath:/META-INF/resources/,classpath:/META-INF/resources/webjars/,file:${gc.starter.file.basePath}
 
-gc:
-  starter:
-    file:
-      # minio | local
-      uploadType: local
-
-# Minio配置
-#minio:
-#  url: http://192.168.20.98:9000
-#  accessKey: admin
-#  secretKey: 123456
-#  bucketName: test
 
 mybatis-plus:
   # mybatis plus xml配置文件扫描,多个通过分号隔开

+ 3 - 0
DataRoom/pom.xml

@@ -47,6 +47,9 @@
         <minio.version>8.2.2</minio.version>
         <dataset.core.version>2.0.0.RELEASE</dataset.core.version>
         <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
+        <commons-net.version>3.6</commons-net.version>
+        <commons-dbcp2.version>2.10.0</commons-dbcp2.version>
+        <jsch.version>0.1.55</jsch.version>
     </properties>
 
     <dependencyManagement>