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