blob: 455d3f475852dbe46718ff4ebc94b25610d58598 [file] [log] [blame]
package org.apache.maven.wagon.providers.ftp;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.commons.io.IOUtils;
import org.apache.commons.net.ProtocolCommandEvent;
import org.apache.commons.net.ProtocolCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.InputData;
import org.apache.maven.wagon.OutputData;
import org.apache.maven.wagon.PathUtils;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.StreamWagon;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.WagonConstants;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.repository.RepositoryPermissions;
import org.apache.maven.wagon.resource.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
* FtpWagon
*
*
* @plexus.component role="org.apache.maven.wagon.Wagon"
* role-hint="ftp"
* instantiation-strategy="per-lookup"
*/
public class FtpWagon
extends StreamWagon
{
private FTPClient ftp;
/**
* @plexus.configuration default-value="true"
*/
private boolean passiveMode = true;
/**
* @plexus.configuration default-value="ISO-8859-1"
*/
private String controlEncoding = FTP.DEFAULT_CONTROL_ENCODING;
public boolean isPassiveMode()
{
return passiveMode;
}
public void setPassiveMode( boolean passiveMode )
{
this.passiveMode = passiveMode;
}
protected void openConnectionInternal()
throws ConnectionException, AuthenticationException
{
AuthenticationInfo authInfo = getAuthenticationInfo();
if ( authInfo == null )
{
throw new IllegalArgumentException( "Authentication Credentials cannot be null for FTP protocol" );
}
if ( authInfo.getUserName() == null )
{
authInfo.setUserName( System.getProperty( "user.name" ) );
}
String username = authInfo.getUserName();
String password = authInfo.getPassword();
if ( username == null )
{
throw new AuthenticationException( "Username not specified for repository " + getRepository().getId() );
}
if ( password == null )
{
throw new AuthenticationException( "Password not specified for repository " + getRepository().getId() );
}
String host = getRepository().getHost();
ftp = new FTPClient();
ftp.setDefaultTimeout( getTimeout() );
ftp.setDataTimeout( getTimeout() );
ftp.setControlEncoding( getControlEncoding() );
ftp.addProtocolCommandListener( new PrintCommandListener( this ) );
try
{
if ( getRepository().getPort() != WagonConstants.UNKNOWN_PORT )
{
ftp.connect( host, getRepository().getPort() );
}
else
{
ftp.connect( host );
}
// After connection attempt, you should check the reply code to
// verify
// success.
int reply = ftp.getReplyCode();
if ( !FTPReply.isPositiveCompletion( reply ) )
{
ftp.disconnect();
throw new AuthenticationException( "FTP server refused connection." );
}
}
catch ( IOException e )
{
if ( ftp.isConnected() )
{
try
{
fireSessionError( e );
ftp.disconnect();
}
catch ( IOException f )
{
// do nothing
}
}
throw new AuthenticationException( "Could not connect to server.", e );
}
try
{
if ( !ftp.login( username, password ) )
{
throw new AuthenticationException( "Cannot login to remote system" );
}
fireSessionDebug( "Remote system is " + ftp.getSystemName() );
// Set to binary mode.
ftp.setFileType( FTP.BINARY_FILE_TYPE );
ftp.setListHiddenFiles( true );
// Use passive mode as default because most of us are
// behind firewalls these days.
if ( isPassiveMode() )
{
ftp.enterLocalPassiveMode();
}
}
catch ( IOException e )
{
throw new ConnectionException( "Cannot login to remote system", e );
}
}
protected void firePutCompleted( Resource resource, File file )
{
try
{
// TODO [BP]: verify the order is correct
ftp.completePendingCommand();
RepositoryPermissions permissions = repository.getPermissions();
if ( permissions != null && permissions.getGroup() != null )
{
// ignore failures
ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + resource.getName() );
}
if ( permissions != null && permissions.getFileMode() != null )
{
// ignore failures
ftp.sendSiteCommand( "CHMOD " + permissions.getFileMode() + " " + resource.getName() );
}
}
catch ( IOException e )
{
// TODO: handle
// michal I am not sure what error means in that context
// I think that we will be able to recover or simply we will fail later on
}
super.firePutCompleted( resource, file );
}
protected void fireGetCompleted( Resource resource, File localFile )
{
try
{
ftp.completePendingCommand();
}
catch ( IOException e )
{
// TODO: handle
// michal I am not sure what error means in that context
// actually I am not even sure why we have to invoke that command
// I think that we will be able to recover or simply we will fail later on
}
super.fireGetCompleted( resource, localFile );
}
public void closeConnection()
throws ConnectionException
{
if ( ftp != null && ftp.isConnected() )
{
try
{
// This is a NPE rethink shutting down the streams
ftp.disconnect();
}
catch ( IOException e )
{
throw new ConnectionException( "Failed to close connection to FTP repository", e );
}
}
}
public void fillOutputData( OutputData outputData )
throws TransferFailedException
{
OutputStream os;
Resource resource = outputData.getResource();
RepositoryPermissions permissions = repository.getPermissions();
try
{
if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
{
throw new TransferFailedException(
"Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
}
String[] dirs = PathUtils.dirnames( resource.getName() );
for ( String dir : dirs )
{
boolean dirChanged = ftp.changeWorkingDirectory( dir );
if ( !dirChanged )
{
// first, try to create it
boolean success = ftp.makeDirectory( dir );
if ( success )
{
if ( permissions != null && permissions.getGroup() != null )
{
// ignore failures
ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() + " " + dir );
}
if ( permissions != null && permissions.getDirectoryMode() != null )
{
// ignore failures
ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() + " " + dir );
}
dirChanged = ftp.changeWorkingDirectory( dir );
}
}
if ( !dirChanged )
{
throw new TransferFailedException( "Unable to create directory " + dir );
}
}
// we come back to original basedir so
// FTP wagon is ready for next requests
if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
{
throw new TransferFailedException( "Unable to return to the base directory" );
}
os = ftp.storeFileStream( resource.getName() );
if ( os == null )
{
String msg =
"Cannot transfer resource: '" + resource + "'. Output stream is null. FTP Server response: "
+ ftp.getReplyString();
throw new TransferFailedException( msg );
}
fireTransferDebug( "resource = " + resource );
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring over FTP", e );
}
outputData.setOutputStream( os );
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
public void fillInputData( InputData inputData )
throws TransferFailedException, ResourceDoesNotExistException
{
InputStream is;
Resource resource = inputData.getResource();
try
{
ftpChangeDirectory( resource );
String filename = PathUtils.filename( resource.getName() );
FTPFile[] ftpFiles = ftp.listFiles( filename );
if ( ftpFiles == null || ftpFiles.length <= 0 )
{
throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
}
long contentLength = ftpFiles[0].getSize();
//@todo check how it works! javadoc of common login says:
// Returns the file timestamp. This usually the last modification time.
//
Calendar timestamp = ftpFiles[0].getTimestamp();
long lastModified = timestamp != null ? timestamp.getTimeInMillis() : 0;
resource.setContentLength( contentLength );
resource.setLastModified( lastModified );
is = ftp.retrieveFileStream( filename );
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring file via FTP", e );
}
inputData.setInputStream( is );
}
private void ftpChangeDirectory( Resource resource )
throws IOException, TransferFailedException, ResourceDoesNotExistException
{
if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
{
throw new ResourceDoesNotExistException(
"Required directory: '" + getRepository().getBasedir() + "' " + "is missing" );
}
String[] dirs = PathUtils.dirnames( resource.getName() );
for ( String dir : dirs )
{
boolean dirChanged = ftp.changeWorkingDirectory( dir );
if ( !dirChanged )
{
String msg = "Resource " + resource + " not found. Directory " + dir + " does not exist";
throw new ResourceDoesNotExistException( msg );
}
}
}
/**
*
*/
public class PrintCommandListener
implements ProtocolCommandListener
{
private FtpWagon wagon;
public PrintCommandListener( FtpWagon wagon )
{
this.wagon = wagon;
}
public void protocolCommandSent( ProtocolCommandEvent event )
{
wagon.fireSessionDebug( "Command sent: " + event.getMessage() );
}
public void protocolReplyReceived( ProtocolCommandEvent event )
{
wagon.fireSessionDebug( "Reply received: " + event.getMessage() );
}
}
protected void fireSessionDebug( String msg )
{
super.fireSessionDebug( msg );
}
public List<String> getFileList( String destinationDirectory )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
Resource resource = new Resource( destinationDirectory );
try
{
ftpChangeDirectory( resource );
String filename = PathUtils.filename( resource.getName() );
FTPFile[] ftpFiles = ftp.listFiles( filename );
if ( ftpFiles == null || ftpFiles.length <= 0 )
{
throw new ResourceDoesNotExistException( "Could not find file: '" + resource + "'" );
}
List<String> ret = new ArrayList<String>();
for ( FTPFile file : ftpFiles )
{
String name = file.getName();
if ( file.isDirectory() && !name.endsWith( "/" ) )
{
name += "/";
}
ret.add( name );
}
return ret;
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring file via FTP", e );
}
}
public boolean resourceExists( String resourceName )
throws TransferFailedException, AuthorizationException
{
Resource resource = new Resource( resourceName );
try
{
ftpChangeDirectory( resource );
String filename = PathUtils.filename( resource.getName() );
int status = ftp.stat( filename );
return ( ( status == FTPReply.FILE_STATUS ) || ( status == FTPReply.DIRECTORY_STATUS ) || ( status
== FTPReply.FILE_STATUS_OK ) // not in the RFC but used by some FTP servers
|| ( status == FTPReply.COMMAND_OK ) // not in the RFC but used by some FTP servers
|| ( status == FTPReply.SYSTEM_STATUS ) );
}
catch ( IOException e )
{
throw new TransferFailedException( "Error transferring file via FTP", e );
}
catch ( ResourceDoesNotExistException e )
{
return false;
}
}
public boolean supportsDirectoryCopy()
{
return true;
}
public void putDirectory( File sourceDirectory, String destinationDirectory )
throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
{
// Change to root.
try
{
if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
{
RepositoryPermissions permissions = getRepository().getPermissions();
if ( !makeFtpDirectoryRecursive( getRepository().getBasedir(), permissions ) )
{
throw new TransferFailedException(
"Required directory: '" + getRepository().getBasedir() + "' " + "could not get created" );
}
// try it again sam ...
if ( !ftp.changeWorkingDirectory( getRepository().getBasedir() ) )
{
throw new TransferFailedException( "Required directory: '" + getRepository().getBasedir() + "' "
+ "is missing and could not get created" );
}
}
}
catch ( IOException e )
{
throw new TransferFailedException( "Cannot change to root path " + getRepository().getBasedir(), e );
}
fireTransferDebug(
"Recursively uploading directory " + sourceDirectory.getAbsolutePath() + " as " + destinationDirectory );
ftpRecursivePut( sourceDirectory, destinationDirectory );
}
private void ftpRecursivePut( File sourceFile, String fileName )
throws TransferFailedException
{
final RepositoryPermissions permissions = repository.getPermissions();
fireTransferDebug( "processing = " + sourceFile.getAbsolutePath() + " as " + fileName );
if ( sourceFile.isDirectory() )
{
if ( !fileName.equals( "." ) )
{
try
{
// change directory if it already exists.
if ( !ftp.changeWorkingDirectory( fileName ) )
{
// first, try to create it
if ( makeFtpDirectoryRecursive( fileName, permissions ) )
{
if ( !ftp.changeWorkingDirectory( fileName ) )
{
throw new TransferFailedException(
"Unable to change cwd on ftp server to " + fileName + " when processing "
+ sourceFile.getAbsolutePath() );
}
}
else
{
throw new TransferFailedException(
"Unable to create directory " + fileName + " when processing "
+ sourceFile.getAbsolutePath() );
}
}
}
catch ( IOException e )
{
throw new TransferFailedException(
"IOException caught while processing path at " + sourceFile.getAbsolutePath(), e );
}
}
File[] files = sourceFile.listFiles();
if ( files != null && files.length > 0 )
{
fireTransferDebug( "listing children of = " + sourceFile.getAbsolutePath() + " found " + files.length );
// Directories first, then files. Let's go deep early.
for ( File file : files )
{
if ( file.isDirectory() )
{
ftpRecursivePut( file, file.getName() );
}
}
for ( File file : files )
{
if ( !file.isDirectory() )
{
ftpRecursivePut( file, file.getName() );
}
}
}
// Step back up a directory once we're done with the contents of this one.
try
{
ftp.changeToParentDirectory();
}
catch ( IOException e )
{
throw new TransferFailedException( "IOException caught while attempting to step up to parent directory"
+ " after successfully processing "
+ sourceFile.getAbsolutePath(), e );
}
}
else
{
// Oh how I hope and pray, in denial, but today I am still just a file.
FileInputStream sourceFileStream = null;
try
{
sourceFileStream = new FileInputStream( sourceFile );
// It's a file. Upload it in the current directory.
if ( ftp.storeFile( fileName, sourceFileStream ) )
{
if ( permissions != null )
{
// Process permissions; note that if we get errors or exceptions here, they are ignored.
// This appears to be a conscious decision, based on other parts of this code.
String group = permissions.getGroup();
if ( group != null )
{
try
{
ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
}
catch ( IOException e )
{
// ignore
}
}
String mode = permissions.getFileMode();
if ( mode != null )
{
try
{
ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
}
catch ( IOException e )
{
// ignore
}
}
}
}
else
{
String msg =
"Cannot transfer resource: '" + sourceFile.getAbsolutePath() + "' FTP Server response: "
+ ftp.getReplyString();
throw new TransferFailedException( msg );
}
sourceFileStream.close();
sourceFileStream = null;
}
catch ( IOException e )
{
throw new TransferFailedException(
"IOException caught while attempting to upload " + sourceFile.getAbsolutePath(), e );
}
finally
{
IOUtils.closeQuietly( sourceFileStream );
}
}
fireTransferDebug( "completed = " + sourceFile.getAbsolutePath() );
}
/**
* Set the permissions (if given) for the underlying folder.
* Note: Since the FTP SITE command might be server dependent, we cannot
* rely on the functionality available on each FTP server!
* So we can only try and hope it works (and catch away all Exceptions).
*
* @param permissions group and directory permissions
*/
private void setPermissions( RepositoryPermissions permissions )
{
if ( permissions != null )
{
// Process permissions; note that if we get errors or exceptions here, they are ignored.
// This appears to be a conscious decision, based on other parts of this code.
String group = permissions.getGroup();
if ( group != null )
{
try
{
ftp.sendSiteCommand( "CHGRP " + permissions.getGroup() );
}
catch ( IOException e )
{
// ignore
}
}
String mode = permissions.getDirectoryMode();
if ( mode != null )
{
try
{
ftp.sendSiteCommand( "CHMOD " + permissions.getDirectoryMode() );
}
catch ( IOException e )
{
// ignore
}
}
}
}
/**
* Recursively create directories.
*
* @param fileName the path to create (might be nested)
* @param permissions
* @return ok
* @throws IOException
*/
private boolean makeFtpDirectoryRecursive( String fileName, RepositoryPermissions permissions )
throws IOException
{
if ( fileName == null || fileName.length() == 0
|| fileName.replace( '/', '_' ).trim().length() == 0 ) // if a string is '/', '//' or similar
{
return false;
}
int slashPos = fileName.indexOf( "/" );
String oldPwd = null;
boolean ok = true;
if ( slashPos == 0 )
{
// this is an absolute directory
oldPwd = ftp.printWorkingDirectory();
// start with the root
ftp.changeWorkingDirectory( "/" );
fileName = fileName.substring( 1 );
// look up the next path separator
slashPos = fileName.indexOf( "/" );
}
if ( slashPos >= 0 && slashPos < ( fileName.length() - 1 ) ) // not only a slash at the end, like in 'newDir/'
{
if ( oldPwd == null )
{
oldPwd = ftp.printWorkingDirectory();
}
String nextDir = fileName.substring( 0, slashPos );
// we only create the nextDir if it doesn't yet exist
if ( !ftp.changeWorkingDirectory( nextDir ) )
{
ok &= ftp.makeDirectory( nextDir );
}
if ( ok )
{
// set the permissions for the freshly created directory
setPermissions( permissions );
ftp.changeWorkingDirectory( nextDir );
// now create the deeper directories
String remainingDirs = fileName.substring( slashPos + 1 );
ok &= makeFtpDirectoryRecursive( remainingDirs, permissions );
}
}
else
{
ok = ftp.makeDirectory( fileName );
}
if ( oldPwd != null )
{
// change back to the old working directory
ftp.changeWorkingDirectory( oldPwd );
}
return ok;
}
public String getControlEncoding()
{
return controlEncoding;
}
public void setControlEncoding( String controlEncoding )
{
this.controlEncoding = controlEncoding;
}
}