关于java:Java实现SFTP上传下载文件及遇到的问题

25次阅读

共计 7673 个字符,预计需要花费 20 分钟才能阅读完成。

最近工作中用到了 JSch 去操作 SFTP 文件的上传和下载,本文记录一下封装的一个工具类,以及理论遇到的两个问题。

SFTP(Secure File Transfer Protocol,平安文件传送协定)个别指 SSH 文件传输协定(SSH File Transfer Protocol),应用加密传输认证信息和数据,所以绝对于 FTP,SFTP 会十分平安但传输效率要低得多。

JSch(Java Secure Channel)是一个 SSH2 的纯 Java 实现,它容许你连贯到一个 SSH 服务器,并且能够应用端口转发,X11 转发,文件传输等。

SFTP 工具类

pom.xml 文件增加相干包依赖

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

SFTP 工具类,提供文件上传和下载性能

public class SftpClient {public boolean downloadFile(SftpConfig sftpConfig, SftpDownloadRequest request) {
        Session session = null;
        ChannelSftp channelSftp = null;
        try {session = getSession(sftpConfig);
            channelSftp = getChannelSftp(session);

            String remoteFileDir = getRemoteFileDir(request.getRemoteFilePath());
            String remoteFileName = getRemoteFileName(request.getRemoteFilePath());
            // 校验 SFTP 上文件是否存在
            if (!isFileExist(channelSftp, remoteFileDir, remoteFileName, request.getEndFlag())) {return false;}

            // 切换到 SFTP 文件目录
            channelSftp.cd(remoteFileDir);

            // 下载文件
            File localFile = new File(request.getLocalFilePath());
            FileUtils.createDirIfNotExist(localFile);
            FileUtils.deleteQuietly(localFile);
            channelSftp.get(remoteFileName, request.getLocalFilePath());

            return true;
        } catch (JSchException jSchException) {throw new RuntimeException("sftp connect failed:" + JsonUtils.toJson(sftpConfig), jSchException);
        } catch (SftpException sftpException) {throw new RuntimeException("sftp download file failed:" + JsonUtils.toJson(request), sftpException);
        } finally {disconnect(channelSftp, session);
        }
    }

    public void uploadFile(SftpConfig sftpConfig, SftpUploadRequest request) {
        Session session = null;
        ChannelSftp channelSftp = null;
        try {session = getSession(sftpConfig);
            channelSftp = getChannelSftp(session);

            String remoteFileDir = getRemoteFileDir(request.getRemoteFilePath());
            String remoteFileName = getRemoteFileName(request.getRemoteFilePath());

            // 切换到 SFTP 文件目录
            cdOrMkdir(channelSftp, remoteFileDir);

            // 上传文件
            channelSftp.put(request.getLocalFilePath(), remoteFileName);
            if (StringUtils.isNoneBlank(request.getEndFlag())) {channelSftp.put(request.getLocalFilePath() + request.getEndFlag(),
                        remoteFileName + request.getEndFlag());
            }
        } catch (JSchException jSchException) {throw new RuntimeException("sftp connect failed:" + JsonUtils.toJson(sftpConfig), jSchException);
        } catch (SftpException sftpException) {throw new RuntimeException("sftp upload file failed:" + JsonUtils.toJson(request), sftpException);
        } finally {disconnect(channelSftp, session);
        }
    }

    private Session getSession(SftpConfig sftpConfig) throws JSchException {
        Session session;
        JSch jsch = new JSch();
        if (StringUtils.isNoneBlank(sftpConfig.getIdentity())) {jsch.addIdentity(sftpConfig.getIdentity());
        }
        if (sftpConfig.getPort() <= 0) {
            // 默认端口
            session = jsch.getSession(sftpConfig.getUser(), sftpConfig.getHost());
        } else {
            // 指定端口
            session = jsch.getSession(sftpConfig.getUser(), sftpConfig.getHost(), sftpConfig.getPort());
        }
        if (StringUtils.isNoneBlank(sftpConfig.getPassword())) {session.setPassword(sftpConfig.getPassword());
        }
        session.setConfig("StrictHostKeyChecking", "no");
        session.setTimeout(10 * 1000); // 设置超时工夫 10s
        session.connect();

        return session;
    }

