封装FTPSClient连接ftps服务器

主要是connect方法中一些set方法操作是必要的
支持try with resource

/**
 * FTPSecure 客户端工具类
 *  注意参数是文件还是目录
 *  配置不一定兼容其他ftp secure服务器
 *
 * @author manccxxx@163.com
 * @version 2.0
 * @date 2025/8/21 17:30
 */
@Slf4j
public class FtpsClient implements AutoCloseable {
    private final String host;
    private final int port;
    private final String username;
    private final String password;
    private final int connectTimeout;
    private final int dataTimeout;

    private FTPSClient ftpsClient;

    public FtpsClient(String host, int port, String username, String password) {
        this(host, port, username, password, 10000, 20000);
    }

    public FtpsClient(String host, int port, String username, String password, int connectTimeout, int dataTimeout) {
        this.host = host;
        this.port = port;
        this.username = username;
        this.password = password;
        this.connectTimeout = connectTimeout;
        this.dataTimeout = dataTimeout;
    }

    /**
     * 连接到FTPS服务器
     */
    public boolean connect() {
        if (isConnected()) {
            return true;
        }
        try {
            log.debug("[FtpsClient] Initializing FTPS client");
            ftpsClient = new FTPSClient();
            ftpsClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
            ftpsClient.setEndpointCheckingEnabled(false);
            ftpsClient.setConnectTimeout(connectTimeout);
            ftpsClient.setDataTimeout(Duration.ofMillis(dataTimeout));

            FTPClientConfig config = new FTPClientConfig();
            ftpsClient.configure(config);
            ftpsClient.addProtocolCommandListener(new PrintCommandListener(
                    // 打印ftp服务器日志,重定向到Slf4j
                    new PrintWriter(new LogWriter(log))
            ));
            ftpsClient.setControlEncoding("UTF-8");

            log.debug("[FtpsClient] Connecting to {}:{}", host, port);
            ftpsClient.connect(host, port);

            int reply = ftpsClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(reply)) {
                throw new IOException("FTPS server refused connection: " + reply);
            }

            if (!ftpsClient.login(username, password)) {
                throw new IOException("Login failed for user: " + username);
            }

            ftpsClient.enterLocalPassiveMode();
            ftpsClient.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);
            // 数据通道加密
            ftpsClient.execPROT("P");
            ftpsClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 1MB buffer
            ftpsClient.setBufferSize(1024 * 1024);

