暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

分水岭报 2021-06-18
2624


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


SFTP(Secure File Transfer Protocol,安全文件传送协议)一般指SSH文件传输协议(SSH File Transfer Protocol),使用加密传输认证信息和数据,所以相对于FTP,SFTP会非常安全但传输效率要低得多。


JSch(Java Secure Channel,http://www.jcraft.com/jsch/)是一个SSH2的纯Java实现,它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等。



1. 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;
            }



            2. 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,使得不同任务的文件路径不再冲突。



              3. 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();



                      End



                      文章转载自分水岭报,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论