    private ChannelSftp getChannelSftp(Session session) throws JSchException {ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
        channelSftp.connect();

        return channelSftp;
    }

    /**
     * SFTP 文件是否存在
     * true:存在;false:不存在
     */
    private boolean isFileExist(ChannelSftp channelSftp,
                                String fileDir,
                                String fileName,
                                String endFlag) throws SftpException {if (StringUtils.isNoneBlank(endFlag)) {if (!isFileExist(channelSftp, fileDir, fileName + endFlag)) {return false;}
        } else {if (!isFileExist(channelSftp, fileDir, fileName)) {return false;}
        }

        return true;
    }

    /**
     * SFTP 文件是否存在
     * true:存在;false:不存在
     */
    private boolean isFileExist(ChannelSftp channelSftp,
                                String fileDir,
                                String fileName) throws SftpException {if (!isDirExist(channelSftp, fileDir)) {return false;}
        Vector vector = channelSftp.ls(fileDir);
        for (int i = 0; i < vector.size(); ++i) {ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) vector.get(i);
            if (fileName.equals(entry.getFilename())) {return true;}
        }
        return false;
    }

    /**
     * sftp 上目录是否存在
     * true:存在;false:不存在
     */
    private boolean isDirExist(ChannelSftp channelSftp, String fileDir) {
        try {SftpATTRS sftpATTRS = channelSftp.lstat(fileDir);
            return sftpATTRS.isDir();} catch (SftpException e) {return false;}
    }

    private void cdOrMkdir(ChannelSftp channelSftp, String fileDir) throws SftpException {if (StringUtils.isBlank(fileDir)) {return;}

        for (String dirName : fileDir.split(File.separator)) {if (StringUtils.isBlank(dirName)) {dirName = File.separator;}
            if (!isDirExist(channelSftp, dirName)) {channelSftp.mkdir(dirName);
            }
            channelSftp.cd(dirName);
        }
    }

    private String getRemoteFileDir(String remoteFilePath) {int remoteFileNameindex = remoteFilePath.lastIndexOf(File.separator);
        return remoteFileNameindex == -1
                ? ""
                : remoteFilePath.substring(0, remoteFileNameindex);
    }


    private String getRemoteFileName(String remoteFilePath) {int remoteFileNameindex = remoteFilePath.lastIndexOf(File.separator);
        if (remoteFileNameindex == -1) {return remoteFilePath;}

        String remoteFileName = remoteFileNameindex == -1
                ? remoteFilePath
                : remoteFilePath.substring(remoteFileNameindex + 1);
        if (StringUtils.isBlank(remoteFileName)) {throw new RuntimeException("remoteFileName is blank");
        }

        return remoteFileName;
    }

    private void disconnect(ChannelSftp channelSftp, Session session) {if (channelSftp != null) {channelSftp.disconnect();
        }
        if (session != null) {session.disconnect();
        }
    }
}

SFTP 连贯配置


public class SftpConfig {
    /**
     * sftp 服务器地址
     */
    private String host;
    /**
     * sftp 服务器端口
     */
    private int port;
    /**
     * sftp 服务器登陆用户名
     */
    private String user;
    /**
     * sftp 服务器登陆密码
     * 明码和私钥二选一
     */
    private String password;
    /**
     * 私钥文件
     * 私钥和明码二选一
     */
    private String identity;
}

文件上传申请

public class SftpUploadRequest {
    /**
     * 本地残缺文件名
     */
    private String localFilePath;
    /**
     * sftp 上残缺文件名
     */
    private String remoteFilePath;
    /**
     * 文件实现标识
     * 非必选
     */
    private String endFlag;
}

文件下载申请

public class SftpDownloadRequest {

