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) } }