diff --git a/DataRoom/dataroom-core/pom.xml b/DataRoom/dataroom-core/pom.xml index e46d289d..0f1a7db1 100644 --- a/DataRoom/dataroom-core/pom.xml +++ b/DataRoom/dataroom-core/pom.xml @@ -72,5 +72,24 @@ minio ${minio.version} + + + commons-net + commons-net + ${commons-net.version} + + + + org.apache.commons + commons-dbcp2 + ${commons-dbcp2.version} + + + + com.jcraft + jsch + ${jsch.version} + + diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/MinioConfig.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/MinioConfig.java deleted file mode 100644 index ec3ebd6d..00000000 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/MinioConfig.java +++ /dev/null @@ -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(); - } -} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FileConfig.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FileConfig.java index 1ab44833..b4fa1a54 100644 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FileConfig.java +++ b/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; } diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FtpConfig.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/FtpConfig.java new file mode 100644 index 00000000..c4af9c55 --- /dev/null +++ b/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; + + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/MinioConfig.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/MinioConfig.java new file mode 100644 index 00000000..bebb10eb --- /dev/null +++ b/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(); + } +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/SftpConfig.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/config/bean/SftpConfig.java new file mode 100644 index 00000000..6f0220e9 --- /dev/null +++ b/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; + + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/biz/component/service/impl/BizComponentServiceImpl.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/biz/component/service/impl/BizComponentServiceImpl.java index 4d856111..4fd0b0e7 100644 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/biz/component/service/impl/BizComponentServiceImpl.java +++ b/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 getPage(BizComponentSearchDTO searchDTO) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); @@ -93,10 +97,14 @@ public class BizComponentServiceImpl extends ServiceImpl 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; + } } diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomMinioServiceImpl.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomMinioServiceImpl.java index a3f4200d..6dd9185e 100644 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomMinioServiceImpl.java +++ b/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; } } diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomSftpFileServiceImpl.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/impl/DataRoomSftpFileServiceImpl.java new file mode 100644 index 00000000..1b3a0158 --- /dev/null +++ b/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; + } +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpClientFactory.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpClientFactory.java new file mode 100644 index 00000000..19dfd2c0 --- /dev/null +++ b/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 { + /** + * 注入 ftp 连接配置 + */ + @Resource + private DataRoomConfig config; + + /** + * 创建连接到池中 + * + * @return + * @throws Exception + */ + @Override + public PooledObject 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 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 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 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 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); + } + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpPoolServiceImpl.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/ftp/FtpPoolServiceImpl.java new file mode 100644 index 00000000..cf1a3647 --- /dev/null +++ b/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 pool; + + /** + * ftp 客户端配置文件 + */ + @Resource + private FtpConfig config; + + /** + * ftp 客户端工厂 + */ + @Resource + private FtpClientFactory factory; + + /** + * 初始化pool + */ + @PostConstruct + private void initPool() { + log.info("初始化FTP连接池"); + this.pool = new GenericObjectPool(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); + } + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpClientFactory.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpClientFactory.java new file mode 100644 index 00000000..c90ab162 --- /dev/null +++ b/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{ + + @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 wrap(ChannelSftp channelSftp) { + return new DefaultPooledObject<>(channelSftp); + } + + /** + * 销毁一个连接 + * + * @param p + */ + @Override + public void destroyObject(PooledObject p) { + ChannelSftp channelSftp = p.getObject(); + channelSftp.disconnect(); + } + + @Override + public boolean validateObject(final PooledObject 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; + } + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpPoolService.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/file/service/pool/sftp/SftpPoolService.java new file mode 100644 index 00000000..929a995e --- /dev/null +++ b/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 pool; + + /** + * ftp 客户端配置文件 + */ + @Resource + private SftpConfig config; + + /** + * ftp 客户端工厂 + */ + @Resource + private SftpClientFactory factory; + + /** + * 初始化pool + */ + @PostConstruct + private void initPool() { + log.info("初始化SFTP连接池"); + this.pool = new GenericObjectPool(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); + } + } + +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/manage/service/impl/DataRoomPageServiceImpl.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/manage/service/impl/DataRoomPageServiceImpl.java index 3e1c3d07..7fe48eee 100644 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/module/manage/service/impl/DataRoomPageServiceImpl.java +++ b/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 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; + } + +} + diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/MinioFileInterface.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/MinioFileInterface.java index ae2d3fc9..c11553c1 100644 --- a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/MinioFileInterface.java +++ b/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; diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/PathUtils.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/PathUtils.java new file mode 100644 index 00000000..4a1b6102 --- /dev/null +++ b/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}; + } +} diff --git a/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/SftpClientUtils.java b/DataRoom/dataroom-core/src/main/java/com/gccloud/dataroom/core/utils/SftpClientUtils.java new file mode 100644 index 00000000..af8d26b9 --- /dev/null +++ b/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); + } + } + +} diff --git a/DataRoom/dataroom-server/src/main/resources/application.yml b/DataRoom/dataroom-server/src/main/resources/application.yml index 7283cd91..8819574b 100644 --- a/DataRoom/dataroom-server/src/main/resources/application.yml +++ b/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配置文件扫描,多个通过分号隔开 diff --git a/DataRoom/pom.xml b/DataRoom/pom.xml index 266bd267..bc1d548a 100644 --- a/DataRoom/pom.xml +++ b/DataRoom/pom.xml @@ -47,6 +47,9 @@ 8.2.2 2.0.0.RELEASE 2.22.2 + 3.6 + 2.10.0 + 0.1.55