package org.jeecg.modules.iot.util;
|
|
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.net.ftp.FTP;
|
import org.apache.commons.net.ftp.FTPClient;
|
import org.apache.commons.net.ftp.FTPFile;
|
import org.apache.commons.net.ftp.FTPReply;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.stereotype.Component;
|
|
import java.io.*;
|
import java.net.SocketException;
|
import java.nio.charset.StandardCharsets;
|
import java.util.Objects;
|
|
@Component
|
public class FtpUtil {
|
|
private static final Log logger = LogFactory.getLog(FtpUtil.class);
|
|
// 依赖注入配置,建议通过 Nacos 或配置中心管理
|
@Value("${ftp.LOCAL_CHARSET:UTF-8}")
|
private String LOCAL_CHARSET;
|
@Value("${ftp.SERVER_CHARSET:ISO-8859-1}")
|
private String SERVER_CHARSET;
|
@Value("${ftp.ftpHost}")
|
private String ftpHost;
|
@Value("${ftp.ftpPort:21}")
|
private int ftpPort;
|
@Value("${ftp.ftpUserName}")
|
private String ftpUserName;
|
@Value("${ftp.ftpPassword}")
|
private String ftpPassword;
|
@Value("${operatingSystem}")
|
private String operatingSystem;
|
|
|
/**
|
* 验证FTP连接是否可用
|
*
|
* @return true:连接成功;false:连接失败
|
*/
|
public boolean testFtpConnection() {
|
FTPClient ftpClient = new FTPClient();
|
try {
|
// 连接FTP服务器
|
ftpClient.connect(ftpHost, ftpPort);
|
// 登录验证
|
boolean loginResult = ftpClient.login(ftpUserName, ftpPassword);
|
|
// 检查连接状态和登录结果
|
int replyCode = ftpClient.getReplyCode();
|
boolean isConnected = FTPReply.isPositiveCompletion(replyCode) && loginResult;
|
|
if (isConnected) {
|
logger.info("FTP连接验证成功!");
|
} else {
|
logger.error("FTP连接失败:用户名/密码错误或服务器不可达,响应码:" + replyCode);
|
}
|
return isConnected;
|
} catch (SocketException e) {
|
logger.error("FTP服务器连接失败(IP或端口错误):" + e.getMessage(), e);
|
} catch (IOException e) {
|
logger.error("FTP连接异常:" + e.getMessage(), e);
|
} finally {
|
// 释放资源
|
disconnectQuietly(ftpClient);
|
}
|
return false;
|
}
|
// ====================== 核心优化:连接管理重构 ======================
|
|
/**
|
* 获取 FTPClient 连接(带重试、模式配置)
|
*
|
* @return 可用 FTPClient,失败返回 null
|
*/
|
public FTPClient getFTPClient() {
|
FTPClient ftpClient = new FTPClient();
|
try {
|
ftpClient.connect(ftpHost, ftpPort);
|
ftpClient.login(ftpUserName, ftpPassword);
|
|
// 强制开启 UTF-8 支持(优先覆盖服务器配置)
|
if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) {
|
LOCAL_CHARSET = StandardCharsets.UTF_8.name();
|
}
|
ftpClient.setControlEncoding(LOCAL_CHARSET);
|
|
// 关键配置:二进制传输 + 被动模式,解决文件损坏、防火墙阻塞问题
|
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
|
ftpClient.enterLocalPassiveMode();
|
|
int replyCode = ftpClient.getReplyCode();
|
if (!FTPReply.isPositiveCompletion(replyCode)) {
|
logger.error("FTP 连接失败,响应码:" + replyCode);
|
disconnectQuietly(ftpClient);
|
return null;
|
}
|
logger.info("FTP 连接成功,响应码:" + replyCode);
|
return ftpClient;
|
} catch (IOException e) {
|
logger.error("FTP 连接异常:" + e.getMessage(), e);
|
disconnectQuietly(ftpClient);
|
return null;
|
}
|
}
|
|
/**
|
* 安静关闭连接(避免嵌套异常)
|
*/
|
private void disconnectQuietly(FTPClient ftpClient) {
|
if (ftpClient != null && ftpClient.isConnected()) {
|
try {
|
ftpClient.logout();
|
ftpClient.disconnect();
|
} catch (IOException e) {
|
logger.warn("关闭 FTP 连接失败:" + e.getMessage(), e);
|
}
|
}
|
}
|
|
|
// ====================== 文件夹上传优化:递归 + 完整性校验 ======================
|
|
/**
|
* 递归上传文件夹(核心方法)
|
*
|
* @param localFolder 本地文件夹
|
* @param remoteFolder 远程父目录
|
*/
|
public void uploadFolder(File localFolder, String remoteFolder) {
|
if (localFolder == null || !localFolder.isDirectory()) {
|
logger.warn("本地文件夹无效:" + (localFolder == null ? "null" : localFolder.getPath()));
|
return;
|
}
|
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
logger.error("FTP 客户端初始化失败,放弃上传:" + localFolder.getPath());
|
return;
|
}
|
|
try {
|
// 确保远程目录存在(递归创建)
|
if (!createRemoteDirectory(ftpClient, remoteFolder)) {
|
logger.error("创建远程目录失败:" + remoteFolder);
|
return;
|
}
|
|
// 遍历本地文件
|
File[] files = localFolder.listFiles();
|
if (files == null) {
|
logger.warn("本地文件夹为空:" + localFolder.getPath());
|
return;
|
}
|
|
for (File file : files) {
|
if (file.isDirectory()) {
|
// 递归上传子文件夹
|
uploadFolder(file, remoteFolder + "/" + file.getName());
|
} else if (file.isFile()) {
|
// 上传文件(带完整性校验)
|
boolean success = uploadFileWithCheck(ftpClient, file, remoteFolder);
|
if (!success) {
|
logger.error("文件上传失败:" + file.getPath() + " -> " + remoteFolder);
|
}
|
}
|
}
|
} catch (IOException e) {
|
logger.error("上传文件夹异常:" + e.getMessage(), e);
|
} finally {
|
disconnectQuietly(ftpClient);
|
}
|
}
|
|
/**
|
* 麒麟递归上传文件夹(核心方法)
|
*
|
* @param localFolder 本地文件夹
|
* @param remoteFolder 远程父目录
|
*/
|
public void uploadFolderKylin(File localFolder, String remoteFolder) {
|
if (operatingSystem.equals("windows")){
|
if (localFolder == null || !localFolder.isDirectory()) {
|
logger.warn("本地文件夹无效:" + (localFolder == null ? "null" : localFolder.getPath()));
|
return;
|
}
|
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
logger.error("FTP 客户端初始化失败,放弃上传:" + localFolder.getPath());
|
return;
|
}
|
|
try {
|
// 麒麟系统FTP服务器必需的设置
|
ftpClient.enterLocalPassiveMode(); // 启用被动模式(类Unix系统推荐)
|
ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 二进制传输避免文件损坏
|
ftpClient.setControlEncoding("UTF-8"); // 控制连接编码
|
// 确保远程目录存在(递归创建)
|
if (!createRemoteDirectory(ftpClient, remoteFolder)) {
|
logger.error("创建远程目录失败:" + remoteFolder);
|
return;
|
}
|
|
// 遍历本地文件
|
File[] files = localFolder.listFiles();
|
if (files == null) {
|
logger.warn("本地文件夹为空:" + localFolder.getPath());
|
return;
|
}
|
|
for (File file : files) {
|
if (file.isDirectory()) {
|
// 递归上传子文件夹
|
uploadFolder(file, remoteFolder + "/" + file.getName());
|
} else if (file.isFile()) {
|
// 上传文件(带完整性校验)
|
boolean success = uploadFileWithCheck(ftpClient, file, remoteFolder);
|
if (!success) {
|
logger.error("文件上传失败:" + file.getPath() + " -> " + remoteFolder);
|
}
|
}
|
}
|
} catch (IOException e) {
|
logger.error("上传文件夹异常:" + e.getMessage(), e);
|
} finally {
|
disconnectQuietly(ftpClient);
|
}
|
}else {
|
if (localFolder == null || !localFolder.isDirectory()) {
|
logger.warn("本地文件夹无效:" + (localFolder == null ? "null" : localFolder.getPath()));
|
return;
|
}
|
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
logger.error("FTP 客户端初始化失败,放弃上传:" + localFolder.getPath());
|
return;
|
}
|
|
try {
|
// 麒麟系统FTP服务器必需的设置
|
ftpClient.enterLocalPassiveMode(); // 启用被动模式(类Unix系统推荐)
|
ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 二进制传输避免文件损坏
|
ftpClient.setControlEncoding("UTF-8"); // 控制连接编码
|
// 确保远程目录存在(递归创建)
|
if (!createKylinFtpDirectory(ftpClient, "/iot", remoteFolder)) {
|
logger.error("创建远程目录失败:" + remoteFolder);
|
return;
|
}
|
|
// 遍历本地文件
|
File[] files = localFolder.listFiles();
|
if (files == null) {
|
logger.warn("本地文件夹为空:" + localFolder.getPath());
|
return;
|
}
|
|
for (File file : files) {
|
if (file.isDirectory()) {
|
// 递归上传子文件夹
|
uploadFolder(file, "/iot"+remoteFolder + "/" + file.getName());
|
} else if (file.isFile()) {
|
// 上传文件(带完整性校验)
|
boolean success = uploadFileWithCheck(ftpClient, file, "/iot"+remoteFolder);
|
if (!success) {
|
logger.error("文件上传失败:" + file.getPath() + " -> " + remoteFolder);
|
}
|
}
|
}
|
} catch (IOException e) {
|
logger.error("上传文件夹异常:" + e.getMessage(), e);
|
} finally {
|
disconnectQuietly(ftpClient);
|
}
|
}
|
|
}
|
|
private boolean createRemoteDirectory(FTPClient ftpClient, String remoteDir) throws IOException {
|
String[] dirs = remoteDir.split("/");
|
StringBuilder currentDir = new StringBuilder();
|
for (String dir : dirs) {
|
if (dir.trim().isEmpty()) continue;
|
currentDir.append("/").append(dir);
|
String targetDir = currentDir.toString();
|
// 打印当前尝试切换/创建的目录
|
logger.info("尝试操作目录:" + targetDir);
|
|
if (!ftpClient.changeWorkingDirectory(targetDir)) {
|
boolean mkdirResult = ftpClient.makeDirectory(targetDir);
|
// 打印创建结果和 FTP 响应
|
logger.info("创建目录结果: " + mkdirResult + ", 响应码: " + ftpClient.getReplyCode()
|
+ ", 响应信息: " + ftpClient.getReplyString());
|
|
if (!mkdirResult) {
|
logger.error("创建目录失败: " + targetDir);
|
return false;
|
}
|
if (!ftpClient.changeWorkingDirectory(targetDir)) {
|
logger.error("切换目录失败: " + targetDir + ", 响应码: " + ftpClient.getReplyCode()
|
+ ", 响应信息: " + ftpClient.getReplyString());
|
return false;
|
}
|
}
|
}
|
return true;
|
}
|
|
/**
|
* 在麒麟系统的FTP服务器上创建目录(支持多级目录)
|
*
|
* @param baseDir FTP基础目录(如/iot)
|
* @param remoteDir 要创建的目录路径(如data/logs/2023)
|
* @return 是否创建成功
|
* @throws IOException IO异常
|
*/
|
public boolean createKylinFtpDirectory(FTPClient ftpClient, String baseDir, String remoteDir) throws IOException {
|
// 确保基础目录以/结尾
|
String normalizedBase = baseDir.endsWith("/") ? baseDir : baseDir + "/";
|
|
// 处理远程目录,移除开头可能的/,避免重复
|
String normalizedRemote = remoteDir.startsWith("/") ? remoteDir.substring(1) : remoteDir;
|
|
// 分割目录
|
String[] dirs = normalizedRemote.split("/");
|
StringBuilder currentPath = new StringBuilder(normalizedBase);
|
|
for (String dir : dirs) {
|
if (dir.trim().isEmpty()) continue; // 跳过空目录(处理连续//的情况)
|
|
currentPath.append(dir).append("/");
|
String targetDir = currentPath.toString();
|
|
// 尝试切换到目录
|
if (!ftpClient.changeWorkingDirectory(targetDir)) {
|
// 切换失败,尝试创建目录
|
boolean created = ftpClient.makeDirectory(targetDir);
|
logger.info("创建目录[" + targetDir + "]结果: {" + created + "}, 响应码: {" + ftpClient.getReplyCode() + "}");
|
|
if (!created) {
|
logger.error("创建目录失败: {" + targetDir + "}");
|
return false;
|
}
|
|
// 验证是否能切换到新创建的目录
|
if (!ftpClient.changeWorkingDirectory(targetDir)) {
|
logger.error("切换到目录失败: {" + targetDir + "}, 响应: {" + ftpClient.getReplyString() + "}");
|
return false;
|
}
|
}
|
}
|
return true;
|
}
|
|
/**
|
* 上传文件(带大小校验)
|
*
|
* @return 是否上传成功且完整
|
*/
|
private boolean uploadFileWithCheck(FTPClient ftpClient, File localFile, String remoteFolder) throws IOException {
|
String remoteFilePath = remoteFolder + "/" + localFile.getName();
|
long localFileSize = localFile.length();
|
|
// 校验远程文件是否已存在且完整
|
FTPFile[] remoteFiles = ftpClient.listFiles(remoteFilePath);
|
if (remoteFiles != null && remoteFiles.length > 0) {
|
FTPFile remoteFile = remoteFiles[0];
|
if (remoteFile.getSize() == localFileSize) {
|
logger.info("文件已存在且完整,跳过上传:" + localFile.getPath());
|
return true;
|
}
|
}
|
|
// 执行上传
|
try (FileInputStream inputStream = new FileInputStream(localFile)) {
|
boolean uploaded = ftpClient.storeFile(remoteFilePath, inputStream);
|
if (!uploaded) {
|
logger.error("上传失败,响应码:" + ftpClient.getReplyCode() + " -> " + localFile.getPath());
|
return false;
|
}
|
|
// 二次校验远程文件大小
|
FTPFile[] checkFiles = ftpClient.listFiles(remoteFilePath);
|
if (checkFiles == null || checkFiles.length == 0) {
|
logger.error("上传后文件丢失:" + localFile.getPath());
|
return false;
|
}
|
return checkFiles[0].getSize() == localFileSize;
|
}
|
}
|
|
|
// ====================== 其他方法优化:资源释放 + 编码健壮性 ======================
|
|
/**
|
* 下载文件(优化编码处理、资源释放)
|
*/
|
public void downloadFtpFile(String ftpPath, String localPath, String fileName) {
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
return;
|
}
|
|
try {
|
// 编码转换(解决中文路径问题)
|
String remoteFilePath = new String((ftpPath + "/" + fileName).getBytes(LOCAL_CHARSET), SERVER_CHARSET);
|
if (!ftpClient.changeWorkingDirectory(ftpPath)) {
|
logger.error("切换远程目录失败:" + ftpPath);
|
return;
|
}
|
|
// 下载文件
|
try (InputStream is = ftpClient.retrieveFileStream(remoteFilePath);
|
OutputStream os = new FileOutputStream(new File(localPath, fileName))) {
|
if (is == null) {
|
logger.error("远程文件不存在:" + remoteFilePath);
|
return;
|
}
|
byte[] buffer = new byte[4096];
|
int len;
|
while ((len = is.read(buffer)) != -1) {
|
os.write(buffer, 0, len);
|
}
|
os.flush();
|
// 确认下载完成(关键:避免文件截断)
|
if (!ftpClient.completePendingCommand()) {
|
logger.error("FTP 下载未完成:" + remoteFilePath);
|
}
|
}
|
} catch (IOException e) {
|
logger.error("下载文件失败:" + fileName, e);
|
} finally {
|
disconnectQuietly(ftpClient);
|
}
|
}
|
|
/**
|
* 上传文件(独立方法,供复用)
|
*
|
* @param basePath 远程基础路径
|
* @param filePath 远程子路径
|
* @param filename 文件名
|
* @param input 输入流
|
* @return 是否成功
|
*/
|
public boolean uploadFile(String basePath, String filePath, String filename, InputStream input) {
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
return false;
|
}
|
|
try {
|
String remoteDir = basePath + filePath;
|
if (!createRemoteDirectory(ftpClient, remoteDir)) {
|
return false;
|
}
|
|
// 编码转换(解决中文文件名)
|
String remoteFileName = new String(filename.getBytes(LOCAL_CHARSET), SERVER_CHARSET);
|
boolean success = ftpClient.storeFile(remoteFileName, input);
|
if (success) {
|
logger.info("文件上传成功:" + remoteDir + "/" + remoteFileName);
|
} else {
|
logger.error("文件上传失败,响应码:" + ftpClient.getReplyCode());
|
}
|
return success;
|
} catch (IOException e) {
|
logger.error("上传文件异常:" + e.getMessage(), e);
|
return false;
|
} finally {
|
disconnectQuietly(ftpClient);
|
try {
|
input.close();
|
} catch (IOException e) {
|
logger.warn("关闭输入流失败:" + e.getMessage());
|
}
|
}
|
}
|
|
/**
|
* 上传文件(适配麒麟系统,供复用)
|
*
|
* @param basePath 远程基础路径(如/iot)
|
* @param filePath 远程子路径(如data/logs)
|
* @param filename 文件名
|
* @param input 输入流
|
* @return 是否成功
|
*/
|
public boolean uploadFileKylin(String basePath, String filePath, String filename, InputStream input) {
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
logger.error("获取FTPClient实例失败,无法执行上传");
|
return false;
|
}
|
|
try {
|
// 麒麟系统FTP服务器必需的设置
|
ftpClient.enterLocalPassiveMode(); // 启用被动模式(类Unix系统推荐)
|
ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 二进制传输避免文件损坏
|
ftpClient.setControlEncoding("UTF-8"); // 控制连接编码
|
|
// 路径规范化处理(适配麒麟系统Unix路径格式)
|
String normalizedBase = normalizePath(basePath);
|
String normalizedFilePath = normalizePath(filePath);
|
String remoteDir = normalizedBase + normalizedFilePath;
|
logger.info("麒麟系统FTP目标目录:" + remoteDir);
|
|
// 创建目录(修正参数顺序,使用规范化路径)
|
if (!createKylinFtpDirectory(ftpClient, normalizedBase, normalizedFilePath)) {
|
logger.error("目录创建失败,终止上传:" + remoteDir);
|
return false;
|
}
|
|
// 切换到目标目录(增加验证步骤)
|
if (!ftpClient.changeWorkingDirectory(remoteDir)) {
|
logger.error("切换到目录[" + remoteDir + "]失败,响应码: {" + ftpClient.getReplyCode() + "}");
|
return false;
|
}
|
|
// 编码转换(优化中文文件名处理,适配麒麟系统编码)
|
String remoteFileName = new String(filename.getBytes(LOCAL_CHARSET), SERVER_CHARSET);
|
|
// 执行上传
|
boolean success = ftpClient.storeFile(remoteFileName, input);
|
if (success) {
|
logger.info("文件上传成功:{" + remoteDir + "/" + filename + "}");
|
} else {
|
logger.error("文件上传失败,路径:{" + remoteDir + "/" + filename + "},响应码:{" + ftpClient.getReplyCode() + "},响应信息:{" +
|
ftpClient.getReplyString() + "}");
|
}
|
return success;
|
} catch (IOException e) {
|
logger.error("上传文件异常:{" + e.getMessage(), e);
|
return false;
|
} finally {
|
disconnectQuietly(ftpClient);
|
try {
|
input.close();
|
} catch (IOException e) {
|
logger.warn("关闭输入流失败:{}" + e.getMessage());
|
}
|
}
|
}
|
|
/**
|
* 路径规范化处理(适配麒麟系统)
|
* 处理重复斜杠、首尾斜杠问题,统一Unix风格路径
|
*/
|
private String normalizePath(String path) {
|
if (path == null || path.trim().isEmpty()) {
|
return "";
|
}
|
// 替换多个斜杠为单个,移除尾部斜杠(除根目录外)
|
String normalized = path.trim().replaceAll("//+", "/");
|
if (normalized.length() > 1 && normalized.endsWith("/")) {
|
normalized = normalized.substring(0, normalized.length() - 1);
|
}
|
return normalized;
|
}
|
|
/**
|
* 创建文件夹(适配麒麟系统,解决编码/权限/路径问题)
|
*/
|
public boolean createFolder(String pathname, String folderName) {
|
FTPClient ftpClient = getFTPClient();
|
if (ftpClient == null) {
|
return false;
|
}
|
|
try {
|
// 1. 统一路径格式(Linux兼容)
|
String remoteFolder = (pathname + "/" + folderName)
|
.replace("\\", "/") // 替换Windows反斜杠
|
.replaceAll("/+", "/") // 合并连续斜杠
|
.replaceAll("^/", ""); // 去除开头的斜杠(避免绝对路径问题)
|
|
// 2. 设置UTF-8编码(解决中文路径问题)
|
ftpClient.setControlEncoding("UTF-8");
|
remoteFolder = new String(remoteFolder.getBytes("UTF-8"), "ISO-8859-1"); // FTP协议默认编码
|
|
// 3. 检查目录是否存在(幂等性)
|
if (ftpClient.changeWorkingDirectory(remoteFolder)) {
|
logger.info("文件夹已存在:" + remoteFolder);
|
return true;
|
}
|
|
// 4. 尝试创建目录(被动模式适配)
|
ftpClient.enterLocalPassiveMode(); // 麒麟系统建议用被动模式
|
boolean success = ftpClient.makeDirectory(remoteFolder);
|
|
// 5. 记录详细错误信息
|
if (!success) {
|
int replyCode = ftpClient.getReplyCode();
|
String replyMsg = ftpClient.getReplyString();
|
logger.error("创建文件夹失败,路径:" + remoteFolder +
|
",响应码:" + replyCode +
|
",响应信息:" + replyMsg);
|
|
// 特殊处理550错误(权限/SELinux问题)
|
if (replyCode == 550) {
|
logger.error("可能原因:权限不足/SELinux限制/路径不存在");
|
}
|
} else {
|
logger.info("创建文件夹成功:" + remoteFolder);
|
}
|
return success;
|
} catch (IOException e) {
|
logger.error("创建文件夹异常:" + e.getMessage(), e);
|
return false;
|
} finally {
|
disconnectQuietly(ftpClient);
|
}
|
}
|
|
// ====================== 工具方法:字节流转换(保持兼容) ======================
|
public InputStream byte2Input(byte[] buf) {
|
return new ByteArrayInputStream(Objects.requireNonNull(buf));
|
}
|
|
public byte[] input2byte(InputStream inStream) throws IOException {
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
byte[] buffer = new byte[4096];
|
int len;
|
while ((len = inStream.read(buffer)) != -1) {
|
bos.write(buffer, 0, len);
|
}
|
return bos.toByteArray();
|
}
|
}
|
|
public void byte2File(byte[] buf, String filePath, String fileName) {
|
File dir = new File(filePath);
|
if (!dir.exists() && !dir.mkdirs()) {
|
logger.error("创建本地目录失败:" + filePath);
|
return;
|
}
|
try (BufferedOutputStream bos = new BufferedOutputStream(
|
new FileOutputStream(new File(dir, fileName)))) {
|
bos.write(buf);
|
} catch (IOException e) {
|
logger.error("字节数组写入文件失败:" + fileName, e);
|
}
|
}
|
}
|