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 NullPointerException( "authenticationInfo cannot be null" );
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 = 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}