diff --git a/eladmin-common/pom.xml b/eladmin-common/pom.xml
index e1510a7a3..1223b2385 100644
--- a/eladmin-common/pom.xml
+++ b/eladmin-common/pom.xml
@@ -8,6 +8,7 @@
4.0.0
5.8.35
+ 3.7
eladmin-common
@@ -20,5 +21,11 @@
hutool-all
${hutool.version}
+
+
+ commons-net
+ commons-net
+ ${commons-net.version}
+
\ No newline at end of file
diff --git a/eladmin-common/src/main/java/me/zhengjie/config/properties/FileProperties.java b/eladmin-common/src/main/java/me/zhengjie/config/properties/FileProperties.java
index 6b7d2b6f9..2cc77d0dd 100644
--- a/eladmin-common/src/main/java/me/zhengjie/config/properties/FileProperties.java
+++ b/eladmin-common/src/main/java/me/zhengjie/config/properties/FileProperties.java
@@ -17,9 +17,13 @@
import lombok.Data;
import me.zhengjie.utils.ElConstant;
+import me.zhengjie.utils.EncryptUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
+import java.io.File;
+import java.util.Objects;
+
/**
* @author Zheng Jie
*/
@@ -57,4 +61,76 @@ public static class ElPath{
private String avatar;
}
+
+ // region 支持FTP配置
+ private FtpConfig ftp;
+
+ @Data
+ public static class FtpConfig {
+ private String host;
+ private int port;
+ private String username;
+ private String password;
+ private String mainPath;
+ private int connectTimeout = 5000; // ms
+ private int dataTimeout = 30000; // ms
+ // 连接池相关配置
+ private int minIdle = 1;
+ private int maxIdle = 3;
+ private int maxTotal = 5;
+ private long maxWait = 5000; // ms
+
+ public String getPassword() {
+ String password = this.password;
+ try {
+ password = EncryptUtils.desDecrypt(this.password);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return password;
+ }
+ }
+
+ /**
+ * 获取刨除服务器文件主目录外的相对路径(用于FTP的相对路径)
+ *
+ * @param file
+ * @return
+ */
+ public String getRelativePath(File file) {
+ String mainPath = getPath().getPath();
+ String ftpPath = getFtp().getMainPath();
+ if (Objects.nonNull(file)) {
+ if (file.getPath().contains(mainPath)) {
+ int len = file.getPath().length() - file.getName().length();
+ String result = file.getPath().substring(mainPath.length(), len);
+ // 处理分割符
+ return dealPath(result);
+ } else if (file.getPath().contains(ftpPath)) {
+ int len = file.getPath().length() - file.getName().length();
+ String result = file.getPath().substring(ftpPath.length(), len);
+ // 处理分割符
+ return dealPath(result);
+ }
+ }
+ return file.getPath();
+ }
+
+ /**
+ * 处理文件路径分隔符
+ *
+ * @param path
+ * @return
+ */
+ private String dealPath(String path) {
+ String os = System.getProperty("os.name");
+ if (os.toLowerCase().startsWith(ElConstant.WIN) && path.contains("\\")) {
+ return path.replace("\\", "/");
+ } else if (os.toLowerCase().startsWith(ElConstant.MAC)) {
+ return path;
+ }
+ return path;
+ }
+
+ // endregion
}
diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpConnectionPoolManager.java b/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpConnectionPoolManager.java
new file mode 100644
index 000000000..cfdff9454
--- /dev/null
+++ b/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpConnectionPoolManager.java
@@ -0,0 +1,149 @@
+package me.zhengjie.utils.ftp;
+
+import lombok.extern.slf4j.Slf4j;
+import me.zhengjie.config.properties.FileProperties;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * FTP连接池管理器
+ *
+ * @author pcshao.cn
+ * @date 13/04/2024
+ */
+@Slf4j
+@Service
+public class FtpConnectionPoolManager {
+
+ private volatile static GenericObjectPool pool = null;
+
+ @Resource
+ private FileProperties fileProperties;
+
+ /**
+ * 初始化连接池
+ */
+ public void initPool() {
+ if (Objects.nonNull(pool))
+ return;
+ else {
+ // 懒加载
+ synchronized (this) {
+ if (Objects.nonNull(pool))
+ return;
+ }
+ }
+ FileProperties.FtpConfig ftp = fileProperties.getFtp();
+ GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig<>();
+ poolConfig.setMaxTotal(ftp.getMaxTotal()); // 设置连接池最大连接数
+ poolConfig.setMaxIdle(ftp.getMaxIdle()); // 设置连接池最大空闲连接数
+ poolConfig.setMinIdle(ftp.getMinIdle()); // 设置连接池最小空闲连接数
+ poolConfig.setMaxWaitMillis(ftp.getMaxWait()); // 设置连接池最大等待时间,单位为毫秒
+ poolConfig.setTestOnBorrow(true); // 设置获取连接前进行检查
+
+ pool = new GenericObjectPool<>(new BasePooledObjectFactory() {
+ @Override
+ public FTPClient create() throws Exception {
+ String server = ftp.getHost();
+ int port = ftp.getPort();
+ String username = ftp.getUsername();
+ String password = ftp.getPassword();
+ FTPClient ftpClient = new FTPClient();
+ // 设置控制连接的字符编码为UTF-8
+ ftpClient.setControlEncoding("UTF-8");
+ ftpClient.connect(server, port);
+ ftpClient.login(username, password);
+ ftpClient.enterLocalPassiveMode();
+ ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+ ftpClient.setConnectTimeout(ftp.getConnectTimeout()); // 连接超时时间为5秒
+ ftpClient.setDataTimeout(ftp.getDataTimeout()); // 数据传输超时时间为30秒
+ return ftpClient;
+ }
+
+ @Override
+ public void destroyObject(PooledObject p) throws Exception {
+ FTPClient ftpClient = p.getObject();
+ ftpClient.logout();
+ ftpClient.disconnect();
+ }
+
+ @Override
+ public boolean validateObject(PooledObject p) {
+ FTPClient ftpClient = p.getObject();
+ try {
+ log.warn("FTP连接池管理 检测FTP连接状态 reply:{} status:{} obj:{}", ftpClient.getReplyString(), ftpClient.getStatus(), ftpClient);
+ return ftpClient.isConnected() && ftpClient.sendNoOp();
+ } catch (Exception e) {
+ log.warn("FTP连接池管理 检测FTP连接失败 {}", e.getMessage());
+ if (log.isDebugEnabled())
+ log.debug(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ @Override
+ public PooledObject wrap(FTPClient ftpClient) {
+ DefaultPooledObject defaultPooledObject = new DefaultPooledObject(ftpClient);
+ return defaultPooledObject;
+ }
+ }, poolConfig);
+ }
+
+ /**
+ * 执行方法,包含获取和归还连接
+ *
+ * @param function
+ * @param
+ * @return
+ * @throws Exception
+ */
+ public R execute(Function function) throws Exception {
+ FTPClient ftpClient = null;
+ try {
+ ftpClient = borrowFTPClient();
+ return function.apply(ftpClient);
+ } finally {
+ returnFTPClient(ftpClient);
+ }
+ }
+
+ /**
+ * 获取一个连接
+ *
+ * @return
+ * @throws Exception
+ */
+ public FTPClient borrowFTPClient() throws Exception {
+ initPool();
+ return pool.borrowObject();
+ }
+
+ /**
+ * 归还连接
+ *
+ * @param ftpClient
+ */
+ public void returnFTPClient(FTPClient ftpClient) {
+ initPool();
+ pool.returnObject(ftpClient);
+ }
+
+ /**
+ * 关闭连接池
+ */
+ public void close() {
+ if (Objects.isNull(pool))
+ return;
+ pool.close();
+ }
+}
diff --git a/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpUtil.java b/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpUtil.java
new file mode 100644
index 000000000..28398e5f6
--- /dev/null
+++ b/eladmin-common/src/main/java/me/zhengjie/utils/ftp/FtpUtil.java
@@ -0,0 +1,299 @@
+package me.zhengjie.utils.ftp;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPFile;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author pcshao.cn
+ * @date 20/01/2024
+ */
+@Slf4j
+public class FtpUtil {
+
+ /**
+ * FTP文件上传
+ *
+ * @param ftpClient
+ * @param localFilePath
+ * @param remoteDirectory
+ * @return
+ */
+ public static boolean uploadFile(FTPClient ftpClient, String localFilePath, String remoteDirectory) {
+ boolean done = false;
+ try {
+ // 检查远程目录是否存在
+ checkRemoteDir(ftpClient, remoteDirectory);
+
+ // 设置远程目录的字符编码为UTF-8
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ File localFile = new File(localFilePath);
+
+ FileInputStream inputStream = new FileInputStream(localFile);
+
+ String remoteFilePath = remoteDirectory + localFile.getName();
+
+ done = ftpClient.storeFile(remoteFilePath, inputStream);
+
+ if (done) {
+ log.info("FtpUtil uploadFile successfully.");
+ } else {
+ log.error("FtpUtil uploadFile failed. reply: {} remotePath: {} obj: {}", ftpClient.getReplyString(), remoteFilePath, ftpClient);
+ }
+ inputStream.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ return done;
+ }
+
+ /**
+ * FTP文件上传
+ *
+ * @param server
+ * @param port
+ * @param username
+ * @param password
+ * @param localFilePath
+ * @param remoteDirectory
+ */
+ public static boolean uploadFile(String server, int port, String username, String password, String localFilePath, String remoteDirectory) {
+ boolean done = false;
+ FTPClient ftpClient = new FTPClient();
+ try {
+ // 设置控制连接的字符编码为UTF-8
+ ftpClient.setControlEncoding("UTF-8");
+ ftpClient.connect(server, port);
+ ftpClient.login(username, password);
+ ftpClient.enterLocalPassiveMode();
+ ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+ // 检查远程目录是否存在
+ checkRemoteDir(ftpClient, remoteDirectory);
+
+ // 设置远程目录的字符编码为UTF-8
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ File localFile = new File(localFilePath);
+
+ FileInputStream inputStream = new FileInputStream(localFile);
+
+ String remoteFilePath = remoteDirectory + localFile.getName();
+
+ done = ftpClient.storeFile(remoteFilePath, inputStream);
+
+ if (done) {
+ log.info("FtpUtil uploadFile successfully.");
+ } else {
+ log.info("FtpUtil uploadFile failed. reply: {} remotePath: {}", ftpClient.getReplyString(), remoteFilePath);
+ }
+ inputStream.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ try {
+ if (ftpClient.isConnected()) {
+ ftpClient.logout();
+ ftpClient.disconnect();
+ }
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return done;
+ }
+
+ /**
+ * 检查远程目录,如果不存在则创建
+ *
+ * @param ftpClient
+ * @param remoteDirectory
+ * @throws IOException
+ */
+ private static void checkRemoteDir(FTPClient ftpClient, String remoteDirectory) throws IOException {
+ FTPFile[] ftpFiles = ftpClient.mlistDir(remoteDirectory);
+ log.info("FtpUtil mlistDir {} reply: {}", remoteDirectory, ftpClient.getReplyString());
+ if (ftpFiles.length <= 0) {
+ ftpClient.makeDirectory(remoteDirectory);
+ log.info("FtpUtil makeDirectory {} reply: {}", remoteDirectory, ftpClient.getReplyString());
+ }
+ }
+
+ /**
+ * FTP文件下载
+ *
+ * @param ftpClient
+ * @param remoteFilePath
+ * @param localDirectory
+ * @return
+ */
+ public static File downloadFile(FTPClient ftpClient, String remoteFilePath, String localDirectory) {
+ File localFile = null;
+ try {
+ // 设置远程目录的字符编码为UTF-8
+ String remoteDirectory = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/") + 1);
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ String remoteFileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
+ localFile = new File(localDirectory + remoteFileName);
+
+ OutputStream outputStream = new FileOutputStream(localFile);
+
+ boolean success = ftpClient.retrieveFile(remoteFilePath, outputStream);
+
+ if (success) {
+ log.info("FtpUtil downloaded successfully.");
+ } else {
+ log.info("FtpUtil download failed. reply: {} remotePath: {}", ftpClient.getReplyString(), remoteFilePath);
+ return null;
+ }
+
+ outputStream.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ return null;
+ }
+ return localFile;
+ }
+
+ /**
+ * FTP文件下载
+ *
+ * @param server
+ * @param port
+ * @param username
+ * @param password
+ * @param remoteFilePath
+ * @param localDirectory
+ * @return
+ */
+ public static File downloadFile(String server, int port, String username, String password, String remoteFilePath, String localDirectory) {
+ File localFile = null;
+ FTPClient ftpClient = new FTPClient();
+ try {
+ // 设置控制连接的字符编码为UTF-8
+ ftpClient.setControlEncoding("UTF-8");
+ ftpClient.connect(server, port);
+ ftpClient.login(username, password);
+ ftpClient.enterLocalPassiveMode();
+ ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+ // 设置远程目录的字符编码为UTF-8
+ String remoteDirectory = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/") + 1);
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ String remoteFileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
+ localFile = new File(localDirectory + remoteFileName);
+
+ OutputStream outputStream = new FileOutputStream(localFile);
+
+ boolean success = ftpClient.retrieveFile(remoteFilePath, outputStream);
+
+ if (success) {
+ log.info("FtpUtil downloaded successfully.");
+ } else {
+ log.info("FtpUtil download failed. reply: {} remotePath: {}", ftpClient.getReplyString(), remoteFilePath);
+ return null;
+ }
+
+ outputStream.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ return null;
+ } finally {
+ try {
+ if (ftpClient.isConnected()) {
+ ftpClient.logout();
+ ftpClient.disconnect();
+ }
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return localFile;
+ }
+
+ /**
+ * FTP文件删除
+ *
+ * @param ftpClient
+ * @param remoteFilePath
+ * @return
+ */
+ public static boolean deleteFile(FTPClient ftpClient, String remoteFilePath) {
+ boolean success = false;
+ try {
+ // 设置远程目录的字符编码为UTF-8
+ String remoteDirectory = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/") + 1);
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ String remoteFileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
+
+ success = ftpClient.deleteFile(remoteFileName);
+
+ if (success) {
+ log.info("FtpUtil delete successfully.");
+ } else {
+ log.info("FtpUtil delete failed. reply: {} remotePath: {}", ftpClient.getReplyString(), remoteFilePath);
+ }
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ return success;
+ }
+
+ /**
+ * FTP文件删除
+ *
+ * @param server
+ * @param port
+ * @param username
+ * @param password
+ * @param remoteFilePath
+ * @return
+ */
+ public static boolean deleteFile(String server, int port, String username, String password, String remoteFilePath) {
+ boolean success = false;
+ FTPClient ftpClient = new FTPClient();
+ try {
+ // 设置控制连接的字符编码为UTF-8
+ ftpClient.setControlEncoding("UTF-8");
+ ftpClient.connect(server, port);
+ ftpClient.login(username, password);
+ ftpClient.enterLocalPassiveMode();
+ ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
+
+ // 设置远程目录的字符编码为UTF-8
+ String remoteDirectory = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/") + 1);
+ ftpClient.changeWorkingDirectory(new String(remoteDirectory.getBytes(StandardCharsets.UTF_8)));
+
+ String remoteFileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
+
+ success = ftpClient.deleteFile(remoteFileName);
+
+ if (success) {
+ log.info("FtpUtil delete successfully.");
+ } else {
+ log.info("FtpUtil delete failed. reply: {} remotePath: {}", ftpClient.getReplyString(), remoteFilePath);
+ }
+
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ try {
+ if (ftpClient.isConnected()) {
+ ftpClient.logout();
+ ftpClient.disconnect();
+ }
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return success;
+ }
+}
diff --git a/eladmin-common/src/test/java/me/zhengjie/utils/FtpUtilTest.java b/eladmin-common/src/test/java/me/zhengjie/utils/FtpUtilTest.java
new file mode 100644
index 000000000..f9b5c5d91
--- /dev/null
+++ b/eladmin-common/src/test/java/me/zhengjie/utils/FtpUtilTest.java
@@ -0,0 +1,23 @@
+package me.zhengjie.utils;
+
+import me.zhengjie.utils.ftp.FtpUtil;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author pcshao.cn
+ * @date 2025/3/12
+ */
+public class FtpUtilTest {
+
+ @Test
+ public void testDowndload() throws Exception {
+ String password = EncryptUtils.desDecrypt("xxx");
+ FtpUtil.downloadFile("ftp.xxx.xx", 21, "ftptest", password,
+ "/test.xlsx", "tempFile" + System.currentTimeMillis() + ".xlsx");
+ }
+
+ public void testUpload() throws Exception {
+ String password = EncryptUtils.desDecrypt("xxx");
+ FtpUtil.uploadFile("ftp.xxx.xx", 21, "ftptest", password, "tempFile", "");
+ }
+}
diff --git a/eladmin-system/src/main/resources/config/application-dev.yml b/eladmin-system/src/main/resources/config/application-dev.yml
index e227c7797..d8b7bf28b 100644
--- a/eladmin-system/src/main/resources/config/application-dev.yml
+++ b/eladmin-system/src/main/resources/config/application-dev.yml
@@ -116,3 +116,17 @@ file:
# 文件大小 /M
maxSize: 100
avatarMaxSize: 5
+ # FTP文件服务器配置
+ ftp:
+ mainPath: \home\eladmin\file\
+ username: ftptest
+ password: B6890183857ABC452C9E3170906AF9AB97872252D8A4769E
+ host: pcshao.cn
+ port: 21
+ connectTimeout: 5000 # 连接超时时间 ms
+ dataTimeout: 30000 # 数据传输超时时间 ms
+ # FTP线程池
+ minIdle: 1
+ maxIdle: 3
+ maxTotal: 5
+ maxWait: 5000 # ms
diff --git a/eladmin-system/src/main/resources/config/application-prod.yml b/eladmin-system/src/main/resources/config/application-prod.yml
index 77fef99bb..6b8b9e2e7 100644
--- a/eladmin-system/src/main/resources/config/application-prod.yml
+++ b/eladmin-system/src/main/resources/config/application-prod.yml
@@ -127,3 +127,17 @@ file:
# 文件大小 /M
maxSize: 100
avatarMaxSize: 5
+ # FTP文件服务器配置
+ ftp:
+ mainPath: \home\eladmin\file\
+ username: ftptest
+ password: B6890183857ABC452C9E3170906AF9AB97872252D8A4769E
+ host: pcshao.cn
+ port: 21
+ connectTimeout: 5000 # 连接超时时间 ms
+ dataTimeout: 30000 # 数据传输超时时间 ms
+ # FTP线程池
+ minIdle: 1
+ maxIdle: 3
+ maxTotal: 5
+ maxWait: 5000 # ms