Java で ssh や scp を呼び出す(7) - JSch で sftp 編


間があいてしまったが、id:n_shuyo:20060820:1156054408 の続き。


JSch で sftp を行うサンプルプログラムを書き散らかしただけで終わっていたので、簡単に説明。
以下ではサンプルソースを抜粋して個々に説明している。サンプルソース全体を見たい場合は前回の記事 id:n_shuyo:20060820:1156054408 参照。


JSch では、セッションインスタンスを生成し、コネクトメソッドで認証したあと、行いたい操作の種別に応じてチャネルを取得するという流れになる。

JSch jsch=new JSch();
// connect session
Session session=jsch.getSession(userid, hostname, 22);
session.setPassword(password);
session.connect();

これがセッションを生成してコネクトするまで。
このサンプルではパスワード認証しているが、鍵ペアで認証したい場合は、jsch.addIdentity([key の filepath], [パスフレーズ]) によって鍵を登録しておく。


またここでは省略してしまったが、コネクトしたときに接続の成功失敗、認証の成功・失敗を処理する必要がある。
これらは connect() メソッドが JSchException 例外を投げてくるので、これを捉えて処理する。

// sftp remotely
ChannelSftp channel=(ChannelSftp)session.openChannel("sftp");
channel.connect();

セッションのコネクトに成功したら、sftp 用のチャネルインスタンスを取得する。


OpenChanel() メソッドの引数に応じて、実装の種別に応じた Channel インターフェース*1インスタンスを返してくれるのだが、実装の違いをあまり意識せず、Channel インターフェースとして抽象化して扱うことができる……ということは残念ながら無い。
command execute と sftp と port forwarding といった代表的な ssh 操作だけを取り上げても、それぞれ独自の操作を山盛り必要としており、逆に共通の操作は接続と切断くらいしかない。


上記サンプルでも、チャネルインスタンスを取得すると同時にいきなり ChannelSftp にキャストしちゃっている。*2

// get
channel.get("./index.html", "./index.html.dst");

file get してみたサンプル。ちょうど ftp の get コマンドのように、とてもシンプルに記述できる。
とはいえ、get/put 相当は scp でもできる。
sftp のありがたみが出るのはやはり ls と lstat だろう。

// ls
Vector list = channel.ls(".");
System.out.println("---- ls");
for (int i=0;i<list.size();i++) {
	System.out.println(list.get(i));
}

JSch の sftp でファイルリストを取得するサンプル。


ls() メソッドで指定パスのファイルリストが Vector で取得できるが、この Vector の要素1つ1つは ChannelSftp の内部クラスである LsEntry のインスタンスとなっている。
LsEntry のインスタンスはそのまま表示すると(つまり toString() すると)ロングネーム形式( ls -l の結果のようなフォーマット)で表示されるが、ファイル名だけが欲しいときは下のように書けばよい。

Vector list = channel.ls(".");
for (int i=0;i<list.size();i++) {
	com.jcraft.jsch.ChannelSftp.LsEntry entry = (com.jcraft.jsch.ChannelSftp.LsEntry)list.get(i);
	System.out.println(entry.getFilename());
}

続いて lstat のサンプル。ファイルが存在しなかった場合は SftpException を投げてくるので、catch して処理すればよい。

// lstat
try {
	SftpATTRS stat = channel.lstat("index.html");
	System.out.println("---- lstat");
	System.out.println(stat);
	System.out.println(stat.getSize());
} catch (SftpException ex) { 
	// ファイルが存在しないとき
	ex.printStackTrace();
}

リモートファイルの存在チェックと状態取得ができるというのは、プログラムからファイル転送を行う場合には特に強力に役立つ。


わざわざプログラムにてファイル転送を行うという場合、ファイルをあっちのサーバからこっちのサーバへコピーするだけ、という単純な処理であることは少なく、「古ければ上書き」とか「新しければ何もしない」とか「存在しなければ作成」とか「ファイル数の上限は7。それ以上存在したらローテート」とか、ちょっとはインテリジェンスな制御を行いたい、という「色気」が添えてあったりある。


ところが、通常ファイル転送といえば ftp だが、こいつは「ファイルリストの取得」が満足にできず、さらに「ファイルの存在チェック」の手段が用意されていない。
ftp の ls の返値は ftpd の実装によってフォーマットが微妙に異なるし、「ファイルの存在チェック」はそのもののコマンドが存在しない*3


というわけで sftp と、それを使える JSch は、プログラム(Java)でファイル転送する場合にとても大きなアドバンテージを持っている。のだが。


以前も書いたが、JSch はドキュメントが全くない。
サンプルソースではエラーチェックをさぼって printStackTrace() しているだけだが、じゃあ本番のコードでまじめにエラー処理を行おうとしても、どういう例外が投げられるのか(あるいは投げられないのか)参考にできるドキュメントがないのだ。とても、もったいない。
ソースに JavaDoc のためのコメントすらないこともすでに書いた。が、それでもせめて引数名に意味のある名前が付いていれば良かったのだが……。


鍵認証のやりかたを調べるため、JSch.addIdentity() の仕様を確認しようと、ソースを覗いて、さすがにうなってしまった。

  public void addIdentity(String foo, String bar) throws JSchException{

ちなみに foo は鍵ファイルのパス、bar はパスフレーズだ。


やっぱりどう考えてももったいないので、JavaDoc 用コメントだけでも付加されることを希望……

*1:実際は abstruct class

*2:JSch のサンプルコードでは、OpenChannel の返値は Channel のインスタンスとして扱い、(共通インターフェースである)接続を呼び出した後に やっと ChannelSftp などにキャストしていた。その方が JSch の設計思想からしたら清く正しいんだろうなあ、とは思うけどね。

*3:RENAME で存在チェックの代用とするというあやうい方法もなくはないが、薄氷を踏んで渡るようで、精神衛生に悪い