001package org.apache.maven.wagon.providers.ftp;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.commons.io.IOUtils;
023import org.apache.commons.net.ProtocolCommandEvent;
024import org.apache.commons.net.ProtocolCommandListener;
025import org.apache.commons.net.ftp.FTP;
026import org.apache.commons.net.ftp.FTPClient;
027import org.apache.commons.net.ftp.FTPFile;
028import org.apache.commons.net.ftp.FTPReply;
029import org.apache.maven.wagon.ConnectionException;
030import org.apache.maven.wagon.InputData;
031import org.apache.maven.wagon.OutputData;
032import org.apache.maven.wagon.PathUtils;
033import org.apache.maven.wagon.ResourceDoesNotExistException;
034import org.apache.maven.wagon.StreamWagon;
035import org.apache.maven.wagon.TransferFailedException;
036import org.apache.maven.wagon.WagonConstants;
037import org.apache.maven.wagon.authentication.AuthenticationException;
038import org.apache.maven.wagon.authentication.AuthenticationInfo;
039import org.apache.maven.wagon.authorization.AuthorizationException;
040import org.apache.maven.wagon.repository.RepositoryPermissions;
041import org.apache.maven.wagon.resource.Resource;
042
043import java.io.File;
044import java.io.FileInputStream;
045import java.io.IOException;
046import java.io.InputStream;
047import java.io.OutputStream;
048import java.util.ArrayList;
049import java.util.Calendar;
050import java.util.List;
051
052/**
053 * FtpWagon
054 *
055 *
056 * @plexus.component role="org.apache.maven.wagon.Wagon"
057 * role-hint="ftp"
058 * instantiation-strategy="per-lookup"
059 */
060public class FtpWagon
061    extends StreamWagon
062{
063    private FTPClient ftp;
064
065    /**
066     * @plexus.configuration default-value="true"
067     */
068    private boolean passiveMode = true;
069
070    /**
071     * @plexus.configuration default-value="ISO-8859-1"
072     */
073    private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING;
074
075    public boolean isPassiveMode()
076    {
077        return passiveMode;
078    }
079
080    public void setPassiveMode( boolean passiveMode )
081    {
082        this.passiveMode = passiveMode;
083    }
084
085    protected void openConnectionInternal()
086        throws ConnectionException, AuthenticationException
087    {
088        AuthenticationInfo authInfo = getAuthenticationInfo();
089
090        if ( authInfo == null )
091        {
092            throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" );
093        }
094
095        if ( authInfo.getUserName() == null )
096        {
097            authInfo.setUserName( System.getProperty( "user.name" ) );
098        }
099
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}