Java實現SFTP上傳下載文件及遇到的問題


最近用到了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/sftp_codes#code_4 ),出現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();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM