Java通过FTP协议实现文件上传下载

1、依赖

java 复制代码
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.11.1</version>
            <scope>compile</scope>
        </dependency>

2、FTP下载

2.1 前端效果图

2.2 前端代码(HMTL)

html 复制代码
<template name="ftp_download">
    <div class="row" ng-controller="ftpDownloadController as ctrl">
        <div class="col-12">
            <div>
                <!--共享目录-->
                <cb-title sub-title="仅支持本地文件下载"
                          title="ftp文件下载">
                </cb-title>
                <div class="col-6">
                    FTP协议IP:
                    <cb-flow-value-input ng-model="entity.ip"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    端口:
                    <cb-flow-value-input ng-model="entity.port" title="默认21"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    用户名:
                    <cb-flow-value-input ng-model="entity.username"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    密码:
                    <cb-flow-value-input ng-model="entity.password"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    读取模式:
                    <cb-select ng-model="entity.ftpType" options="typeOptions"></cb-select>
                </div>
                <div class="col-6" ng-show="entity.ftpType.id == '1'||entity.ftpType.id == '2'">
                    文件名:
                    <cb-flow-value-input ng-model="entity.fileName"></cb-flow-value-input>
                </div>
                <div class="col-6" ng-show="entity.ftpType.id == '5'">
                    过滤已下载的文件:
                    <cb-flow-value-input ng-model="entity.downloadedList" title="用于过滤已下载的文件,格式要求List<String>"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    目录路径:
                    <cb-flow-value-input ng-model="entity.directoryPath"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    本地保存路径:
                    <cb-flow-value-input ng-model="entity.localFilePath"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    ip在线校验超时时间:
                    <cb-flow-value-input ng-model="entity.timeoutMillis"
                                         title="用于控制校验ip是否在线校验超时时间毫秒,默认1s"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    KeepAlive超时时间:
                    <cb-flow-value-input ng-model="entity.keepAliveTimeout"
                                         title="设置控制连接的KeepAlive超时时间单位(秒)默认6秒,作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    传输超时时间:
                    <cb-flow-value-input ng-model="entity.dataTimeout"
                                         title="设置数据传输的超时时间单位(毫秒)默认5秒,作用:给文件传输足够的时间,避免因超时导致传输中断"></cb-flow-value-input>
                </div>
            </div>
        </div>
    </div>
</template>

2.3 前端代码(JS)

javascript 复制代码
define([], function () {
    var app = angular.module('app');

    app.controller('ftpDownloadController', ['$scope', 'dialog', '$timeout',
        'http', 'util',
        function ($scope, dialog, $timeout, http, util,) {
            var ctrl = this;

            $scope.typeOptions = [{
                id: '1',
                name: '精确名字匹配'
            }, {
                id: '2',
                name: '模糊名字匹配'
            }, {
                id: '3',
                name: '目录下第一个文件'
            }, {
                id: '4',
                name: '目录下最新一个文件'
            }, {
                id: '5',
                name: '目录下所有最新文件'
            }, {
                id: '6',
                name: '目录下全部文件'
            }];


            _.extend(ctrl, {
                initialize: function () {
                    ctrl.bindEvent();
                    ctrl.initEntity();
                },
                initEntity() {
                    if (_.isEmpty($scope.entity)) {
                        $scope.entity = {
                            open: true,
                            dataPointList: [],
                            contents: {}
                        }
                    }
                    util.apply($scope);
                },
                checkSave: function () {
                    if ($scope.entity.open) {
                        /*检查必备的配置*/
                    }
                },
                bindEvent: function () {
                    $scope.$on("onSave", function (event, data, checkMessage) {
                        var result = ctrl.checkSave(data, checkMessage);
                        if (result === false) {
                            return;
                        }
                    });
                }
            });
            ctrl.initialize();
        }]);
})

2.4 后端代码

java 复制代码
package com.skybird.iot.addons.flow.flowCreator.web.nodes.fileManage.backend;

