View Javadoc

1   package org.apache.maven.wagon.providers.ftp;
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 org.apache.commons.net.ProtocolCommandEvent;
23  import org.apache.commons.net.ProtocolCommandListener;
24  import org.apache.commons.net.ftp.FTP;
25  import org.apache.commons.net.ftp.FTPClient;
26  import org.apache.commons.net.ftp.FTPFile;
27  import org.apache.commons.net.ftp.FTPReply;
28  import org.apache.maven.wagon.ConnectionException;
29  import org.apache.maven.wagon.InputData;
30  import org.apache.maven.wagon.OutputData;
31  import org.apache.maven.wagon.PathUtils;
32  import org.apache.maven.wagon.ResourceDoesNotExistException;
33  import org.apache.maven.wagon.StreamWagon;
34  import org.apache.maven.wagon.TransferFailedException;
35  import org.apache.maven.wagon.WagonConstants;
36  import org.apache.maven.wagon.authentication.AuthenticationException;
37  import org.apache.maven.wagon.authentication.AuthenticationInfo;
38  import org.apache.maven.wagon.authorization.AuthorizationException;
39  import org.apache.maven.wagon.repository.RepositoryPermissions;
40  import org.apache.maven.wagon.resource.Resource;
41  import org.codehaus.plexus.util.IOUtil;
42  
43  import java.io.File;
44  import java.io.FileInputStream;
45  import java.io.IOException;
46  import java.io.InputStream;
47  import java.io.OutputStream;
48  import java.util.ArrayList;
49  import java.util.Calendar;
50  import java.util.List;
51  
52  /**
53   * FtpWagon
54   *
55   *
56   * @plexus.component role="org.apache.maven.wagon.Wagon"
57   * role-hint="ftp"
58   * instantiation-strategy="per-lookup"
59   */
60  public class FtpWagon
61      extends StreamWagon
62  {
63      private FTPClient ftp;
64  
65      /**
66       * @plexus.configuration default-value="true"
67       */
68      private boolean passiveMode = true;
69  
70      /**
71       * @plexus.configuration default-value="ISO-8859-1"
72       */
73      private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING;
74  
75      public boolean isPassiveMode()
76      {
77          return passiveMode;
78      }
79  
80      public void setPassiveMode( boolean passiveMode )
81      {
82          this.passiveMode = passiveMode;
83      }
84  
85      protected void openConnectionInternal()
86          throws ConnectionException, AuthenticationException
87      {
88          AuthenticationInfo authInfo = getAuthenticationInfo();
89  
90          if ( authInfo == null )
91          {
92              throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" );
93          }
94  
95          if ( authInfo.getUserName() == null )
96          {
97              authInfo.setUserName( System.getProperty( "user.name" ) );
98          }
99  
100         String username = authInfo.getUserName();
101 
102         String password = authInfo.getPassword();
103 
104         if ( username == null )
105         {
106             throw new AuthenticationException( "Username not specified for repository " + getRepository().getId() );
107         }
108         if ( password == null )
109         {
110             throw new AuthenticationException( "Password not specified for repository " + getRepository().getId() );
111         }
112 
113         String host = getRepository().getHost();
114 
115         ftp = new FTPClient();
116         ftp.setDefaultTimeout( getTimeout() );
117         ftp.setDataTimeout( getTimeout() );
118         ftp.setControlEncoding( getControlEncoding() );
119 
120         ftp.addProtocolCommandListener( new PrintCommandListener( this ) );
121 
122         try
123         {
124             if ( getRepository().getPort() != WagonConstants.UNKNOWN_PORT )
125             {
126                 ftp.connect( host, getRepository().getPort() );
127             }
128             else
129             {
130                 ftp.connect( host );
131             }
132 
133             // After connection attempt, you should check the reply code to
134             // verify
135             // success.
136             int reply = ftp.getReplyCode();
137 
138             if ( !FTPReply.isPositiveCompletion( reply ) )
139             {
140                 ftp.disconnect();
141 
142                 throw new AuthenticationException( "FTP server refused connection." );
143             }
144         }
145         catch ( IOException e )
146         {
147             if ( ftp.isConnected() )
148             {
149                 try
150                 {
151                     fireSessionError( e );
152 
153                     ftp.disconnect();
154                 }
155                 catch ( IOException f )
156                 {
157                     // do nothing
158                 }
159             }
160 
161             throw new AuthenticationException( "Could not connect to server.", e );
162         }
163 
164         try
165         {
166             if ( !ftp.login( username, password ) )
167             {
168                 throw new AuthenticationException( "Cannot login to remote system" );
169             }
170 
171             fireSessionDebug( "Remote system is " + ftp.getSystemName() );
172 
173             // Set to binary mode.
174             ftp.setFileType( FTP.BINARY_FILE_TYPE );
175             ftp.setListHiddenFiles( true );
176 
177             // Use passive mode as default because most of us are
178             // behind firewalls these days.
179             if ( isPassiveMode() )
180             {
181                 ftp.enterLocalPassiveMode();
182             }
183         }
184         catch ( IOException e )
185         {
186             throw new ConnectionException( "Cannot login to remote system", e );
187         }
188     }
189 
190     protected void firePutCompleted( Resource resource, File file )
191     {
192         try
193         {
194             // TODO [BP]: verify the order is correct
195             ftp.completePendingCommand();
196 
197             RepositoryPermissions permissions = repository.getPermissions();
198 
199             if ( permissions != null && permissions.getGroup() != null )
200             {
201                 // ignore failures
202                 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() );
203             }
204 
205             if ( permissions != null && permissions.getFileMode() != null )
206             {
207                 // ignore failures
208                 ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() );
209             }
210         }
211         catch ( IOException e )
212         {
213             // TODO: handle
214             // michal I am not sure  what error means in that context
215             // I think that we will be able to recover or simply we will fail later on
216         }
217 
218         super.firePutCompleted( resource, file );
219     }
220 
221     protected void fireGetCompleted( Resource resource, File localFile )
222     {
223         try
224         {
225             ftp.completePendingCommand();
226         }
227         catch ( IOException e )
228         {
229             // TODO: handle
230             // michal I am not sure  what error means in that context
231             // actually I am not even sure why we have to invoke that command
232             // I think that we will be able to recover or simply we will fail later on
233         }
234         super.fireGetCompleted( resource, localFile );
235     }
236 
237     public void closeConnection()
238         throws ConnectionException
239     {
240         if ( ftp != null && ftp.isConnected() )
241         {
242             try
243             {
244                 // This is a NPE rethink shutting down the streams
245                 ftp.disconnect();
246             }
247             catch ( IOException e )
248             {
249                 throw new ConnectionException( "Failed to close connection to FTP repository", e );
250             }
251         }
252     }
253 
254     public void fillOutputData( OutputData outputData )
255         throws TransferFailedException
256     {
257         OutputStream os;
258 
259         Resource resource = outputData.getResource();
260 
261         RepositoryPermissions permissions = repository.getPermissions();
262 
263         try
264         {
265             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
266             {
267                 throw new TransferFailedException(
268                     "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
269             }
270 
271             String[] dirs = PathUtils.dirnames( resource.getName() );
272 
273             for ( int i = 0; i < dirs.length; i++ )
274             {
275                 boolean dirChanged = ftp.changeWorkingDirectory( dirs[i] );
276 
277                 if ( !dirChanged )
278                 {
279                     // first, try to create it
280                     boolean success = ftp.makeDirectory( dirs[i] );
281 
282                     if ( success )
283                     {
284                         if ( permissions != null && permissions.getGroup() != null )
285                         {
286                             // ignore failures
287                             ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dirs[i] );
288                         }
289 
290                         if ( permissions != null && permissions.getDirectoryMode() != null )
291                         {
292                             // ignore failures
293                             ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dirs[i] );
294                         }
295 
296                         dirChanged = ftp.changeWorkingDirectory( dirs[i] );
297                     }
298                 }
299 
300                 if ( !dirChanged )
301                 {
302                     throw new TransferFailedException( "Unable to create directory " + dirs[i] );
303                 }
304             }
305 
306             // we come back to original basedir so
307             // FTP wagon is ready for next requests
308             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
309             {
310                 throw new TransferFailedException( "Unable to return to the base directory" );
311             }
312 
313             os = ftp.storeFileStream( resource.getName() );
314 
315             if ( os == null )
316             {
317                 String msg =
318                     "Cannot transfer resource:  '" + resource + "'. Output stream is null. FTP Server response: "
319                         + ftp.getReplyString();
320 
321                 throw new TransferFailedException( msg );
322 
323             }
324 
325             fireTransferDebug( "resource = " + resource );
326 
327         }
328         catch ( IOException e )
329         {
330             throw new TransferFailedException( "Error transferring over FTP", e );
331         }
332 
333         outputData.setOutputStream( os );
334 
335     }
336 
337     // ----------------------------------------------------------------------
338     //
339     // ----------------------------------------------------------------------
340 
341     public void fillInputData( InputData inputData )
342         throws TransferFailedException, ResourceDoesNotExistException
343     {
344         InputStream is;
345 
346         Resource resource = inputData.getResource();
347 
348         try
349         {
350             ftpChangeDirectory( resource );
351 
352             String filename = PathUtils.filename( resource.getName() );
353             FTPFile[] ftpFiles = ftp.listFiles( filename );
354 
355             if ( ftpFiles == null || ftpFiles.length <= 0 )
356             {
357                 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
358             }
359 
360             long contentLength = ftpFiles[0].getSize();
361 
362             //@todo check how it works! javadoc of common login says:
363             // Returns the file timestamp. This usually the last modification time.
364             //
365             Calendar timestamp = ftpFiles[0].getTimestamp();
366             long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0;
367 
368             resource.setContentLength( contentLength );
369 
370             resource.setLastModified( lastModified );
371 
372             is = ftp.retrieveFileStream( filename );
373         }
374         catch ( IOException e )
375         {
376             throw new TransferFailedException( "Error transferring file via FTP", e );
377         }
378 
379         inputData.setInputStream( is );
380     }
381 
382     private void ftpChangeDirectory( Resource resource )
383         throws IOException, TransferFailedException, ResourceDoesNotExistException
384     {
385         if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
386         {
387             throw new ResourceDoesNotExistException(
388                 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
389         }
390 
391         String[] dirs = PathUtils.dirnames( resource.getName() );
392 
393         for ( int i = 0; i < dirs.length; i++ )
394         {
395             boolean dirChanged = ftp.changeWorkingDirectory( dirs[i] );
396 
397             if ( !dirChanged )
398             {
399                 String msg = "Resource " + resource + " not found. Directory " + dirs[i] + " does not exist";
400 
401                 throw new ResourceDoesNotExistException( msg );
402             }
403         }
404     }
405 
406     public class PrintCommandListener
407         implements ProtocolCommandListener
408     {
409         private FtpWagon wagon;
410 
411         public PrintCommandListener( FtpWagon wagon )
412         {
413             this.wagon = wagon;
414         }
415 
416         public void protocolCommandSent( ProtocolCommandEvent event )
417         {
418             wagon.fireSessionDebug( "Command sent: " + event.getMessage() );
419 
420         }
421 
422         public void protocolReplyReceived( ProtocolCommandEvent event )
423         {
424             wagon.fireSessionDebug( "Reply received: " + event.getMessage() );
425         }
426     }
427 
428     protected void fireSessionDebug( String msg )
429     {
430         super.fireSessionDebug( msg );
431     }
432 
433     public List<String> getFileList( String destinationDirectory )
434         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
435     {
436         Resource resource = new Resource( destinationDirectory );
437 
438         try
439         {
440             ftpChangeDirectory( resource );
441 
442             String filename = PathUtils.filename( resource.getName() );
443             FTPFile[] ftpFiles = ftp.listFiles( filename );
444 
445             if ( ftpFiles == null || ftpFiles.length <= 0 )
446             {
447                 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
448             }
449 
450             List<String> ret = new ArrayList<String>();
451             for ( int i = 0; i < ftpFiles.length; i++ )
452             {
453                 String name = ftpFiles[i].getName();
454 
455                 if ( ftpFiles[i].isDirectory() && !name.endsWith( "/" ) )
456                 {
457                     name += "/";
458                 }
459 
460                 ret.add( name );
461             }
462 
463             return ret;
464         }
465         catch ( IOException e )
466         {
467             throw new TransferFailedException( "Error transferring file via FTP", e );
468         }
469     }
470 
471     public boolean resourceExists( String resourceName )
472         throws TransferFailedException, AuthorizationException
473     {
474         Resource resource = new Resource( resourceName );
475 
476         try
477         {
478             ftpChangeDirectory( resource );
479 
480             String filename = PathUtils.filename( resource.getName() );
481             int status = ftp.stat( filename );
482 
483             return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status
484                 == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers
485                 || ( status == FTPReply.COMMAND_OK )     // not in the RFC but used by some FTP servers
486                 || ( status == FTPReply.SYSTEM_STATUS ) );
487         }
488         catch ( IOException e )
489         {
490             throw new TransferFailedException( "Error transferring file via FTP", e );
491         }
492         catch ( ResourceDoesNotExistException e )
493         {
494             return false;
495         }
496     }
497 
498     public boolean supportsDirectoryCopy()
499     {
500         return true;
501     }
502 
503     public void putDirectory( File sourceDirectory, String destinationDirectory )
504         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
505     {
506 
507         // Change to root.
508         try
509         {
510             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
511             {
512                 RepositoryPermissions permissions = getRepository().getPermissions();
513                 if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) )
514                 {
515                     throw new TransferFailedException(
516                         "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" );
517                 }
518 
519                 // try it again sam ...
520                 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
521                 {
522                     throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' "
523                                                            + "is missing and could not get created" );
524                 }
525             }
526         }
527         catch ( IOException e )
528         {
529             throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e );
530         }
531 
532         fireTransferDebug(
533             "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory );
534         ftpRecursivePut( sourceDirectory, destinationDirectory );
535     }
536 
537     private void ftpRecursivePut( File sourceFile, String fileName )
538         throws TransferFailedException
539     {
540         final RepositoryPermissions permissions = repository.getPermissions();
541 
542         fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName );
543 
544         if ( sourceFile.isDirectory() )
545         {
546             if ( !fileName.equals( "." ) )
547             {
548                 try
549                 {
550                     // change directory if it already exists.
551                     if ( !ftp.changeWorkingDirectory( fileName ) )
552                     {
553                         // first, try to create it
554                         if ( makeFtpDirectoryRecursive( fileName, permissions ) )
555                         {
556                             if ( !ftp.changeWorkingDirectory( fileName ) )
557                             {
558                                 throw new TransferFailedException(
559                                     "Unable to change cwd on ftp server to " + fileName + " when processing "
560                                         + sourceFile.getAbsolutePath() );
561                             }
562                         }
563                         else
564                         {
565                             throw new TransferFailedException(
566                                 "Unable to create directory " + fileName + " when processing "
567                                     + sourceFile.getAbsolutePath() );
568                         }
569                     }
570                 }
571                 catch ( IOException e )
572                 {
573                     throw new TransferFailedException(
574                         "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e );
575                 }
576             }
577 
578             File[] files = sourceFile.listFiles();
579             if ( files != null && files.length > 0 )
580             {
581                 fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length );
582 
583                 // Directories first, then files. Let's go deep early.
584                 for ( int i = 0; i < files.length; i++ )
585                 {
586                     if ( files[i].isDirectory() )
587                     {
588                         ftpRecursivePut( files[i], files[i].getName() );
589                     }
590                 }
591                 for ( int i = 0; i < files.length; i++ )
592                 {
593                     if ( !files[i].isDirectory() )
594                     {
595                         ftpRecursivePut( files[i], files[i].getName() );
596                     }
597                 }
598             }
599 
600             // Step back up a directory once we're done with the contents of this one.
601             try
602             {
603                 ftp.changeToParentDirectory();
604             }
605             catch ( IOException e )
606             {
607                 throw new TransferFailedException( "IOException caught while attempting to step up to parent directory"
608                                                        + " after successfully processing "
609                                                        + sourceFile.getAbsolutePath(), e );
610             }
611         }
612         else
613         {
614             // Oh how I hope and pray, in denial, but today I am still just a file.
615 
616             FileInputStream sourceFileStream = null;
617             try
618             {
619                 sourceFileStream = new FileInputStream( sourceFile );
620 
621                 // It's a file. Upload it in the current directory.
622                 if ( ftp.storeFile( fileName, sourceFileStream ) )
623                 {
624                     if ( permissions != null )
625                     {
626                         // Process permissions; note that if we get errors or exceptions here, they are ignored.
627                         // This appears to be a conscious decision, based on other parts of this code.
628                         String group = permissions.getGroup();
629                         if ( group != null )
630                         {
631                             try
632                             {
633                                 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
634                             }
635                             catch ( IOException e )
636                             {
637                             }
638                         }
639                         String mode = permissions.getFileMode();
640                         if ( mode != null )
641                         {
642                             try
643                             {
644                                 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
645                             }
646                             catch ( IOException e )
647                             {
648                             }
649                         }
650                     }
651                 }
652                 else
653                 {
654                     String msg =
655                         "Cannot transfer resource:  '" + sourceFile.getAbsolutePath() + "' FTP Server response: "
656                             + ftp.getReplyString();
657                     throw new TransferFailedException( msg );
658                 }
659             }
660             catch ( IOException e )
661             {
662                 throw new TransferFailedException(
663                     "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e );
664             }
665             finally
666             {
667                 IOUtil.close( sourceFileStream );
668             }
669 
670         }
671 
672         fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() );
673     }
674 
675     /**
676      * Set the permissions (if given) for the underlying folder.
677      * Note: Since the FTP SITE command might be server dependent, we cannot
678      * rely on the functionality available on each FTP server!
679      * So we can only try and hope it works (and catch away all Exceptions).
680      *
681      * @param permissions group and directory permissions
682      */
683     private void setPermissions( RepositoryPermissions permissions )
684     {
685         if ( permissions != null )
686         {
687             // Process permissions; note that if we get errors or exceptions here, they are ignored.
688             // This appears to be a conscious decision, based on other parts of this code.
689             String group = permissions.getGroup();
690             if ( group != null )
691             {
692                 try
693                 {
694                     ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
695                 }
696                 catch ( IOException e )
697                 {
698                 }
699             }
700             String mode = permissions.getDirectoryMode();
701             if ( mode != null )
702             {
703                 try
704                 {
705                     ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
706                 }
707                 catch ( IOException e )
708                 {
709                 }
710             }
711         }
712     }
713 
714     /**
715      * Recursively create directories.
716      *
717      * @param fileName    the path to create (might be nested)
718      * @param permissions
719      * @return ok
720      * @throws IOException
721      */
722     private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions )
723         throws IOException
724     {
725         if ( fileName == null || fileName.length() == 0
726             || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar
727         {
728             return false;
729         }
730 
731         int slashPos = fileName.indexOf( "/" );
732         String oldPwd = null;
733         boolean ok = true;
734 
735         if ( slashPos == 0 )
736         {
737             // this is an absolute directory
738             oldPwd = ftp.printWorkingDirectory();
739 
740             // start with the root
741             ftp.changeWorkingDirectory( "/" );
742             fileName = fileName.substring( 1 );
743 
744             // look up the next path separator
745             slashPos = fileName.indexOf( "/" );
746         }
747 
748         if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/'
749         {
750             if ( oldPwd == null )
751             {
752                 oldPwd = ftp.printWorkingDirectory();
753             }
754 
755             String nextDir = fileName.substring( 0, slashPos );
756 
757             // we only create the nextDir if it doesn't yet exist
758             if ( !ftp.changeWorkingDirectory( nextDir ) )
759             {
760                 ok &= ftp.makeDirectory( nextDir );
761             }
762 
763             if ( ok )
764             {
765                 // set the permissions for the freshly created directory
766                 setPermissions( permissions );
767 
768                 ftp.changeWorkingDirectory( nextDir );
769 
770                 // now create the deeper directories
771                 String remainingDirs = fileName.substring( slashPos + 1 );
772                 ok &= makeFtpDirectoryRecursive( remainingDirs, permissions );
773             }
774         }
775         else
776         {
777             ok = ftp.makeDirectory( fileName );
778         }
779 
780         if ( oldPwd != null )
781         {
782             // change back to the old working directory
783             ftp.changeWorkingDirectory( oldPwd );
784         }
785 
786         return ok;
787     }
788 
789     public String getControlEncoding()
790     {
791         return controlEncoding;
792     }
793 
794     public void setControlEncoding( String controlEncoding )
795     {
796         this.controlEncoding = controlEncoding;
797     }
798 }