    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.util;
    017
    018import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
    019import java.io.IOException;
    020import java.util.Map;
    021import java.util.LinkedHashMap ;
    022import java.util.Vector;
    023import java.util.Hashtable;
    024
    025import com.jcraft.jsch.JSch;
    026import com.jcraft.jsch.Session;
    027import com.jcraft.jsch.ChannelSftp;
    028import com.jcraft.jsch.ChannelSftp.LsEntry;
    029import com.jcraft.jsch.SftpATTRS;
    030import com.jcraft.jsch.JSchException;
    031import com.jcraft.jsch.SftpException;
    032
    033import org.opengion.fukurou.system.ThrowUtil;                           // 6.4.2.0 (2016/01/29) package変更 fukurou.util → fukurou.system
    034
    035/**
    036 * SFTPConnect.java は、共通的に使用される SFTP関連の基本機能を実装した、クラスです。
    037 *
    038 * これは、org.apache.commons.net.ftp.FTPClient をベースに開発されています。
    039 * このクラスの実行には、commons-net-ftp-2.0.jar が必要です。
    040 *
    041 * -host=SFTPサーバー -user=ユーザー -passwd=パスワード -remoteFile=SFTP先のファイル名 を必須設定します。
    042 * -localFile=ローカルのファイル名は、必須ではありませんが、-command=DEL の場合にのみ不要であり、
    043 * それ以外の command の場合は、必要です。
    044 *
    045 * -command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] は、SFTPサーバーに対しての処理の方法を指定します。
    046 *   GET:SFTPサーバーからローカルにファイル転送します(初期値)。
    047 *   PUT:ローカルファイルをSFTPサーバーに PUT(STORE、SAVE、UPLOAD、などと同意語)します。
    048 *   DEL:SFTPサーバーの指定のファイルを削除します。この場合のみ、-localFile 属性の指定は不要です。
    049 *   GETDIR,PUTDIR,DELDIR:指定のフォルダ以下のファイルを処理します。
    050 *
    051 * -mkdirs=[true/false] は、受け側のファイル(GET時:LOCAL、PUT時:SFTPサーバー)に取り込むファイルのディレクトリが
    052 * 存在しない場合に、作成するかどうかを指定します(初期値:true)。
    053 * 通常、SFTPサーバーに、フォルダ階層を作成してPUTする場合、動的にフォルダ階層を作成したいケースで便利です。
    054 * 逆に、フォルダは確定しており、指定フォルダ以外に PUT するのはバグっていると事が分かっている場合には
    055 * false に設定して、存在しないフォルダにPUT しようとすると、エラーになるようにします。
    056 *
    057 * 引数文字列中に空白を含む場合は、ダブルコーテーション("") で括って下さい。
    058 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
    059 * 繋げてください。
    060 *
    061 * @og.formSample
    062 *  SFTPConnect -host=SFTPサーバー -user=ユーザー -passwd=パスワード -remoteFile=SFTP先のファイル名 [-localFile=ローカルのファイル名]
    063 *                   [-mode=[ASCII/BINARY]  ] [-command=[GET/PUT/DEL/GETDIR/PUTDIR/DELDIR] ] [-passive=[true/false] ]
    064 *
    065 *    -host=SFTPサーバー                ：接続先のSFTPサーバーのアドレスまたは、サーバー名
    066 *    -user=ユーザー                    ：接続するユーザー名
    067 *    -remoteFile=SFTP先のファイル名    ：接続先のSFTPサーバー側のファイル名。PUT,GET 関係なくSFTP側として指定します。
    068 *   [-passwd=パスワード]               ：接続するユーザーのパスワード
    069 *   [-localFile=ローカルのファイル名]  ：ローカルのファイル名。PUT,GET 関係なくローカルファイルを指定します。
    070 *   [-port=ポート ]                    ：接続するサーバーのポートを指定します。
    071 *   [-keyFile=秘密キーファイル ]       ：公開キー暗号化方式を利用する場合のキーファイル名を指定します。
    072 *   [-command=[GET/PUT/DEL] ]          ：SFTPサーバー側での処理の方法を指定します。
    073 *             [GETDIR/PUTDIR/DELDIR]]          GET:SFTP⇒LOCAL、PUT:LOCAL⇒SFTP への転送です(初期値:GET)
    074 *                                              DEL:SFTPファイルを削除します。
    075 *                                              GETDIR,PUTDIR,DELDIR 指定のフォルダ以下のファイルを処理します。
    076 *   [-mkdirs=[true/false]  ]           ：受け側ファイル(GET時:LOCAL、PUT時:SFTPサーバー)にディレクトリを作成するかどうか(初期値:true)
    077 *                                              (false:ディレクトリが無ければ、エラーにします。)
    078 *   [-timeout=タイムアウト[秒] ]       ：Dataタイムアウト(初期値:600 [秒])
    079 *   [-display=[false/true] ]           ：trueは、検索状況を表示します(初期値:false)
    080 *   [-debug=[false|true]   ]           ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
    081 *
    082 * @og.rev 5.1.6.0 (2010/05/01) 新規追加
    083 *
    084 * @version  5.0
    085 * @author       Kazuhiko Hasegawa
    086 * @since    JDK5.0,
    087 */
    088public final class SFTPConnect extends AbstractConnect {
    089        private final JSch jsch;
    090
    091        private static final int DEF_PORT       = 22;   // ポート
    092
    093        private boolean isConnect       ;                               // コネクト済みかどうか。
    094
    095        private String  lastRemoteDir   = "/";          // SFTP先の最後に登録したフォルダ名(mkdir の高速化のため)
    096        private String  keyFile         ;                               // 公開キー暗号化方式を利用する場合のキーファイル名を指定します。
    097
    098        private Session         session ;
    099        private ChannelSftp channel     ;
    100
    101        /**
    102         * デフォルトコンストラクター
    103         *
    104         * @og.rev 6.4.1.1 (2016/01/16) PMD refactoring. It is a good practice to call super() in a constructor
    105         */
    106        public SFTPConnect() {
    107                super();
    108                jsch = new JSch();
    109        }
    110
    111        /**
    112         * SFTPサーバーへの接続、ログインを行います。
    113         *
    114         * このメソッドは、初期化メソッドです。
    115         * SFTPサーバーへの接続、ログインを行いますので、複数ファイルを転送する
    116         * ケースでは、最初に１度だけ呼び出すだけです。
    117         * 接続先を変更する場合は、もう一度このメソッドをコールする必要があります。
    118         * (そのような場合は、通常、オブジェクトを構築しなおす方がよいと思います。)
    119         *
    120         * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
    121         */
    122        @Override       // AbstractConnect#ConnectIF
    123        public void connect() {
    124                if( isDisplay ) { System.out.println( "CONNECT: HOST=" + host + ",USER=" + user + ",PORT=" + port ); }
    125
    126                // もし、すでに接続されていた場合は、クロース処理を行います。
    127                if( isConnect ) { disconnect(); }
    128
    129                // HostKeyチェックを行わない
    130                final Hashtable<String,String> config = new Hashtable<>();
    131                config.put( "StrictHostKeyChecking", "no" );
    132                JSch.setConfig( config );
    133
    134                // サーバーに対して接続を行います。
    135                try {
    136                        if( keyFile == null ) {
    137                                // パスワード認証
    138                                session=jsch.getSession( user, host, getPort( DEF_PORT ) );
    139                                session.setPassword( passwd );
    140                        }
    141                        else {
    142                                // 公開キー、秘密キー認証
    143                                jsch.addIdentity( keyFile );
    144                                session=jsch.getSession( user, host, getPort( DEF_PORT ) );
    145                //              session.setUserInfo(new MyUserInfo());
    146                        }
    147
    148                        session.connect( timeout*1000 );                // タイムアウトの設定
    149
    150                        channel=(ChannelSftp)session.openChannel("sftp");
    151                        channel.connect();
    152                }
    153                catch( final JSchException ex ) {
    154                        errAppend( "SFTP server refused connection. " );
    155                        errAppend( "   host    = [" , host      , "]" );
    156                        errAppend( "   user    = [" , user      , "]" );
    157                        errAppend( "   port    = [" , port      , "]" );
    158                        errAppend( ex );
    159                        if( isDebug ) { System.err.println( ThrowUtil.ogStackTrace( ex ) ); }                           // 6.4.2.0 (2016/01/29)
    160                        disconnect();
    161                        throw new OgRuntimeException( getErrMsg(),ex );
    162                }
    163
    164                isConnect = true;
    165        }
    166
    167        /**
    168         * SFTPサーバーとの接続をクローズします。
    169         *
    170         * ログインされている場合は、ログアウトも行います。
    171         * コネクトされている場合は、ディスコネクトします。
    172         *
    173         * @og.rev 6.4.2.0 (2016/01/29) ex.printStackTrace() を、ThrowUtil#ogStackTrace(Throwable) に置き換え。
    174         */
    175        @Override       // AbstractConnect#ConnectIF
    176        public void disconnect() {
    177                if( isDisplay ) { System.out.println( "DISCONNECT:" ); }
    178
    179                if( isConnect ) {
    180                        isConnect = false;
    181                        try {
    182                                channel.disconnect();
    183                                session.disconnect();
    184                        }
    185                        catch( final Throwable th ) {
    186                                errAppend( "disconnect Error." );
    187                                errAppend( th );
    188                                if( isDebug ) { System.err.println( ThrowUtil.ogStackTrace( th ) ); }                           // 6.4.2.0 (2016/01/29)
    189                                throw new OgRuntimeException( getErrMsg(),th );
    190                        }
    191                }
    192        }
    193
    194        /**
    195         * command="GET" が指定されたときの処理を行います。
    196         *
    197         * ローカルファイルを、接続先のSFTPサーバー側にアップロードします。
    198         *
    199         * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
    200         *
    201         * @param       localFile       ローカルのファイル名
    202         * @param       remoteFile      SFTP先のファイル名
    203         * @throws  IOException  入出力エラーが発生したとき
    204         */
    205        @Override       // AbstractConnect
    206        protected void actionGET( final String localFile, final String remoteFile ) throws IOException {
    207                if( isDebug ) { System.out.println( "GET: " + remoteFile + " => " + localFile ); }
    208
    209                // GET(DOWNLOAD)取得時は、ローカルファイルのディレクトリを作成する必要がある。
    210                if( isMkdirs ) {
    211                        makeLocalDir( localFile );
    212                }
    213                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    214                        channel.get( remoteFile,localFile );
    215                }
    216                catch( final SftpException ex ) {
    217                        final String errMsg = "チャネル(get)でエラーが発生しました。localFile=[" + localFile + "], remoteFile=[" + remoteFile + "]" ;
    218                        throw new IOException( errMsg,ex );
    219                }
    220        }
    221
    222        /**
    223         * command="GETDIR" が指定されたときの処理を行います。
    224         *
    225         * 接続先のSFTPサーバー側のディレクトリ以下をローカルディレクトリに階層構造のままダウンロードします。
    226         *
    227         * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
    228         *
    229         * @param       localDir        ローカルのディレクトリ名
    230         * @param       remoteDir       SFTP先のディレクトリ名
    231         * @throws  IOException  入出力エラーが発生したとき
    232         */
    233        @Override       // AbstractConnect
    234        protected void actionGETdir( final String localDir, final String remoteDir )  throws IOException {
    235                final Vector<?> list;
    236                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    237                        list = channel.ls( remoteDir );
    238                }
    239                catch( final SftpException ex ) {
    240                        final String errMsg = "チャネル(ls)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
    241                        throw new IOException( errMsg,ex );
    242                }
    243
    244                for( int i=0;i<list.size();i++ ) {
    245                        final LsEntry entry = (LsEntry)list.get(i);
    246                        final String rmt = entry.getFilename();
    247                        if( ".".equals( rmt ) || "..".equals( rmt ) ) { continue; }             // "." で始まるファイルもあるので、equasl 判定
    248                        final SftpATTRS stat = entry.getAttrs();
    249                        if( stat.isDir() ) {
    250                                actionGETdir( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
    251                        }
    252                        else {
    253                                actionGET( addFile( localDir,rmt ),addFile( remoteDir,rmt ) );
    254                        }
    255                }
    256        }
    257
    258        /**
    259         * command="PUT" が指定されたときの処理を行います。
    260         *
    261         * 接続先のSFTPサーバー側のファイル名をローカルにダウンロードします。
    262         *
    263         * @og.rev 6.0.2.5 (2014/10/31) throws で、JSchException,SftpException を返していたのを、IOException に限定します。
    264         *
    265         * @param       localFile       ローカルのファイル名
    266         * @param       remoteFile      SFTP先のファイル名
    267         * @throws IOException 処理中に Sftp エラーが発生した場合
    268         */
    269        @Override       // AbstractConnect
    270        protected void actionPUT( final String localFile, final String remoteFile ) throws IOException {
    271                if( isDebug ) { System.out.println( "PUT: " + localFile + " => " + remoteFile ); }
    272
    273                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    274                        // PUT(UPLOAD)登録時は、リモートファイルのディレクトリを作成する必要がある。
    275                        if( isMkdirs ) {
    276                                // 前回のDIRとの比較で、すでに存在していれば、makeDirectory 処理をパスする。
    277                                final int ad = remoteFile.lastIndexOf( '/' ) + 1;                       // 区切り文字を＋１する。
    278                                final String tmp = remoteFile.substring( 0,ad );
    279
    280                                if( ad > 0 && !lastRemoteDir.startsWith( tmp ) ) {
    281                                        lastRemoteDir = tmp;
    282                                        if( StringUtil.startsChar( remoteFile , '/' ) ) {               // 6.2.0.0 (2015/02/27) １文字 String.startsWith
    283                                                final String[] fls = remoteFile.split( "/" );
    284                                                channel.cd( "/" );
    285                                                for( int i=1; i<fls.length-1; i++ ) {
    286                                                        try {
    287                                //                              SftpATTRS stat = channel.lstat(fls[i]);         // 存在しないと、SftpException
    288                                                                channel.cd( fls[i] );                                           // 存在しないと、SftpException
    289                                                                continue;
    290                                                        } catch( final SftpException ex) {
    291                                                                // ファイルが存在しないとき
    292                                                                channel.mkdir( fls[i] );
    293                                                                channel.cd( fls[i] );
    294                                                        }
    295                                                }
    296                                        }
    297                                }
    298                        }
    299
    300                        channel.put( localFile,remoteFile );
    301                }
    302                catch( final SftpException ex ) {
    303                        final String errMsg = "チャネル(put)でエラーが発生しました。localFile=[" + localFile + "], remoteFile=[" + remoteFile + "]" ;
    304                        throw new IOException( errMsg,ex );
    305                }
    306        }
    307
    308        /**
    309         * command="DEL" が指定されたときの処理を行います。
    310         *
    311         * 接続先のSFTPサーバー側のファイル名を削除します。
    312         *
    313         * @og.rev 6.0.2.5 (2014/10/31) throws で、SftpException を返していたのを、IOException に限定します。
    314         *
    315         * @param       remoteFile      SFTP先のファイル名
    316         * @throws IOException SFTPサーバー側のファイル名の削除に失敗したとき
    317         */
    318        @Override       // AbstractConnect
    319        protected void actionDEL( final String remoteFile ) throws IOException {
    320                if( isDebug ) { System.out.println( "DEL: " + remoteFile ); }
    321
    322                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    323                        channel.rm( remoteFile );
    324                }
    325                catch( final SftpException ex ) {
    326                        final String errMsg = "チャネル(rm)でエラーが発生しました。remoteFile=[" + remoteFile + "]" ;
    327                        throw new IOException( errMsg,ex );
    328                }
    329        }
    330
    331        /**
    332         * command="DELDIR" が指定されたときの処理を行います。
    333         *
    334         * 接続先のSFTPサーバー側のディレクトリ名を削除します。
    335         *
    336         * @og.rev 6.0.2.5 (2014/10/31) throws で、SftpException を返していたのを、IOException に限定します。
    337         *
    338         * @param       remoteDir       SFTP先のディレクトリ名
    339         * @throws IOException SFTPサーバー側のディレクトリ名の削除に失敗したとき
    340         */
    341        @Override       // AbstractConnect
    342        protected void actionDELdir( final String remoteDir ) throws IOException {
    343
    344                final Vector<?> list;
    345                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    346                        list = channel.ls( remoteDir );
    347                }
    348                catch( final SftpException ex ) {
    349                        final String errMsg = "チャネル(ls)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
    350                        throw new IOException( errMsg,ex );
    351                }
    352
    353                for( int i=0;i<list.size();i++ ) {
    354                        final LsEntry entry = (LsEntry)list.get(i);
    355                        final String rmt = entry.getFilename();
    356                        if( ".".equals( rmt ) || "..".equals( rmt ) ) { continue; }             // "." で始まるファイルもあるので、equasl 判定
    357                        final SftpATTRS stat = entry.getAttrs();
    358                        if( stat.isDir() ) {
    359                                actionDELdir( addFile( remoteDir,rmt ) );
    360                        }
    361                        else {
    362                                actionDEL( addFile( remoteDir,rmt ) );
    363                        }
    364                }
    365                try {           // 6.0.2.5 (2014/10/31) IOException に限定
    366                        channel.rmdir( remoteDir );
    367                }
    368                catch( final SftpException ex ) {
    369                        final String errMsg = "チャネル(rmdir)でエラーが発生しました。remoteDir=[" + remoteDir + "]" ;
    370                        throw new IOException( errMsg,ex );
    371                }
    372        }
    373
    374        /**
    375         * 公開キー暗号化方式を利用する場合のキーファイル名を指定します。
    376         *
    377         * @param       keyFile 秘密キーファイル名
    378         */
    379        public void setKeyFile( final String keyFile ) {
    380                if( keyFile != null ) {
    381                        this.keyFile = keyFile ;
    382                }
    383        }
    384
    385        /**
    386         * このクラスの動作確認用の、main メソッドです。
    387         *
    388         * @param       args    コマンド引数配列
    389         */
    390        public static void main( final String[] args ) {
    391
    392                final String[] CMD_LST  = new String[] { "GET","PUT","DEL","GETDIR","PUTDIR","DELDIR" };
    393
    394                final Map<String,String> mustProparty   ;               // ［プロパティ］必須チェック用 Map
    395                final Map<String,String> usableProparty ;               // ［プロパティ］整合性チェック Map
    396
    397                mustProparty = new LinkedHashMap<>();
    398                mustProparty.put( "host",               "接続先のSFTPサーバーのアドレスまたは、サーバー名(必須)" );
    399                mustProparty.put( "user",               "接続するユーザー名(必須)" );
    400                mustProparty.put( "remoteFile", "接続先のSFTPサーバー側のファイル名(必須)" );
    401
    402                usableProparty = new LinkedHashMap<>();
    403                usableProparty.put( "passwd",           "接続するユーザーのパスワード" );
    404                usableProparty.put( "localFile",        "ローカルのファイル名" );
    405                usableProparty.put( "port",                     "接続に利用するポート番号を設定します。" );
    406                usableProparty.put( "keyFile",          "公開キー暗号化方式を利用する場合のキーファイル名を指定します。" );
    407                usableProparty.put( "command",          "SFTPサーバー側での処理の方法(GET/PUT/DEL)を指定します(初期値:GET)" );
    408                usableProparty.put( "mkdirs",           "受け側ファイル(GET時:LOCAL、PUT時:SFTPサーバー)にディレクトリを作成するかどうか(初期値:true)" );
    409                usableProparty.put( "timeout",          "Dataタイムアウト(初期値:600 [秒])" );
    410                usableProparty.put( "display",          "[false/true]:trueは、検索状況を表示します(初期値:false)" );
    411                usableProparty.put( "debug",            "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
    412                                                                                        CR + "(初期値:false:表示しない)" );
    413
    414                // ******************************************************************************************************* //
    415                //       以下、単独で使用する場合の main処理
    416                // ******************************************************************************************************* //
    417                final Argument arg = new Argument( "org.opengion.fukurou.util.SFTPConnect" );
    418                arg.setMustProparty( mustProparty );
    419                arg.setUsableProparty( usableProparty );
    420                arg.setArgument( args );
    421
    422                final SFTPConnect sftp = new SFTPConnect();
    423
    424                final String host   = arg.getProparty( "host");                 // SFTPサーバー
    425                final String user   = arg.getProparty( "user" );                        // ユーザー
    426                final String passwd = arg.getProparty( "passwd" );              // パスワード
    427
    428                sftp.setHostUserPass( host , user , passwd );
    429
    430                sftp.setPort(           arg.getProparty( "port"                                 ) );    // 接続に利用するポート番号を設定します。
    431                sftp.setKeyFile(        arg.getProparty( "keyFile"                              ) );    // 公開キー暗号化方式を利用する場合のキーファイル名を指定します。
    432                sftp.setMkdirs(         arg.getProparty( "mkdirs"       ,true           ) );    // 受け側ファイルにディレクトリを作成するかどうか
    433                sftp.setTimeout(        arg.getProparty( "timeout"      ,TIMEOUT        ) );    // Dataタイムアウト(初期値:600 [秒])
    434                sftp.setDisplay(        arg.getProparty( "display"      ,false          ) );    // trueは、検索状況を表示します(初期値:false)
    435                sftp.setDebug(          arg.getProparty( "debug"        ,false          ) );    // デバッグ情報を標準出力に表示する(true)かしない(false)か
    436
    437                try {
    438                        // コネクトします。
    439                        sftp.connect();
    440
    441                        final String command            = arg.getProparty( "command" ,"GET" ,CMD_LST  );        // SFTP処理の方法を指定します。
    442                        final String localFile  = arg.getProparty( "localFile"  );                                      // ローカルのファイル名
    443                        final String remoteFile = arg.getProparty( "remoteFile" );                                      // SFTP先のファイル名
    444
    445                        // command , localFile , remoteFile を元に、SFTP処理を行います。
    446                        sftp.action( command,localFile,remoteFile );
    447                }
    448                catch( final RuntimeException ex ) {
    449                        System.err.println( sftp.getErrMsg() );
    450                }
    451                finally {
    452                        // ホストとの接続を終了します。
    453                        sftp.disconnect();
    454                }
    455        }
    456}