import cn.hutool.core.util.StrUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.skybird.commons.mongo.DocuLib;
import com.skybird.iot.addons.flow.flowCreator.backend.base.service.FlowNodeRunBaseService;
import com.skybird.iot.addons.flow.flowCreator.backend.dto.FlowRunDataDto;
import com.skybird.iot.addons.flow.flowCreator.backend.util.FlowFtpUtils;
import com.skybird.iot.addons.flow.flowCreator.backend.util.FlowProtocolAccessLogUtil;
import com.skybird.iot.addons.flow.flowCreator.web.nodes.fileManage.backend.service.FlowOssService;
import jakarta.annotation.Resource;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class FlowFtpDownloadService extends FlowNodeRunBaseService {
    private static final Logger LOGGER =
            LoggerFactory.getLogger(FlowFtpDownloadService.class.getName());
    @Resource
    FlowFolderJudgmentReadService flowFolderJudgmentReadService;

    @Resource
    FlowOssService flowOssService;

    @Override
    public String[] getComponentKey() {
        return new String[]{"ftp_download"};
    }

    @Override
    public void run(FlowRunDataDto flowRunDataDto) throws Exception {

        Document nodeConfig = flowRunDataDto.getNodeConfig();

        String ipRow = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "ip")));
        String username = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "username")));
        String password = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "password")));
        String fileName = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "fileName")));
        String localFilePath = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "localFilePath")));
        String directoryPath = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "directoryPath")));
        String timeoutMillis1 = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "timeoutMillis")));
        String keepAliveTimeout1 = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "keepAliveTimeout")));
        String dataTimeout1 = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "dataTimeout")));
        String ports = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "port")));
        int port = 21;
        int keepAliveTimeout = 6;//默认6秒
        int dataTimeout = 5000;//默认5秒
        if (StrUtil.isEmpty(timeoutMillis1) || timeoutMillis1.startsWith("Document{")) {
            timeoutMillis1 = "1000";
        }
        if (StrUtil.isNotBlank(keepAliveTimeout1) && !keepAliveTimeout1.startsWith("Document{")) {
            keepAliveTimeout = Integer.parseInt(keepAliveTimeout1);
        }
        if (StrUtil.isNotBlank(dataTimeout1) && !dataTimeout1.startsWith("Document{")) {
            dataTimeout = Integer.parseInt(dataTimeout1);
        }
        if (StrUtil.isNotBlank(ports) && !ports.startsWith("Document{")) {
            port = Integer.parseInt(ports);
        }
        Integer timeoutMillis = Integer.parseInt(timeoutMillis1);//ftp ip校验超时时间毫秒
        String ftpType = DocuLib.getStr(nodeConfig, "ftpType.id");
        Document result = DocuLib.newDoc();
        if (FlowFtpUtils.pingHost(ipRow, timeoutMillis)) {
            if (StrUtil.equals(ftpType, "1")) {
                Document dto = FlowFtpUtils.downloadFileByName(username, password, ipRow, port, localFilePath, fileName, directoryPath, keepAliveTimeout, dataTimeout);
                result.putAll(dto);
            } else if (StrUtil.equals(ftpType, "2")) {
                Document dto = FlowFtpUtils.vagueDownloadFile(username, password, ipRow, port, localFilePath, fileName, directoryPath, keepAliveTimeout, dataTimeout);
                result.putAll(dto);
            } else if (StrUtil.equals(ftpType, "4")) {
                Document dto = FlowFtpUtils.listDownloadGetLatestFile(username, password, ipRow, port, localFilePath, directoryPath, keepAliveTimeout, dataTimeout);
                result.putAll(dto);
            } else if (StrUtil.equals(ftpType, "5")) {
                Object downloadedListObj = flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "downloadedList"));
                List<String> downloadedList = null;
                if (downloadedListObj instanceof List) {
                    try {
                        downloadedList = ((List<?>) downloadedListObj).stream()
                                .map(Object::toString)
                                .toList();
                    } catch (Exception e) {
                        result.append("message", "过滤已下载的文件请传List<String>");
                    }
                }
                Document dto = FlowFtpUtils.listDownloadGetAllLatestDateFiles(username, password, ipRow, port, localFilePath, directoryPath, keepAliveTimeout, dataTimeout,downloadedList);
                result.putAll(dto);
            } else if (StrUtil.equals(ftpType, "6")) {
                Document dto = FlowFtpUtils.downloadAllFile(username, password, ipRow, port, localFilePath, directoryPath, keepAliveTimeout, dataTimeout);
                result.putAll(dto);
            } else {
                Document dto = FlowFtpUtils.listDownloadGetFirstFile(username, password, ipRow, port, localFilePath, directoryPath, keepAliveTimeout, dataTimeout);
                result.putAll(dto);
            }
        } else {
            result.append("message", "请检查网络情况,IP未通!");
        }

        flowRunDataDto.nodeValueAppend(new Document().append("result", result));
        FlowProtocolAccessLogUtil.addProtocolReadLog(nodeConfig, result);
    }

    @Override
    public void unRegister() {

    }
}

3、FTP上传

3.1 前端效果图

3.2 前端代码(HTML)

html 复制代码
<template name="ftp_upload">
    <div class="row" ng-controller="ftpUploadController as ctrl">
        <div class="col-12">
            <div>
                <!--共享目录-->
                <cb-title sub-title="仅支持本地文件上传"
                          title="ftp文件上传">
                </cb-title>
                <div class="col-6">
                    FTP协议IP:
                    <cb-flow-value-input ng-model="entity.ip"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    端口:
                    <cb-flow-value-input ng-model="entity.port" title="默认21"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    用户名:
                    <cb-flow-value-input ng-model="entity.username"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    密码:
                    <cb-flow-value-input ng-model="entity.password"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    文件名:
                    <cb-flow-value-input ng-model="entity.fileName"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    本地文件上传完整地址:
                    <cb-flow-value-input ng-model="entity.address"></cb-flow-value-input>
                </div>
                <div class="col-6">
                    格式:
                    <cb-select inline="true" input-width="145" ng-model="entity.format" options="ctrl.strEncodingOptions"></cb-select>
                </div>
                <div class="col-6">
                    添加NC尾标内容:
                    <cb-flow-value-input ng-model="entity.trailerLabel"></cb-flow-value-input>
                </div>
            </div>
        </div>
    </div>
</template>

3.3 前端代码(JS)

javascript 复制代码
define([], function () {
    var app = angular.module('app');

    app.controller('ftpUploadController', ['$scope', 'dialog', '$timeout',
        'http', 'util', 'PLCService',
        function ($scope, dialog, $timeout, http, util,PLCService) {
            var ctrl = this;

            ctrl.strEncodingOptions = PLCService.getStrEncodingOptions();
            _.extend(ctrl, {
                initialize: function () {
                    ctrl.bindEvent();
                    ctrl.initEntity();
                },
                initEntity() {
                    if (_.isEmpty($scope.entity)) {
                        $scope.entity = {
                            open: true,
                            dataPointList: [],
                            contents: {}
                        }
                    }
                    util.apply($scope);
                },
                checkSave: function () {
                    if ($scope.entity.open) {
                        /*检查必备的配置*/
                    }
                },
                bindEvent: function () {
                    $scope.$on("onSave", function (event, data, checkMessage) {
                        var result = ctrl.checkSave(data, checkMessage);
                        if (result === false) {
                            return;
                        }
                    });
                }
            });
            ctrl.initialize();
        }]);
})

3.4 后端代码

java 复制代码
package com.skybird.iot.addons.flow.flowCreator.web.nodes.fileManage.backend;

