View Javadoc

1   /*
2    * $Id: SftpClient.java 22060 2011-06-01 08:51:27Z dirk.olmes $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.transport.sftp;
12  
13  import org.mule.api.endpoint.ImmutableEndpoint;
14  import org.mule.transport.sftp.notification.SftpNotifier;
15  
16  import com.jcraft.jsch.Channel;
17  import com.jcraft.jsch.ChannelSftp;
18  import com.jcraft.jsch.ChannelSftp.LsEntry;
19  import com.jcraft.jsch.JSch;
20  import com.jcraft.jsch.JSchException;
21  import com.jcraft.jsch.Session;
22  import com.jcraft.jsch.SftpATTRS;
23  import com.jcraft.jsch.SftpException;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Properties;
31  import java.util.Vector;
32  
33  import org.apache.commons.lang.NotImplementedException;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  
37  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_DELETE_ACTION;
38  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_GET_ACTION;
39  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_PUT_ACTION;
40  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_RENAME_ACTION;
41  
42  /**
43   * <code>SftpClient</code> Wrapper around jsch sftp library. Provides access to basic
44   * sftp commands.
45   */
46  
47  public class SftpClient
48  {
49      private Log logger = LogFactory.getLog(getClass());
50  
51      public static final String CHANNEL_SFTP = "sftp";
52  
53      public static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
54  
55      private ChannelSftp channelSftp;
56  
57      private JSch jsch;
58      private SftpNotifier notifier;
59  
60      private Session session;
61  
62      private final String host;
63  
64      private int port = 22;
65  
66      private String home;
67  
68      // Keep track of the current working directory for improved logging.
69      private String currentDirectory = "";
70  
71      private static final Object lock = new Object();
72  
73      public SftpClient(String host)
74      {
75          this(host, null);
76      }
77  
78      public SftpClient(String host, SftpNotifier notifier)
79      {
80          this.host = host;
81          this.notifier = notifier;
82  
83          jsch = new JSch();
84      }
85  
86      public void changeWorkingDirectory(String wd) throws IOException
87      {
88          currentDirectory = wd;
89  
90          try
91          {
92              wd = getAbsolutePath(wd);
93              if (logger.isDebugEnabled())
94              {
95                  logger.debug("Attempting to cwd to: " + wd);
96              }
97              channelSftp.cd(wd);
98          }
99          catch (SftpException e)
100         {
101             String message = "Error '" + e.getMessage() + "' occurred when trying to CDW to '" + wd + "'.";
102             logger.error(message);
103             throw new IOException(message);
104         }
105     }
106 
107     /**
108      * Converts a relative path to an absolute path according to
109      * http://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04.
110      *
111      * @param path relative path
112      * @return Absolute path
113      */
114     public String getAbsolutePath(String path)
115     {
116         if (path.startsWith("/~"))
117         {
118             return home + path.substring(2, path.length());
119         }
120 
121         // Already absolute!
122         return path;
123     }
124 
125     public void login(String user, String password) throws IOException
126     {
127         try
128         {
129             Properties hash = new Properties();
130             hash.put(STRICT_HOST_KEY_CHECKING, "no");
131 
132             session = jsch.getSession(user, host);
133             session.setConfig(hash);
134             session.setPort(port);
135             session.setPassword(password);
136             session.connect();
137 
138             Channel channel = session.openChannel(CHANNEL_SFTP);
139             channel.connect();
140 
141             channelSftp = (ChannelSftp) channel;
142             setHome(channelSftp.pwd());
143         }
144         catch (JSchException e)
145         {
146             logAndThrowLoginError(user, e);
147         }
148         catch (SftpException e)
149         {
150             logAndThrowLoginError(user, e);
151         }
152     }
153 
154     public void login(String user, String identityFile, String passphrase) throws IOException
155     {
156         // Lets first check that the identityFile exist
157         if (!new File(identityFile).exists())
158         {
159             throw new IOException("IdentityFile '" + identityFile + "' not found");
160         }
161 
162         try
163         {
164             if (passphrase == null || "".equals(passphrase))
165             {
166                 jsch.addIdentity(new File(identityFile).getAbsolutePath());
167             }
168             else
169             {
170                 jsch.addIdentity(new File(identityFile).getAbsolutePath(), passphrase);
171             }
172 
173             Properties hash = new Properties();
174             hash.put(STRICT_HOST_KEY_CHECKING, "no");
175 
176             session = jsch.getSession(user, host);
177             session.setConfig(hash);
178             session.setPort(port);
179             session.connect();
180 
181             Channel channel = session.openChannel(CHANNEL_SFTP);
182             channel.connect();
183 
184             channelSftp = (ChannelSftp) channel;
185             setHome(channelSftp.pwd());
186         }
187         catch (JSchException e)
188         {
189             logAndThrowLoginError(user, e);
190         }
191         catch (SftpException e)
192         {
193             logAndThrowLoginError(user, e);
194         }
195     }
196 
197     private void logAndThrowLoginError(String user, Exception e) throws IOException
198     {
199         logger.error("Error during login to " + user + "@" + host, e);
200         throw new IOException("Error during login to " + user + "@" + host + ": " + e.getMessage());
201     }
202 
203     public void setPort(int port)
204     {
205         this.port = port;
206     }
207 
208     public void rename(String filename, String dest) throws IOException
209     {
210         // Notify sftp rename file action
211         if (notifier != null)
212         {
213             notifier.notify(SFTP_RENAME_ACTION, "from: " + currentDirectory + "/" + filename + " - to: "
214                                                 + dest);
215         }
216 
217         String absolutePath = getAbsolutePath(dest);
218         try
219         {
220             if (logger.isDebugEnabled())
221             {
222                 logger.debug("Will try to rename " + currentDirectory + "/" + filename + " to "
223                              + absolutePath);
224             }
225             channelSftp.rename(filename, absolutePath);
226         }
227         catch (SftpException e)
228         {
229             throw new IOException(e.getMessage());
230             // throw new IOException("Error occured when renaming " +
231             // currentDirectory + "/" + filename + " to " + absolutePath +
232             // ". Error Message=" + e.getMessage());
233         }
234     }
235 
236     public void deleteFile(String fileName) throws IOException
237     {
238         // Notify sftp delete file action
239         if (notifier != null)
240         {
241             notifier.notify(SFTP_DELETE_ACTION, currentDirectory + "/" + fileName);
242         }
243 
244         try
245         {
246             if (logger.isDebugEnabled())
247             {
248                 logger.debug("Will try to delete " + fileName);
249             }
250             channelSftp.rm(fileName);
251         }
252         catch (SftpException e)
253         {
254             throw new IOException(e.getMessage());
255         }
256     }
257 
258     public void disconnect()
259     {
260         if (channelSftp != null)
261         {
262             channelSftp.disconnect();
263         }
264         if ((session != null) && session.isConnected())
265         {
266             session.disconnect();
267         }
268     }
269 
270     public boolean isConnected()
271     {
272         return (channelSftp != null) && channelSftp.isConnected() && !channelSftp.isClosed()
273                && (session != null) && session.isConnected();
274     }
275 
276     public String[] listFiles() throws IOException
277     {
278         return listFiles(".");
279     }
280 
281     public String[] listFiles(String path) throws IOException
282     {
283         return listDirectory(path, true, false);
284     }
285 
286     public String[] listDirectories() throws IOException
287     {
288         return listDirectory(".", false, true);
289     }
290 
291     public String[] listDirectories(String path) throws IOException
292     {
293         return listDirectory(path, false, true);
294     }
295 
296     private String[] listDirectory(String path, boolean includeFiles, boolean includeDirectories)
297         throws IOException
298     {
299         try
300         {
301             Vector vv = channelSftp.ls(path);
302             if (vv != null)
303             {
304                 List<String> ret = new ArrayList<String>();
305                 for (int i = 0; i < vv.size(); i++)
306                 {
307                     Object obj = vv.elementAt(i);
308                     if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry)
309                     {
310                         LsEntry entry = (LsEntry) obj;
311                         if (includeFiles && !entry.getAttrs().isDir())
312                         {
313                             ret.add(entry.getFilename());
314                         }
315                         if (includeDirectories && entry.getAttrs().isDir())
316                         {
317                             if (!entry.getFilename().equals(".") && !entry.getFilename().equals(".."))
318                             {
319                                 ret.add(entry.getFilename());
320                             }
321                         }
322                     }
323                 }
324                 return ret.toArray(new String[ret.size()]);
325             }
326         }
327         catch (SftpException e)
328         {
329             throw new IOException(e.getMessage());
330         }
331         return null;
332     }
333 
334     // public boolean logout()
335     // {
336     // return true;
337     // }
338 
339     public InputStream retrieveFile(String fileName) throws IOException
340     {
341         // Notify sftp get file action
342         long size = getSize(fileName);
343         if (notifier != null)
344         {
345             notifier.notify(SFTP_GET_ACTION, currentDirectory + "/" + fileName, size);
346         }
347 
348         try
349         {
350             return channelSftp.get(fileName);
351         }
352         catch (SftpException e)
353         {
354             throw new IOException(e.getMessage() + ".  Filename is " + fileName);
355         }
356     }
357 
358     // public OutputStream storeFileStream(String fileName) throws IOException
359     // {
360     // try
361     // {
362     // return channelSftp.put(fileName);
363     // } catch (SftpException e)
364     // {
365     // throw new IOException(e.getMessage());
366     // }
367     // }
368 
369     public void storeFile(String fileName, InputStream stream) throws IOException
370     {
371         try
372         {
373 
374             // Notify sftp put file action
375             if (notifier != null)
376             {
377                 notifier.notify(SFTP_PUT_ACTION, currentDirectory + "/" + fileName);
378             }
379 
380             if (logger.isDebugEnabled())
381             {
382                 logger.debug("Sending to SFTP service: Stream = " + stream + " , filename = " + fileName);
383             }
384 
385             channelSftp.put(stream, fileName);
386         }
387         catch (SftpException e)
388         {
389             logger.error("Error writing data over SFTP service, error was: " + e.getMessage(), e);
390             throw new IOException(e.getMessage());
391         }
392     }
393 
394     public void storeFile(String fileNameLocal, String fileNameRemote) throws IOException
395     {
396         try
397         {
398             channelSftp.put(fileNameLocal, fileNameRemote);
399         }
400         catch (SftpException e)
401         {
402             throw new IOException(e.getMessage());
403         }
404     }
405 
406     public long getSize(String filename) throws IOException
407     {
408         try
409         {
410             return channelSftp.stat(filename).getSize();
411         }
412         catch (SftpException e)
413         {
414             throw new IOException(e.getMessage() + " (" + currentDirectory + "/" + filename + ")");
415         }
416     }
417 
418     /**
419      * @param filename File name
420      * @return Number of seconds since the file was written to
421      * @throws IOException If an error occurs
422      */
423     public long getLastModifiedTime(String filename) throws IOException
424     {
425         try
426         {
427             SftpATTRS attrs = channelSftp.stat("./" + filename);
428             return attrs.getMTime() * 1000L;
429         }
430         catch (SftpException e)
431         {
432             throw new IOException(e.getMessage());
433         }
434     }
435 
436     /**
437      * Creates a directory
438      *
439      * @param directoryName The directory name
440      * @throws IOException If an error occurs
441      */
442     public void mkdir(String directoryName) throws IOException
443     {
444         try
445         {
446             if (logger.isDebugEnabled())
447             {
448                 logger.debug("Will try to create directory " + directoryName);
449             }
450             channelSftp.mkdir(directoryName);
451         }
452         catch (SftpException e)
453         {
454             // Don't throw e.getmessage since we only get "2: No such file"..
455             throw new IOException("Could not create the directory '" + directoryName + "', caused by: "
456                                   + e.getMessage());
457             // throw new IOException("Could not create the directory '" +
458             // directoryName + "' in '" + currentDirectory + "', caused by: " +
459             // e.getMessage());
460         }
461     }
462 
463     public void deleteDirectory(String path) throws IOException
464     {
465         path = getAbsolutePath(path);
466         try
467         {
468             if (logger.isDebugEnabled())
469             {
470                 logger.debug("Will try to delete directory " + path);
471             }
472             channelSftp.rmdir(path);
473         }
474         catch (SftpException e)
475         {
476             throw new IOException(e.getMessage());
477         }
478     }
479 
480     /**
481      * Setter for 'home'
482      *
483      * @param home The path to home
484      */
485     void setHome(String home)
486     {
487         this.home = home;
488     }
489 
490     /**
491      * @return the ChannelSftp - useful for some tests
492      */
493     public ChannelSftp getChannelSftp()
494     {
495         return channelSftp;
496     }
497 
498     /**
499      * Creates the directory if it not already exists. TODO: check if the SftpUtil &
500      * SftpClient methods can be merged Note, this method is synchronized because it
501      * in rare cases can be called from two threads at the same time and thus cause
502      * an error.
503      *
504      * @param endpoint
505      * @param newDir
506      * @throws IOException
507      */
508     public void createSftpDirIfNotExists(ImmutableEndpoint endpoint, String newDir) throws IOException
509     {
510         String newDirAbs = endpoint.getEndpointURI().getPath() + "/" + newDir;
511 
512         String currDir = currentDirectory;
513 
514         if (logger.isDebugEnabled())
515         {
516             logger.debug("CHANGE DIR FROM " + currentDirectory + " TO " + newDirAbs);
517         }
518 
519         // We need to have a synchronized block if two++ threads tries to
520         // create the same directory at the same time
521         synchronized (lock)
522         {
523             // Try to change directory to the new dir, if it fails - create it
524             try
525             {
526                 // This method will throw an exception if the directory does not
527                 // exist.
528                 changeWorkingDirectory(newDirAbs);
529             }
530             catch (IOException e)
531             {
532                 logger.info("Got an exception when trying to change the working directory to the new dir. "
533                             + "Will try to create the directory " + newDirAbs);
534                 changeWorkingDirectory(endpoint.getEndpointURI().getPath());
535                 mkdir(newDir);
536 
537                 // Now it should exist!
538                 changeWorkingDirectory(newDirAbs);
539             }
540             finally
541             {
542                 changeWorkingDirectory(currDir);
543                 if (logger.isDebugEnabled())
544                 {
545                     logger.debug("DIR IS NOW BACK TO " + currentDirectory);
546                 }
547             }
548         }
549     }
550 
551     public String duplicateHandling(String destDir, String filename, String duplicateHandling)
552         throws IOException
553     {
554         if (duplicateHandling.equals(SftpConnector.PROPERTY_DUPLICATE_HANDLING_ASS_SEQ_NO))
555         {
556             filename = createUniqueName(destDir, filename);
557 
558         }
559         else if (duplicateHandling.equals(SftpConnector.PROPERTY_DUPLICATE_HANDLING_OVERWRITE))
560         {
561             // TODO. ML FIX. Implement this!
562             throw new NotImplementedException("Strategy "
563                                               + SftpConnector.PROPERTY_DUPLICATE_HANDLING_OVERWRITE
564                                               + " is not yet implemented");
565 
566         }
567         else
568         {
569             // Nothing to do in the case of
570             // PROPERTY_DUPLICATE_HANDLING_THROW_EXCEPTION, if the file already
571             // exists then an error will be throwed...
572         }
573 
574         return filename;
575     }
576 
577     private String createUniqueName(String dir, String path) throws IOException
578     {
579         int fileIdx = 1;
580 
581         String filename;
582         String fileType;
583         int fileTypeIdx = path.lastIndexOf('.');
584         if (fileTypeIdx == -1)
585         {
586             // No file type/extension found
587             filename = path;
588             fileType = "";
589         }
590         else
591         {
592             fileType = path.substring(fileTypeIdx); // Let the fileType include the
593             // leading '.'
594             filename = path.substring(0, fileTypeIdx);
595         }
596 
597         if (logger.isDebugEnabled())
598         {
599             logger.debug("Create a unique name for: " + path + " (" + dir + " - " + filename + " - "
600                          + fileType + ")");
601         }
602 
603         String uniqueFilename = filename;
604         String[] existingFiles = listFiles(getAbsolutePath(dir));
605 
606         while (existsFile(existingFiles, uniqueFilename, fileType))
607         {
608             uniqueFilename = filename + '_' + fileIdx++;
609         }
610 
611         uniqueFilename = uniqueFilename + fileType;
612         if (!path.equals(uniqueFilename) && logger.isInfoEnabled())
613         {
614             logger.info("A file with the original filename (" + dir + "/" + path
615                         + ") already exists, new name: " + uniqueFilename);
616         }
617         if (logger.isDebugEnabled())
618         {
619             logger.debug("Unique name returned: " + uniqueFilename);
620         }
621         return uniqueFilename;
622     }
623 
624     private boolean existsFile(String[] files, String filename, String fileType)
625     {
626         boolean existsFile = false;
627         filename += fileType;
628         for (String file : files)
629         {
630             if (file.equals(filename))
631             {
632                 if (logger.isDebugEnabled())
633                 {
634                     logger.debug("Found existing file: " + file);
635                 }
636                 existsFile = true;
637             }
638         }
639         return existsFile;
640     }
641 
642     public void chmod(String path, int permissions) throws SftpException
643     {
644         path = getAbsolutePath(path);
645         if (logger.isDebugEnabled())
646         {
647             logger.debug("Will try to chmod directory '" + path + "' to permission " + permissions);
648         }
649         channelSftp.chmod(permissions, path);
650     }
651 
652     public void setNotifier(SftpNotifier notifier)
653     {
654         this.notifier = notifier;
655     }
656 
657     public String getHost()
658     {
659         return host;
660     }
661 }