| | |
| | | |
| | | package org.jeecg.modules.iot.util; |
| | | |
| | | /** |
| | | * @Description: Ftp配置 |
| | | * @Author: cuikaidong |
| | | * @Date: 2024-12-10 |
| | | * @Version: V1.0 |
| | | */ |
| | | |
| | | 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 final static Log logger = LogFactory.getLog(FtpUtil.class); |
| | | |
| | | /** |
| | | * 本地字符编码 |
| | | */ |
| | | private static String LOCAL_CHARSET = "GBK"; |
| | | private static final Log logger = LogFactory.getLog(FtpUtil.class); |
| | | |
| | | // FTP协议里面,规定文件名编码为iso-8859-1 |
| | | private static String SERVER_CHARSET = "ISO-8859-1"; |
| | | //ftp服务器IP地址 ,新火炬10.210.199.2,起落架10.0.221.200 |
| | | private static String ftpHost = "10.0.221.200"; |
| | | //ftp服务器端口 |
| | | private static int ftpPort = 21; |
| | | //ftp服务器用户名 |
| | | private static String ftpUserName = "admin"; |
| | | //ftp服务器密码 |
| | | private static String ftpPassword = "lx@2024"; |
| | | // 依赖注入配置,建议通过 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; |
| | | |
| | | |
| | | /** |
| | |
| | | * |
| | | * @return true:连接成功;false:连接失败 |
| | | */ |
| | | public static boolean testFtpConnection() { |
| | | public boolean testFtpConnection() { |
| | | FTPClient ftpClient = new FTPClient(); |
| | | try { |
| | | // 连接FTP服务器 |
| | |
| | | if (isConnected) { |
| | | logger.info("FTP连接验证成功!"); |
| | | } else { |
| | | logger.error("FTP连接失败:用户名/密码错误或服务器不可达"); |
| | | logger.error("FTP连接失败:用户名/密码错误或服务器不可达,响应码:" + replyCode); |
| | | } |
| | | return isConnected; |
| | | } catch (SocketException e) { |
| | | logger.error("FTP服务器IP地址错误或端口被占用:" + e.getMessage()); |
| | | logger.error("FTP服务器连接失败(IP或端口错误):" + e.getMessage(), e); |
| | | } catch (IOException e) { |
| | | logger.error("FTP连接异常:" + e.getMessage()); |
| | | logger.error("FTP连接异常:" + e.getMessage(), e); |
| | | } finally { |
| | | // 释放资源 |
| | | if (ftpClient.isConnected()) { |
| | | try { |
| | | ftpClient.logout(); |
| | | ftpClient.disconnect(); |
| | | } catch (IOException e) { |
| | | logger.warn("关闭FTP连接时发生异常:" + e.getMessage()); |
| | | } |
| | | } |
| | | disconnectQuietly(ftpClient); |
| | | } |
| | | return false; |
| | | } |
| | | // ====================== 核心优化:连接管理重构 ====================== |
| | | |
| | | /** |
| | | * 获取FTPClient对象 |
| | | * 获取 FTPClient 连接(带重试、模式配置) |
| | | * |
| | | * @return |
| | | * @return 可用 FTPClient,失败返回 null |
| | | */ |
| | | public static FTPClient getFTPClient() { |
| | | |
| | | FTPClient ftpClient = null; |
| | | try { |
| | | ftpClient = new FTPClient(); |
| | | ftpClient.connect(ftpHost, ftpPort);// 连接FTP服务器 |
| | | ftpClient.login(ftpUserName, ftpPassword);// 登陆FTP服务器 |
| | | if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) { |
| | | logger.info("未连接到FTP,用户名或密码错误。"); |
| | | ftpClient.disconnect(); |
| | | } else { |
| | | logger.info("FTP连接成功。"); |
| | | } |
| | | } catch (SocketException e) { |
| | | e.printStackTrace(); |
| | | logger.info("FTP的IP地址可能错误,请正确配置。"); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | logger.info("FTP的端口错误,请正确配置。"); |
| | | } |
| | | return ftpClient; |
| | | } |
| | | |
| | | /** |
| | | * 从FTP服务器下载文件 |
| | | * |
| | | * @param ftpHost FTP IP地址 |
| | | * @param ftpUserName FTP 用户名 |
| | | * @param ftpPassword FTP用户名密码 |
| | | * @param ftpPort FTP端口 |
| | | * @param ftpPath FTP服务器中文件所在路径 格式: ftptest/aa |
| | | * @param localPath 下载到本地的位置 格式:H:/download |
| | | * @param fileName 文件名称 |
| | | */ |
| | | public static void downloadFtpFile(String ftpHost, String ftpUserName, String ftpPassword, int ftpPort, |
| | | String ftpPath, String localPath, String fileName) { |
| | | |
| | | FTPClient ftpClient = null; |
| | | |
| | | try { |
| | | ftpClient = getFTPClient(); |
| | | // 设置上传文件的类型为二进制类型 |
| | | if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) {// 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码,否则就使用本地编码(GBK). |
| | | LOCAL_CHARSET = "UTF-8"; |
| | | } |
| | | ftpClient.setControlEncoding(LOCAL_CHARSET); |
| | | ftpClient.enterLocalPassiveMode();// 设置被动模式 |
| | | ftpClient.setFileType(FTP.BINARY_FILE_TYPE);// 设置传输的模式 |
| | | // 上传文件 |
| | | //对中文文件名进行转码,否则中文名称的文件下载失败 |
| | | String fileNameTemp = new String(fileName.getBytes(LOCAL_CHARSET), SERVER_CHARSET); |
| | | ftpClient.changeWorkingDirectory(ftpPath); |
| | | |
| | | InputStream retrieveFileStream = ftpClient.retrieveFileStream(fileNameTemp); |
| | | |
| | | // 第一种方式下载文件(推荐) |
| | | /* File localFile = new File(localPath + File.separatorChar + fileName); |
| | | OutputStream os = new FileOutputStream(localFile); |
| | | ftpClient.retrieveFile(fileName, os); os.close();*/ |
| | | |
| | | |
| | | // 第二种方式下载:将输入流转成字节,再生成文件,这种方式方便将字节数组直接返回给前台jsp页面 |
| | | byte[] input2byte = input2byte(retrieveFileStream); |
| | | byte2File(input2byte, localPath, fileName); |
| | | |
| | | if (null != retrieveFileStream) { |
| | | retrieveFileStream.close(); |
| | | } |
| | | } catch (FileNotFoundException e) { |
| | | logger.error("没有找到" + ftpPath + "文件"); |
| | | e.printStackTrace(); |
| | | } catch (SocketException e) { |
| | | logger.error("连接FTP失败."); |
| | | e.printStackTrace(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | logger.error("文件读取错误。"); |
| | | e.printStackTrace(); |
| | | } finally { |
| | | |
| | | if (ftpClient.isConnected()) { |
| | | try { |
| | | //退出登录 |
| | | ftpClient.logout(); |
| | | //关闭连接 |
| | | ftpClient.disconnect(); |
| | | } catch (IOException e) { |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Description: 向FTP服务器上传文件 |
| | | * |
| | | * @param basePath FTP服务器基础目录 |
| | | * @param filePath FTP服务器文件存放路径。例如分日期存放:/2015/01/01。文件的路径为basePath+filePath |
| | | * @param filename 上传到FTP服务器上的文件名 |
| | | * @param input 输入流 |
| | | * @return 成功返回true,否则返回false |
| | | */ |
| | | public static boolean uploadFile(String basePath, String filePath, String filename, InputStream input) { |
| | | boolean result = false; |
| | | FTPClient ftpClient = null; |
| | | try { |
| | | int reply; |
| | | ftpClient = getFTPClient(); |
| | | reply = ftpClient.getReplyCode(); |
| | | if (!FTPReply.isPositiveCompletion(reply)) { |
| | | ftpClient.disconnect(); |
| | | return result; |
| | | } |
| | | // 切换到上传目录 |
| | | if (!ftpClient.changeWorkingDirectory(basePath + filePath)) { |
| | | // 如果目录不存在创建目录 |
| | | String[] dirs = filePath.split("/"); |
| | | String tempPath = basePath; |
| | | for (String dir : dirs) { |
| | | if (null == dir || "".equals(dir)) |
| | | continue; |
| | | tempPath += "/" + dir; |
| | | if (!ftpClient.changeWorkingDirectory(tempPath)) { |
| | | if (!ftpClient.makeDirectory(tempPath)) { |
| | | return result; |
| | | } else { |
| | | ftpClient.changeWorkingDirectory(tempPath); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // 设置上传文件的类型为二进制类型 |
| | | if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) {// 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码,否则就使用本地编码(GBK). |
| | | LOCAL_CHARSET = "UTF-8"; |
| | | } |
| | | ftpClient.setControlEncoding(LOCAL_CHARSET); |
| | | ftpClient.enterLocalPassiveMode();// 设置被动模式 |
| | | ftpClient.setFileType(FTP.BINARY_FILE_TYPE);// 设置传输的模式 |
| | | // 上传文件 |
| | | filename = new String(filename.getBytes(LOCAL_CHARSET), SERVER_CHARSET); |
| | | if (!ftpClient.storeFile(filename, input)) { |
| | | return result; |
| | | } |
| | | |
| | | if (null != input) { |
| | | input.close(); |
| | | } |
| | | |
| | | result = true; |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } finally { |
| | | if (ftpClient.isConnected()) { |
| | | try { |
| | | //退出登录 |
| | | ftpClient.logout(); |
| | | //关闭连接 |
| | | ftpClient.disconnect(); |
| | | } catch (IOException ioe) { |
| | | } |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 删除文件 未测试 |
| | | * |
| | | * @param ftpHost FTP服务器地址 |
| | | * @param ftpPort FTP服务器端口号 |
| | | * @param ftpUserName FTP登录帐号 |
| | | * @param ftpPassword FTP登录密码 |
| | | * @param pathname FTP服务器保存目录 |
| | | * @param filename 要删除的文件名称 |
| | | * @return |
| | | */ |
| | | public static boolean deleteFile(String ftpHost, int ftpPort, String ftpUserName, String ftpPassword, String pathname, |
| | | String filename) { |
| | | boolean flag = false; |
| | | public FTPClient getFTPClient() { |
| | | FTPClient ftpClient = new FTPClient(); |
| | | try { |
| | | ftpClient = getFTPClient(); |
| | | // 验证FTP服务器是否登录成功 |
| | | int replyCode = ftpClient.getReplyCode(); |
| | | if (!FTPReply.isPositiveCompletion(replyCode)) { |
| | | return flag; |
| | | } |
| | | // 切换FTP目录 |
| | | ftpClient.changeWorkingDirectory(pathname); |
| | | // 设置上传文件的类型为二进制类型 |
| | | if (FTPReply.isPositiveCompletion(ftpClient.sendCommand("OPTS UTF8", "ON"))) {// 开启服务器对UTF-8的支持,如果服务器支持就用UTF-8编码,否则就使用本地编码(GBK). |
| | | LOCAL_CHARSET = "UTF-8"; |
| | | 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.enterLocalPassiveMode();// 设置被动模式 |
| | | ftpClient.setFileType(FTP.BINARY_FILE_TYPE);// 设置传输的模式 |
| | | //对中文名称进行转码 |
| | | filename = new String(filename.getBytes(LOCAL_CHARSET), SERVER_CHARSET); |
| | | ftpClient.dele(filename); |
| | | flag = true; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } finally { |
| | | if (ftpClient.isConnected()) { |
| | | try { |
| | | //退出登录 |
| | | ftpClient.logout(); |
| | | //关闭连接 |
| | | ftpClient.disconnect(); |
| | | } catch (IOException e) { |
| | | } |
| | | |
| | | // 关键配置:二进制传输 + 被动模式,解决文件损坏、防火墙阻塞问题 |
| | | 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; |
| | | } |
| | | return flag; |
| | | } |
| | | |
| | | /** |
| | | * 创建文件夹 |
| | | * |
| | | * @param pathname |
| | | * @param filename |
| | | * @return |
| | | * 安静关闭连接(避免嵌套异常) |
| | | */ |
| | | public static boolean createFolder(String pathname, String filename) { |
| | | boolean flag = false; |
| | | FTPClient ftpClient = new FTPClient(); |
| | | try { |
| | | ftpClient = getFTPClient(); |
| | | // 验证FTP服务器是否登录成功 |
| | | int replyCode = ftpClient.getReplyCode(); |
| | | if (!FTPReply.isPositiveCompletion(replyCode)) { |
| | | return flag; |
| | | } |
| | | // 切换FTP目录 |
| | | ftpClient.changeWorkingDirectory(pathname); |
| | | boolean created = ftpClient.makeDirectory(filename); |
| | | if (created) { |
| | | logger.info(filename + "文件夹创建成功"); |
| | | } else { |
| | | logger.info(filename + "创建文件夹失败"); |
| | | } |
| | | flag = true; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } finally { |
| | | if (ftpClient.isConnected()) { |
| | | try { |
| | | //退出登录 |
| | | ftpClient.logout(); |
| | | //关闭连接 |
| | | ftpClient.disconnect(); |
| | | } catch (IOException e) { |
| | | } |
| | | private void disconnectQuietly(FTPClient ftpClient) { |
| | | if (ftpClient != null && ftpClient.isConnected()) { |
| | | try { |
| | | ftpClient.logout(); |
| | | ftpClient.disconnect(); |
| | | } catch (IOException e) { |
| | | logger.warn("关闭 FTP 连接失败:" + e.getMessage(), e); |
| | | } |
| | | } |
| | | return flag; |
| | | } |
| | | |
| | | public static void uploadFolder(File localFolder, String remoteFolder) { |
| | | if (!localFolder.exists()) { |
| | | |
| | | // ====================== 文件夹上传优化:递归 + 完整性校验 ====================== |
| | | |
| | | /** |
| | | * 递归上传文件夹(核心方法) |
| | | * |
| | | * @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(); |
| | | try { |
| | | // 创建远程文件夹 |
| | | if (!ftpClient.changeWorkingDirectory(remoteFolder)) { |
| | | ftpClient.makeDirectory(remoteFolder); |
| | | ftpClient.changeWorkingDirectory(remoteFolder); |
| | | } |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | if (ftpClient == null) { |
| | | logger.error("FTP 客户端初始化失败,放弃上传:" + localFolder.getPath()); |
| | | return; |
| | | } |
| | | // 遍历本地文件夹中的文件和子文件夹 |
| | | File[] files = localFolder.listFiles(); |
| | | if (files != null) { |
| | | |
| | | 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 { |
| | | try { |
| | | // 上传文件 |
| | | uploadFile1(ftpClient, file, remoteFolder); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } else if (file.isFile()) { |
| | | // 上传文件(带完整性校验) |
| | | boolean success = uploadFileWithCheck(ftpClient, file, remoteFolder); |
| | | if (!success) { |
| | | logger.error("文件上传失败:" + file.getPath() + " -> " + remoteFolder); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static void uploadFile1(FTPClient ftpClient, File localFile, String remoteFolder) throws IOException { |
| | | try (FileInputStream inputStream = new FileInputStream(localFile)) { |
| | | ftpClient.storeFile(remoteFolder + "/" + localFile.getName(), inputStream); |
| | | } |
| | | } |
| | | |
| | | // 将字节数组转换为输入流 |
| | | public static final InputStream byte2Input(byte[] buf) { |
| | | return new ByteArrayInputStream(buf); |
| | | } |
| | | |
| | | // 将输入流转为byte[] |
| | | public static final byte[] input2byte(InputStream inStream) throws IOException { |
| | | ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); |
| | | byte[] buff = new byte[100]; |
| | | int rc = 0; |
| | | while ((rc = inStream.read(buff, 0, 100)) > 0) { |
| | | swapStream.write(buff, 0, rc); |
| | | } |
| | | byte[] in2b = swapStream.toByteArray(); |
| | | return in2b; |
| | | } |
| | | |
| | | // 将byte[]转为文件 |
| | | public static void byte2File(byte[] buf, String filePath, String fileName) { |
| | | BufferedOutputStream bos = null; |
| | | FileOutputStream fos = null; |
| | | File file = null; |
| | | try { |
| | | File dir = new File(filePath); |
| | | if (!dir.exists() && dir.isDirectory()) { |
| | | dir.mkdirs(); |
| | | } |
| | | file = new File(filePath + File.separator + fileName); |
| | | fos = new FileOutputStream(file); |
| | | bos = new BufferedOutputStream(fos); |
| | | bos.write(buf); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } catch (IOException e) { |
| | | logger.error("上传文件夹异常:" + e.getMessage(), e); |
| | | } finally { |
| | | if (bos != null) { |
| | | try { |
| | | bos.close(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | 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; |
| | | } |
| | | } |
| | | if (fos != null) { |
| | | try { |
| | | fos.close(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | 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); |
| | | } |
| | | } |
| | | } |