import cn.hutool.core.util.StrUtil;
import com.skybird.commons.mongo.DocuLib;
import com.skybird.iot.addons.flow.flowCreator.backend.base.service.FlowNodeRunBaseService;
import com.skybird.iot.addons.flow.flowCreator.backend.dto.FlowRunDataDto;
import com.skybird.iot.addons.flow.flowCreator.backend.util.FlowFtpUtils;
import com.skybird.iot.addons.flow.flowCreator.backend.util.FlowProtocolAccessLogUtil;
import com.skybird.iot.addons.flow.flowCreator.web.nodes.fileManage.backend.service.FlowOssService;
import jakarta.annotation.Resource;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class FlowFtpUploadService extends FlowNodeRunBaseService {
    private static final Logger LOGGER =
            LoggerFactory.getLogger(FlowFtpUploadService.class.getName());

    @Resource
    FlowOssService flowOssService;

    @Override
    public String[] getComponentKey() {
        return new String[]{"ftp_upload"};
    }

    @Override
    public void run(FlowRunDataDto flowRunDataDto) throws Exception {

        Document nodeConfig = flowRunDataDto.getNodeConfig();

        String ipRow = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "ip")));
        String username = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "username")));
        String password = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "password")));
        String fileName = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "fileName")));
        String localFilePath = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "address")));
        String ports = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "port")));
        Document result = DocuLib.newDoc();
        if (StrUtil.isNotBlank(ports) && !ports.startsWith("Document{")) {
            String trailerLabel = String.valueOf(flowRunDataDto.getFieldValue(DocuLib.getDocu(nodeConfig, "trailerLabel")));
            String format = DocuLib.getStr(nodeConfig, "format.id");
            int port = Integer.parseInt(ports);
            Document dto = FlowFtpUtils.uploadFileToDirectory(username, password, ipRow, port, localFilePath, fileName,trailerLabel,format);
            result.putAll(dto);
        } else {
            Document dto = FlowFtpUtils.uploadFile(username, password, ipRow, localFilePath, fileName);
            result.putAll(dto);
        }
        flowRunDataDto.nodeValueAppend(new Document().append("result", result));
        FlowProtocolAccessLogUtil.addProtocolReadLog(nodeConfig, result);
    }

    @Override
    public void unRegister() {

    }
}

4、工具类方法

java 复制代码
package com.skybird.iot.addons.flow.flowCreator.backend.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpConfig;
import cn.hutool.extra.ftp.FtpMode;
import com.skybird.commons.mongo.DocuLib;
import com.skybird.commons.utils.DigestLib;
import com.skybird.commons.utils.StreamUtils;

import java.net.InetAddress;
import java.time.LocalDate;
import java.time.ZoneId;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap;

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.bson.Document;


public class FlowFtpUtils {
    private static Map<String, Ftp> ftpServerMap = new ConcurrentHashMap();

