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         * ケースでは、最初に1度だけ呼び出すだけです。
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;                       // 区切り文字を+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) 1文字 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}