            log.info("[FtpsClient] Connected to FTPS server: {}:{}", host, port);
            return true;
        } catch (Exception e) {
            log.error("[FtpsClient] Connection failed to {}:{} - {}", host, port, e.getMessage());
            disconnectSilently();
            throw new RuntimeException("FTPS connection failed: " + e.getMessage(), e);
        }
    }

    /**
     * 返回底层FTPS客户端实例
     */
    public FTPSClient operation() {
        connectedCheck();
        return ftpsClient;
    }

    /**
     * 上传文件到FTPS服务器
     *
     * @param localFilePath 本地文件路径
     * @param remoteDirectory 远程目录路径
     * @return 是否上传成功
     */
    public boolean uploadFile(String localFilePath, String remoteDirectory) {
        connectedCheck();
        File localFile = new File(localFilePath);
        try (InputStream is = Files.newInputStream(Paths.get(localFilePath))) {
            createRemoteDirectory(remoteDirectory);
            String remoteFileFullPath = Paths.get(remoteDirectory, localFile.getName()).toString();

            log.debug("[FtpsClient] Uploading file: {}", localFilePath);
            if (!ftpsClient.storeFile(remoteFileFullPath, is)) {
                throw new IOException("Upload failed. Server reply: " + ftpsClient.getReplyString());
            }

            verifyUploadSuccess(localFile, remoteFileFullPath);
            log.info("[FtpsClient] File uploaded successfully: {}", remoteFileFullPath);
            return true;
        } catch (Exception e) {
            log.error("[FtpsClient] Upload failed: {}", e.getMessage());
            throw new RuntimeException("FTPS upload failed: " + e.getMessage(), e);
        }
    }

    /**
     * 从FTPS服务器下载文件
     *
     * @param remoteFilePath 远程文件路径
     * @param localFilePath 本地存储文件路径
     * @return 是否下载成功
     */
    public boolean retrieveFile(String remoteFilePath, String localFilePath) {
        connectedCheck();
        try (OutputStream os = Files.newOutputStream(Paths.get(localFilePath))) {
            log.debug("[FtpsClient] Downloading file: {}", remoteFilePath);
            if (!ftpsClient.retrieveFile(remoteFilePath, os)) {
                throw new IOException("Download failed. Server reply: " + ftpsClient.getReplyString());
            }

            File localFile = new File(localFilePath);
            if (!localFile.exists() || localFile.length() == 0) {
                throw new IOException("Local file not created or empty");
            }

            log.info("[FtpsClient] File downloaded successfully: {}", localFilePath);
            return true;
        } catch (Exception e) {
            log.error("[FtpsClient] Download failed: {}", e.getMessage());
            throw new RuntimeException("FTPS download failed: " + e.getMessage(), e);
        }
    }

    /**
     * 递归创建远程目录
     */
    public boolean createRemoteDirectory(String path) throws IOException {
        connectedCheck();
        if (path == null || path.isEmpty()) {
            return true;
        }

        String[] dirs = path.split("/");
        StringBuilder currentPath = new StringBuilder();

        for (String dir : dirs) {
            if (dir.isEmpty()) {
                continue;
            }
            currentPath.append("/").append(dir);
            String dirPath = currentPath.toString();

            if (!ftpsClient.changeWorkingDirectory(dirPath)) {
                if (ftpsClient.makeDirectory(dirPath)) {
                    log.debug("[FtpsClient] Created directory: {}", dirPath);
                } else {
                    throw new IOException("Failed to create directory: " + dirPath);
                }
                ftpsClient.changeWorkingDirectory(dirPath);
            }
        }
        return true;
    }

    /**
     * 验证上传文件完整性
     *
     * @param localFile 本地文件
     * @param remotePath 远程路径
     * @throws IOException 抛出异常
     */
    private void verifyUploadSuccess(File localFile, String remotePath) throws IOException {
        FTPFile[] ftpFiles = checkAndListFiles(remotePath);
        FTPFile uploadedFile = checkAndFilterTargetFile(localFile.getName(), ftpFiles);
        long remoteSize = checkFileSize(localFile, uploadedFile);
        log.debug("[FtpsClient] File verification passed: {} bytes", remoteSize);
    }

    private static long checkFileSize(File localFile, FTPFile uploadedFile) throws IOException {
        long localSize = localFile.length();
        long remoteSize = uploadedFile.getSize();
        if (localSize != remoteSize) {
            throw new IOException(String.format(
                    "[FtpsClient] Size mismatch! Local: %d bytes, Remote: %d bytes",
                    localSize, remoteSize
            ));
        }
        return remoteSize;
    }

    private FTPFile checkAndFilterTargetFile(String fileName, FTPFile[] ftpFiles) throws IOException {
        FTPFile uploadedFile = Arrays.stream(ftpFiles)
                .filter(ftpFile -> ftpFile.getName().equals(fileName))
                .findFirst()
                .orElse(null);
        if (uploadedFile == null) {
            throw new IOException("[FtpsClient] Remote file not found after upload");
        }
        return uploadedFile;
    }

    private FTPFile[] checkAndListFiles(String remotePath) throws IOException {
        FTPFile[] ftpFiles = ftpsClient.listFiles(remotePath);
        if (ftpFiles == null || ftpFiles.length == 0) {
            throw new IOException("[FtpsClient] Remote file not found after upload");
        }
        return ftpFiles;
    }

    /**
     * 连接状态检查
     */
    private void connectedCheck() {
        if (!isConnected()) {
            throw new IllegalStateException("FTPS connection not established");
        }
    }

    /**
     * 判断连接状态
     * @return boolean
     */
    public boolean isConnected() {
        return ftpsClient != null && ftpsClient.isConnected();
    }

    /**
     * 静默断开连接
     */
    private void disconnectSilently() {
        try {
            close();
        } catch (Exception e) {
            log.debug("[FtpsClient] Error during disconnect: {}", e.getMessage());
        }
    }

    @Override
    public void close() {
        if (ftpsClient != null) {
            try {
                if (ftpsClient.isConnected()) {
                    ftpsClient.logout();
                }
            } catch (IOException e) {
                log.debug("[FtpsClient] Logout failed: {}", e.getMessage());
            } finally {
                try {
                    ftpsClient.disconnect();
                } catch (IOException e) {
                    log.debug("[FtpsClient] Disconnect failed: {}", e.getMessage());
                }
            }
        }
    }

    /**
     * 日志重定向辅助类
     * @author manccxxx@163.com
     * @date 2025/08/21
     */
    private static class LogWriter extends Writer {
        private final org.slf4j.Logger logger;

        LogWriter(org.slf4j.Logger logger) {
            this.logger = logger;
        }

        @Override
        public void write(char[] cbuf, int off, int len) {
            if (logger.isDebugEnabled()) {
                String message = new String(cbuf, off, len).trim();
                if (!message.isEmpty()) {
                    logger.debug("[FTPS] {}", message);
                }
            }
        }

        @Override
        public void flush() {}

        @Override
        public void close() {}
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值