001/*
002 * Copyright (c) 2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: kulkarni $'
006 * '$Date: 2010-10-05 21:46:36 +0000 (Tue, 05 Oct 2010) $' 
007 * '$Revision: 26020 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.kepler.ssh;
031
032import java.io.File;
033import java.util.Vector;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.kepler.util.FilenameFilter_RegularPattern;
040
041import com.jcraft.jsch.ChannelSftp;
042import com.jcraft.jsch.JSchException;
043import com.jcraft.jsch.SftpException;
044
045/**
046 * Class to handle file transfer based on sftp protocol. It uses the Jsch
047 * package to transfer files from local to remote host or remote host to local
048 * machine. It extends <code>SshExec</code> and uses its method to execute a
049 * sftp command in batch mode to transfer files between two remote machines
050 * <p>
051 * 
052 * @see org.kepler.ssh.SshExec
053 * @author Chandrika Sivaramakrishnan
054 * 
055 *         Based on - JSch examples http://www.jcraft.com/jsch and SshExec code
056 *         written by Norbert Podhorszki
057 * 
058 */
059public class SftpExec extends SshExec {
060
061        // private variables
062        private static final Log log = LogFactory.getLog(SftpExec.class.getName());
063        private static final boolean isDebugging = log.isDebugEnabled();
064
065        /**
066         * Creates session with give user and host name and default port
067         * 
068         * @param user
069         * @param host
070         */
071        public SftpExec(String user, String host) {
072                super(user, host);
073        }
074
075        /**
076         * Creates a session with given user name, host and port
077         * 
078         * @param user
079         * @param host
080         * @param port
081         */
082        public SftpExec(String user, String host, int port) {
083                super(user, host, port);
084        }
085
086        /**
087         * Copies file/directory from a remote machine to local machine using Jsch
088         * sftp channel. Since the sftp command is not directly used any command
089         * line options set in the super class will be ignored.
090         * 
091         * @param rfile
092         *            - remote file that should be copied to local machine
093         * @param localPath
094         *            - path into to which the source file should be copied
095         * @param recursive
096         *            - flag to represent recursive copy of a directory
097         */
098        @Override
099        public int copyFrom(String rfile, File localPath, boolean recursive)
100                        throws SshException {
101                int exitCode = 0;
102
103                if (!openConnection())
104                        throw new SshException(
105                                        "Ssh connection could not be opened for copying.");
106
107                // at this point we have a living, opened session to the remote machine
108                if (isDebugging)
109                        log.debug(" %   Copy " + rfile + " to " + localPath);
110
111                // Validate local path
112                /*if (recursive && (!localPath.isDirectory())) {
113                        throw new SshException("Destination " + localPath
114                                        + " is missing or not a directory");
115                }*/
116
117                try {
118                        ChannelSftp sftpChannel;
119                        synchronized (session) { // for thread safety here we need to sync
120                                sftpChannel = (ChannelSftp) jschSession.openChannel("sftp");
121                                sftpChannel.connect();
122                        }
123
124                        if (isRegularOrLinkFile(rfile)) {
125                                // If source is a regular file
126                                sftpChannel.get(rfile, localPath.getAbsolutePath());
127                        } else if (rfile.contains("*") || rfile.contains("+")){
128                                //If the source file is a wildcard
129                                try{
130                                        if (rfile.contains("\\")) {
131                                                copyWildcardFrom(rfile, localPath, "\\", sftpChannel );
132                                        } else {
133                                                copyWildcardFrom(rfile, localPath, "/", sftpChannel );                                                  
134                                        }
135                                }catch (Exception e){
136                                        sftpChannel.disconnect();
137                                        throw new SshException(e.getMessage());
138                                }
139                        }else {//Source is directory
140                                try {
141                                        sftpChannel.cd(rfile); // to check if remote file is
142                                                                                // directory
143                                } catch (SftpException e) {
144                                        throw new SshException("Source " + rfile
145                                                        + " is missing or not a directory");
146                                }
147                                if (rfile.contains("\\")) {
148                                        copyDirectoryFrom(rfile, localPath, "\\", sftpChannel);
149                                } else {
150                                        copyDirectoryFrom(rfile, localPath, "/", sftpChannel);
151                                }
152                        }
153                        // close channel
154                        sftpChannel.disconnect();
155                } catch (Exception e) {
156                        log.error("Exception doing put: " + e);
157                        throw new SshException("Unable to copy " + rfile + " to "
158                                        + localPath + "\n" + e.getMessage());
159                }
160                return exitCode;
161        }
162/**
163 * Anand: This function takes in remote file name, and local destination dir name.
164 * It scans the remote parent directory (of the wildcard), for all the matching file names.
165 * And the copies all the files from remote directory to local destination dir.
166 * 
167 * @param rfile  
168 *                      - remote wildcard pattern with full path
169 * @param localPath 
170 *                      - Local directory path
171 * @param seperator 
172 *                      - Path seperator to extract remote parent directory from rfile
173 * @param sftpChannel 
174 *                      - SFTP connection channel
175 * @throws Exception 
176 *                      - Exception code to indicate failure in operation
177 */
178        private void copyWildcardFrom(String rfile, File localPath, String seperator, 
179                        ChannelSftp sftpChannel) throws Exception {
180                        
181                Vector<String> list = getWildcardFileListing(rfile, seperator);
182                for (String curFile : list) {
183                        try{
184                                sftpChannel.get(curFile, localPath.getAbsolutePath());
185                        }catch (Exception e)
186                        {
187                                throw new SshException("Error in copying remote file " + curFile + 
188                                                " to local directory " + localPath.getAbsolutePath());
189                        }
190                }
191        }
192
193        /**
194         * Anand: Receives the remote wildcard and returns list of files matching the pattern 
195         * @param remoteFile : Wildcard pattern with path
196         * @param seperator : OS path seperator
197         * @return String array containing list of files matching pattern
198         * @throws Exception
199         */
200        @SuppressWarnings("unchecked")
201        public Vector<String> getWildcardFileListing(String remoteFile, String seperator) throws Exception {
202                //string to store pattern
203                String filePattern = "";
204                //string to store path
205                String filePath = "";
206                //list of files which match pattern
207                Vector<String> fileList = new Vector();
208                ChannelSftp sftpChannel = null;
209                
210                if (!openConnection())
211                        throw new SshException(
212                                        "Ssh connection could not be opened for copying.");
213                
214                try{
215                synchronized (session) { // for thread safety here we need to sync
216                        sftpChannel = (ChannelSftp) jschSession.openChannel("sftp");
217                        sftpChannel.connect();
218                }
219                }catch (Exception e){
220                        e.printStackTrace();
221                        throw new SshException("Failed acquire SFTP Channel \n " + e.getMessage());
222                }
223                
224                while(!sftpChannel.isConnected()){}
225                //check if wildcard pattern is provided
226                if (remoteFile.indexOf("*") > 0 || remoteFile.indexOf("?") > 0) {
227
228                        //list to store list of files from remote directory
229                        Vector<ChannelSftp.LsEntry> list;
230                        
231                        //extract pattern and directory name from rfile
232                        int index = remoteFile.lastIndexOf(seperator);
233                        if (index != -1) {
234                                filePattern = remoteFile.substring(index + 1);
235                                filePath = remoteFile.substring(0, index);
236                        }
237                        
238                        //Get the list of files from directory
239                        try {
240                                list = sftpChannel.ls(filePath.trim());
241                        } catch (SftpException e) {
242                                e.printStackTrace();
243                                throw new SshException("Unable to list files in remote directory "
244                                                + filePath + " \n " + e.getMessage());
245                        }
246                        if (list.size() < 1) {
247                                throw new SshException("Unable to retrieve contents of directory "
248                                                + filePath
249                                                + "\nPlease check the permissions on this directory.");
250                        }
251                
252                        //create the wildcard pattern
253                        String pattern = filePattern.replaceAll("\\.", "\\\\.").replaceAll("\\*",
254                        ".*").replaceAll("\\?", ".");
255                        Pattern p = Pattern.compile(pattern);
256                        for (ChannelSftp.LsEntry curFile : list) {
257                                //match the input pattern(wildcard) to each file in the list
258                                if (curFile.getFilename().equals(".")
259                                                || curFile.getFilename().equals("..") 
260                                                || curFile.getAttrs().isDir()) {
261                                        continue;
262                                }
263                                Matcher m = p.matcher(curFile.getFilename());
264                                if (m.matches()){
265                                        String newfile = filePath + seperator + curFile.getFilename();
266                                        //add newfile to list of files to be copied
267                                        fileList.add(newfile.trim());
268                                }
269                        }
270                }else
271                {
272                        throw new SshException("Cannot find file "
273                                        + remoteFile
274                                        + ".\n");
275                }
276                sftpChannel.disconnect();
277                return fileList;
278        }
279        
280        /**
281         * Copies files/directories from local machine to remote host
282         * 
283         * @param lfile
284         *            - local file that should be copied to remote machine
285         * @param targetPath
286         *            - path on remote host into to which the file should be copied
287         * @param recursive
288         *            - flag to represent recursive copy of a directory
289         * 
290         */
291        @Override
292        public int copyTo(File lfile, String targetPath, boolean recursive)
293                        throws SshException {
294
295                ChannelSftp sftpChannel;
296                File[] files = null;
297                
298                //Connection Setup - Start
299                if (!openConnection())
300                        throw new SshException(
301                                        "Ssh connection could not be opened for copying.");
302                // at this point we have a living, opened session to the remote machine
303
304                if (isDebugging)
305                        log.debug(" %   Copy " + lfile + " to " + targetPath);
306                
307                try {
308                        synchronized (session) { // for thread safety here we need to sync
309                                sftpChannel = (ChannelSftp) jschSession.openChannel("sftp");
310                                sftpChannel.connect();
311                        }
312                } catch (JSchException e) {
313                        e.printStackTrace();
314                        throw new SshException("Unable to create connection to copy " + lfile + " to "
315                                        + targetPath + "\n" + e.getMessage());
316                }
317                //Connection Setup - End
318                
319                if (lfile.isDirectory()) { // if file is a directory
320                        // recursive is not set
321                        if (!recursive)
322                                throw new SshException("File " + lfile + " is a directory. Set recursive copy!");
323                        // Validation of remote dir
324                        try {
325                                        sftpChannel.cd(targetPath); // to check if remote file is
326                                                                                                // directory
327                                } catch (SftpException e) {
328                                        throw new SshException("Destination " + targetPath
329                                                        + " is missing or not a directory");
330                                }
331                                log.debug("Calling dir copy");
332                                try{
333                                        if (targetPath.contains("\\")) {
334                                                // Anand: for windows - file seperator is \\
335                                                copyDirectoryTo(lfile, targetPath, "\\", sftpChannel);
336                                        } else {
337                                                // Anand: for linx file seperator is /
338                                                copyDirectoryTo(lfile, targetPath, "/", sftpChannel);
339                                        }
340                                } catch (Exception e){
341                                        throw new SshException("Error while copying directory" + "\n" + e.getMessage());
342                                }
343                        }
344                 else { //file is a single file or wildcard
345                        // Anand: Get the complete file name
346                        String name = lfile.getPath();
347                        // if file contains a wildcard
348                        if (name.indexOf("*") >=0  || name.indexOf("?") >= 0) {
349System.out.println("**********found wildcard pattern for SFTP copyTo");                         
350                                name = lfile.getName();
351                                // create a pattern for wildcard, and form a filter
352                                String pattern = name.replaceAll("\\.", "\\\\.").replaceAll("\\*",
353                                                ".*").replaceAll("\\?", ".");
354                                FilenameFilter_RegularPattern filter = new FilenameFilter_RegularPattern(
355                                                pattern);
356                                // search for file matching pattern in the directory
357                                String dirname = lfile.getParent();
358                                if (dirname == null || dirname == "")
359                                        dirname = ".";
360                                File dir = new File(dirname);
361                                files = dir.listFiles(filter);
362                                try {
363                                        // Copy files from wildcard using a loop
364                                        for (int i = 0; i < files.length; i++)
365                                                sftpChannel.put(files[i].getAbsolutePath(), targetPath);
366                                } catch (SftpException e) {
367                                        e.printStackTrace();
368                                        throw new SshException(
369                                                        "SFTP Channel failed to copy files to remote machine.");
370                                }
371                }else {// Only a single file is to be copied. 
372                        //Check if the file exists
373                        if (!lfile.exists()) {
374                                throw new SshException("Source file " + lfile
375                                                + " doesn't exists");
376                        }
377                        files = new File[1];
378                        files[0] = lfile;
379                }
380                        //copy the files -  includes wildcard files or single file
381                        log.debug("Calling file copy");
382                        try {
383                                for (int i = 0; i < files.length; i++)
384                                        sftpChannel.put(files[i].getAbsolutePath(), targetPath);
385                                } catch (Exception e) {
386                                        throw new SshException("Unable to copy " + lfile + " to "
387                                                        + targetPath + "\n" + e.getMessage());
388                                }
389                }//else for file or directory ends
390                // close channel
391                sftpChannel.disconnect();
392                return 0;
393        }
394
395        /**
396         * Returns the list of sftp commands (combination of "mkdir" and "put"
397         * commands) that are required to recursively copy a directory.
398         * 
399         * @param srcFile
400         *            - source file to be copied
401         * @param destFile
402         *            - destination directory
403         * @param isConnectionOrigin 
404         * @return command - String representing the list of commands seperated by
405         *         \n character
406         * @throws SshException
407         */
408        public String getRecursiveCopyCmd(String srcFile, String destFile, boolean isConnectionOrigin)
409                        throws SshException {
410                StringBuffer cmd = new StringBuffer(100);
411                String destseperator = "/";
412                String srcseperator = "/";
413                try {
414
415                        if (!openConnection())
416                                throw new SshException(
417                                                "Ssh connection could not be opened for copying.");
418
419                        ChannelSftp sftpChannel;
420                        synchronized (session) { // for thread safety here we need to sync
421                                sftpChannel = (ChannelSftp) jschSession.openChannel("sftp");
422                                sftpChannel.connect();
423                        }
424
425                        try {
426                                log.debug("changing dir to " + srcFile);
427                                sftpChannel.cd(srcFile.trim()); // to check if remote file is directory
428                        } catch (SftpException e) {
429                                throw new SshException("Source " + srcFile
430                                                + " is missing or not a directory");
431                        }
432
433                        if (destFile.indexOf("\\") > -1) {
434                                destseperator = "\\";
435                        }
436                        if (srcFile.indexOf("\\") > -1) {
437                                srcseperator = "\\";
438                        }
439
440                        cmd.append(getDirCopyCmd(srcFile, destFile, srcseperator,
441                                        destseperator, sftpChannel, isConnectionOrigin));
442                        cmd.append("exit");
443
444                        // close channel
445                        sftpChannel.disconnect();
446                } catch (Exception e) {
447                        throw new SshException(e.getMessage());
448                }
449                if (isDebugging) {
450                        log.debug("Returning cmd= " + cmd);
451                }
452                return cmd.toString();
453        }
454
455        /**
456         * Checks if a given file is a regular file.
457         * 
458         * @param srcFile
459         *            - file that is to be checked for its type
460         * @return flag - true if the input is a regular file, false otherwise
461         * @throws SshException
462         * @throws JSchException
463         */
464        @SuppressWarnings("unchecked")
465        public boolean isRegularOrLinkFile(String srcFile) throws SshException,
466                        JSchException {
467                if (!openConnection())
468                        throw new SshException(
469                                        "Ssh connection could not be opened for copying.");
470
471                ChannelSftp sftpChannel;
472                Vector<ChannelSftp.LsEntry> list;
473
474                synchronized (session) { // for thread safety here we need to sync
475                        sftpChannel = (ChannelSftp) jschSession.openChannel("sftp");
476                        sftpChannel.connect();
477                }
478
479                try {
480                        list = sftpChannel.ls(srcFile);
481                        sftpChannel.disconnect();
482                } catch (SftpException e) {
483                        e.printStackTrace();
484                        log.error("Error checking file type- " + e + " :: for file ::" + srcFile);
485                        throw new SshException(
486                                        "Unable to determine file type of source file " + srcFile
487                                                        + "\n" + e);
488                }
489
490                // ls of regular file should have only one entry
491                if (list.size() != 1) {
492                        return false;
493                }
494                for (ChannelSftp.LsEntry curFile : list) {
495                        if (curFile.getLongname().startsWith("-")
496                                        || curFile.getLongname().startsWith("l")) {
497                                return true;
498                        } else {
499                                return false;
500                        }
501                }
502                return false;
503        }
504
505        // /////////////////////////private
506        // methods///////////////////////////////////
507
508        /*
509         * Recursively copy directory to target
510         */
511        private void copyDirectoryTo(File lfile, String targetPath,
512                        String seperator, ChannelSftp sftpChannel) throws Exception {
513
514                if (isDebugging) {
515                        log.debug("Copying directory-" + lfile);
516                }
517                File[] list = lfile.listFiles();
518                if (list == null) {
519                        throw new Exception(
520                                        "Unable to access local directory "
521                                                        + lfile
522                                                        + "\nPlease check if directory exists and has appropriate permissions for the user");
523                }
524                try {
525                        sftpChannel.cd(targetPath);
526                } catch (SftpException e) {
527                        throw new Exception("Unable to access " + targetPath + "\n"
528                                        + e.getMessage());
529                }
530
531                try {
532                        sftpChannel.mkdir(lfile.getName());
533                } catch (SftpException e) {
534                        StringBuffer message = new StringBuffer(100);
535                        message.append("Unable to create directory ").append(
536                                        lfile.getName()).append(" in ").append(targetPath).append(
537                                        "\n");
538                        String exmessage = e.getMessage().trim();
539                        if (exmessage.equals("") || exmessage.equals("Failure")) {
540                                message
541                                                .append("Probable error: another file exists with the same name");
542                        } else {
543                                message.append(exmessage);
544                        }
545                        throw new Exception(message.toString());
546                }
547
548                targetPath = targetPath + seperator + lfile.getName();
549
550                // Start copying files in the directory
551                for (File curFile : list) {
552                        if (curFile.isDirectory()) {
553                                copyDirectoryTo(curFile, targetPath, seperator, sftpChannel);
554                        } else {
555                                sftpChannel.put(curFile.getAbsolutePath(), targetPath);
556                        }
557                }
558
559        }
560
561        // Recursively copy directory from remote host to local
562        private void copyDirectoryFrom(String rfile, File localPath,
563                        String seperator, ChannelSftp sftpChannel) throws Exception {
564
565                if (isDebugging) {
566                        log.debug("Copying remote directory-" + rfile);
567                }
568                Vector<ChannelSftp.LsEntry> list = null;
569                try {
570                        list = sftpChannel.ls(rfile);
571                } catch (SftpException e) {
572                        throw new SshException("Unable to list files in remote directory "
573                                        + rfile + " \n " + e.getMessage());
574                }
575                if (list.size() < 1) {
576                        throw new SshException("Unable to retrieve contents of directory "
577                                        + rfile
578                                        + "\nPlease check the permissions on this directory.");
579                }
580
581                // Create a sub directory in localPath with the same name as remote dir
582                String filename = rfile;
583                int index = rfile.lastIndexOf(seperator);
584                if (index != -1) {
585                        filename = rfile.substring(index + 1);
586                }
587                File subdir = new File(localPath.getAbsolutePath()
588                                + localPath.separator + filename);
589                subdir.mkdir();
590
591                for (ChannelSftp.LsEntry curFile : list) {
592
593                        if (curFile.getFilename().equals(".")
594                                        || curFile.getFilename().equals("..")) {
595                                continue;
596                        }
597                        String newfile = rfile + seperator + curFile.getFilename();
598
599                        if (curFile.getAttrs().isDir()) {
600                                copyDirectoryFrom(newfile, subdir, seperator, sftpChannel);
601                        } else {
602                                sftpChannel.get(newfile, subdir.getAbsolutePath());
603                        }
604                }
605        }
606
607        // Returns the the 'mkdir' command for a given source directory and a list
608        // of
609        // put command for all the files present in the directory
610        // This method is recursively called for every subdir in the source
611        // directory
612        private StringBuffer getDirCopyCmd(String srcFile, String destFile,
613                        String srcseperator, String destseperator, ChannelSftp sftpChannel, boolean isConnectionOrigin)
614                        throws SshException {
615
616                String curDestDir;
617                String curSrcDir;
618                Vector<ChannelSftp.LsEntry> list;
619                StringBuffer cmd = new StringBuffer();
620                try {
621                        list = sftpChannel.ls(srcFile);
622                } catch (SftpException e) {
623                        throw new SshException("Unable to list files in source directory "
624                                        + srcFile + " \n " + e.getMessage());
625                }
626                if (list.size() < 1) {
627                        throw new SshException("Unable to retrieve contents of directory "
628                                        + srcFile
629                                        + "\nPlease check the permissions on this directory.");
630                }
631
632                curSrcDir = srcFile + srcseperator;
633                curDestDir = destFile + destseperator
634                                + srcFile.substring(srcFile.lastIndexOf(srcseperator) + 1);
635                if (isConnectionOrigin)
636                        cmd.append("mkdir ");
637                else
638                        cmd.append("lmkdir ");
639                cmd.append("\\\"");
640                cmd.append(curDestDir);
641                cmd.append("\\\"");
642                cmd.append("\\n");
643
644                for (ChannelSftp.LsEntry curFile : list) {
645
646                        if (curFile.getFilename().equals(".")
647                                        || curFile.getFilename().equals("..")) {
648                                continue;
649                        }
650
651                        if (curFile.getAttrs().isDir()) {
652                                // recursive call with sub directory as the new source directory
653                                cmd.append(getDirCopyCmd(curSrcDir + curFile.getFilename(),
654                                                curDestDir, srcseperator, destseperator, sftpChannel, isConnectionOrigin));
655
656                        } else {
657                                if (isConnectionOrigin)
658                                        cmd.append("put ");
659                                else
660                                        cmd.append("get ");                                     
661                                cmd.append("\\\"");
662                                cmd.append(curSrcDir);
663                                cmd.append(curFile.getFilename());
664                                cmd.append("\\\"");
665                                cmd.append(" ");
666                                cmd.append("\\\"");;
667                                cmd.append(curDestDir);
668                                cmd.append(destseperator);
669                                cmd.append(curFile.getFilename());
670                                cmd.append("\\\"");
671                                cmd.append("\\n");
672                        }
673                }
674                return cmd;
675        }
676
677}