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); } } }