    /**
     * 判断IP地址是否可达
     *
     * @param ip
     * @param timeoutMillis 超时时间毫秒
     * @return
     */
    public static boolean isHostReachable(String ip, int timeoutMillis) {
        try {
            return InetAddress.getByName(ip).isReachable(timeoutMillis);
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 判断IP地址是否可达 ping方式
     *
     * @param ipOrHost
     * @param timeoutMs
     * @return
     */
    public static boolean pingHost(String ipOrHost, int timeoutMs) {
        String command = System.getProperty("os.name").toLowerCase().contains("win")
                ? "ping -n 1 -w " + timeoutMs + " " + ipOrHost
                : "ping -c 1 -W " + (timeoutMs / 1000) + " " + ipOrHost;

        String result = RuntimeUtil.execForStr(command);
        return result.contains("TTL=") || result.contains("ttl=");
    }

    /***
     * 获取ftp连接(不开启复用)
     *
     * @param ftpConfig
     * @return
     */
    public static Ftp getFtpClient(Document ftpConfig) {
        return getFtpClient(ftpConfig, false);
    }

    /***
     * 获取ftp连接
     *
     * @param ftpConfig
     * @param reUse 需要留意 这里会有超时执行的等待时间损耗 等待超时后才会进行初始化重连(频率高的操作不能开启)
     * @return
     */
    public static Ftp getFtpClient(Document ftpConfig, boolean reUse) {
        String host = DocuLib.getStr(ftpConfig, "ip");
        int port = DocuLib.getInt(ftpConfig, "port");

        if (StrUtil.isEmpty(host) || port <= 0) {
            return null;
        }
        String username = DocuLib.getStr(ftpConfig, "username");
        String password = DocuLib.getStr(ftpConfig, "password");
        String modeValue = DocuLib.getStr(ftpConfig, "connType.id");
        String encoding = DocuLib.getStr(ftpConfig, "encoding.id");

        // 如果有复用连接的设置 从内存里取回连接实例 尝试重连激活即可
        String key;
        if (reUse) {
            key = DigestLib.md5Hex(host + port + username + password);
            Ftp ftp = ftpServerMap.get(key);
            if (Objects.nonNull(ftp)) {
                ftp.reconnectIfTimeout();
                return ftp;
            }
        }

        Charset charset =
                StreamUtils.mayIf(
                        StrUtil.isNotEmpty(encoding),
                        () -> CharsetUtil.charset(encoding),
                        () -> CharsetUtil.CHARSET_UTF_8);

        int conn_timeout = DocuLib.getInt(ftpConfig, "conn_timeout");
        if (conn_timeout < 60000) {
            conn_timeout = 60000;
        }

        FtpConfig config =
                FtpConfig.create()
                        .setCharset(charset)
                        .setConnectionTimeout(conn_timeout)
                        .setHost(host)
                        .setPort(port);

        if (StrUtil.isNotEmpty(username)) {
            config.setUser(username);
        }
        if (StrUtil.isNotEmpty(password)) {
            config.setPassword(password);
        }

        FtpMode mode = FtpMode.Passive;
        if (StrUtil.equalsIgnoreCase(modeValue, "active")) {
            mode = FtpMode.Active;
        }
        Ftp ftp = new Ftp(config, mode);
        return ftp;
    }


    /**
     * 从FTP服务器下载文件
     *
     * @param userName      用户名
     * @param password      密码
     * @param ip            ip地址
     * @param localFilePath 本地保存路径
     * @param fileName      文件名
     */
    public static Document downloadFile(String userName, String password, String ip, String localFilePath, String fileName) {
        String ftpUrl = StrUtil.format("ftp://{}:{}@{}/{}", userName, password, ip, fileName);
        File localDir = new File(localFilePath);

        try {
            // 确保本地路径存在
            if (!localDir.exists()) {
                if (!localDir.mkdirs()) {
                    return DocuLib.newDoc("message", "无法创建本地目录: " + localFilePath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }
            String key = localFilePath + generateHexId() + "-" + fileName;
            File localFile = new File(key);

            URL url = new URL(ftpUrl);
            try (ReadableByteChannel rbc = Channels.newChannel(url.openStream());
                 FileOutputStream fos = new FileOutputStream(localFile);
                 WritableByteChannel wbc = fos.getChannel()) {

                ByteBuffer buffer = ByteBuffer.allocate(2048);
                while (rbc.read(buffer) != -1) {
                    buffer.flip();
                    wbc.write(buffer);
                    buffer.clear();
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw e;
            }
            return DocuLib.newDoc("filePath", key);
        } catch (MalformedURLException e) {
            return DocuLib.newDoc("message", "无效的URL格式: " + e.getMessage());
        } catch (FileNotFoundException e) {
            return DocuLib.newDoc("message", "文件未找到: " + e.getMessage());
        } catch (IOException e) {
            return DocuLib.newDoc("message", "文件下载失败: " + e.getMessage());
        } catch (Exception e) {
            return DocuLib.newDoc("message", "下载异常: " + e.getMessage());
        }
    }

    /**
     * ftp根据文件目录获取相同文件名的文件下载到本地
     *
     * @param userName         用户名
     * @param password         密码
     * @param ip               ip地址
     * @param localFilePath    本地保存路径
     * @param fileName         文件名
     * @param directoryPath    文件目录,如果为空则默认根目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     * @return 下载文件的本地路径
     */
    public static Document downloadFileByName(String userName, String password, String ip, int port, String localFilePath, String fileName, String directoryPath, int keepAliveTimeout, int dataTimeout) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();//被动模式
            //设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            //设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            //设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);
            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");

            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                for (FTPFile file : files) {
                    if (file.isFile() && file.getName().equals(fileName)) {
                        String remoteFileName = file.getName();
                        String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                        File localFile = new File(localFileName);
                        // 处理中文文件名编码
                        String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                        // 下载文件到本地
                        try (FileOutputStream fos = new FileOutputStream(localFile)) {
                            InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                            if (inputStream == null) {
                                // 检查FTP响应代码以获取更多信息
                                int replyCode = ftpClient.getReplyCode();
                                String replyString = ftpClient.getReplyString();
                                return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                            }

                            ReadableByteChannel rbc = Channels.newChannel(inputStream);
                            ByteBuffer buffer = ByteBuffer.allocate(2048);
                            while (rbc.read(buffer) != -1) {
                                buffer.flip();
                                fos.write(buffer.array(), 0, buffer.limit());
                                buffer.clear();
                            }
                            // 完成文件传输
                            boolean complete = ftpClient.completePendingCommand();
                            if (complete) {
                                Document dto = DocuLib.newDoc("filePath", localFileName);
                                // 获取文件的最后修改时间
                                ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                                LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                String formattedDateTime = lastModifiedTime.format(formatter);
                                dto.put("updateTime", formattedDateTime);
                                return dto;
                            } else {
                                return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                            }
                        } catch (IOException e) {
                            return DocuLib.newDoc("message", "下载文件时发生错误: " + e.getMessage());
                        }
                    }
                }
            }
            return DocuLib.newDoc("message", "在目录中找不到文件名 " + fileName + " 的文件: " + (StrUtil.isNotEmpty(directoryPath) ? directoryPath : "根目录"));
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从本地上传文件到FTP服务器指定目录
     *
     * @param userName      用户名
     * @param password      密码
     * @param ip            ip地址
     * @param port          端口号
     * @param localFilePath 本地文件完整路径
     * @param fileName      上传后的文件名(可包含路径)
     * @param trailerLabel  添加尾标内容,不为空则在文件最后面添加这个内容
     * @param format        编码格式
     * @return 上传结果
     */
    public static Document uploadFileToDirectory(String userName, String password, String ip, int port,
                                                 String localFilePath, String fileName, String trailerLabel, String format) {
        FTPClient ftpClient = new FTPClient();
        File processedFile = null;
        File localFile = new File(localFilePath);

        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            int connectReply = ftpClient.getReplyCode();
            if (!FTPReply.isPositiveCompletion(connectReply)) {
                return DocuLib.newDoc("message", "连接FTP服务器失败,响应码: " + connectReply);
            }

            boolean loginSuccess = ftpClient.login(userName, password);
            if (!loginSuccess) {
                return DocuLib.newDoc("message", "FTP登录失败: " + ftpClient.getReplyString());
            }

            ftpClient.enterLocalPassiveMode(); // 被动模式
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); // 设置传输模式为二进制

            // 确保在根目录开始
            ftpClient.changeWorkingDirectory("/");

            if (!localFile.exists()) {
                return DocuLib.newDoc("message", "本地文件不存在: " + localFilePath);
            }

            // 如果trailerLabel不为空,则需要先在本地文件末尾添加内容
            processedFile = localFile;
            if (trailerLabel != null && !trailerLabel.isEmpty()) {
                try {
                    processedFile = appendTrailerToFile(localFile, trailerLabel, format);
                } catch (IOException e) {
                    // 清理临时文件
                    if (!processedFile.equals(localFile)) {
                        try {
                            processedFile.delete();
                        } catch (Exception deleteEx) {
                            System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                        }
                    }
                    return DocuLib.newDoc("message", "添加尾标内容时发生错误: " + e.getMessage() + ", 文件路径: " + localFilePath);
                }
            }

            // 标准化文件名中的路径分隔符
            fileName = fileName.replace("\\", "/");

            String targetFileName = fileName;
            String remoteDir = null;

            // 如果fileName包含路径,先创建远程目录结构
            if (fileName.contains("/")) {
                // 提取远程目录路径
                int lastSlashIndex = fileName.lastIndexOf('/');
                if (lastSlashIndex > 0) { // 确保不是根目录下的文件
                    remoteDir = fileName.substring(0, lastSlashIndex);
                    String actualFileName = fileName.substring(lastSlashIndex + 1);

                    // 切换到目标目录
                    if (!ftpClient.changeWorkingDirectory(remoteDir)) {
                        // 目录切换失败,尝试创建目录结构
                        boolean dirCreated = false;
                        try {
                            dirCreated = createRemoteDirectory(ftpClient, remoteDir);
                        } catch (IOException e) {
                            // 清理临时文件
                            if (!processedFile.equals(localFile)) {
                                try {
                                    processedFile.delete();
                                } catch (Exception deleteEx) {
                                    System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                                }
                            }
                            return DocuLib.newDoc("message", "创建远程目录时发生错误: " + e.getMessage() + ", 目标目录: " + remoteDir);
                        }

                        if (!dirCreated) {
                            // 清理临时文件
                            if (!processedFile.equals(localFile)) {
                                try {
                                    processedFile.delete();
                                } catch (Exception deleteEx) {
                                    System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                                }
                            }
                            return DocuLib.newDoc("message", "无法访问或创建远程目录: " + remoteDir);
                        }
                        // 再次尝试切换到目标目录
                        if (!ftpClient.changeWorkingDirectory(remoteDir)) {
                            // 清理临时文件
                            if (!processedFile.equals(localFile)) {
                                try {
                                    processedFile.delete();
                                } catch (Exception deleteEx) {
                                    System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                                }
                            }
                            return DocuLib.newDoc("message", "无法切换到目标目录: " + remoteDir);
                        }
                    }
                    targetFileName = actualFileName;
                }
            }

            try (InputStream inputStream = new FileInputStream(processedFile)) {
                // 直接上传文件,FTP会自动覆盖已存在的文件
                boolean done = ftpClient.storeFile(targetFileName, inputStream);
                if (done) {
                    // 清理临时文件
                    if (!processedFile.equals(localFile)) {
                        try {
                            processedFile.delete();
                        } catch (Exception deleteEx) {
                            System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                        }
                    }
                    return DocuLib.newDoc("check", true);
                } else {
                    // 清理临时文件
                    if (!processedFile.equals(localFile)) {
                        try {
                            processedFile.delete();
                        } catch (Exception deleteEx) {
                            System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                        }
                    }
                    // 提供具体的FTP错误信息
                    int replyCode = ftpClient.getReplyCode();
                    String replyString = ftpClient.getReplyString();
                    return DocuLib.newDoc("message", "文件上传失败: " + replyString + " (响应码: " + replyCode + ")" +
                            ", 当前目录: " + getCurrentDirectory(ftpClient) + ", 目标文件: " + targetFileName);
                }
            } catch (IOException e) {
                // 清理临时文件
                if (!processedFile.equals(localFile)) {
                    try {
                        processedFile.delete();
                    } catch (Exception deleteEx) {
                        System.err.println("清理临时文件失败: " + deleteEx.getMessage());
                    }
                }
                return DocuLib.newDoc("message", "上传文件时发生I/O错误: " + e.getMessage() +
                        ", 本地文件: " + processedFile.getAbsolutePath() + ", 目标文件: " + targetFileName);
            }
        } catch (IOException e) {
            return DocuLib.newDoc("message", "连接FTP服务器时发生I/O错误: " + e.getMessage() +
                    ", 服务器地址: " + ip + ":" + port + ", 用户名: " + userName);
        } catch (Exception e) {
            return DocuLib.newDoc("message", "上传过程中发生未预期错误: " + e.getMessage() +
                    ", 服务器地址: " + ip + ":" + port);
        } finally {
            // 确保在所有情况下都清理临时文件
            if (processedFile != null && !processedFile.equals(localFile)) {
                try {
                    if (processedFile.exists()) {
                        processedFile.delete();
                    }
                } catch (Exception e) {
                    System.err.println("清理临时文件失败: " + e.getMessage());
                }
            }

            // 关闭FTP连接
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                System.err.println("关闭FTP连接时发生错误: " + e.getMessage());
            }
        }
    }


    /**
     * 在文件末尾追加尾标内容
     *
     * @param originalFile 原始文件
     * @param trailerLabel 尾标内容
     * @param format 编码格式
     * @return 添加尾标后的新文件
     * @throws IOException IO异常
     */
    /**
     * 在文件末尾追加尾标内容
     *
     * @param originalFile 原始文件
     * @param trailerLabel 尾标内容
     * @param format
     * @return 添加尾标后的新文件
     * @throws IOException IO异常
     */
    private static File appendTrailerToFile(File originalFile, String trailerLabel, String format) throws IOException {
        // 创建临时文件
        String tempFileName = originalFile.getAbsolutePath() + ".tmp";
        File tempFile = new File(tempFileName);

        try (FileInputStream fis = new FileInputStream(originalFile);
             InputStreamReader isr = new InputStreamReader(fis, Charset.forName(format));
             FileOutputStream fos = new FileOutputStream(tempFile);
             OutputStreamWriter osw = new OutputStreamWriter(fos, Charset.forName(format))) {

            // 复制原始文件内容
            char[] buffer = new char[2048];
            int charsRead;
            while ((charsRead = isr.read(buffer)) != -1) {
                osw.write(buffer, 0, charsRead);
            }

            // 添加尾标内容
            if (trailerLabel != null && !trailerLabel.isEmpty()) {
                osw.write(System.lineSeparator());
                osw.write(trailerLabel);
            }

            osw.flush();
        }

        return tempFile;
    }

    /**
     * 获取当前工作目录(带错误处理)
     *
     * @param ftpClient FTP客户端
     * @return 当前工作目录
     */
    private static String getCurrentDirectory(FTPClient ftpClient) {
        try {
            return ftpClient.printWorkingDirectory();
        } catch (IOException e) {
            return "无法获取当前目录";
        }
    }


    /**
     * 创建远程目录结构
     *
     * @param ftpClient FTP客户端
     * @param remoteDir 远程目录路径
     * @return 是否成功创建目录
     * @throws IOException IO异常
     */
    private static boolean createRemoteDirectory(FTPClient ftpClient, String remoteDir) throws IOException {
        // 标准化路径分隔符
        remoteDir = remoteDir.replace("\\", "/");

        // 移除开头的斜杠(如果存在)
        if (remoteDir.startsWith("/")) {
            remoteDir = remoteDir.substring(1);
        }

        // 如果路径为空,直接返回成功
        if (remoteDir.isEmpty()) {
            return true;
        }

        String[] dirs = remoteDir.split("/");
        String currentDir = "";

        for (String dir : dirs) {
            if (dir.isEmpty()) continue;

            if (currentDir.isEmpty()) {
                currentDir = dir;
            } else {
                currentDir += "/" + dir;
            }

            // 检查目录是否存在
            if (ftpClient.changeWorkingDirectory("/" + currentDir)) {
                // 目录存在,继续下一个
                continue;
            }

            // 目录不存在,尝试创建
            if (ftpClient.makeDirectory(currentDir)) {
                // 创建成功
                ftpClient.changeWorkingDirectory("/" + currentDir);
            } else {
                // 检查是否是因为目录已存在导致创建失败
                int replyCode = ftpClient.getReplyCode();
                // 550可能表示目录已存在或权限不足
                if (replyCode == 550) {
                    // 尝试切换到目录,确认是否真的存在
                    if (!ftpClient.changeWorkingDirectory("/" + currentDir)) {
                        System.err.println("无法访问目录: " + currentDir + ", 错误: " + ftpClient.getReplyString());
                        return false;
                    }
                } else {
                    System.err.println("创建目录失败: " + currentDir + ", 错误码: " + replyCode + ", 错误信息: " + ftpClient.getReplyString());
                    return false;
                }
            }
        }
        // 最后切换回根目录
        ftpClient.changeWorkingDirectory("/");
        return true;
    }


    /**
     * ftp根据文件目录获取第一个包含文件名的文件下载到本地
     *
     * @param userName         用户名
     * @param password         密码
     * @param ip               ip地址
     * @param port             端口
     * @param localFilePath    本地保存路径
     * @param fileName         文件名
     * @param directoryPath    文件目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     */
    public static Document vagueDownloadFile(String userName, String password, String ip, int port, String localFilePath, String fileName, String directoryPath, int keepAliveTimeout, int dataTimeout) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();//被动模式
            //设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            //设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            //设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);
            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");

            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                for (FTPFile file : files) {
                    if (file.isFile() && file.getName().contains(fileName)) {
                        String remoteFileName = file.getName();
                        String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                        File localFile = new File(localFileName);
                        // 处理中文文件名编码
                        String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                        // 下载文件到本地
                        try (FileOutputStream fos = new FileOutputStream(localFile)) {
                            InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                            if (inputStream == null) {
                                // 检查FTP响应代码以获取更多信息
                                int replyCode = ftpClient.getReplyCode();
                                String replyString = ftpClient.getReplyString();
                                return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                            }

                            ReadableByteChannel rbc = Channels.newChannel(inputStream);
                            ByteBuffer buffer = ByteBuffer.allocate(2048);
                            while (rbc.read(buffer) != -1) {
                                buffer.flip();
                                fos.write(buffer.array(), 0, buffer.limit());
                                buffer.clear();
                            }
                            // 完成文件传输
                            boolean complete = ftpClient.completePendingCommand();
                            if (complete) {
                                Document dto = DocuLib.newDoc("filePath", localFileName);
                                // 获取文件的最后修改时间
                                ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                                LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                String formattedDateTime = lastModifiedTime.format(formatter);
                                dto.put("updateTime", formattedDateTime);
                                return dto;
                            } else {
                                return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                            }
                        } catch (IOException e) {
                            return DocuLib.newDoc("message", "下载文件时发生错误: " + e.getMessage());
                        }
                    }
                }
            }
            return DocuLib.newDoc("message", "在目录中找不到包含文件名 " + fileName + " 的文件: " + directoryPath);
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * ftp根据文件目录获取第一个文件下载到本地
     *
     * @param userName         用户名
     * @param password         密码
     * @param ip               ip地址
     * @param port
     * @param localFilePath    本地保存路径
     * @param directoryPath    文件目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     */
    public static Document listDownloadGetFirstFile(String userName, String password, String ip, int port, String localFilePath, String directoryPath, int keepAliveTimeout, int dataTimeout) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();//被动模式
            //设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            //设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            //设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);

            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");
            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                for (FTPFile file : files) {
                    if (file.isFile()) {
                        String remoteFileName = file.getName();
                        String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                        File localFile = new File(localFileName);
                        // 处理中文文件名编码
                        String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                        // 下载文件到本地
                        try (FileOutputStream fos = new FileOutputStream(localFile)) {
                            InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                            if (inputStream == null) {
                                // 检查FTP响应代码以获取更多信息
                                int replyCode = ftpClient.getReplyCode();
                                String replyString = ftpClient.getReplyString();
                                return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                            }

                            ReadableByteChannel rbc = Channels.newChannel(inputStream);
                            ByteBuffer buffer = ByteBuffer.allocate(2048);
                            while (rbc.read(buffer) != -1) {
                                buffer.flip();
                                fos.write(buffer.array(), 0, buffer.limit());
                                buffer.clear();
                            }
                            // 完成文件传输
                            boolean complete = ftpClient.completePendingCommand();
                            if (complete) {
                                Document dto = DocuLib.newDoc("filePath", localFileName);
                                // 获取文件的最后修改时间
                                ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                                LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                String formattedDateTime = lastModifiedTime.format(formatter);
                                dto.put("updateTime", formattedDateTime);
                                return dto;
                            } else {
                                return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                            }
                        } catch (IOException e) {
                            return DocuLib.newDoc("message", "下载文件时发生错误: " + e.getMessage());
                        }
                    }
                }
            }
            return DocuLib.newDoc("message", "在目录中找不到文件: " + directoryPath);
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * ftp根据文件目录获取全部文件下载到本地
     *
     * @param username         用户名
     * @param password         密码
     * @param ipRow            ip地址
     * @param port             端口
     * @param localFilePath    本地保存路径
     * @param directoryPath    文件目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     * @return 下载结果,包含所有下载文件的路径列表
     */
    public static Document downloadAllFile(String username, String password, String ipRow, int port,
                                           String localFilePath, String directoryPath,
                                           int keepAliveTimeout, int dataTimeout) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ipRow, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(username, password);
            ftpClient.enterLocalPassiveMode();     // 被动模式
            // 设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            // 设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            // 设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);

            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");
            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 创建本地目录(如果不存在)
            File localDir = new File(localFilePath);
            if (!localDir.exists()) {
                if (!localDir.mkdirs()) {
                    return DocuLib.newDoc("message", "无法创建本地目录: " + localFilePath);
                }
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                List<String> downloadedFiles = new ArrayList<>();
                String latestDate = "";

                // 下载所有文件
                for (FTPFile file : files) {
                    if (file.isFile()) {
                        String remoteFileName = file.getName();
                        String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                        File localFile = new File(localFileName);
                        // 处理中文文件名编码
                        String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                        // 下载文件到本地
                        try (FileOutputStream fos = new FileOutputStream(localFile)) {
                            InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                            if (inputStream == null) {
                                // 检查FTP响应代码以获取更多信息
                                int replyCode = ftpClient.getReplyCode();
                                String replyString = ftpClient.getReplyString();
                                return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                            }

                            ReadableByteChannel rbc = Channels.newChannel(inputStream);
                            ByteBuffer buffer = ByteBuffer.allocate(2048);
                            while (rbc.read(buffer) != -1) {
                                buffer.flip();
                                fos.write(buffer.array(), 0, buffer.limit());
                                buffer.clear();
                            }
                            // 完成文件传输
                            boolean complete = ftpClient.completePendingCommand();
                            if (complete) {
                                downloadedFiles.add(localFileName);

                                // 获取文件的最后修改时间
                                ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                                LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                                String formattedDateTime = lastModifiedTime.format(formatter);

                                // 更新最新的文件时间
                                if (latestDate.isEmpty() || formattedDateTime.compareTo(latestDate) > 0) {
                                    latestDate = formattedDateTime;
                                }
                            } else {
                                return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                            }
                        } catch (IOException e) {
                            return DocuLib.newDoc("message", "下载文件时发生错误: " + remoteFileName + " " + e.getMessage());
                        }
                    }
                }

                Document result = DocuLib.newDoc("filePath", downloadedFiles);
                result.put("updateTime", latestDate);
                return result;
            }
            return DocuLib.newDoc("message", "在目录中找不到文件: " + directoryPath);
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * ftp根据文件目录获取最新的一个文件下载到本地
     *
     * @param userName         用户名
     * @param password         密码
     * @param ip               ip地址
     * @param port             端口
     * @param localFilePath    本地保存路径
     * @param directoryPath    文件目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     * @return 下载结果
     */
    public static Document listDownloadGetLatestFile(String userName, String password, String ip, int port, String localFilePath, String directoryPath, int keepAliveTimeout, int dataTimeout) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();//被动模式
            //设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            //设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            //设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);

            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");
            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                // 查找最新的文件
                FTPFile latestFile = null;
                for (FTPFile file : files) {
                    if (file.isFile()) {
                        if (latestFile == null || file.getTimestamp().getTimeInMillis() > latestFile.getTimestamp().getTimeInMillis()) {
                            latestFile = file;
                        }
                    }
                }

                if (latestFile != null) {
                    String remoteFileName = latestFile.getName();
                    String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                    File localFile = new File(localFileName);
                    // 处理中文文件名编码
                    String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                    // 下载文件到本地
                    try (FileOutputStream fos = new FileOutputStream(localFile)) {
                        InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                        if (inputStream == null) {
                            // 检查FTP响应代码以获取更多信息
                            int replyCode = ftpClient.getReplyCode();
                            String replyString = ftpClient.getReplyString();
                            return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                        }

                        ReadableByteChannel rbc = Channels.newChannel(inputStream);
                        ByteBuffer buffer = ByteBuffer.allocate(2048);
                        while (rbc.read(buffer) != -1) {
                            buffer.flip();
                            fos.write(buffer.array(), 0, buffer.limit());
                            buffer.clear();
                        }
                        // 完成文件传输
                        boolean complete = ftpClient.completePendingCommand();
                        if (complete) {
                            Document dto = DocuLib.newDoc("filePath", localFileName);
                            // 获取文件的最后修改时间
                            ZonedDateTime zonedDateTime = latestFile.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                            LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                            String formattedDateTime = lastModifiedTime.format(formatter);
                            dto.put("updateTime", formattedDateTime);
                            return dto;
                        } else {
                            return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                        }
                    } catch (IOException e) {
                        return DocuLib.newDoc("message", "下载文件时发生错误: " + e.getMessage());
                    }
                }
            }
            return DocuLib.newDoc("message", "在目录中找不到文件: " + directoryPath);
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * ftp根据文件目录获取最新日期的所有文件下载到本地
     *
     * @param userName         用户名
     * @param password         密码
     * @param ip               ip地址
     * @param port             端口
     * @param localFilePath    本地保存路径
     * @param directoryPath    文件目录
     * @param keepAliveTimeout 控制连接的KeepAlive超时时间
     * @param dataTimeout      数据传输的超时时间
     * @param downloadedList   已经下载过的文件名列表
     * @return 下载结果,包含所有下载文件的路径列表
     */
    public static Document listDownloadGetAllLatestDateFiles(String userName, String password, String ip, int port,
                                                             String localFilePath, String directoryPath,
                                                             int keepAliveTimeout, int dataTimeout, List<String> downloadedList) {
        FTPClient ftpClient = new FTPClient();
        try {
            // 连接到 FTP 服务器
            ftpClient.connect(ip, port);
            ftpClient.setControlEncoding("UTF-8"); // 设置控制连接编码
            ftpClient.setAutodetectUTF8(true);     // 自动检测UTF-8编码
            ftpClient.login(userName, password);
            ftpClient.enterLocalPassiveMode();//被动模式
            //设置文件传输模式为二进制模式
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            //设置控制连接的KeepAlive超时时间单位(秒),作用:长时间文件传输过程中保持控制连接活跃,防止被服务器断开
            ftpClient.setControlKeepAliveTimeout(keepAliveTimeout);
            //设置数据传输的超时时间(毫秒)
            ftpClient.setDataTimeout(dataTimeout);

            directoryPath = new String(directoryPath.getBytes("UTF-8"), "ISO-8859-1");
            // 如果 directoryPath 不为空,则切换到指定目录
            if (directoryPath != null && !directoryPath.trim().isEmpty()) {
                if (!ftpClient.changeWorkingDirectory(directoryPath)) {
                    return DocuLib.newDoc("message", "找不到目录: " + directoryPath);
                }
            }

            // 确保本地路径以斜杠结尾
            if (!localFilePath.endsWith("/") && !localFilePath.endsWith("\\")) {
                localFilePath += File.separator;
            }

            // 获取目录下的文件列表
            FTPFile[] files = ftpClient.listFiles();
            if (files != null && files.length > 0) {
                // 查找最新时间(精确到分钟)
                LocalDateTime latestDateTime = null;
                for (FTPFile file : files) {
                    if (file.isFile()) {
                        ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                        LocalDateTime fileDateTime = zonedDateTime.toLocalDateTime().withSecond(0).withNano(0); // 精确到分钟
                        if (latestDateTime == null || fileDateTime.isAfter(latestDateTime)) {
                            latestDateTime = fileDateTime;
                        }
                    }
                }

                // 收集所有最新时间的文件
                List<FTPFile> latestDateFiles = new ArrayList<>();
                if (latestDateTime != null) {
                    for (FTPFile file : files) {
                        if (file.isFile()) {
                            ZonedDateTime zonedDateTime = file.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                            LocalDateTime fileDateTime = zonedDateTime.toLocalDateTime().withSecond(0).withNano(0); // 精确到分钟
                            if (fileDateTime.isEqual(latestDateTime)) {
                                latestDateFiles.add(file);
                            }
                        }
                    }
                }

                if (!latestDateFiles.isEmpty()) {
                    List<String> downloadedFiles = new ArrayList<>();

                    // 下载所有最新时间的文件
                    for (FTPFile file : latestDateFiles) {
                        String remoteFileName = file.getName();
                        // 过滤掉已经下载过的文件
                        if (CollUtil.isNotEmpty(downloadedList) && downloadedList.contains(remoteFileName)) {
                            continue;
                        }
                        String localFileName = localFilePath + generateHexId() + "-" + remoteFileName;
                        File localFile = new File(localFileName);
                        // 处理中文文件名编码
                        String encodedFileName = new String(remoteFileName.getBytes("UTF-8"), "ISO-8859-1");

                        // 下载文件到本地
                        try (FileOutputStream fos = new FileOutputStream(localFile)) {
                            InputStream inputStream = ftpClient.retrieveFileStream(encodedFileName);
                            if (inputStream == null) {
                                // 检查FTP响应代码以获取更多信息
                                int replyCode = ftpClient.getReplyCode();
                                String replyString = ftpClient.getReplyString();
                                return DocuLib.newDoc("message", "无法获取文件流,FTP响应码: " + replyCode + ", 信息: " + replyString);
                            }

                            ReadableByteChannel rbc = Channels.newChannel(inputStream);
                            ByteBuffer buffer = ByteBuffer.allocate(2048);
                            while (rbc.read(buffer) != -1) {
                                buffer.flip();
                                fos.write(buffer.array(), 0, buffer.limit());
                                buffer.clear();
                            }
                            // 完成文件传输
                            boolean complete = ftpClient.completePendingCommand();
                            if (complete) {
                                downloadedFiles.add(localFileName);
                            } else {
                                return DocuLib.newDoc("message", "下载文件失败: " + remoteFileName);
                            }
                        } catch (IOException e) {
                            return DocuLib.newDoc("message", "下载文件时发生错误: " + remoteFileName + " " + e.getMessage());
                        }
                    }

                    Document result = DocuLib.newDoc("filePath", downloadedFiles);

                    // 添加最新时间信息
                    if (!latestDateFiles.isEmpty()) {
                        FTPFile firstFile = latestDateFiles.get(0);
                        ZonedDateTime zonedDateTime = firstFile.getTimestamp().toInstant().atZone(ZoneId.systemDefault());
                        LocalDateTime lastModifiedTime = zonedDateTime.toLocalDateTime();
                        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                        String formattedDateTime = lastModifiedTime.format(formatter);
                        result.put("updateTime", formattedDateTime);
                    }
                    return result;
                }
            }
            return DocuLib.newDoc("message", "在目录中找不到文件: " + directoryPath);
        } catch (IOException e) {
            e.printStackTrace();
            return DocuLib.newDoc("message", e.getMessage());
        } finally {
            try {
                if (ftpClient.isConnected()) {
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 从本地上传文件到FTP服务器
     *
     * @param userName      用户名
     * @param password      密码
     * @param ip            ip地址
     * @param localFilePath 本地保存文件完整路径
     * @param fileName      文件名
     * @return
     */
    public static Document uploadFile(String userName, String password, String ip, String localFilePath, String fileName) {
        String ftpUrl = StrUtil.format("ftp://{}:{}@{}/{}", userName, password, ip, fileName);
        File file = new File(localFilePath);

        try {
            // 检查文件是否存在
            if (!file.exists()) {
                return DocuLib.newDoc("message", "找不到文件: " + localFilePath).append("check", false);
            }

            URL url = new URL(ftpUrl);
            URLConnection urlConnection = url.openConnection();
            urlConnection.setDoOutput(true);

            try (InputStream in = new FileInputStream(file);
                 OutputStream out = urlConnection.getOutputStream()) {
                byte[] buffer = new byte[2048];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
                out.flush();
                return DocuLib.newDoc("check", true);
            } catch (Exception e) {
                return DocuLib.newDoc("message", "上传文件时发生错误: " + e.getMessage()).append("check", false);
            }
        } catch (MalformedURLException e) {
            return DocuLib.newDoc("message", "无效的URL格式: " + e.getMessage()).append("check", false);
        } catch (FileNotFoundException e) {
            return DocuLib.newDoc("message", "本地文件未找到: " + e.getMessage()).append("check", false);
        } catch (IOException e) {
            return DocuLib.newDoc("message", "I/O错误: " + e.getMessage()).append("check", false);
        } catch (Exception e) {
            return DocuLib.newDoc("message", "其他异常: " + e.getMessage()).append("check", false);
        }
    }

    /**
     * 生成一个十六位数的ID
     *
     * @return 十六位数的ID
     */
    private static String generateHexId() {
        UUID uuid = UUID.randomUUID();
        String uuidStr = uuid.toString().replace("-", ""); // 去掉UUID中的连字符
        return uuidStr.substring(0, 16); // 截取前16位
    }
}
相关推荐
逸Y 仙X1 小时前
文章三十:Elasticsearch SQL实战案例
java·大数据·sql·elasticsearch·搜索引擎·全文检索
小则又沐风a1 小时前
初步了解进程的概念
java·linux·服务器·前端
斌果^O^1 小时前
普通 SpringBoot 单体项目改造成微服务(Nacos+Gateway + 内部服务免鉴权)
java·spring boot·spring
摩羯座-小齐1 小时前
java excel级联下拉框
java·excel
砍材农夫1 小时前
物联网 基于netty入门与线程模型探秘简述
java·物联网·struts
GentleDevin1 小时前
IntelliJ Idea常用快捷键(Window和Mac对照表)
java·ide·intellij-idea
Paxon Zhang1 小时前
JavaEE 初阶变强宝典 **线程安全问题,线程状态,锁synchronized**
java·java-ee
阿拉金alakin1 小时前
Java IO 核心类 File、InputStream/OutputStream 实战总结
java·开发语言
之歆1 小时前
DAY_25 JavaScript 原型、原型链与值类型/引用类型 ── 深度全解(上)
开发语言·javascript·原型模式