blob: 7fd651d478e371f029dba4e84e8bd96dd0642553 [file] [log] [blame]
/*
* 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.
*/
package org.apache.maven.wagon.providers.ftp;
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;
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;
/**
* 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;
}
@Override
protected void openConnectionInternal() throws ConnectionException, AuthenticationException {
AuthenticationInfo authInfo = getAuthenticationInfo();
if (authInfo == null) {
throw new NullPointerException("authenticationInfo cannot be null");
}
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 = createClient();
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 FTPClient createClient() {
return new FTPClient();
}
@Override
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);
}
@Override
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);
}
@Override
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);
}
}
}
@Override
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);
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
@Override
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;
}
@Override
public void protocolCommandSent(ProtocolCommandEvent event) {
wagon.fireSessionDebug("Command sent: " + event.getMessage());
}
@Override
public void protocolReplyReceived(ProtocolCommandEvent event) {
wagon.fireSessionDebug("Reply received: " + event.getMessage());
}
}
@Override
protected void fireSessionDebug(String msg) {
super.fireSessionDebug(msg);
}
@Override
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<>();
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);
}
}
@Override
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;
}
}
@Override
public boolean supportsDirectoryCopy() {
return true;
}
@Override
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);
boolean changedDir = false;
// we only create the nextDir if it doesn't yet exist
if (!ftp.changeWorkingDirectory(nextDir)) {
ok &= ftp.makeDirectory(nextDir);
} else {
changedDir = true;
}
if (ok) {
// set the permissions for the freshly created directory
setPermissions(permissions);
if (!changedDir) {
ftp.changeWorkingDirectory(nextDir);
}
// now create the deeper directories
final 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;
}
}