View Javadoc

1   package org.apache.maven.wagon.providers.ssh.jsch;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.util.ArrayList;
26  import java.util.List;
27  
28  import org.apache.maven.wagon.InputData;
29  import org.apache.maven.wagon.OutputData;
30  import org.apache.maven.wagon.PathUtils;
31  import org.apache.maven.wagon.ResourceDoesNotExistException;
32  import org.apache.maven.wagon.TransferFailedException;
33  import org.apache.maven.wagon.authentication.AuthenticationException;
34  import org.apache.maven.wagon.authorization.AuthorizationException;
35  import org.apache.maven.wagon.events.TransferEvent;
36  import org.apache.maven.wagon.providers.ssh.ScpHelper;
37  import org.apache.maven.wagon.repository.RepositoryPermissions;
38  import org.apache.maven.wagon.resource.Resource;
39  
40  import com.jcraft.jsch.ChannelSftp;
41  import com.jcraft.jsch.JSchException;
42  import com.jcraft.jsch.SftpATTRS;
43  import com.jcraft.jsch.SftpException;
44  
45  /**
46   * SFTP protocol wagon.
47   *
48   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
49   *
50   * @todo [BP] add compression flag
51   * @todo see if SftpProgressMonitor allows us to do streaming (without it, we can't do checksums as the input stream is lost)
52   * 
53   * @plexus.component role="org.apache.maven.wagon.Wagon" 
54   *   role-hint="sftp"
55   *   instantiation-strategy="per-lookup"
56   */
57  public class SftpWagon
58      extends AbstractJschWagon
59  {
60      private static final String SFTP_CHANNEL = "sftp";
61  
62      private static final int S_IFDIR = 0x4000;
63  
64      private static final long MILLIS_PER_SEC = 1000L;
65  
66      private ChannelSftp channel;
67      
68      public void closeConnection()
69      {
70          if ( channel != null )
71          {
72              channel.disconnect();
73          }
74          super.closeConnection();
75      }
76  
77      public void openConnectionInternal()
78          throws AuthenticationException
79      {
80          super.openConnectionInternal();
81  
82          try
83          {
84              channel = (ChannelSftp) session.openChannel( SFTP_CHANNEL );
85  
86              channel.connect();
87          }
88          catch ( JSchException e )
89          {
90              throw new AuthenticationException( "Error connecting to remote repository: " + getRepository().getUrl(),
91                                                 e );
92          }
93      }
94  
95      private void returnToParentDirectory( Resource resource )
96      {
97          try
98          {
99              String dir = ScpHelper.getResourceDirectory( resource.getName() );
100             String[] dirs = PathUtils.dirnames( dir );
101             for ( int i = 0; i < dirs.length; i++ )
102             {
103                 channel.cd( ".." );
104             }
105         }
106         catch ( SftpException e )
107         {
108             fireTransferDebug( "Error returning to parent directory: " + e.getMessage() );
109         }
110     }
111 
112     private void putFile( File source, Resource resource, RepositoryPermissions permissions )
113         throws SftpException, TransferFailedException
114     {
115         resource.setContentLength( source.length() );
116         
117         resource.setLastModified( source.lastModified() );
118         
119         String filename = ScpHelper.getResourceFilename( resource.getName() );
120 
121         firePutStarted( resource, source );
122 
123         channel.put( source.getAbsolutePath(), filename );
124 
125         postProcessListeners( resource, source, TransferEvent.REQUEST_PUT );
126 
127         if ( permissions != null && permissions.getGroup() != null )
128         {
129             setGroup( filename, permissions );
130         }
131 
132         if ( permissions != null && permissions.getFileMode() != null )
133         {
134             setFileMode( filename, permissions );
135         }
136 
137         firePutCompleted( resource, source );
138     }
139 
140     private void setGroup( String filename, RepositoryPermissions permissions )
141     {
142         try
143         {
144             int group = Integer.valueOf( permissions.getGroup() ).intValue();
145             channel.chgrp( group, filename );
146         }
147         catch ( NumberFormatException e )
148         {
149             // TODO: warning level
150             fireTransferDebug( "Not setting group: must be a numerical GID for SFTP" );
151         }
152         catch ( SftpException e )
153         {
154             fireTransferDebug( "Not setting group: " + e.getMessage() );            
155         }
156     }
157 
158     private void setFileMode( String filename, RepositoryPermissions permissions )
159     {
160         try
161         {
162             int mode = getOctalMode( permissions.getFileMode() );
163             channel.chmod( mode, filename );
164         }
165         catch ( NumberFormatException e )
166         {
167             // TODO: warning level
168             fireTransferDebug( "Not setting mode: must be a numerical mode for SFTP" );
169         }
170         catch ( SftpException e )
171         {
172             fireTransferDebug( "Not setting mode: " + e.getMessage() );            
173         }
174     }
175 
176     private void mkdirs( String resourceName, int mode )
177         throws SftpException, TransferFailedException
178     {
179         String[] dirs = PathUtils.dirnames( resourceName );
180         for ( String dir : dirs )
181         {
182             mkdir( dir, mode );
183 
184             channel.cd( dir );
185         }
186     }
187 
188     private void mkdir( String dir, int mode )
189         throws TransferFailedException, SftpException
190     {
191         try
192         {
193             SftpATTRS attrs = channel.stat( dir );
194             if ( ( attrs.getPermissions() & S_IFDIR ) == 0 )
195             {
196                 throw new TransferFailedException( "Remote path is not a directory: " + dir );
197             }
198         }
199         catch ( SftpException e )
200         {
201             // doesn't exist, make it and try again
202             channel.mkdir( dir );
203             if ( mode != -1 )
204             {
205                 try
206                 {
207                     channel.chmod( mode, dir );
208                 }
209                 catch ( SftpException e1 )
210                 {
211                     // for some extrange reason we recive this exception,
212                     // even when chmod success
213                 }
214             }
215         }
216     }
217 
218     private SftpATTRS changeToRepositoryDirectory( String dir, String filename )
219         throws ResourceDoesNotExistException, SftpException
220     {
221         // This must be called first to ensure that if the file doesn't exist it throws an exception
222         SftpATTRS attrs;
223         try
224         {
225             channel.cd( repository.getBasedir() );
226 
227             if ( dir.length() > 0 )
228             {
229                 channel.cd( dir );
230             }
231 
232             if ( filename.length() == 0 )
233             {
234                 filename = ".";
235             }
236             
237             attrs = channel.stat( filename );
238         }
239         catch ( SftpException e )
240         {
241             if ( e.toString().trim().endsWith( "No such file" ) )
242             {
243                 throw new ResourceDoesNotExistException( e.toString(), e );
244             }
245             else if ( e.toString().trim().indexOf( "Can't change directory" ) != -1 )
246             {
247                 throw new ResourceDoesNotExistException( e.toString(), e );
248             }   
249             else
250             {
251                 throw e;
252             }
253         }
254         return attrs;
255     }
256 
257     public void putDirectory( File sourceDirectory, String destinationDirectory )
258         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
259     {
260         final RepositoryPermissions permissions = repository.getPermissions();
261 
262         try
263         {
264             channel.cd( "/" );
265             
266             String basedir = getRepository().getBasedir();
267             int directoryMode = getDirectoryMode( permissions );
268             
269             mkdirs( basedir + "/", directoryMode );
270             
271             fireTransferDebug( "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as "
272                 + destinationDirectory );
273             
274             mkdirs( destinationDirectory, directoryMode );
275             ftpRecursivePut( sourceDirectory, null, ScpHelper.getResourceFilename( destinationDirectory ),
276                              directoryMode );
277         }
278         catch ( SftpException e )
279         {
280             String msg =
281                 "Error occurred while deploying '" + sourceDirectory.getAbsolutePath() + "' " + "to remote repository: "
282                     + getRepository().getUrl() + ": " + e.getMessage();
283 
284             throw new TransferFailedException( msg, e );
285         }
286     }
287 
288     private void ftpRecursivePut( File sourceFile, String prefix, String fileName, int directoryMode )
289         throws TransferFailedException, SftpException
290     {
291         final RepositoryPermissions permissions = repository.getPermissions();
292 
293         if ( sourceFile.isDirectory() )
294         {
295             if ( !fileName.equals( "." ) )
296             {
297                 prefix = getFileName( prefix, fileName );
298                 mkdir( fileName, directoryMode );
299                 channel.cd( fileName );
300             }
301 
302             File[] files = sourceFile.listFiles();
303             if ( files != null && files.length > 0 )
304             {
305                 // Directories first, then files. Let's go deep early.
306                 for ( File file : files )
307                 {
308                     if ( file.isDirectory() )
309                     {
310                         ftpRecursivePut( file, prefix, file.getName(), directoryMode );
311                     }
312                 }
313                 for ( File file : files )
314                 {
315                     if ( !file.isDirectory() )
316                     {
317                         ftpRecursivePut( file, prefix, file.getName(), directoryMode );
318                     }
319                 }
320             }
321             
322             channel.cd( ".." );
323         }
324         else
325         {
326             Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) );
327 
328             firePutInitiated( resource, sourceFile );
329 
330             putFile( sourceFile, resource, permissions );
331         }
332     }
333 
334     private String getFileName( String prefix, String fileName )
335     {
336         if ( prefix != null )
337         {
338             prefix = prefix + "/" + fileName;
339         }
340         else
341         {
342             prefix = fileName;
343         }
344         return prefix;
345     }
346     
347     public List<String> getFileList( String destinationDirectory )
348         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
349     {
350         if ( destinationDirectory.length() == 0 )
351         {
352             destinationDirectory = ".";
353         }
354         
355         String filename = ScpHelper.getResourceFilename( destinationDirectory );
356 
357         String dir = ScpHelper.getResourceDirectory( destinationDirectory );
358 
359         // we already setuped the root directory. Ignore beginning /
360         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
361         {
362             dir = dir.substring( 1 );
363         }
364 
365         try
366         {
367             SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
368             if ( ( attrs.getPermissions() & S_IFDIR ) == 0 )
369             {
370                 throw new TransferFailedException( "Remote path is not a directory:" + dir );
371             }
372 
373             @SuppressWarnings( "unchecked" )
374             List<ChannelSftp.LsEntry> fileList = channel.ls( filename );
375             List<String> files = new ArrayList<String>( fileList.size() );
376             for ( ChannelSftp.LsEntry entry : fileList )
377             {
378                 String name = entry.getFilename();
379                 if ( entry.getAttrs().isDir() )
380                 {
381                     if ( !name.equals( "." ) && !name.equals( ".." ) )
382                     {
383                         if ( !name.endsWith( "/" ) )
384                         {
385                             name += "/";
386                         }
387                         files.add( name );
388                     }
389                 }
390                 else
391                 {
392                     files.add( name );
393                 }
394             }
395             return files;
396         }
397         catch ( SftpException e )
398         {
399             String msg =
400                 "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: "
401                     + getRepository().getUrl() + ": " + e.getMessage();
402 
403             throw new TransferFailedException( msg, e );
404         }
405     }
406     
407     public boolean resourceExists( String resourceName )
408         throws TransferFailedException, AuthorizationException
409     {
410         String filename = ScpHelper.getResourceFilename( resourceName );
411 
412         String dir = ScpHelper.getResourceDirectory( resourceName );
413 
414         // we already setuped the root directory. Ignore beginning /
415         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
416         {
417             dir = dir.substring( 1 );
418         }
419 
420         try
421         {
422             changeToRepositoryDirectory( dir, filename );
423             
424             return true;
425         }
426         catch ( ResourceDoesNotExistException e )
427         {
428             return false;
429         }
430         catch ( SftpException e )
431         {
432             String msg =
433                 "Error occurred while looking for '" + resourceName + "' " + "on remote repository: "
434                     + getRepository().getUrl() + ": " + e.getMessage();
435 
436             throw new TransferFailedException( msg, e );
437         }
438     }
439 
440     protected void cleanupGetTransfer( Resource resource )
441     {
442         returnToParentDirectory( resource );
443     }
444     
445     protected void cleanupPutTransfer( Resource resource )
446     {
447         returnToParentDirectory( resource );
448     }
449 
450     protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
451         throws TransferFailedException
452     {
453         RepositoryPermissions permissions = getRepository().getPermissions();
454 
455         String filename = ScpHelper.getResourceFilename( resource.getName() );
456         if ( permissions != null && permissions.getGroup() != null )
457         {
458             setGroup( filename, permissions );
459         }
460         
461         if ( permissions != null && permissions.getFileMode() != null )
462         {
463             setFileMode( filename, permissions );
464         }
465     }
466 
467     public void fillInputData( InputData inputData )
468         throws TransferFailedException, ResourceDoesNotExistException
469     {
470         Resource resource = inputData.getResource();
471         
472         String filename = ScpHelper.getResourceFilename( resource.getName() );
473 
474         String dir = ScpHelper.getResourceDirectory( resource.getName() );
475 
476         // we already setuped the root directory. Ignore beginning /
477         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
478         {
479             dir = dir.substring( 1 );
480         }
481 
482         try
483         {
484             SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
485 
486             long lastModified = attrs.getMTime() * MILLIS_PER_SEC;
487             resource.setContentLength( attrs.getSize() );
488 
489             resource.setLastModified( lastModified );
490             
491             inputData.setInputStream( channel.get( filename ) );
492         }
493         catch ( SftpException e )
494         {
495             handleGetException( resource, e );
496         }
497     }
498 
499     public void fillOutputData( OutputData outputData )
500         throws TransferFailedException
501     {
502         int directoryMode = getDirectoryMode( getRepository().getPermissions() );
503 
504         Resource resource = outputData.getResource();
505         
506         try
507         {
508             channel.cd( "/" );
509 
510             String basedir = getRepository().getBasedir();
511             mkdirs( basedir + "/", directoryMode );
512 
513             mkdirs( resource.getName(), directoryMode );
514 
515             String filename = ScpHelper.getResourceFilename( resource.getName() );
516             outputData.setOutputStream( channel.put( filename ) );
517         }
518         catch ( TransferFailedException e )
519         {
520             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
521 
522             throw e;
523         }
524         catch ( SftpException e )
525         {
526             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
527 
528             String msg =
529                 "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: "
530                     + getRepository().getUrl() + ": " + e.getMessage();
531 
532             throw new TransferFailedException( msg, e );
533         }
534     }
535     
536     /**
537      * @param permissions repository's permissions
538      * @return the directory mode for the repository or <code>-1</code> if it
539      *         wasn't set
540      */
541     public int getDirectoryMode( RepositoryPermissions permissions )
542     {
543         int ret = -1;
544 
545         if ( permissions != null )
546         {
547             ret = getOctalMode( permissions.getDirectoryMode() );
548         }
549 
550         return ret;
551     }
552 
553     public int getOctalMode( String mode )
554     {
555         int ret;
556         try
557         {
558             ret = Integer.valueOf( mode, 8 ).intValue();
559         }
560         catch ( NumberFormatException e )
561         {
562             // TODO: warning level
563             fireTransferDebug( "the file mode must be a numerical mode for SFTP" );
564             ret = -1;
565         }
566         return ret;
567     }
568 }