Scalaでsshjを使ってsftpでファイルを読み込んでみる

ファイル監視のプログラム実装でリモートのファイル読み込みを行う必要がありまして、リモート側にagentを入れるのであれば実装が簡単そうだったのですが運用が面倒になると思ったのでsftpにてファイルを監視できるようにしてみました。

といってもsshjを利用するだけなのですが、ググっても参考の情報が少なそうだったのでメモ代わりに残しておきます。

実装はscalaで行いまして、build.sbtには以下を追加しておきます。

libraryDependencies += "com.hierynomus" % "sshj" % "0.30.0"

実装は以下になりました。

import net.schmizz.sshj.SSHClient
import net.schmizz.sshj.sftp.RemoteFile
import net.schmizz.sshj.transport.verification.PromiscuousVerifier

object SftpFile {
  def apply(host: String, port: Int, user: String, password: String, dir: String) = {
    new SftpFile(sshConnectSetting(host, port, user, password), dir)
  }

  private def sshConnectSetting(host: String, port: Int, username: String, password: String) = {
    val ssh = new SSHClient
    ssh.addHostKeyVerifier(new PromiscuousVerifier)
    ssh.loadKnownHosts()
    ssh.connect(host, port)
    // keepAliveの感覚設定
    ssh.getConnection.getKeepAlive.setKeepAliveInterval(30)
    // 接続時のsshキーの指定はauthPublickeyを認証時のパスワードにはauthPasswordを使う
    // ssh.authPublickey(keyPath);
    ssh.authPassword(username, password)
    ssh
  }
}

class SftpFile(sshClient: SSHClient, var currentDir: String) {
  val fileSeparator = "/"
  lazy val sftpClient = sshClient.newSFTPClient()
  var fileNameOpt = Option.empty[String]
  var charCode = "UTF-8"

  var remoteFileOpt: Option[RemoteFile] = Option.empty[RemoteFile]
  var readIndex = 0
  val buf = new Array[Byte](1024)

  def selectFile(fileName: String, charCode: String): Unit = {
    fileNameOpt = Some(fileName)
    this.charCode = charCode
    remoteFileOpt = Some(sftpClient.open(currentDir + fileSeparator + fileName))
    readIndex = 0
  }
  def unSelectFile(): Unit = {
    fileNameOpt = None
    remoteFileOpt = None
    readIndex = 0
  }

  def readLines(length: Int): List[String] = {
    if(length > 0) {
      readOneLine() match {
        case Some(s) => s :: readLines(length - 1)
        case _ => List.empty[String]
      }
    } else {
      List.empty[String]
    }
  }

  def readOneLine(): Option[String] = {
    remoteFileOpt match {
      case Some(remoteFile) =>
        val readSize = remoteFile.read(readIndex, buf, 0, buf.length)
        indexOfKaigyou(buf, readSize) match {
          case (a,b) if a >= 0 =>
            readIndex += a
            Some(new String(buf.slice(0,b), charCode))
          case _ => None
        }
      case _ => None
    }
  }

  private def indexOfKaigyou(buf: Array[Byte], length: Int): (Int, Int) = {
    for (i <- 0 to length) {
      if (buf(i) == 13) {
        if (i + 1 < length && buf(i + 1) == 10) {
          return (i + 2, i)
        } else {
          return (i + 1, i)
        }
      } else if (buf(i) == 10) {
        return (i + 1, i)
      }
    }
    (-1, -1)
  }

  def close(): Unit = {
    remoteFileOpt match {
      case Some(remoteFile) => remoteFile.close()
      case _ => {}
    }
    sftpClient.close()
    sshClient.close()
  }
}

実装内容について、以下でSSHクライアントを返していて

  private def sshConnectSetting(host: String, port: Int, username: String, password: String) = {
    val ssh = new SSHClient
    ssh.addHostKeyVerifier(new PromiscuousVerifier)
    ssh.loadKnownHosts()
    ssh.connect(host, port)
    // keepAliveの感覚設定
    ssh.getConnection.getKeepAlive.setKeepAliveInterval(30)
    // 接続時のsshキーの指定はauthPublickeyを認証時のパスワードにはauthPasswordを使う
    // ssh.authPublickey(keyPath);
    ssh.authPassword(username, password)
    ssh
  }

SSHクライアントからsftpクライアントを生成します

lazy val sftpClient = sshClient.newSFTPClient()

あとdef readLines(length: Int): List[String]で行数指定でファイルを読み込むのですがjava標準にあるようなreadLine関数がないのでremoteFile.read(readIndex, buf, 0, buf.length)でbufferに読み込んだ後改行文字の位置を取得したあとreadIndex += aの部分で読み込み中のインデックスを変更しnew String(buf.slice(0,b), charCode)で一行分をStringに変換して返しています。

このクラスを使うとsftp経由でのtailコマンドも以下のように実装できます。

object SftpFileMain extends App {
  val sftpFile = SftpFile(ホスト ポート, ユーザ名, パスワード,ディレクトリ)
  sftpFile.selectFile(ファイル名, 文字コード)

  while(true) {
    sftpFile.readLines(10).foreach(println)
    Thread.sleep(3000)
  }
}