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 NullPointerException( "authenticationInfo cannot be null" );
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 = createClient();
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 FTPClient createClient()
191     {
192         return new FTPClient();
193     }
194 
195     protected void firePutCompleted( Resource resource, File file )
196     {
197         try
198         {
199             // TODO [BP]: verify the order is correct
200             ftp.completePendingCommand();
201 
202             RepositoryPermissions permissions = repository.getPermissions();
203 
204             if ( permissions != null && permissions.getGroup() != null )
205             {
206                 // ignore failures
207                 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() );
208             }
209 
210             if ( permissions != null && permissions.getFileMode() != null )
211             {
212                 // ignore failures
213                 ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() );
214             }
215         }
216         catch ( IOException e )
217         {
218             // TODO: handle
219             // michal I am not sure  what error means in that context
220             // I think that we will be able to recover or simply we will fail later on
221         }
222 
223         super.firePutCompleted( resource, file );
224     }
225 
226     protected void fireGetCompleted( Resource resource, File localFile )
227     {
228         try
229         {
230             ftp.completePendingCommand();
231         }
232         catch ( IOException e )
233         {
234             // TODO: handle
235             // michal I am not sure  what error means in that context
236             // actually I am not even sure why we have to invoke that command
237             // I think that we will be able to recover or simply we will fail later on
238         }
239         super.fireGetCompleted( resource, localFile );
240     }
241 
242     public void closeConnection()
243         throws ConnectionException
244     {
245         if ( ftp != null && ftp.isConnected() )
246         {
247             try
248             {
249                 // This is a NPE rethink shutting down the streams
250                 ftp.disconnect();
251             }
252             catch ( IOException e )
253             {
254                 throw new ConnectionException( "Failed to close connection to FTP repository", e );
255             }
256         }
257     }
258 
259     public void fillOutputData( OutputData outputData )
260         throws TransferFailedException
261     {
262         OutputStream os;
263 
264         Resource resource = outputData.getResource();
265 
266         RepositoryPermissions permissions = repository.getPermissions();
267 
268         try
269         {
270             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
271             {
272                 throw new TransferFailedException(
273                     "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
274             }
275 
276             String[] dirs = PathUtils.dirnames( resource.getName() );
277 
278             for ( String dir : dirs )
279             {
280                 boolean dirChanged = ftp.changeWorkingDirectory( dir );
281 
282                 if ( !dirChanged )
283                 {
284                     // first, try to create it
285                     boolean success = ftp.makeDirectory( dir );
286 
287                     if ( success )
288                     {
289                         if ( permissions != null && permissions.getGroup() != null )
290                         {
291                             // ignore failures
292                             ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir );
293                         }
294 
295                         if ( permissions != null && permissions.getDirectoryMode() != null )
296                         {
297                             // ignore failures
298                             ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir );
299                         }
300 
301                         dirChanged = ftp.changeWorkingDirectory( dir );
302                     }
303                 }
304 
305                 if ( !dirChanged )
306                 {
307                     throw new TransferFailedException( "Unable to create directory " + dir );
308                 }
309             }
310 
311             // we come back to original basedir so
312             // FTP wagon is ready for next requests
313             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
314             {
315                 throw new TransferFailedException( "Unable to return to the base directory" );
316             }
317 
318             os = ftp.storeFileStream( resource.getName() );
319 
320             if ( os == null )
321             {
322                 String msg =
323                     "Cannot transfer resource:  '" + resource + "'. Output stream is null. FTP Server response: "
324                         + ftp.getReplyString();
325 
326                 throw new TransferFailedException( msg );
327 
328             }
329 
330             fireTransferDebug( "resource = " + resource );
331 
332         }
333         catch ( IOException e )
334         {
335             throw new TransferFailedException( "Error transferring over FTP", e );
336         }
337 
338         outputData.setOutputStream( os );
339 
340     }
341 
342     // ----------------------------------------------------------------------
343     //
344     // ----------------------------------------------------------------------
345 
346     public void fillInputData( InputData inputData )
347         throws TransferFailedException, ResourceDoesNotExistException
348     {
349         InputStream is;
350 
351         Resource resource = inputData.getResource();
352 
353         try
354         {
355             ftpChangeDirectory( resource );
356 
357             String filename = PathUtils.filename( resource.getName() );
358             FTPFile[] ftpFiles = ftp.listFiles( filename );
359 
360             if ( ftpFiles == null || ftpFiles.length <= 0 )
361             {
362                 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
363             }
364 
365             long contentLength = ftpFiles[0].getSize();
366 
367             //@todo check how it works! javadoc of common login says:
368             // Returns the file timestamp. This usually the last modification time.
369             //
370             Calendar timestamp = ftpFiles[0].getTimestamp();
371             long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0;
372 
373             resource.setContentLength( contentLength );
374 
375             resource.setLastModified( lastModified );
376 
377             is = ftp.retrieveFileStream( filename );
378         }
379         catch ( IOException e )
380         {
381             throw new TransferFailedException( "Error transferring file via FTP", e );
382         }
383 
384         inputData.setInputStream( is );
385     }
386 
387     private void ftpChangeDirectory( Resource resource )
388         throws IOException, TransferFailedException, ResourceDoesNotExistException
389     {
390         if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
391         {
392             throw new ResourceDoesNotExistException(
393                 "Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
394         }
395 
396         String[] dirs = PathUtils.dirnames( resource.getName() );
397 
398         for ( String dir : dirs )
399         {
400             boolean dirChanged = ftp.changeWorkingDirectory( dir );
401 
402             if ( !dirChanged )
403             {
404                 String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist";
405 
406                 throw new ResourceDoesNotExistException( msg );
407             }
408         }
409     }
410 
411     /**
412      *
413      */
414     public class PrintCommandListener
415         implements ProtocolCommandListener
416     {
417         private FtpWagon wagon;
418 
419         public PrintCommandListener( FtpWagon wagon )
420         {
421             this.wagon = wagon;
422         }
423 
424         public void protocolCommandSent( ProtocolCommandEvent event )
425         {
426             wagon.fireSessionDebug( "Command sent: " + event.getMessage() );
427 
428         }
429 
430         public void protocolReplyReceived( ProtocolCommandEvent event )
431         {
432             wagon.fireSessionDebug( "Reply received: " + event.getMessage() );
433         }
434     }
435 
436     protected void fireSessionDebug( String msg )
437     {
438         super.fireSessionDebug( msg );
439     }
440 
441     public List<String> getFileList( String destinationDirectory )
442         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
443     {
444         Resource resource = new Resource( destinationDirectory );
445 
446         try
447         {
448             ftpChangeDirectory( resource );
449 
450             String filename = PathUtils.filename( resource.getName() );
451             FTPFile[] ftpFiles = ftp.listFiles( filename );
452 
453             if ( ftpFiles == null || ftpFiles.length <= 0 )
454             {
455                 throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
456             }
457 
458             List<String> ret = new ArrayList<String>();
459             for ( FTPFile file : ftpFiles )
460             {
461                 String name = file.getName();
462 
463                 if ( file.isDirectory() && !name.endsWith( "/" ) )
464                 {
465                     name += "/";
466                 }
467 
468                 ret.add( name );
469             }
470 
471             return ret;
472         }
473         catch ( IOException e )
474         {
475             throw new TransferFailedException( "Error transferring file via FTP", e );
476         }
477     }
478 
479     public boolean resourceExists( String resourceName )
480         throws TransferFailedException, AuthorizationException
481     {
482         Resource resource = new Resource( resourceName );
483 
484         try
485         {
486             ftpChangeDirectory( resource );
487 
488             String filename = PathUtils.filename( resource.getName() );
489             int status = ftp.stat( filename );
490 
491             return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status
492                 == FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers
493                 || ( status == FTPReply.COMMAND_OK )     // not in the RFC but used by some FTP servers
494                 || ( status == FTPReply.SYSTEM_STATUS ) );
495         }
496         catch ( IOException e )
497         {
498             throw new TransferFailedException( "Error transferring file via FTP", e );
499         }
500         catch ( ResourceDoesNotExistException e )
501         {
502             return false;
503         }
504     }
505 
506     public boolean supportsDirectoryCopy()
507     {
508         return true;
509     }
510 
511     public void putDirectory( File sourceDirectory, String destinationDirectory )
512         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
513     {
514 
515         // Change to root.
516         try
517         {
518             if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
519             {
520                 RepositoryPermissions permissions = getRepository().getPermissions();
521                 if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) )
522                 {
523                     throw new TransferFailedException(
524                         "Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" );
525                 }
526 
527                 // try it again sam ...
528                 if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
529                 {
530                     throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' "
531                                                            + "is missing and could not get created" );
532                 }
533             }
534         }
535         catch ( IOException e )
536         {
537             throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e );
538         }
539 
540         fireTransferDebug(
541             "Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory );
542         ftpRecursivePut( sourceDirectory, destinationDirectory );
543     }
544 
545     private void ftpRecursivePut( File sourceFile, String fileName )
546         throws TransferFailedException
547     {
548         final RepositoryPermissions permissions = repository.getPermissions();
549 
550         fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName );
551 
552         if ( sourceFile.isDirectory() )
553         {
554             if ( !fileName.equals( "." ) )
555             {
556                 try
557                 {
558                     // change directory if it already exists.
559                     if ( !ftp.changeWorkingDirectory( fileName ) )
560                     {
561                         // first, try to create it
562                         if ( makeFtpDirectoryRecursive( fileName, permissions ) )
563                         {
564                             if ( !ftp.changeWorkingDirectory( fileName ) )
565                             {
566                                 throw new TransferFailedException(
567                                     "Unable to change cwd on ftp server to " + fileName + " when processing "
568                                         + sourceFile.getAbsolutePath() );
569                             }
570                         }
571                         else
572                         {
573                             throw new TransferFailedException(
574                                 "Unable to create directory " + fileName + " when processing "
575                                     + sourceFile.getAbsolutePath() );
576                         }
577                     }
578                 }
579                 catch ( IOException e )
580                 {
581                     throw new TransferFailedException(
582                         "IOException caught while processing path at " + sourceFile.getAbsolutePath(), e );
583                 }
584             }
585 
586             File[] files = sourceFile.listFiles();
587             if ( files != null && files.length > 0 )
588             {
589                 fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length );
590 
591                 // Directories first, then files. Let's go deep early.
592                 for ( File file : files )
593                 {
594                     if ( file.isDirectory() )
595                     {
596                         ftpRecursivePut( file, file.getName() );
597                     }
598                 }
599                 for ( File file : files )
600                 {
601                     if ( !file.isDirectory() )
602                     {
603                         ftpRecursivePut( file, file.getName() );
604                     }
605                 }
606             }
607 
608             // Step back up a directory once we're done with the contents of this one.
609             try
610             {
611                 ftp.changeToParentDirectory();
612             }
613             catch ( IOException e )
614             {
615                 throw new TransferFailedException( "IOException caught while attempting to step up to parent directory"
616                                                        + " after successfully processing "
617                                                        + sourceFile.getAbsolutePath(), e );
618             }
619         }
620         else
621         {
622             // Oh how I hope and pray, in denial, but today I am still just a file.
623 
624             FileInputStream sourceFileStream = null;
625             try
626             {
627                 sourceFileStream = new FileInputStream( sourceFile );
628 
629                 // It's a file. Upload it in the current directory.
630                 if ( ftp.storeFile( fileName, sourceFileStream ) )
631                 {
632                     if ( permissions != null )
633                     {
634                         // Process permissions; note that if we get errors or exceptions here, they are ignored.
635                         // This appears to be a conscious decision, based on other parts of this code.
636                         String group = permissions.getGroup();
637                         if ( group != null )
638                         {
639                             try
640                             {
641                                 ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
642                             }
643                             catch ( IOException e )
644                             {
645                                 // ignore
646                             }
647                         }
648                         String mode = permissions.getFileMode();
649                         if ( mode != null )
650                         {
651                             try
652                             {
653                                 ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
654                             }
655                             catch ( IOException e )
656                             {
657                                 // ignore
658                             }
659                         }
660                     }
661                 }
662                 else
663                 {
664                     String msg =
665                         "Cannot transfer resource:  '" + sourceFile.getAbsolutePath() + "' FTP Server response: "
666                             + ftp.getReplyString();
667                     throw new TransferFailedException( msg );
668                 }
669 
670                 sourceFileStream.close();
671                 sourceFileStream = null;
672             }
673             catch ( IOException e )
674             {
675                 throw new TransferFailedException(
676                     "IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e );
677             }
678             finally
679             {
680                 IOUtils.closeQuietly( sourceFileStream );
681             }
682 
683         }
684 
685         fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() );
686     }
687 
688     /**
689      * Set the permissions (if given) for the underlying folder.
690      * Note: Since the FTP SITE command might be server dependent, we cannot
691      * rely on the functionality available on each FTP server!
692      * So we can only try and hope it works (and catch away all Exceptions).
693      *
694      * @param permissions group and directory permissions
695      */
696     private void setPermissions( RepositoryPermissions permissions )
697     {
698         if ( permissions != null )
699         {
700             // Process permissions; note that if we get errors or exceptions here, they are ignored.
701             // This appears to be a conscious decision, based on other parts of this code.
702             String group = permissions.getGroup();
703             if ( group != null )
704             {
705                 try
706                 {
707                     ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
708                 }
709                 catch ( IOException e )
710                 {
711                     // ignore
712                 }
713             }
714             String mode = permissions.getDirectoryMode();
715             if ( mode != null )
716             {
717                 try
718                 {
719                     ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
720                 }
721                 catch ( IOException e )
722                 {
723                     // ignore
724                 }
725             }
726         }
727     }
728 
729     /**
730      * Recursively create directories.
731      *
732      * @param fileName    the path to create (might be nested)
733      * @param permissions
734      * @return ok
735      * @throws IOException
736      */
737     private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions )
738         throws IOException
739     {
740         if ( fileName == null || fileName.length() == 0
741             || fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar
742         {
743             return false;
744         }
745 
746         int slashPos = fileName.indexOf( "/" );
747         String oldPwd = null;
748         boolean ok = true;
749 
750         if ( slashPos == 0 )
751         {
752             // this is an absolute directory
753             oldPwd = ftp.printWorkingDirectory();
754 
755             // start with the root
756             ftp.changeWorkingDirectory( "/" );
757             fileName = fileName.substring( 1 );
758 
759             // look up the next path separator
760             slashPos = fileName.indexOf( "/" );
761         }
762 
763         if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/'
764         {
765             if ( oldPwd == null )
766             {
767                 oldPwd = ftp.printWorkingDirectory();
768             }
769 
770             String nextDir = fileName.substring( 0, slashPos );
771 
772             // we only create the nextDir if it doesn't yet exist
773             if ( !ftp.changeWorkingDirectory( nextDir ) )
774             {
775                 ok &= ftp.makeDirectory( nextDir );
776             }
777 
778             if ( ok )
779             {
780                 // set the permissions for the freshly created directory
781                 setPermissions( permissions );
782 
783                 ftp.changeWorkingDirectory( nextDir );
784 
785                 // now create the deeper directories
786                 String remainingDirs = fileName.substring( slashPos + 1 );
787                 ok &= makeFtpDirectoryRecursive( remainingDirs, permissions );
788             }
789         }
790         else
791         {
792             ok = ftp.makeDirectory( fileName );
793         }
794 
795         if ( oldPwd != null )
796         {
797             // change back to the old working directory
798             ftp.changeWorkingDirectory( oldPwd );
799         }
800 
801         return ok;
802     }
803 
804     public String getControlEncoding()
805     {
806         return controlEncoding;
807     }
808 
809     public void setControlEncoding( String controlEncoding )
810     {
811         this.controlEncoding = controlEncoding;
812     }
813 }