Java+JSchでsftpによるファイル操作を行う


Javaアプリからsftpでファイルアップロードする、という用事がありまして、
[JSch](http://www.jcraft.com/jsch/)を使用したのでそのメモ書きです。

## JSchの導入

MavenのCentral Repositoryに登録されているので、dependencyの追加で導入できます。
2015/11/26現在の最新バージョンは「0.1.53」でした。

```xml:pom.xml
...中略
    <dependencies>
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.53</version>
        </dependency>
    </dependencies>
...中略
```

## 基本的な流れ
パスフレーズ無しの鍵認証で接続する場合、ざっくりこんな感じ。
※ ファイルをアップロードする例

```Java:ファイルアップロードサンプル
package jp.gr.java_conf.nenokido2000;

import java.util.Arrays;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;

public class SftpSample {

    /** Channel接続タイプ */
    private static final String CHANNEL_TYPE = "sftp";

    /**
     * ファイルアップロード
     *
     * @param hostname
     *            接続先ホスト
     * @param port
     *            接続先ポート
     * @param userId
     *            接続するユーザ
     * @param identityKeyFileName
     *            鍵ファイル名
     * @param sourcePath
     *            アップロード対象ファイルのパス<br>
     *            アプリ実行環境上の絶対パスを指定
     * @param destPath
     *            アップ先のパス
     * @throws JSchException
     *             Session・Channelの設定/接続エラー時に発生
     * @throws SftpException
     *             sftp操作失敗時に発生
     */
    public void putFile(final String hostname, final int port,
            final String userId, final String identityKeyFileName,
            final String sourcePath, final String destPath)
            throws JSchException, SftpException {

        Session session = null;
        ChannelSftp channel = null;

        try {
            session = connectSession(hostname, port, userId, identityKeyFileName);
            channel = connectChannelSftp(session);
            channel.put(sourcePath, destPath);
        } finally {
            disconnect(session, channel);
        }
    }

    /**
     * Sessionを開始
     *
     * @param hostname
     *            接続先ホスト
     * @param port
     *            接続先ポート
     * @param userId
     *            接続するユーザ
     * @param identityKeyFileName
     *            鍵ファイル名
     */
    private Session connectSession(final String hostname, final int port,
            final String userId, final String identityKeyFileName)
            throws JSchException {

        final JSch jsch = new JSch();
        // 鍵追加
        jsch.addIdentity(Thread.currentThread().getContextClassLoader()
                .getResource(identityKeyFileName).getFile());
        // Session設定
        final Session session = jsch.getSession(userId, hostname, port);
        final UserInfo userInfo = new SftpUserInfo();

        // TODO 今回は使用しないがパスフレーズ等が必要な場合はUserInfoインスタンス経由で設定する

        session.setUserInfo(userInfo);

        session.connect();

        return session;
    }

    /**
     * SFTPのChannelを開始
     *
     * @param session
     *            開始されたSession情報
     */
    private ChannelSftp connectChannelSftp(final Session session)
            throws JSchException {
        final ChannelSftp channel = (ChannelSftp) session
                .openChannel(CHANNEL_TYPE);
        channel.connect();

        return channel;
    }

    /**
     * Session・Channelの終了
     *
     * @param session
     *            開始されたSession情報
     * @param channels
     *            開始されたChannel情報.複数指定可能
     */
    private void disconnect(final Session session, final Channel... channels) {
        if (channels != null) {
            Arrays.stream(channels).forEach(c -> {
                if (c != null) {
                    c.disconnect();
                }
            });
        }
        if (session != null) {
            session.disconnect();
        }
    }

    /**
     * SFTPに接続するユーザ情報を保持するクラス
     */
    private static class SftpUserInfo implements UserInfo {

        @Override
        public String getPassword() {
            return null;
        }

        @Override
        public boolean promptPassword(String arg0) {
            return true;
        }

        @Override
        public boolean promptPassphrase(String arg0) {
            return true;
        }

        @Override
        public boolean promptYesNo(String arg0) {
            return true;
        }

        @Override
        public void showMessage(String arg0) {
        }

        @Override
        public String getPassphrase() {
            return null;
        }
    }

    public static void main(String[] args) throws JSchException, SftpException {

        final SftpSample sftp = new SftpSample();
        sftp.putFile("hostName", 22, "user", "xxxxx_rsa",
                "/Users/xxxxx/test.jpg", "/tmp/test.jpg");
    }
}
```

1. JSchのインスタンス生成
2. Sessionの設定と接続(ssh接続確立）
3. 利用する操作(sftpとか）に応じてChannel取得
4. 実際の操作実行(上記だとChannelSftp#put)
5. Channelを閉じる(disconnect)
6. Sessionを閉じる(disconnect)

という流れ。
UserInfoを実装したクラスでユーザ情報を引き渡しますが、今回は特に使用してません。
※ 必要に応じて各メソッドの内容を実装する必要あり。

ファイル取得したい場合は「ChannelSftp#get」
ファイル削除したい場合は「ChannelSftp#rm」
ファイルリネームしたい場合は「ChannelSftp#rename」

と、sftp操作に対応した各種IFが用意されているので、
http://epaul.github.io/jsch-documentation/simple.javadoc/com/jcraft/jsch/ChannelSftp.html

引数や戻り値の型も扱いやすいものを使って操作すれば、大概は事足りそうです。

### ちょっとしたTips

#### ファイルの存在確認

```Java:ファイル存在確認
import com.jcraft.jsch.SftpException;
public class SftpSample {
...中略
    public boolean isExist(final ChannelSftp channel,
            final String targetFilePath) {
        try {
            channel.lstat(targetFilePath);
        } catch (SftpException e) {
            return false;
        }
        return true;
    }

```

ChannelSftp#lstat(String)を呼び出すと、引数に指定されたパスに該当するファイルが存在しない場合には例外(SftpException)を投げてくるので、これを捕捉します。
指定したput先に既にファイルがいたら処理中断、といった判定に使えます。

#### cp的なファイルコピー
put、get、renameとあるので、所謂「cp」コマンド相当のことも
できるような錯覚を覚えます(実際StackOverFlowに質問挙がってたりする）が、
あくまでftpに対応した処理ができるだけなので、「cp」はできません。

sftpを使うのであれば、一度ローカルにgetした後に別名でputするということが
必要になります。

```java:get/putのパターン
    /**
     * ファイルコピー get/put版
     * 
     * @param hostname
     *            接続先ホスト
     * @param port
     *            接続先ポート
     * @param userId
     *            接続するユーザ
     * @param identityKeyFileName
     *            鍵ファイル名
     * @param sourcePath
     *            コピー元ファイルのパス
     * @param destPath
     *            コピー先のパス
     * @throws JSchException
     *             Session・Channelの設定/接続エラー時に発生
     * @throws SftpException
     *             sftp操作失敗時に発生
     */
    public void cp(final String hostname, final int port, final String userId,
            final String identityKeyFileName, final String sourcePath,
            final String destPath) throws JSchException, SftpException {

        Session session = null;
        ChannelSftp getChannel = null;
        ChannelSftp putChannel = null;
        InputStream source = null;

        try {
            session = connectSession(hostname, port, userId,
                    identityKeyFileName);
            getChannel = connectChannelSftp(session);
            putChannel = connectChannelSftp(session);
            source = getChannel.get(sourcePath);
            putChannel.put(source, destPath);
        } finally {
            IOUtils.closeQuietly(source);
            disconnect(session, getChannel, putChannel);
        }
    }

```

※ get/putを同じChannelインスタンスで実行すると以下のような例外が発生するため、
別Channelを定義して実行しています。

```
4: java.io.IOException: error: 4: RequestQueue: unknown request id 6
```

これでそれっぽいことはできますが、ファイルコピーしたいだけなのに冗長なデータ転送が発生してしまいます。

JSchには、コマンドを発行できるChannel(ChannelExec)も用意されているので、
環境的に許されるのであればこちらを使用したほうが簡潔に処理ができるかと思います。

```java:コマンド発行のパターン
...中略
import com.jcraft.jsch.ChannelExec;
public class SftpSample {
...中略
    /** cpコマンドのテンプレート文字列 */
    private static final String CP_COMMAND_TEMPLATE = "cp -p %s %s";

    /** ssh実行の接続タイプ */
    private static final String CHANNEL_TYPE_EXEC = "exec";
...中略
    /**
     * ファイルコピー コマンド発行版
     * 
     * @param hostname
     *            接続先ホスト
     * @param port
     *            接続先ポート
     * @param userId
     *            接続するユーザ
     * @param identityKeyFileName
     *            鍵ファイル名
     * @param sourcePath
     *            コピー元ファイルのパス
     * @param destPath
     *            コピー先のパス
     * @throws JSchException
     *             Session・Channelの設定/接続エラー時に発生
     * @throws SftpException
     *             sftp操作失敗時に発生
     */
    public void cp(final String hostname, final int port, final String userId,
            final String identityKeyFileName, final String sourcePath,
            final String destPath) throws JSchException, SftpException {

        Session session = null;
        ChannelExec channel = null;

        try {
            session = connectSession(hostname, port, userId,
                    identityKeyFileName);
            channel = (ChannelExec) session.openChannel(CHANNEL_TYPE_EXEC);
            channel.setCommand(String.format(CP_COMMAND_TEMPLATE, sourcePath,
                    destPath));
            channel.connect();
        } finally {
            disconnect(session, channel);
        }
    }

```

## 使用したライブラリ
* JSch：0.1.53
* Java：8u65


## 参考ページ
以下を参考にさせていただきました。
ありがとうございました。  
http://d.hatena.ne.jp/n_shuyo/20060908/1157655411
http://www.atmarkit.co.jp/ait/articles/1505/20/news004.html