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