    /**
     * sftp 上残缺文件名
     */
    private String remoteFilePath;
    /**
     * 本地残缺文件名
     */
    private String localFilePath;
    /**
     * 文件实现标识
     * 非必选
     */
    private String endFlag;
}

SftpException: Failure

多个工作同时上传文件时,局部工作会上传失败,报错信息如下:

Caused by: com.jcraft.jsch.SftpException: Failure
        at com.jcraft.jsch.ChannelSftp.throwStatusError(ChannelSftp.java:2873) ~[jsch-0.1.55.jar!/:?]
        at com.jcraft.jsch.ChannelSftp.mkdir(ChannelSftp.java:2182) ~[jsch-0.1.55.jar!/:?]

网上搜了下(https://winscp.net/eng/docs/s…),呈现 Failure 谬误有以下几种可能:

  • 重命名文件时存在同名文件;
  • 创立了一个曾经存在的文件夹;
  • 磁盘满了;

从报错信息的第三行能够看出,应该是命中了第二种可能:创立了一个曾经存在的文件夹。

看一下下面 SftpClient 类的 cdOrMkdir 函数的逻辑,当目录存在时,进入到该目录;否则会创立该目录。SFTP 上传文件的门路为:bizType/{yyyyMMdd}/{dataLabel}/biz.txt,不同工作的 dataLabel 值不一样,这里会有并发问题:

  1. A 工作判断 bizType/20210101 目录不存在;
  2. B 工作判断 bizType/20210101 目录不存在;
  3. A 工作创立 bizType/20210101 目录;
  4. B 工作创立 bizType/20210101 目录时,因该目录已被 A 工作创立,所以报错;

解决方案:将 SFTP 上传文件的门路改为 bizType/{dataLabel}/{yyyyMMdd}/biz.txt,使得不同工作的文件门路不再抵触。

JSchException

多个工作同时下载文件时,局部工作会下载失败,报错信息如下:

Caused by: com.jcraft.jsch.JSchException: channel is not opened.
        at com.jcraft.jsch.Channel.sendChannelOpen(Channel.java:765) ~[jsch-0.1.55.jar!/:?]
        at com.jcraft.jsch.Channel.connect(Channel.java:151) ~[jsch-0.1.55.jar!/:?]

一开始狐疑还是并发问题,网上搜了下,可能是零碎 SSH 终端连接数配置过小,该参数在 /etc/ssh/sshd_config 中配置,因权限问题(须要 root 权限)去找 OP 沟通时,OP 感觉不应该是这个起因,于是从新看了下报错处代码:

protected void sendChannelOpen() throws Exception {Session _session = getSession();
        if (!_session.isConnected()) {throw new JSchException("session is down");
        }

        Packet packet = genChannelOpenPacket();
        _session.write(packet);

        int retry = 2000;
        long start = System.currentTimeMillis();
        long timeout = connectTimeout;
        if (timeout != 0L) retry = 1;
        synchronized (this) {while (this.getRecipient() == -1 &&
                    _session.isConnected() &&
                    retry > 0) {if (timeout > 0L) {if ((System.currentTimeMillis() - start) > timeout) {
                        retry = 0;
                        continue;
                    }
                }
                try {
                    long t = timeout == 0L ? 10L : timeout;
                    this.notifyme = 1;
                    wait(t);
                } catch (java.lang.InterruptedException e) { } finally {this.notifyme = 0;}
                retry--;
            }
        }
        if (!_session.isConnected()) {throw new JSchException("session is down");
        }
        if (this.getRecipient() == -1) {  // timeout
            throw new JSchException("channel is not opened.");
        }
        if (this.open_confirmation == false) {  // SSH_MSG_CHANNEL_OPEN_FAILURE
            throw new JSchException("channel is not opened.");
        }
        connected = true;
    }

从第 38~39 行可看出谬误起因是超时了,原来是一开始设置的超时工夫太短:

channelSftp.connect(1000); // 设置超时工夫 1s

解决方案:将超时工夫改大,或者应用默认值。

channelSftp.connect();

正文完
 0