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.io.IOUtils;
23  import org.apache.commons.net.ProtocolCommandEvent;
24  import org.apache.commons.net.ProtocolCommandListener;
25  import org.apache.commons.net.ftp.FTP;
26  import org.apache.commons.net.ftp.FTPClient;
27  import org.apache.commons.net.ftp.FTPFile;
28  import org.apache.commons.net.ftp.FTPReply;
29  import org.apache.maven.wagon.ConnectionException;
30  import org.apache.maven.wagon.InputData;
31  import org.apache.maven.wagon.OutputData;
32  import org.apache.maven.wagon.PathUtils;
33  import org.apache.maven.wagon.ResourceDoesNotExistException;
34  import org.apache.maven.wagon.StreamWagon;
35  import org.apache.maven.wagon.TransferFailedException;
36  import org.apache.maven.wagon.WagonConstants;
37  import org.apache.maven.wagon.authentication.AuthenticationException;
38  import org.apache.maven.wagon.authentication.AuthenticationInfo;
39  import org.apache.maven.wagon.authorization.AuthorizationException;
40  import org.apache.maven.wagon.repository.RepositoryPermissions;
41  import org.apache.maven.wagon.resource.Resource;
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 ( String dir : dirs )
274             {
275                 boolean dirChanged = ftp.changeWorkingDirectory( dir );
276 
277                 if ( !dirChanged )
278                 {
279                     // first, try to create it
280                     boolean success = ftp.makeDirectory( dir );
281 
282                     if ( success )
283                     {
284                         if ( permissions != null && permissions.getGroup() != null )
285                         {
286                             // ignore failures
287                             ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir );
288                         }
289 
290                         if ( permissions != null && permissions.getDirectoryMode() != null )
291                         {
292                             // ignore failures
293                             ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir );
294                         }
295 
296                         dirChanged = ftp.changeWorkingDirectory( dir );
297                     }
298                 }
299 
300                 if ( !dirChanged )
301                 {
302                     throw new TransferFailedException( "Unable to create directory " + dir );
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 ( String dir : dirs )
394         {
395             boolean dirChanged = ftp.changeWorkingDirectory( dir );
396 
397             if ( !dirChanged )
398             {
399                 String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist";
400 
401                 throw new ResourceDoesNotExistException( msg );
402             }
403         }
404     }
405 
406     /**
407      * 
408      */
409     public class PrintCommandListener
410         implements ProtocolCommandListener
411     {
412         private FtpWagon wagon;
413 
414         public PrintCommandListener( FtpWagon wagon )
415         {
416             this.wagon = wagon;
417         }
418 
419         public void protocolCommandSent( ProtocolCommandEvent event )
420         {
421             wagon.fireSessionDebug( "Command sent: " + event.getMessage() );
422 
423         }
424 
425         public void protocolReplyReceived( ProtocolCommandEvent event )
426         {
427             wagon.fireSessionDebug( "Reply received: " + event.getMessage() );
428         }
429     }
430 
431     protected void fireSessionDebug( String msg )
432     {
433         super.fireSessionDebug( msg );
434     }
435 
436     public List<String> getFileList( String destinationDirectory )
437         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
438     {
439         Resource resource = new Resource( destinationDirectory );
440 
441         try
442         {
443             ftpChangeDirectory( resource );
444 
445             String filename = PathUtils.filename( resource.getName() );
446             FTPFile[] ftpFiles = ftp.listFiles( filename );
447 
448             if ( ftpFiles == null || ftpFiles.length <= 0 )
449             {
450                 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
451             }
452 
453             List<String> ret = new ArrayList<String>();
454             for ( FTPFile file : ftpFiles )
455             {
456                 String name = file.getName();
457 
458                 if ( file.isDirectory() && !name.endsWith( "/" ) )
459                 {
460                     name += "/";
461                 }
462 
463                 ret.add( name );
464             }
465 
466             return ret;
467         }
468         catch ( IOException e )
469         {
470             throw new TransferFailedException( "Error transferring file via FTP", e );
471         }
472     }
473 
474     public boolean resourceExists( String resourceName )
475         throws TransferFailedException, AuthorizationException
476     {
477         Resource resource = new Resource( resourceName );
478 
479         try
480         {
481             ftpChangeDirectory( resource );
482 
483             String filename = PathUtils.filename( resource.getName() );
484             int status = ftp.stat( filename );
485 
486             return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status
487                 == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers
488                 || ( status == FTPReply.COMMAND_OK )     // not in the RFC but used by some FTP servers
489                 || ( status == FTPReply.SYSTEM_STATUS ) );
490         }
491         catch ( IOException e )
492         {
493             throw new TransferFailedException( "Error transferring file via FTP", e );
494         }
495         catch ( ResourceDoesNotExistException e )
496         {
497             return false;
498         }
499     }
500 
501     public boolean supportsDirectoryCopy()
502     {
503         return true;
504     }
505 
506     public void putDirectory( File sourceDirectory, String destinationDirectory )
507         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
508     {
509 
510         // Change to root.
511         try
512         {
513             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
514             {
515                 RepositoryPermissions permissions = getRepository().getPermissions();
516                 if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) )
517                 {
518                     throw new TransferFailedException(
519                         "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" );
520                 }
521 
522                 // try it again sam ...
523                 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
524                 {
525                     throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' "
526                                                            + "is missing and could not get created" );
527                 }
528             }
529         }
530         catch ( IOException e )
531         {
532             throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e );
533         }
534 
535         fireTransferDebug(
536             "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory );
537         ftpRecursivePut( sourceDirectory, destinationDirectory );
538     }
539 
540     private void ftpRecursivePut( File sourceFile, String fileName )
541         throws TransferFailedException
542     {
543         final RepositoryPermissions permissions = repository.getPermissions();
544 
545         fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName );
546 
547         if ( sourceFile.isDirectory() )
548         {
549             if ( !fileName.equals( "." ) )
550             {
551                 try
552                 {
553                     // change directory if it already exists.
554                     if ( !ftp.changeWorkingDirectory( fileName ) )
555                     {
556                         // first, try to create it
557                         if ( makeFtpDirectoryRecursive( fileName, permissions ) )
558                         {
559                             if ( !ftp.changeWorkingDirectory( fileName ) )
560                             {
561                                 throw new TransferFailedException(
562                                     "Unable to change cwd on ftp server to " + fileName + " when processing "
563                                         + sourceFile.getAbsolutePath() );
564                             }
565                         }
566                         else
567                         {
568                             throw new TransferFailedException(
569                                 "Unable to create directory " + fileName + " when processing "
570                                     + sourceFile.getAbsolutePath() );
571                         }
572                     }
573                 }
574                 catch ( IOException e )
575                 {
576                     throw new TransferFailedException(
577                         "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e );
578                 }
579             }
580 
581             File[] files = sourceFile.listFiles();
582             if ( files != null && files.length > 0 )
583             {
584                 fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length );
585 
586                 // Directories first, then files. Let's go deep early.
587                 for ( File file : files )
588                 {
589                     if ( file.isDirectory() )
590                     {
591                         ftpRecursivePut( file, file.getName() );
592                     }
593                 }
594                 for ( File file : files )
595                 {
596                     if ( !file.isDirectory() )
597                     {
598                         ftpRecursivePut( file, file.getName() );
599                     }
600                 }
601             }
602 
603             // Step back up a directory once we're done with the contents of this one.
604             try
605             {
606                 ftp.changeToParentDirectory();
607             }
608             catch ( IOException e )
609             {
610                 throw new TransferFailedException( "IOException caught while attempting to step up to parent directory"
611                                                        + " after successfully processing "
612                                                        + sourceFile.getAbsolutePath(), e );
613             }
614         }
615         else
616         {
617             // Oh how I hope and pray, in denial, but today I am still just a file.
618 
619             FileInputStream sourceFileStream = null;
620             try
621             {
622                 sourceFileStream = new FileInputStream( sourceFile );
623 
624                 // It's a file. Upload it in the current directory.
625                 if ( ftp.storeFile( fileName, sourceFileStream ) )
626                 {
627                     if ( permissions != null )
628                     {
629                         // Process permissions; note that if we get errors or exceptions here, they are ignored.
630                         // This appears to be a conscious decision, based on other parts of this code.
631                         String group = permissions.getGroup();
632                         if ( group != null )
633                         {
634                             try
635                             {
636                                 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
637                             }
638                             catch ( IOException e )
639                             {
640                                 // ignore
641                             }
642                         }
643                         String mode = permissions.getFileMode();
644                         if ( mode != null )
645                         {
646                             try
647                             {
648                                 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
649                             }
650                             catch ( IOException e )
651                             {
652                                 // ignore
653                             }
654                         }
655                     }
656                 }
657                 else
658                 {
659                     String msg =
660                         "Cannot transfer resource:  '" + sourceFile.getAbsolutePath() + "' FTP Server response: "
661                             + ftp.getReplyString();
662                     throw new TransferFailedException( msg );
663                 }
664             }
665             catch ( IOException e )
666             {
667                 throw new TransferFailedException(
668                     "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e );
669             }
670             finally
671             {
672                 IOUtils.closeQuietly( sourceFileStream );
673             }
674 
675         }
676 
677         fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() );
678     }
679 
680     /**
681      * Set the permissions (if given) for the underlying folder.
682      * Note: Since the FTP SITE command might be server dependent, we cannot
683      * rely on the functionality available on each FTP server!
684      * So we can only try and hope it works (and catch away all Exceptions).
685      *
686      * @param permissions group and directory permissions
687      */
688     private void setPermissions( RepositoryPermissions permissions )
689     {
690         if ( permissions != null )
691         {
692             // Process permissions; note that if we get errors or exceptions here, they are ignored.
693             // This appears to be a conscious decision, based on other parts of this code.
694             String group = permissions.getGroup();
695             if ( group != null )
696             {
697                 try
698                 {
699                     ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
700                 }
701                 catch ( IOException e )
702                 {
703                     // ignore
704                 }
705             }
706             String mode = permissions.getDirectoryMode();
707             if ( mode != null )
708             {
709                 try
710                 {
711                     ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
712                 }
713                 catch ( IOException e )
714                 {
715                     // ignore
716                 }
717             }
718         }
719     }
720 
721     /**
722      * Recursively create directories.
723      *
724      * @param fileName    the path to create (might be nested)
725      * @param permissions
726      * @return ok
727      * @throws IOException
728      */
729     private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions )
730         throws IOException
731     {
732         if ( fileName == null || fileName.length() == 0
733             || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar
734         {
735             return false;
736         }
737 
738         int slashPos = fileName.indexOf( "/" );
739         String oldPwd = null;
740         boolean ok = true;
741 
742         if ( slashPos == 0 )
743         {
744             // this is an absolute directory
745             oldPwd = ftp.printWorkingDirectory();
746 
747             // start with the root
748             ftp.changeWorkingDirectory( "/" );
749             fileName = fileName.substring( 1 );
750 
751             // look up the next path separator
752             slashPos = fileName.indexOf( "/" );
753         }
754 
755         if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/'
756         {
757             if ( oldPwd == null )
758             {
759                 oldPwd = ftp.printWorkingDirectory();
760             }
761 
762             String nextDir = fileName.substring( 0, slashPos );
763 
764             // we only create the nextDir if it doesn't yet exist
765             if ( !ftp.changeWorkingDirectory( nextDir ) )
766             {
767                 ok &= ftp.makeDirectory( nextDir );
768             }
769 
770             if ( ok )
771             {
772                 // set the permissions for the freshly created directory
773                 setPermissions( permissions );
774 
775                 ftp.changeWorkingDirectory( nextDir );
776 
777                 // now create the deeper directories
778                 String remainingDirs = fileName.substring( slashPos + 1 );
779                 ok &= makeFtpDirectoryRecursive( remainingDirs, permissions );
780             }
781         }
782         else
783         {
784             ok = ftp.makeDirectory( fileName );
785         }
786 
787         if ( oldPwd != null )
788         {
789             // change back to the old working directory
790             ftp.changeWorkingDirectory( oldPwd );
791         }
792 
793         return ok;
794     }
795 
796     public String getControlEncoding()
797     {
798         return controlEncoding;
799     }
800 
801     public void setControlEncoding( String controlEncoding )
802     {
803         this.controlEncoding = controlEncoding;
804     }
805 }