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 ( String d : dirs )
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().contains( "Can't change directory" ) )
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             // ScpHelper.getResourceFilename( destinationDirectory ) - could return empty string
296             if ( !fileName.equals( "." ) && !fileName.equals( "" ) )
297             {
298                 prefix = getFileName( prefix, fileName );
299                 mkdir( fileName, directoryMode );
300                 channel.cd( fileName );
301             }
302 
303             File[] files = sourceFile.listFiles();
304             if ( files != null && files.length > 0 )
305             {
306                 // Directories first, then files. Let's go deep early.
307                 for ( File file : files )
308                 {
309                     if ( file.isDirectory() )
310                     {
311                         ftpRecursivePut( file, prefix, file.getName(), directoryMode );
312                     }
313                 }
314                 for ( File file : files )
315                 {
316                     if ( !file.isDirectory() )
317                     {
318                         ftpRecursivePut( file, prefix, file.getName(), directoryMode );
319                     }
320                 }
321             }
322             
323             channel.cd( ".." );
324         }
325         else
326         {
327             Resource resource = ScpHelper.getResource( getFileName( prefix, fileName ) );
328 
329             firePutInitiated( resource, sourceFile );
330 
331             putFile( sourceFile, resource, permissions );
332         }
333     }
334 
335     private String getFileName( String prefix, String fileName )
336     {
337         if ( prefix != null )
338         {
339             prefix = prefix + "/" + fileName;
340         }
341         else
342         {
343             prefix = fileName;
344         }
345         return prefix;
346     }
347     
348     public List<String> getFileList( String destinationDirectory )
349         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
350     {
351         if ( destinationDirectory.length() == 0 )
352         {
353             destinationDirectory = ".";
354         }
355         
356         String filename = ScpHelper.getResourceFilename( destinationDirectory );
357 
358         String dir = ScpHelper.getResourceDirectory( destinationDirectory );
359 
360         // we already setuped the root directory. Ignore beginning /
361         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
362         {
363             dir = dir.substring( 1 );
364         }
365 
366         try
367         {
368             SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
369             if ( ( attrs.getPermissions() & S_IFDIR ) == 0 )
370             {
371                 throw new TransferFailedException( "Remote path is not a directory:" + dir );
372             }
373 
374             @SuppressWarnings( "unchecked" )
375             List<ChannelSftp.LsEntry> fileList = channel.ls( filename );
376             List<String> files = new ArrayList<String>( fileList.size() );
377             for ( ChannelSftp.LsEntry entry : fileList )
378             {
379                 String name = entry.getFilename();
380                 if ( entry.getAttrs().isDir() )
381                 {
382                     if ( !name.equals( "." ) && !name.equals( ".." ) )
383                     {
384                         if ( !name.endsWith( "/" ) )
385                         {
386                             name += "/";
387                         }
388                         files.add( name );
389                     }
390                 }
391                 else
392                 {
393                     files.add( name );
394                 }
395             }
396             return files;
397         }
398         catch ( SftpException e )
399         {
400             String msg =
401                 "Error occurred while listing '" + destinationDirectory + "' " + "on remote repository: "
402                     + getRepository().getUrl() + ": " + e.getMessage();
403 
404             throw new TransferFailedException( msg, e );
405         }
406     }
407     
408     public boolean resourceExists( String resourceName )
409         throws TransferFailedException, AuthorizationException
410     {
411         String filename = ScpHelper.getResourceFilename( resourceName );
412 
413         String dir = ScpHelper.getResourceDirectory( resourceName );
414 
415         // we already setuped the root directory. Ignore beginning /
416         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
417         {
418             dir = dir.substring( 1 );
419         }
420 
421         try
422         {
423             changeToRepositoryDirectory( dir, filename );
424             
425             return true;
426         }
427         catch ( ResourceDoesNotExistException e )
428         {
429             return false;
430         }
431         catch ( SftpException e )
432         {
433             String msg =
434                 "Error occurred while looking for '" + resourceName + "' " + "on remote repository: "
435                     + getRepository().getUrl() + ": " + e.getMessage();
436 
437             throw new TransferFailedException( msg, e );
438         }
439     }
440 
441     protected void cleanupGetTransfer( Resource resource )
442     {
443         returnToParentDirectory( resource );
444     }
445     
446     protected void cleanupPutTransfer( Resource resource )
447     {
448         returnToParentDirectory( resource );
449     }
450 
451     protected void finishPutTransfer( Resource resource, InputStream input, OutputStream output )
452         throws TransferFailedException
453     {
454         RepositoryPermissions permissions = getRepository().getPermissions();
455 
456         String filename = ScpHelper.getResourceFilename( resource.getName() );
457         if ( permissions != null && permissions.getGroup() != null )
458         {
459             setGroup( filename, permissions );
460         }
461         
462         if ( permissions != null && permissions.getFileMode() != null )
463         {
464             setFileMode( filename, permissions );
465         }
466     }
467 
468     public void fillInputData( InputData inputData )
469         throws TransferFailedException, ResourceDoesNotExistException
470     {
471         Resource resource = inputData.getResource();
472         
473         String filename = ScpHelper.getResourceFilename( resource.getName() );
474 
475         String dir = ScpHelper.getResourceDirectory( resource.getName() );
476 
477         // we already setuped the root directory. Ignore beginning /
478         if ( dir.length() > 0 && dir.charAt( 0 ) == ScpHelper.PATH_SEPARATOR )
479         {
480             dir = dir.substring( 1 );
481         }
482 
483         try
484         {
485             SftpATTRS attrs = changeToRepositoryDirectory( dir, filename );
486 
487             long lastModified = attrs.getMTime() * MILLIS_PER_SEC;
488             resource.setContentLength( attrs.getSize() );
489 
490             resource.setLastModified( lastModified );
491             
492             inputData.setInputStream( channel.get( filename ) );
493         }
494         catch ( SftpException e )
495         {
496             handleGetException( resource, e );
497         }
498     }
499 
500     public void fillOutputData( OutputData outputData )
501         throws TransferFailedException
502     {
503         int directoryMode = getDirectoryMode( getRepository().getPermissions() );
504 
505         Resource resource = outputData.getResource();
506         
507         try
508         {
509             channel.cd( "/" );
510 
511             String basedir = getRepository().getBasedir();
512             mkdirs( basedir + "/", directoryMode );
513 
514             mkdirs( resource.getName(), directoryMode );
515 
516             String filename = ScpHelper.getResourceFilename( resource.getName() );
517             outputData.setOutputStream( channel.put( filename ) );
518         }
519         catch ( TransferFailedException e )
520         {
521             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
522 
523             throw e;
524         }
525         catch ( SftpException e )
526         {
527             fireTransferError( resource, e, TransferEvent.REQUEST_PUT );
528 
529             String msg =
530                 "Error occurred while deploying '" + resource.getName() + "' " + "to remote repository: "
531                     + getRepository().getUrl() + ": " + e.getMessage();
532 
533             throw new TransferFailedException( msg, e );
534         }
535     }
536     
537     /**
538      * @param permissions repository's permissions
539      * @return the directory mode for the repository or <code>-1</code> if it
540      *         wasn't set
541      */
542     public int getDirectoryMode( RepositoryPermissions permissions )
543     {
544         int ret = -1;
545 
546         if ( permissions != null )
547         {
548             ret = getOctalMode( permissions.getDirectoryMode() );
549         }
550 
551         return ret;
552     }
553 
554     public int getOctalMode( String mode )
555     {
556         int ret;
557         try
558         {
559             ret = Integer.valueOf( mode, 8 ).intValue();
560         }
561         catch ( NumberFormatException e )
562         {
563             // TODO: warning level
564             fireTransferDebug( "the file mode must be a numerical mode for SFTP" );
565             ret = -1;
566         }
567         return ret;
568     }
569 }