FtpUtils.java 13.4 KB
package com.genersoft.iot.vmp.vmanager.util;

import cn.hutool.extra.ftp.FtpException;
import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.FtpConfigBean;
import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryEnum;
import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.stereotype.Component;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * FTP服务工具类
 */
@Log4j2
@Component
@RequiredArgsConstructor
public class FtpUtils {

    private final FtpConfigBean ftpConfigBean;

    /**
     * 获取 FTPClient对象
     * @return FTPClient对象
     */
    private FTPClient getFTPClient() {
        /**
         * 创建 FTPClient对象(对于连接ftp服务器,以及上传和上传都必须要用到一个对象)
         */
        try {
            FTPClient ftpClient = new FTPClient();
            /**
             * 连接 FTP服务
             */
            // 设置编码
            ftpClient.setControlEncoding("UTF-8");
            // 设置连接超时时间(单位:毫秒)
            ftpClient.setConnectTimeout(10 * 1000);
            // 连接
            ftpClient.connect(ftpConfigBean.getHost(), ftpConfigBean.getPort());
            // 登录
            ftpClient.login(ftpConfigBean.getUsername(), ftpConfigBean.getPassword());
            /**
             * ftpClient.getReplyCode():接受状态码(如果成功,返回230,如果失败返回503)
             * FTPReply.isPositiveCompletion():如果连接成功返回true,否则返回false
             */
            if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
                log.error("未连接到FTP服务,用户名或密码错误");
                // 连接失败,断开连接
                ftpClient.disconnect();
                return null;
            } else {
                log.info("连接到FTP服务成功");
                // 设置二进制方式传输文件
                ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
                // 设置被动工作模式,文件传输端口设置,否则文件上传不成功,也不报错
                ftpClient.enterLocalPassiveMode();
            }
            return ftpClient;
        } catch (SocketException e) {
            log.error("FTP的IP地址错误,请正确配置。");
            throw new FtpException("FTP的IP地址错误,请正确配置。");
        } catch (IOException e) {
            log.error("FTP的端口错误,请正确配置。");
            throw new FtpException("FTP的端口错误,请正确配置。");
        } catch (Exception e) {
            log.error("获取ftp客户端异常");
            throw new FtpException("获取ftp客户端异常");
        }
    }

    /**
     * 断开 FTPClient对象
     */
    private void closeConnect(FTPClient ftpClient) {
        try {
            if (ftpClient != null && ftpClient.isConnected()) {
                ftpClient.logout();
                // 断开ftp的连接
                ftpClient.disconnect();
                log.info("关闭ftp客户端成功");
            }
        } catch (Exception e) {
            log.error("关闭ftp客户端异常");
            throw new FtpException("关闭ftp客户端异常");
        }
    }

    /**
     * 创建文件夹
     * @param ftpBasePath FTP用户上传的根目录
     * @param dirPath     需要创建的文件夹,多层使用/隔开
     * @return
     */
    public boolean createDirectory(String ftpBasePath, String dirPath) {
        FTPClient ftpClient = getFTPClient();
        try {
            /**
             * 切换到ftp的服务器路径。
             * FTP服务为FTP虚拟用户默认了根目录,所以我们可以切换也可以不切换,结果是一样的,都会到用户的根目录下。推荐显示指定。
             * FTP服务会判断文件夹已存在,不会创建,不存在,则会创建。
             */
            ftpClient.changeWorkingDirectory(ftpBasePath);
            if (StringUtils.isBlank(dirPath)) {
                return false;
            }

            String[] dirPathArr = dirPath.split("/");
            for (String dir : dirPathArr) {
                if (StringUtils.isNotBlank(dir)) {
                    ftpClient.makeDirectory(dir);
                    // 切换到ftp的创建目录
                    ftpClient.changeWorkingDirectory(dir);
                }
            }
            return true;
        } catch (IOException e) {
            log.error("创建文件夹异常");
            throw new FtpException("创建文件夹异常");
        } finally {
            closeConnect(ftpClient);
        }
    }

    /**
     * 查询指定路径下的所有文件的文件名
     * @param dirPath     查询指定路径
     * @return
     */
    public List<String> listFileName(String dirPath) {
        if (StringUtils.isBlank(dirPath)) {
            return null;
        }
        FTPClient ftpClient = getFTPClient();

        // 获得指定目录下所有文件名
        FTPFile[] ftpFiles = null;
        try {
            //ftpClient.enterLocalPassiveMode(); // 列出路径下的所有文件的文件名
            ftpFiles = ftpClient.listFiles(dirPath);
        } catch (IOException e) {
            log.info("获取文件列表失败");
            throw new FtpException("获取文件列表失败");
        } finally {
            closeConnect(ftpClient);
        }
        List<String> fileNameList = new LinkedList<>();
        for (int i = 0; ftpFiles != null && i < ftpFiles.length; i++) {
            FTPFile file = ftpFiles[i];
            if (file.isFile()) {
                fileNameList.add(file.getName());
            }
        }
        return fileNameList;
    }

    /**
     * 上传文件到ftp服务
     * @param ftpBasePath FTP用户上传的根目录
     * @param fileDirPath 上传的文件存储目录
     * @param fileName    上传的文件名
     * @param is          上传的文件输入流
     */
    public boolean uploadFileToFtp(String ftpBasePath, String fileDirPath, String fileName, InputStream is) {
        FTPClient ftpClient = getFTPClient();
        boolean result = false;
        try {
            // 创建文件存储目录
            createDirectory(ftpBasePath, fileDirPath);
            // 切换到ftp的文件目录,即文件上传目录
            ftpClient.changeWorkingDirectory(fileDirPath);
            ftpClient.setControlEncoding("UTF-8");
            ftpClient.setBufferSize(1024 * 10);
            // 设置文件类型为二进制方式传输文件
            ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpClient.enterLocalPassiveMode();
            ftpClient.setDefaultTimeout(18000);
            ftpClient.setConnectTimeout(6000);
            ftpClient.setSoTimeout(6000);
            result = ftpClient.storeFile(fileName, is);
        } catch (IOException e) {
            log.error("上传文件到ftp服务失败:{}", e.getMessage());
            throw new FtpException("上传文件到ftp服务失败:{}", e.getMessage());
        } finally {
            closeConnect(ftpClient);
        }
        return result;
    }

    /**
     * 从FTP中获取文件的输入流
     * @param ftpFilePath ftp文件路径,根目录开始
     * @return
     */
    public InputStream getInputStreamOfFtpFile(String ftpFilePath) {
        FTPClient ftpClient = getFTPClient();

        InputStream is = null;
        try {
            is = ftpClient.retrieveFileStream(ftpFilePath);
        } catch (IOException e) {
            log.error("获取文件输入流异常");
            throw new FtpException("获取文件输入流异常");
        } finally {
            closeConnect(ftpClient);
        }
        return is;
    }

    public InputStream downloadFile(HistoryRecord historyRecord){
        return getInputStreamOfFtpFile(historyRecord.getPath());
    }

    /**
     * 删除ftp文件
     * @param ftpFilePath ftp文件路径,根目录开始
     * @return
     */
    public boolean deleteFtpFile(String ftpFilePath) {
        FTPClient ftpClient = getFTPClient();
        boolean result = false;
        try {
            result = ftpClient.deleteFile(ftpFilePath);
        } catch (IOException e) {
            log.error("删除ftp文件失败:{}", e.getMessage());
            throw new FtpException("删除ftp文件失败:{}", e.getMessage());
        } finally {
            closeConnect(ftpClient);
        }
        return result;
    }

    /**
     * 验证指定目录下是否存在至少一个文件。
     *
     * @param directoryPath 目录路径
     * @return 如果目录中存在至少一个文件,则返回文件名;否则返回null
     */
    public String checkDirectoryContainsFiles(String directoryPath){
        FTPClient ftpClient = getFTPClient();
        try {
            // 改变工作目录
            if (ftpClient.changeWorkingDirectory(directoryPath)) {
                // 列出目录中的所有文件和子目录
                FTPFile[] files = ftpClient.listFiles();
                for (FTPFile file : files) {
                    if (file.isFile()) { // 检查是否为文件
                        return file.getName(); // 找到了至少一个文件
                    }
                }
            } else {
                log.error("无法进入目录:{}", directoryPath);
                return null;
            }
        } catch (IOException e) {
            log.error("发生错误:{}", e.getMessage());
            throw new FtpException(String.format("发生错误:{%s}", e.getMessage()));
        } finally {
            closeConnect(ftpClient);
        }
        return null; // 没有找到任何文件
    }

    public boolean checkHistoryVideo(HistoryRecord record) {
        String fileName = checkDirectoryContainsFiles(record.getPath());
        boolean flag = fileName != null;
        if (flag){
            record.setPath( String.join(record.getPath(),"/",fileName));
            record.setStatus(HistoryEnum.UPLOADED.getCode());
        }
        return flag;
    }
    /**
     * 批量删除指定路径下的文件和文件夹。
     */
    public List<HistoryRecord> batchDelete(List<HistoryRecord> historyRecordList) throws IOException {
        HashMap<String, List<HistoryRecord>> recordMap = new HashMap<>();
        recordMap.put("true",new ArrayList<>(historyRecordList));
        recordMap.put("false",new ArrayList<>(historyRecordList));
        for (HistoryRecord historyRecord : historyRecordList) {
            boolean b = deleteRecursive(historyRecord.getPath());
            if (!b) {
                historyRecord.setPath(null);
                historyRecord.setStatus(HistoryEnum.NOT_UPLOADED.getCode());
                historyRecord.setCreateTime(new Date());
                recordMap.get("true").add(historyRecord);
            }else {
                recordMap.get("false").add(historyRecord);
            }
        }
        log.debug("====================== FTP 删除过期文件 =====================");
        String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        log.debug("==> {} 成功删除文件 {} 个 ", format,recordMap.get("true").size());
        log.debug("==> {} 未删除文件 {} 个", format,recordMap.get("false").size());
        log.debug("===========================================================");
        return recordMap.get("false");
    }

    /**
     * 递归删除指定路径下的文件和文件夹。
     *
     * @param path 文件或文件夹路径
     */
    private boolean deleteRecursive(String path) throws IOException {
        FTPClient ftpClient = getFTPClient();
        FTPFile[] files = ftpClient.listFiles(path);
        if (files == null || files.length == 0) {
            // 如果路径不存在或者为空,尝试删除它
            if (!ftpClient.removeDirectory(path)) {
                if (!ftpClient.deleteFile(path)) {
                    log.error("无法删除: {}", path);
                    return false;
                } else {
                    log.info("已删除文件: {}", path);
                    return true;
                }
            } else {
                log.info("已删除目录: {}", path);
                return true;
            }
        }
        // 删除文件夹中的所有文件和子文件夹
        for (FTPFile file : files) {
            String filePath = path.endsWith("/") ? path + file.getName() : path + "/" + file.getName();
            if (file.isDirectory()) {
                deleteRecursive(filePath); // 递归删除子文件夹
            } else {
                if (!ftpClient.deleteFile(filePath)) {
                    log.error("无法删除文件: {}", filePath);
                    return false;
                } else {
                    log.info("已删除目录: {}", path);
                    return true;
                }
            }
        }
        // 最后尝试删除当前文件夹(如果它是空的)
        if (!ftpClient.removeDirectory(path)) {
            log.error("无法删除目录: {}", path);
            return false;
        } else {
            log.info("已删除目录: {}", path);
            return true;
        }
    }


}