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/**
054 * FtpWagon
055 *
056 *
057 * @plexus.component role="org.apache.maven.wagon.Wagon"
058 * role-hint="ftp"
059 * instantiation-strategy="per-lookup"
060 */
061public class FtpWagon
062    extends StreamWagon
063{
064    private FTPClient ftp;
065
066    /**
067     * @plexus.configuration default-value="true"
068     */
069    private boolean passiveMode = true;
070
071    /**
072     * @plexus.configuration default-value="ISO-8859-1"
073     */
074    private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING;
075
076    public boolean isPassiveMode()
077    {
078        return passiveMode;
079    }
080
081    public void setPassiveMode( boolean passiveMode )
082    {
083        this.passiveMode = passiveMode;
084    }
085
086    @Override
087    protected void openConnectionInternal()
088        throws ConnectionException, AuthenticationException
089    {
090        AuthenticationInfo authInfo = getAuthenticationInfo();
091
092        if ( authInfo == null )
093        {
094            throw new NullPointerException( "authenticationInfo cannot be null" );
095        }
096
097        if ( authInfo.getUserName() == null )
098        {
099            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}