blob: 90bb9a1845d0a13194b0296ee293ad54569e206e [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
*
* https://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.tools.ant.taskdefs.optional.net;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.types.EnumeratedAttribute;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.selectors.SelectorUtils;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.RetryHandler;
import org.apache.tools.ant.util.Retryable;
import org.apache.tools.ant.util.VectorSet;
/**
* Basic FTP client. Performs the following actions:
* <ul>
* <li><strong>send</strong> - send files to a remote server. This is the
* default action.</li>
* <li><strong>get</strong> - retrieve files from a remote server.</li>
* <li><strong>del</strong> - delete files from a remote server.</li>
* <li><strong>list</strong> - create a file listing.</li>
* <li><strong>chmod</strong> - change unix file permissions.</li>
* <li><strong>rmdir</strong> - remove directories, if empty, from a
* remote server.</li>
* </ul>
* <strong>Note:</strong> Some FTP servers - notably the Solaris server - seem
* to hold data ports open after a "retr" operation, allowing them to timeout
* instead of shutting them down cleanly. This happens in active or passive
* mode, and the ports will remain open even after ending the FTP session. FTP
* "send" operations seem to close ports immediately. This behavior may cause
* problems on some systems when downloading large sets of files.
*
* @since Ant 1.3
*/
public class FTP extends Task implements FTPTaskConfig {
protected static final int SEND_FILES = 0;
protected static final int GET_FILES = 1;
protected static final int DEL_FILES = 2;
protected static final int LIST_FILES = 3;
protected static final int MK_DIR = 4;
protected static final int CHMOD = 5;
protected static final int RM_DIR = 6;
protected static final int SITE_CMD = 7;
/** return code of ftp */
private static final int CODE_521 = 521;
private static final int CODE_550 = 550;
private static final int CODE_553 = 553;
/** adjust uptodate calculations where server timestamps are HH:mm and client's
* are HH:mm:ss */
private static final long GRANULARITY_MINUTE = 60000L;
/** Date formatter used in logging, note not thread safe! */
private static final SimpleDateFormat TIMESTAMP_LOGGING_SDF =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/** Default port for FTP */
public static final int DEFAULT_FTP_PORT = 21;
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
private String remotedir;
private String server;
private String userid;
private String password;
private String account;
private File listing;
private boolean binary = true;
private boolean passive = false;
private boolean verbose = false;
private boolean newerOnly = false;
private long timeDiffMillis = 0;
private long granularityMillis = 0L;
private boolean timeDiffAuto = false;
private int action = SEND_FILES;
private Vector<FileSet> filesets = new Vector<>();
private Set<File> dirCache = new HashSet<>();
private int transferred = 0;
private String remoteFileSep = "/";
private int port = DEFAULT_FTP_PORT;
private boolean skipFailedTransfers = false;
private int skipped = 0;
private boolean ignoreNoncriticalErrors = false;
private boolean preserveLastModified = false;
private String chmod = null;
private String umask = null;
private FTPSystemType systemTypeKey = FTPSystemType.getDefault();
private String defaultDateFormatConfig = null;
private String recentDateFormatConfig = null;
private LanguageCode serverLanguageCodeConfig = LanguageCode.getDefault();
private String serverTimeZoneConfig = null;
private String shortMonthNamesConfig = null;
private Granularity timestampGranularity = Granularity.getDefault();
private boolean isConfigurationSet = false;
private int retriesAllowed = 0;
private String siteCommand = null;
private String initialSiteCommand = null;
private boolean enableRemoteVerification = true;
private int dataTimeout = -1;
private int wakeUpTransferInterval = -1;
private long lastWakeUpTime = 0;
protected static final String[] ACTION_STRS = {//NOSONAR
"sending",
"getting",
"deleting",
"listing",
"making directory",
"chmod",
"removing",
"site"
};
protected static final String[] COMPLETED_ACTION_STRS = {//NOSONAR
"sent",
"retrieved",
"deleted",
"listed",
"created directory",
"mode changed",
"removed",
"site command executed"
};
protected static final String[] ACTION_TARGET_STRS = {//NOSONAR
"files",
"files",
"files",
"files",
"directory",
"files",
"directories",
"site command"
};
/**
* internal class providing a File-like interface to some of the information
* available from the FTP server
*
*/
protected static class FTPFileProxy extends File {
private static final long serialVersionUID = 1L;
private final FTPFile file;
private final String[] parts;
private final String name;
/**
* creates a proxy to a FTP file
* @param file FTPFile
*/
public FTPFileProxy(FTPFile file) {
super(file.getName());
name = file.getName();
this.file = file;
parts = FileUtils.getPathStack(name);
}
/**
* creates a proxy to a FTP directory
* @param completePath the remote directory.
*/
public FTPFileProxy(String completePath) {
super(completePath);
file = null;
name = completePath;
parts = FileUtils.getPathStack(completePath);
}
/* (non-Javadoc)
* @see java.io.File#exists()
*/
@Override
public boolean exists() {
return true;
}
/* (non-Javadoc)
* @see java.io.File#getAbsolutePath()
*/
@Override
public String getAbsolutePath() {
return name;
}
/* (non-Javadoc)
* @see java.io.File#getName()
*/
@Override
public String getName() {
return parts.length > 0 ? parts[parts.length - 1] : name;
}
/* (non-Javadoc)
* @see java.io.File#getParent()
*/
@Override
public String getParent() {
return File.separator + String.join(File.separator, parts);
}
/* (non-Javadoc)
* @see java.io.File#getPath()
*/
@Override
public String getPath() {
return name;
}
/**
* FTP files are stored as absolute paths
* @return true
*/
@Override
public boolean isAbsolute() {
return true;
}
/* (non-Javadoc)
* @see java.io.File#isDirectory()
*/
@Override
public boolean isDirectory() {
return file == null;
}
/* (non-Javadoc)
* @see java.io.File#isFile()
*/
@Override
public boolean isFile() {
return file != null;
}
/**
* FTP files cannot be hidden
*
* @return false
*/
@Override
public boolean isHidden() {
return false;
}
/* (non-Javadoc)
* @see java.io.File#lastModified()
*/
@Override
public long lastModified() {
if (file != null) {
return file.getTimestamp().getTimeInMillis();
}
return 0;
}
/* (non-Javadoc)
* @see java.io.File#length()
*/
@Override
public long length() {
if (file != null) {
return file.getSize();
}
return 0;
}
}
/**
* internal class allowing to read the contents of a remote file system
* using the FTP protocol
* used in particular for ftp get operations
* differences with DirectoryScanner
* "" (the root of the fileset) is never included in the included directories
* followSymlinks defaults to false
*/
protected class FTPDirectoryScanner extends DirectoryScanner {
// CheckStyle:VisibilityModifier OFF - bc
protected FTPClient ftp = null;
// CheckStyle:VisibilityModifier ON
private String rootPath = null;
/**
* since ant 1.6
* this flag should be set to true on UNIX and can save scanning time
*/
private boolean remoteSystemCaseSensitive = false;
private boolean remoteSensitivityChecked = false;
/**
* constructor
* @param ftp ftpclient object
*/
public FTPDirectoryScanner(FTPClient ftp) {
super();
this.ftp = ftp;
this.setFollowSymlinks(false);
}
/**
* scans the remote directory,
* storing internally the included files, directories, ...
*/
@Override
public void scan() {
if (includes == null) {
// No includes supplied, so set it to 'matches all'
includes = new String[1];
includes[0] = "**";
}
if (excludes == null) {
excludes = new String[0];
}
filesIncluded = new VectorSet<>();
filesNotIncluded = new Vector<>();
filesExcluded = new VectorSet<>();
dirsIncluded = new VectorSet<>();
dirsNotIncluded = new Vector<>();
dirsExcluded = new VectorSet<>();
try {
String cwd = ftp.printWorkingDirectory();
// always start from the current ftp working dir
forceRemoteSensitivityCheck();
checkIncludePatterns();
clearCaches();
ftp.changeWorkingDirectory(cwd);
} catch (IOException e) {
throw new BuildException("Unable to scan FTP server: ", e);
}
}
/**
* this routine is actually checking all the include patterns in
* order to avoid scanning everything under base dir
* @since ant1.6
*/
private void checkIncludePatterns() {
Map<String, String> newroots = new HashMap<>();
// put in the newroots vector the include patterns without
// wildcard tokens
for (String include : includes) {
String newpattern
= SelectorUtils.rtrimWildcardTokens(include);
newroots.put(newpattern, include);
}
if (remotedir == null) {
try {
remotedir = ftp.printWorkingDirectory();
} catch (IOException e) {
throw new BuildException("could not read current ftp directory",
getLocation());
}
}
AntFTPFile baseFTPFile = new AntFTPRootFile(ftp, remotedir);
rootPath = baseFTPFile.getAbsolutePath();
// construct it
if (newroots.containsKey("")) {
// we are going to scan everything anyway
scandir(rootPath, "", true);
} else {
// only scan directories that can include matched files or
// directories
newroots.forEach((k, v) -> scanRoots(baseFTPFile, k, v));
}
}
private void scanRoots(AntFTPFile baseFTPFile, String currentelement,
String originalpattern) {
AntFTPFile myfile = new AntFTPFile(baseFTPFile, currentelement);
boolean isOK = true;
boolean traversesSymlinks = false;
String path = null;
if (myfile.exists()) {
forceRemoteSensitivityCheck();
if (remoteSensitivityChecked
&& remoteSystemCaseSensitive && isFollowSymlinks()) {
// cool case,
//we do not need to scan all the subdirs in the relative path
path = myfile.getFastRelativePath();
} else {
// may be on a case insensitive file system. We want
// the results to show what's really on the disk, so
// we need to double check.
try {
path = myfile.getRelativePath();
traversesSymlinks = myfile.isTraverseSymlinks();
} catch (IOException be) {
throw new BuildException(be, getLocation());
} catch (BuildException be) {
isOK = false;
}
}
} else {
isOK = false;
}
if (isOK) {
currentelement = path.replace(remoteFileSep.charAt(0), File.separatorChar);
if (!isFollowSymlinks() && traversesSymlinks) {
return;
}
if (myfile.isDirectory()) {
if (isIncluded(currentelement) && !currentelement.isEmpty()) {
accountForIncludedDir(currentelement, myfile, true);
} else {
if (!currentelement.isEmpty()
&& currentelement.charAt(currentelement.length() - 1)
!= File.separatorChar) {
currentelement += File.separatorChar;
}
scandir(myfile.getAbsolutePath(), currentelement, true);
}
} else if (isCaseSensitive && originalpattern.equals(currentelement)) {
accountForIncludedFile(currentelement);
} else if (!isCaseSensitive && originalpattern.equalsIgnoreCase(currentelement)) {
accountForIncludedFile(currentelement);
}
}
}
/**
* scans a particular directory. populates the scannedDirs cache.
*
* @param dir directory to scan
* @param vpath relative path to the base directory of the remote fileset
* always ended with a File.separator
* @param fast seems to be always true in practice
*/
protected void scandir(String dir, String vpath, boolean fast) {
// avoid double scanning of directories, can only happen in fast mode
if (fast && hasBeenScanned(vpath)) {
return;
}
try {
if (!ftp.changeWorkingDirectory(dir)) {
return;
}
String completePath = null;
if (!vpath.isEmpty()) {
completePath = rootPath + remoteFileSep
+ vpath.replace(File.separatorChar, remoteFileSep.charAt(0));
} else {
completePath = rootPath;
}
FTPFile[] newfiles = listFiles(completePath, false);
if (newfiles == null) {
ftp.changeToParentDirectory();
return;
}
for (FTPFile file : newfiles) {
if (file != null
&& !".".equals(file.getName())
&& !"..".equals(file.getName())) {
String name = vpath + file.getName();
scannedDirs.put(name, new FTPFileProxy(file));
if (isFunctioningAsDirectory(ftp, dir, file)) {
boolean slowScanAllowed = true;
if (!isFollowSymlinks() && file.isSymbolicLink()) {
dirsExcluded.addElement(name);
slowScanAllowed = false;
} else if (isIncluded(name)) {
accountForIncludedDir(name,
new AntFTPFile(ftp, file, completePath), fast);
} else {
dirsNotIncluded.addElement(name);
if (fast && couldHoldIncluded(name)) {
scandir(file.getName(),
name + File.separator, fast);
}
}
if (!fast && slowScanAllowed) {
scandir(file.getName(),
name + File.separator, fast);
}
} else {
if (!isFollowSymlinks() && file.isSymbolicLink()) {
filesExcluded.addElement(name);
} else {
// at this point, it's either a symbolic link or a file,
// but not a directory, so we include it
accountForIncludedFile(name);
}
}
}
if (wakeUpTransferInterval > 0) {
if (wakeUpTransferIntervalExpired()) {
getProject().log("wakeUpTransferInterval is reached,"
+ " trigger a data connection ", Project.MSG_DEBUG);
// send a minimalist command to trigger a data connection
ftp.listFiles(file.getName());
}
}
}
ftp.changeToParentDirectory();
} catch (FTPConnectionClosedException ftpcce) {
throw new BuildException("Error while communicating with FTP "
+ "server: ", ftpcce);
} catch (IOException e) {
throw new BuildException("Error while communicating with FTP "
+ "server: ", e);
}
}
/**
* process included file
* @param name path of the file relative to the directory of the fileset
*/
private void accountForIncludedFile(String name) {
if (!filesIncluded.contains(name)
&& !filesExcluded.contains(name)) {
if (isIncluded(name)) {
if (!isExcluded(name)
&& isSelected(name, scannedDirs.get(name))) {
filesIncluded.addElement(name);
} else {
filesExcluded.addElement(name);
}
} else {
filesNotIncluded.addElement(name);
}
}
}
/**
*
* @param name path of the directory relative to the directory of
* the fileset
* @param file directory as file
* @param fast boolean
*/
private void accountForIncludedDir(String name, AntFTPFile file, boolean fast) {
if (!dirsIncluded.contains(name)
&& !dirsExcluded.contains(name)) {
if (!isExcluded(name)) {
if (fast) {
if (file.isSymbolicLink()) {
try {
file.getClient().changeWorkingDirectory(file.curpwd);
} catch (IOException ioe) {
throw new BuildException("could not change directory to curpwd");
}
scandir(file.getLink(),
name + File.separator, fast);
} else {
try {
file.getClient().changeWorkingDirectory(file.curpwd);
} catch (IOException ioe) {
throw new BuildException("could not change directory to curpwd");
}
scandir(file.getName(),
name + File.separator, fast);
}
}
dirsIncluded.addElement(name);
} else {
dirsExcluded.addElement(name);
if (fast && couldHoldIncluded(name)) {
try {
file.getClient().changeWorkingDirectory(file.curpwd);
} catch (IOException ioe) {
throw new BuildException("could not change directory to curpwd");
}
scandir(file.getName(),
name + File.separator, fast);
}
}
}
}
/**
* temporary table to speed up the various scanning methods below
*
* @since Ant 1.6
*/
private Map<String, FTPFile[]> fileListMap = new HashMap<>();
/**
* List of all scanned directories.
*
* @since Ant 1.6
*/
private Map<String, FTPFileProxy> scannedDirs = new HashMap<>();
/**
* Has the directory with the given path relative to the base
* directory already been scanned?
*
* @since Ant 1.6
*/
private boolean hasBeenScanned(String vpath) {
return scannedDirs.containsKey(vpath);
}
/**
* Clear internal caches.
*
* @since Ant 1.6
*/
private void clearCaches() {
fileListMap.clear();
scannedDirs.clear();
}
/**
* list the files present in one directory.
* @param directory full path on the remote side
* @param changedir if true change to directory directory before listing
* @return array of FTPFile
*/
public FTPFile[] listFiles(String directory, boolean changedir) {
String currentPath = directory;
if (changedir) {
try {
if (!ftp.changeWorkingDirectory(directory)) {
return null;
}
currentPath = ftp.printWorkingDirectory();
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
}
}
if (fileListMap.containsKey(currentPath)) {
getProject().log("filelist map used in listing files", Project.MSG_DEBUG);
return fileListMap.get(currentPath);
}
FTPFile[] result;
try {
result = ftp.listFiles();
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
}
fileListMap.put(currentPath, result);
if (!remoteSensitivityChecked) {
checkRemoteSensitivity(result, directory);
}
return result;
}
private void forceRemoteSensitivityCheck() {
if (!remoteSensitivityChecked) {
try {
checkRemoteSensitivity(ftp.listFiles(), ftp.printWorkingDirectory());
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
}
}
}
/**
* cd into one directory and
* list the files present in one directory.
* @param directory full path on the remote side
* @return array of FTPFile
*/
public FTPFile[] listFiles(String directory) {
return listFiles(directory, true);
}
private void checkRemoteSensitivity(FTPFile[] array, String directory) {
if (array == null) {
return;
}
boolean candidateFound = false;
String target = null;
for (int icounter = 0; icounter < array.length; icounter++) {
if (array[icounter] != null && array[icounter].isDirectory()) {
if (!".".equals(array[icounter].getName())
&& !"..".equals(array[icounter].getName())) {
candidateFound = true;
target = fiddleName(array[icounter].getName());
getProject().log("will try to cd to "
+ target + " where a directory called "
+ array[icounter].getName()
+ " exists", Project.MSG_DEBUG);
for (int pcounter = 0; pcounter < array.length; pcounter++) {
if (array[pcounter] != null
&& pcounter != icounter
&& target.equals(array[pcounter].getName())) {
candidateFound = false;
break;
}
}
if (candidateFound) {
break;
}
}
}
}
if (candidateFound) {
try {
getProject().log("testing case sensitivity, attempting to cd to "
+ target, Project.MSG_DEBUG);
remoteSystemCaseSensitive = !ftp.changeWorkingDirectory(target);
} catch (IOException ioe) {
remoteSystemCaseSensitive = true;
} finally {
try {
ftp.changeWorkingDirectory(directory);
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation()); //NOSONAR
}
}
getProject().log("remote system is case sensitive : " + remoteSystemCaseSensitive,
Project.MSG_VERBOSE);
remoteSensitivityChecked = true;
}
}
private String fiddleName(String origin) {
StringBuilder result = new StringBuilder();
for (char ch : origin.toCharArray()) {
if (Character.isLowerCase(ch)) {
result.append(Character.toUpperCase(ch));
} else if (Character.isUpperCase(ch)) {
result.append(Character.toLowerCase(ch));
} else {
result.append(ch);
}
}
return result.toString();
}
/**
* an AntFTPFile is a representation of a remote file
* @since Ant 1.6
*/
protected class AntFTPFile {
/**
* ftp client
*/
private FTPClient client;
/**
* parent directory of the file
*/
private String curpwd;
/**
* the file itself
*/
private FTPFile ftpFile;
/**
*
*/
private AntFTPFile parent = null;
private boolean relativePathCalculated = false;
private boolean traversesSymlinks = false;
private String relativePath = "";
/**
* constructor
* @param client ftp client variable
* @param ftpFile the file
* @param curpwd absolute remote path where the file is found
*/
public AntFTPFile(FTPClient client, FTPFile ftpFile, String curpwd) {
this.client = client;
this.ftpFile = ftpFile;
this.curpwd = curpwd;
}
/**
* other constructor
* @param parent the parent file
* @param path a relative path to the parent file
*/
public AntFTPFile(AntFTPFile parent, String path) {
this.parent = parent;
this.client = parent.client;
List<String> pathElements = SelectorUtils.tokenizePath(path);
try {
//this should not happen, except if parent has been deleted by another process
if (!this.client.changeWorkingDirectory(parent.getAbsolutePath())) {
return;
}
this.curpwd = parent.getAbsolutePath();
} catch (IOException ioe) {
throw new BuildException(
"could not change working dir to %s", parent.curpwd);
}
for (String currentPathElement : pathElements) {
try {
if (!this.client
.changeWorkingDirectory(currentPathElement)) {
if (!isCaseSensitive() && (remoteSystemCaseSensitive
|| !remoteSensitivityChecked)) {
currentPathElement =
findPathElementCaseUnsensitive(this.curpwd,
currentPathElement);
if (currentPathElement == null) {
return;
}
}
return;
}
this.curpwd =
getCurpwdPlusFileSep() + currentPathElement;
} catch (IOException ioe) {
throw new BuildException(
"could not change working dir to %s from %s",
currentPathElement, curpwd);
}
}
String lastpathelement = pathElements.get(pathElements.size() - 1);
FTPFile[] theFiles = listFiles(this.curpwd);
this.ftpFile = getFile(theFiles, lastpathelement);
}
/**
* find a file in a directory in case insensitive way
* @param parentPath where we are
* @param soughtPathElement what is being sought
* @return the first file found or null if not found
*/
private String findPathElementCaseUnsensitive(String parentPath,
String soughtPathElement) {
// we are already in the right path, so the second parameter
// is false
FTPFile[] files = listFiles(parentPath, false);
if (files == null) {
return null;
}
for (FTPFile file : files) {
if (file != null
&& file.getName().equalsIgnoreCase(soughtPathElement)) {
return file.getName();
}
}
return null;
}
/**
* find out if the file exists
* @return true if the file exists
*/
public boolean exists() {
return (ftpFile != null);
}
/**
* if the file is a symbolic link, find out to what it is pointing
* @return the target of the symbolic link
*/
public String getLink() {
return ftpFile.getLink();
}
/**
* get the name of the file
* @return the name of the file
*/
public String getName() {
return ftpFile.getName();
}
/**
* find out the absolute path of the file
* @return absolute path as string
*/
public String getAbsolutePath() {
return getCurpwdPlusFileSep() + ftpFile.getName();
}
/**
* find out the relative path assuming that the path used to construct
* this AntFTPFile was spelled properly with regards to case.
* This is OK on a case sensitive system such as UNIX
* @return relative path
*/
public String getFastRelativePath() {
String absPath = getAbsolutePath();
if (absPath.startsWith(rootPath + remoteFileSep)) {
return absPath.substring(rootPath.length() + remoteFileSep.length());
}
return null;
}
/**
* find out the relative path to the rootPath of the enclosing scanner.
* this relative path is spelled exactly like on disk,
* for instance if the AntFTPFile has been instantiated as ALPHA,
* but the file is really called alpha, this method will return alpha.
* If a symbolic link is encountered, it is followed, but the name of the link
* rather than the name of the target is returned.
* (ie does not behave like File.getCanonicalPath())
* @return relative path, separated by remoteFileSep
* @throws IOException if a change directory fails, ...
* @throws BuildException if one of the components of the relative path cannot
* be found.
*/
public String getRelativePath() throws IOException, BuildException {
if (!relativePathCalculated) {
if (parent != null) {
traversesSymlinks = parent.isTraverseSymlinks();
relativePath = getRelativePath(parent.getAbsolutePath(),
parent.getRelativePath());
} else {
relativePath = getRelativePath(rootPath, "");
relativePathCalculated = true;
}
}
return relativePath;
}
/**
* get the relative path of this file
* @param currentPath base path
* @param currentRelativePath relative path of the base path with regards to remote dir
* @return relative path
*/
private String getRelativePath(String currentPath, String currentRelativePath) {
List<String> pathElements = SelectorUtils.tokenizePath(getAbsolutePath(),
remoteFileSep);
StringBuilder relPath = new StringBuilder(currentRelativePath == null
? "" : currentRelativePath);
for (String currentElement : pathElements.subList(
SelectorUtils.tokenizePath(currentPath, remoteFileSep).size(),
pathElements.size())) {
FTPFile[] theFiles = listFiles(currentPath);
FTPFile theFile = null;
if (theFiles != null) {
theFile = getFile(theFiles, currentElement);
}
if (relPath.length() > 0) {
relPath.append(remoteFileSep);
}
if (theFile == null) {
// hit a hidden file assume not a symlink
relPath.append(currentElement);
currentPath += remoteFileSep + currentElement;
log("Hidden file " + relPath + " assumed to not be a symlink.",
Project.MSG_VERBOSE);
} else {
traversesSymlinks = traversesSymlinks || theFile.isSymbolicLink();
relPath.append(theFile.getName());
currentPath += remoteFileSep + theFile.getName();
}
}
return relPath.toString();
}
/**
* find a file matching a string in an array of FTPFile.
* This method will find "alpha" when requested for "ALPHA"
* if and only if the caseSensitive attribute is set to false.
* When caseSensitive is set to true, only the exact match is returned.
* @param theFiles array of files
* @param lastpathelement the file name being sought
* @return null if the file cannot be found, otherwise return the matching file.
*/
public FTPFile getFile(FTPFile[] theFiles, String lastpathelement) {
if (theFiles == null) {
return null;
}
Predicate<String> test =
isCaseSensitive() ? lastpathelement::equals
: lastpathelement::equalsIgnoreCase;
return Stream.of(theFiles)
.filter(Objects::nonNull)
.filter(f -> test.test(f.getName()))
.findFirst().orElse(null);
}
/**
* tell if a file is a directory.
* note that it will return false for symbolic links pointing to directories.
* @return <code>true</code> for directories
*/
public boolean isDirectory() {
return ftpFile.isDirectory();
}
/**
* tell if a file is a symbolic link
* @return <code>true</code> for symbolic links
*/
public boolean isSymbolicLink() {
return ftpFile.isSymbolicLink();
}
/**
* return the attached FTP client object.
* Warning : this instance is really shared with the enclosing class.
* @return FTP client
*/
protected FTPClient getClient() {
return client;
}
/**
* sets the current path of an AntFTPFile
* @param curpwd the current path one wants to set
*/
protected void setCurpwd(String curpwd) {
this.curpwd = curpwd;
}
/**
* returns the path of the directory containing the AntFTPFile.
* of the full path of the file itself in case of AntFTPRootFile
* @return parent directory of the AntFTPFile
*/
public String getCurpwd() {
return curpwd;
}
/**
* returns the path of the directory containing the AntFTPFile.
* of the full path of the file itself in case of AntFTPRootFile
* and appends the remote file separator if necessary.
* @return parent directory of the AntFTPFile
* @since Ant 1.8.2
*/
public String getCurpwdPlusFileSep() {
return curpwd.endsWith(remoteFileSep) ? curpwd
: curpwd + remoteFileSep;
}
/**
* find out if a symbolic link is encountered in the relative path of this file
* from rootPath.
* @return <code>true</code> if a symbolic link is encountered in the relative path.
* @throws IOException if one of the change directory or directory listing operations
* fails
* @throws BuildException if a path component in the relative path cannot be found.
*/
public boolean isTraverseSymlinks() throws IOException, BuildException {
if (!relativePathCalculated) {
// getRelativePath also finds about symlinks
getRelativePath();
}
return traversesSymlinks;
}
/**
* Get a string rep of this object.
* @return a string containing the pwd and the file.
*/
@Override
public String toString() {
return "AntFtpFile: " + curpwd + "%" + ftpFile;
}
}
/**
* special class to represent the remote directory itself
* @since Ant 1.6
*/
protected class AntFTPRootFile extends AntFTPFile {
private String remotedir;
/**
* constructor
* @param aclient FTP client
* @param remotedir remote directory
*/
public AntFTPRootFile(FTPClient aclient, String remotedir) {
super(aclient, null, remotedir);
this.remotedir = remotedir;
try {
this.getClient().changeWorkingDirectory(this.remotedir);
this.setCurpwd(this.getClient().printWorkingDirectory());
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
}
}
/**
* find the absolute path
* @return absolute path
*/
@Override
public String getAbsolutePath() {
return this.getCurpwd();
}
/**
* find out the relative path to root
* @return empty string
* @throws BuildException actually never
* @throws IOException actually never
*/
@Override
public String getRelativePath() throws BuildException, IOException {
return "";
}
}
}
/**
* check FTPFiles to check whether they function as directories too
* the FTPFile API seem to make directory and symbolic links incompatible
* we want to find out if we can cd to a symbolic link
* @param dir the parent directory of the file to test
* @param file the file to test
* @return true if it is possible to cd to this directory
* @since ant 1.6
*/
private boolean isFunctioningAsDirectory(FTPClient ftp, String dir, FTPFile file)
throws FTPConnectionClosedException {
if (file.isDirectory()) {
return true;
}
if (file.isFile()) {
return false;
}
String currentWorkingDir = null;
try {
currentWorkingDir = ftp.printWorkingDirectory();
} catch (FTPConnectionClosedException ftpcce) {
getProject().log("could not find current working directory " + dir
+ " while checking a symlink because connection was closed",
Project.MSG_DEBUG);
throw(ftpcce);
} catch (IOException ioe) {
getProject().log("could not find current working directory " + dir
+ " while checking a symlink",
Project.MSG_DEBUG);
}
boolean result = false;
if (currentWorkingDir != null) {
try {
result = ftp.changeWorkingDirectory(file.getLink());
} catch (FTPConnectionClosedException ftpcce) {
getProject().log("could not find current working directory " + dir
+ " while checking a symlink because connection was closed",
Project.MSG_DEBUG);
throw(ftpcce);
} catch (IOException ioe) {
getProject().log("could not cd to " + file.getLink() + " while checking a symlink",
Project.MSG_DEBUG);
}
if (result) {
boolean comeback = false;
try {
comeback = ftp.changeWorkingDirectory(currentWorkingDir);
} catch (IOException ioe) {
getProject().log("could not cd back to " + dir + " while checking a symlink",
Project.MSG_ERR);
} finally {
if (!comeback) {
throw new BuildException(
"could not cd back to %s while checking a symlink",
dir);
}
}
}
}
return result;
}
/**
* check FTPFiles to check whether they function as directories too
* the FTPFile API seem to make directory and symbolic links incompatible
* we want to find out if we can cd to a symbolic link
* @param dir the parent directory of the file to test
* @param file the file to test
* @return true if it is possible to cd to this directory
* @since ant 1.6
*/
private boolean isFunctioningAsFile(FTPClient ftp, String dir, FTPFile file)
throws FTPConnectionClosedException {
return !file.isDirectory() && (file.isFile() || !isFunctioningAsDirectory(ftp, dir, file));
}
/**
* Sets the remote directory where files will be placed. This may be a
* relative or absolute path, and must be in the path syntax expected by
* the remote server. No correction of path syntax will be performed.
*
* @param dir the remote directory name.
*/
public void setRemotedir(String dir) {
this.remotedir = dir;
}
/**
* Sets the FTP server to send files to.
*
* @param server the remote server name.
*/
public void setServer(String server) {
this.server = server;
}
/**
* Sets the FTP port used by the remote server.
*
* @param port the port on which the remote server is listening.
*/
public void setPort(int port) {
this.port = port;
}
/**
* Sets the login user id to use on the specified server.
*
* @param userid remote system userid.
*/
public void setUserid(String userid) {
this.userid = userid;
}
/**
* Sets the login password for the given user id.
*
* @param password the password on the remote system.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Sets the login account to use on the specified server.
*
* @param pAccount the account name on remote system
* @since Ant 1.7
*/
public void setAccount(String pAccount) {
this.account = pAccount;
}
/**
* If true, uses binary mode, otherwise text mode (default is binary).
*
* @param binary if true use binary mode in transfers.
*/
public void setBinary(boolean binary) {
this.binary = binary;
}
/**
* Specifies whether to use passive mode. Set to true if you are behind a
* firewall and cannot connect without it. Passive mode is disabled by
* default.
*
* @param passive true is passive mode should be used.
*/
public void setPassive(boolean passive) {
this.passive = passive;
}
/**
* Set to true to receive notification about each file as it is
* transferred.
*
* @param verbose true if verbose notifications are required.
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* A synonym for <code>depends</code>. Set to true to transmit only new
* or changed files.
*
* See the related attributes timediffmillis and timediffauto.
*
* @param newer if true only transfer newer files.
*/
public void setNewer(boolean newer) {
this.newerOnly = newer;
}
/**
* number of milliseconds to add to the time on the remote machine
* to get the time on the local machine.
*
* use in conjunction with <code>newer</code>
*
* @param timeDiffMillis number of milliseconds
*
* @since ant 1.6
*/
public void setTimeDiffMillis(long timeDiffMillis) {
this.timeDiffMillis = timeDiffMillis;
}
/**
* &quot;true&quot; to find out automatically the time difference
* between local and remote machine.
*
* This requires right to create
* and delete a temporary file in the remote directory.
*
* @param timeDiffAuto true = find automatically the time diff
*
* @since ant 1.6
*/
public void setTimeDiffAuto(boolean timeDiffAuto) {
this.timeDiffAuto = timeDiffAuto;
}
/**
* Set to true to preserve modification times for "gotten" files.
*
* @param preserveLastModified if true preserver modification times.
*/
public void setPreserveLastModified(boolean preserveLastModified) {
this.preserveLastModified = preserveLastModified;
}
/**
* Set to true to transmit only files that are new or changed from their
* remote counterparts. The default is to transmit all files.
*
* @param depends if true only transfer newer files.
*/
public void setDepends(boolean depends) {
this.newerOnly = depends;
}
/**
* Sets the remote file separator character. This normally defaults to the
* Unix standard forward slash, but can be manually overridden using this
* call if the remote server requires some other separator. Only the first
* character of the string is used.
*
* @param separator the file separator on the remote system.
*/
public void setSeparator(String separator) {
remoteFileSep = separator;
}
/**
* Sets the file permission mode (Unix only) for files sent to the
* server.
*
* @param theMode unix style file mode for the files sent to the remote
* system.
*/
public void setChmod(String theMode) {
this.chmod = theMode;
}
/**
* Sets the default mask for file creation on a unix server.
*
* @param theUmask unix style umask for files created on the remote server.
*/
public void setUmask(String theUmask) {
this.umask = theUmask;
}
/**
* A set of files to upload or download
*
* @param set the set of files to be added to the list of files to be
* transferred.
*/
public void addFileset(FileSet set) {
filesets.addElement(set);
}
/**
* Sets the FTP action to be taken. Currently accepts "put", "get", "del",
* "mkdir", "chmod", "list", and "site".
*
* @deprecated since 1.5.x.
* setAction(String) is deprecated and is replaced with
* setAction(FTP.Action) to make Ant's Introspection mechanism do the
* work and also to encapsulate operations on the type in its own
* class.
* @ant.attribute ignore="true"
*
* @param action the FTP action to be performed.
*
* @throws BuildException if the action is not a valid action.
*/
@Deprecated
public void setAction(String action) throws BuildException {
log("DEPRECATED - The setAction(String) method has been deprecated."
+ " Use setAction(FTP.Action) instead.");
Action a = new Action();
a.setValue(action);
this.action = a.getAction();
}
/**
* Sets the FTP action to be taken. Currently accepts "put", "get", "del",
* "mkdir", "chmod", "list", and "site".
*
* @param action the FTP action to be performed.
*
* @throws BuildException if the action is not a valid action.
*/
public void setAction(Action action) throws BuildException {
this.action = action.getAction();
}
/**
* The output file for the "list" action. This attribute is ignored for
* any other actions.
*
* @param listing file in which to store the listing.
*/
public void setListing(File listing) {
this.listing = listing;
}
/**
* If true, enables unsuccessful file put, delete and get
* operations to be skipped with a warning and the remainder
* of the files still transferred.
*
* @param skipFailedTransfers true if failures in transfers are ignored.
*/
public void setSkipFailedTransfers(boolean skipFailedTransfers) {
this.skipFailedTransfers = skipFailedTransfers;
}
/**
* set the flag to skip errors on directory creation.
* (and maybe later other server specific errors)
*
* @param ignoreNoncriticalErrors true if non-critical errors should not
* cause a failure.
*/
public void setIgnoreNoncriticalErrors(boolean ignoreNoncriticalErrors) {
this.ignoreNoncriticalErrors = ignoreNoncriticalErrors;
}
private void configurationHasBeenSet() {
this.isConfigurationSet = true;
}
/**
* Sets the systemTypeKey attribute.
* Method for setting <code>FTPClientConfig</code> remote system key.
*
* @param systemKey the key to be set - BUT if blank
* the default value of null (which signifies "autodetect") will be kept.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setSystemTypeKey(FTPSystemType systemKey) {
if (systemKey != null && !systemKey.getValue().isEmpty()) {
this.systemTypeKey = systemKey;
configurationHasBeenSet();
}
}
/**
* Sets the defaultDateFormatConfig attribute.
* @param defaultDateFormat configuration to be set, unless it is
* null or empty string, in which case ignored.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setDefaultDateFormatConfig(String defaultDateFormat) {
if (defaultDateFormat != null && !defaultDateFormat.isEmpty()) {
this.defaultDateFormatConfig = defaultDateFormat;
configurationHasBeenSet();
}
}
/**
* Sets the recentDateFormatConfig attribute.
* @param recentDateFormat configuration to be set, unless it is
* null or empty string, in which case ignored.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setRecentDateFormatConfig(String recentDateFormat) {
if (recentDateFormat != null && !recentDateFormat.isEmpty()) {
this.recentDateFormatConfig = recentDateFormat;
configurationHasBeenSet();
}
}
/**
* Sets the serverLanguageCode attribute.
* @param serverLanguageCode configuration to be set, unless it is
* null or empty string, in which case ignored.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setServerLanguageCodeConfig(LanguageCode serverLanguageCode) {
if (serverLanguageCode != null && !serverLanguageCode.getValue().isEmpty()) {
this.serverLanguageCodeConfig = serverLanguageCode;
configurationHasBeenSet();
}
}
/**
* Sets the serverTimeZoneConfig attribute.
* @param serverTimeZoneId configuration to be set, unless it is
* null or empty string, in which case ignored.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setServerTimeZoneConfig(String serverTimeZoneId) {
if (serverTimeZoneId != null && !serverTimeZoneId.isEmpty()) {
this.serverTimeZoneConfig = serverTimeZoneId;
configurationHasBeenSet();
}
}
/**
* Sets the shortMonthNamesConfig attribute
*
* @param shortMonthNames configuration to be set, unless it is
* null or empty string, in which case ignored.
* @see org.apache.commons.net.ftp.FTPClientConfig
*/
public void setShortMonthNamesConfig(String shortMonthNames) {
if (shortMonthNames != null && !shortMonthNames.isEmpty()) {
this.shortMonthNamesConfig = shortMonthNames;
configurationHasBeenSet();
}
}
/**
* Defines how many times to retry executing FTP command before giving up.
* Default is 0 - try once and if failure then give up.
*
* @param retriesAllowed number of retries to allow. -1 means
* keep trying forever. "forever" may also be specified as a
* synonym for -1.
*/
public void setRetriesAllowed(String retriesAllowed) {
if ("FOREVER".equalsIgnoreCase(retriesAllowed)) {
this.retriesAllowed = Retryable.RETRY_FOREVER;
} else {
try {
int retries = Integer.parseInt(retriesAllowed);
if (retries < Retryable.RETRY_FOREVER) {
throw new BuildException(
"Invalid value for retriesAllowed attribute: %s",
retriesAllowed);
}
this.retriesAllowed = retries;
} catch (NumberFormatException px) {
throw new BuildException(
"Invalid value for retriesAllowed attribute: %s",
retriesAllowed);
}
}
}
/**
* @return Returns the systemTypeKey.
*/
@Override
public String getSystemTypeKey() {
return systemTypeKey.getValue();
}
/**
* @return Returns the defaultDateFormatConfig.
*/
@Override
public String getDefaultDateFormatConfig() {
return defaultDateFormatConfig;
}
/**
* @return Returns the recentDateFormatConfig.
*/
@Override
public String getRecentDateFormatConfig() {
return recentDateFormatConfig;
}
/**
* @return Returns the serverLanguageCodeConfig.
*/
@Override
public String getServerLanguageCodeConfig() {
return serverLanguageCodeConfig.getValue();
}
/**
* @return Returns the serverTimeZoneConfig.
*/
@Override
public String getServerTimeZoneConfig() {
return serverTimeZoneConfig;
}
/**
* @return Returns the shortMonthNamesConfig.
*/
@Override
public String getShortMonthNamesConfig() {
return shortMonthNamesConfig;
}
/**
* @return Returns the timestampGranularity.
*/
Granularity getTimestampGranularity() {
return timestampGranularity;
}
/**
* Sets the timestampGranularity attribute
* @param timestampGranularity The timestampGranularity to set.
*/
public void setTimestampGranularity(Granularity timestampGranularity) {
if (null == timestampGranularity || timestampGranularity.getValue().isEmpty()) {
return;
}
this.timestampGranularity = timestampGranularity;
}
/**
* Sets the siteCommand attribute. This attribute
* names the command that will be executed if the action
* is "site".
* @param siteCommand The siteCommand to set.
*/
public void setSiteCommand(String siteCommand) {
this.siteCommand = siteCommand;
}
/**
* Sets the initialSiteCommand attribute. This attribute
* names a site command that will be executed immediately
* after connection.
* @param initialCommand The initialSiteCommand to set.
*/
public void setInitialSiteCommand(String initialCommand) {
this.initialSiteCommand = initialCommand;
}
/**
* Whether to verify that data and control connections are
* connected to the same remote host.
*
* @param b boolean
* @since Ant 1.8.0
*/
public void setEnableRemoteVerification(boolean b) {
enableRemoteVerification = b;
}
/**
* Sets the timeout on the data connection in milliseconds.
* Any negative value is discarded and leaves the default
* A value of 0 means an infinite timeout
*
* @param dataTimeout int
* @since Ant 1.10.7
*/
public void setDataTimeout(int dataTimeout) {
if (dataTimeout >= 0) {
this.dataTimeout = dataTimeout;
}
}
/**
* Sets the time interval when we should automatically
* call a command triggering a transfer
* The parameter is in seconds
*
* @param wakeUpTransferInterval int
* @since Ant 1.10.7
*/
public void setWakeUpTransferInterval(int wakeUpTransferInterval) {
if (wakeUpTransferInterval > 0) {
this.wakeUpTransferInterval = wakeUpTransferInterval;
}
}
/**
* Checks to see that all required parameters are set.
*
* @throws BuildException if the configuration is not valid.
*/
protected void checkAttributes() throws BuildException {
if (server == null) {
throw new BuildException("server attribute must be set!");
}
if (userid == null) {
throw new BuildException("userid attribute must be set!");
}
if (password == null) {
throw new BuildException("password attribute must be set!");
}
if (action == LIST_FILES && listing == null) {
throw new BuildException(
"listing attribute must be set for list action!");
}
if (action == MK_DIR && remotedir == null) {
throw new BuildException(
"remotedir attribute must be set for mkdir action!");
}
if (action == CHMOD && chmod == null) {
throw new BuildException(
"chmod attribute must be set for chmod action!");
}
if (action == SITE_CMD && siteCommand == null) {
throw new BuildException(
"sitecommand attribute must be set for site action!");
}
if (this.isConfigurationSet) {
try {
Class.forName("org.apache.commons.net.ftp.FTPClientConfig");
} catch (ClassNotFoundException e) {
throw new BuildException(
"commons-net.jar >= 1.4.0 is required for the specified attributes.");
}
}
}
/**
* Executable a retryable object.
* @param h the retry handler.
* @param r the object that should be retried until it succeeds
* or the number of retries is reached.
* @param descr a description of the command that is being run.
* @throws IOException if there is a problem.
*/
protected void executeRetryable(RetryHandler h, Retryable r, String descr)
throws IOException {
h.execute(r, descr);
}
/**
* For each file in the fileset, do the appropriate action: send, get,
* delete, or list.
*
* @param ftp the FTPClient instance used to perform FTP actions
* @param fs the fileset on which the actions are performed.
*
* @return the number of files to be transferred.
*
* @throws IOException if there is a problem reading a file
* @throws BuildException if there is a problem in the configuration.
*/
protected int transferFiles(final FTPClient ftp, FileSet fs)
throws IOException, BuildException {
DirectoryScanner ds;
if (action == SEND_FILES) {
ds = fs.getDirectoryScanner(getProject());
} else {
ds = new FTPDirectoryScanner(ftp);
fs.setupDirectoryScanner(ds, getProject());
ds.setFollowSymlinks(fs.isFollowSymlinks());
ds.scan();
}
String[] dsfiles;
if (action == RM_DIR) {
dsfiles = ds.getIncludedDirectories();
} else {
dsfiles = ds.getIncludedFiles();
}
String dir = null;
if (ds.getBasedir() == null
&& (action == SEND_FILES || action == GET_FILES)) {
throw new BuildException(
"the dir attribute must be set for send and get actions");
}
if (action == SEND_FILES || action == GET_FILES) {
dir = ds.getBasedir().getAbsolutePath();
}
// If we are doing a listing, we need the output stream created now.
BufferedWriter bw = null;
try {
if (action == LIST_FILES) {
File pd = listing.getParentFile();
if (!pd.exists()) {
pd.mkdirs();
}
bw = new BufferedWriter(new FileWriter(listing));
}
RetryHandler h = new RetryHandler(this.retriesAllowed, this);
if (action == RM_DIR) {
// to remove directories, start by the end of the list
// the trunk does not let itself be removed before the leaves
for (int i = dsfiles.length - 1; i >= 0; i--) {
final String dsfile = dsfiles[i];
executeRetryable(h, () -> rmDir(ftp, dsfile), dsfile);
}
} else {
final BufferedWriter fbw = bw;
final String fdir = dir;
if (this.newerOnly) {
this.granularityMillis =
this.timestampGranularity.getMilliseconds(action);
}
for (final String dsfile : dsfiles) {
executeRetryable(h, () -> {
switch (action) {
case SEND_FILES:
sendFile(ftp, fdir, dsfile);
break;
case GET_FILES:
getFile(ftp, fdir, dsfile);
break;
case DEL_FILES:
delFile(ftp, dsfile);
break;
case LIST_FILES:
listFile(ftp, fbw, dsfile);
break;
case CHMOD:
doSiteCommand(ftp, "chmod " + chmod
+ " " + resolveFile(dsfile));
transferred++;
break;
default:
throw new BuildException("unknown ftp action " + action);
}
}, dsfile);
}
}
} finally {
FileUtils.close(bw);
}
return dsfiles.length;
}
/**
* Sends all files specified by the configured filesets to the remote
* server.
*
* @param ftp the FTPClient instance used to perform FTP actions
*
* @throws IOException if there is a problem reading a file
* @throws BuildException if there is a problem in the configuration.
*/
protected void transferFiles(FTPClient ftp)
throws IOException, BuildException {
transferred = 0;
skipped = 0;
if (filesets.isEmpty()) {
throw new BuildException("at least one fileset must be specified.");
}
for (FileSet fs : filesets) {
if (fs != null) {
transferFiles(ftp, fs);
}
}
log(transferred + " " + ACTION_TARGET_STRS[action] + " "
+ COMPLETED_ACTION_STRS[action]);
if (skipped != 0) {
log(skipped + " " + ACTION_TARGET_STRS[action]
+ " were not successfully " + COMPLETED_ACTION_STRS[action]);
}
}
/**
* Correct a file path to correspond to the remote host requirements. This
* implementation currently assumes that the remote end can handle
* Unix-style paths with forward-slash separators. This can be overridden
* with the <code>separator</code> task parameter. No attempt is made to
* determine what syntax is appropriate for the remote host.
*
* @param file the remote file name to be resolved
*
* @return the filename as it will appear on the server.
*/
protected String resolveFile(String file) {
return file.replace(File.separator.charAt(0), remoteFileSep.charAt(0));
}
/**
* Creates all parent directories specified in a complete relative
* pathname. Attempts to create existing directories will not cause
* errors.
*
* @param ftp the FTP client instance to use to execute FTP actions on
* the remote server.
* @param filename the name of the file whose parents should be created.
* @throws IOException under non documented circumstances
* @throws BuildException if it is impossible to cd to a remote directory
*
*/
protected void createParents(FTPClient ftp, String filename)
throws IOException, BuildException {
File dir = new File(filename);
if (dirCache.contains(dir)) {
return;
}
List<File> parents = new Vector<>();
String dirname;
while ((dirname = dir.getParent()) != null) {
File checkDir = new File(dirname);
if (dirCache.contains(checkDir)) {
break;
}
dir = checkDir;
parents.add(dir);
}
// find first non cached dir
int i = parents.size() - 1;
if (i >= 0) {
String cwd = ftp.printWorkingDirectory();
String parent = dir.getParent();
if (parent != null
&& !ftp.changeWorkingDirectory(resolveFile(parent))) {
throw new BuildException("could not change to directory: %s",
ftp.getReplyString());
}
while (i >= 0) {
dir = parents.get(i--);
// check if dir exists by trying to change into it.
if (!ftp.changeWorkingDirectory(dir.getName())) {
// could not change to it - try to create it
log("creating remote directory "
+ resolveFile(dir.getPath()), Project.MSG_VERBOSE);
if (!ftp.makeDirectory(dir.getName())) {
handleMkDirFailure(ftp);
}
if (!ftp.changeWorkingDirectory(dir.getName())) {
throw new BuildException(
"could not change to directory: %s",
ftp.getReplyString());
}
}
dirCache.add(dir);
}
ftp.changeWorkingDirectory(cwd);
}
}
/**
* auto find the time difference between local and remote
* @param ftp handle to ftp client
* @return number of millis to add to remote time to make it comparable to local time
* @since ant 1.6
*/
private long getTimeDiff(FTPClient ftp) {
long returnValue = 0;
File tempFile = findFileName(ftp);
try {
// create a local temporary file
FILE_UTILS.createNewFile(tempFile);
long localTimeStamp = tempFile.lastModified();
BufferedInputStream instream = new BufferedInputStream(
Files.newInputStream(tempFile.toPath()));
ftp.storeFile(tempFile.getName(), instream);
instream.close();
boolean success = FTPReply.isPositiveCompletion(ftp.getReplyCode());
if (success) {
FTPFile[] ftpFiles = ftp.listFiles(tempFile.getName());
if (ftpFiles.length == 1) {
long remoteTimeStamp = ftpFiles[0].getTimestamp().getTime().getTime();
returnValue = localTimeStamp - remoteTimeStamp;
}
ftp.deleteFile(ftpFiles[0].getName());
}
// delegate the deletion of the local temp file to the delete task
// because of race conditions occurring on Windows
Delete mydelete = new Delete();
mydelete.bindToOwner(this);
mydelete.setFile(tempFile.getCanonicalFile());
mydelete.execute();
} catch (Exception e) {
throw new BuildException(e, getLocation());
}
return returnValue;
}
/**
* find a suitable name for local and remote temporary file
*/
private File findFileName(FTPClient ftp) {
FTPFile[] files = null;
final int maxIterations = 1000;
for (int counter = 1; counter < maxIterations; counter++) {
File localFile = FILE_UTILS.createTempFile(getProject(),
"ant" + Integer.toString(counter), ".tmp",
null, false, false);
String fileName = localFile.getName();
boolean found = false;
try {
if (files == null) {
files = ftp.listFiles();
}
for (FTPFile file : files) {
if (file != null && file.getName().equals(fileName)) {
found = true;
break;
}
}
} catch (IOException ioe) {
throw new BuildException(ioe, getLocation());
}
if (!found) {
localFile.deleteOnExit();
return localFile;
}
}
return null;
}
/**
* Checks to see if the remote file is current as compared with the local
* file. Returns true if the target file is up to date.
* @param ftp ftpclient
* @param localFile local file
* @param remoteFile remote file
* @return true if the target file is up to date
* @throws IOException in unknown circumstances
* @throws BuildException if the date of the remote files cannot be found and the action is
* GET_FILES
*/
protected boolean isUpToDate(FTPClient ftp, File localFile,
String remoteFile)
throws IOException, BuildException {
log("checking date for " + remoteFile, Project.MSG_VERBOSE);
FTPFile[] files = ftp.listFiles(remoteFile);
// For Microsoft's Ftp-Service an Array with length 0 is
// returned if configured to return listings in "MS-DOS"-Format
if (files == null || files.length == 0) {
// If we are sending files, then assume out of date.
// If we are getting files, then throw an error
if (action == SEND_FILES) {
log("Could not date test remote file: " + remoteFile
+ "assuming out of date.", Project.MSG_VERBOSE);
return false;
}
throw new BuildException("could not date test remote file: %s",
ftp.getReplyString());
}
long remoteTimestamp = files[0].getTimestamp().getTime().getTime();
long localTimestamp = localFile.lastModified();
long adjustedRemoteTimestamp =
remoteTimestamp + this.timeDiffMillis + this.granularityMillis;
StringBuilder msg;
synchronized (TIMESTAMP_LOGGING_SDF) {
msg = new StringBuilder(" [")
.append(TIMESTAMP_LOGGING_SDF.format(new Date(localTimestamp)))
.append("] local");
}
log(msg.toString(), Project.MSG_VERBOSE);
synchronized (TIMESTAMP_LOGGING_SDF) {
msg = new StringBuilder(" [")
.append(TIMESTAMP_LOGGING_SDF
.format(new Date(adjustedRemoteTimestamp)))
.append("] remote");
}
if (remoteTimestamp != adjustedRemoteTimestamp) {
synchronized (TIMESTAMP_LOGGING_SDF) {
msg.append(" - (raw: ")
.append(
TIMESTAMP_LOGGING_SDF.format(new Date(remoteTimestamp)))
.append(")");
}
}
log(msg.toString(), Project.MSG_VERBOSE);
if (this.action == SEND_FILES) {
return adjustedRemoteTimestamp >= localTimestamp;
}
return localTimestamp >= adjustedRemoteTimestamp;
}
/**
* Sends a site command to the ftp server
* @param ftp ftp client
* @param theCMD command to execute
* @throws IOException in unknown circumstances
* @throws BuildException in unknown circumstances
*/
protected void doSiteCommand(FTPClient ftp, String theCMD)
throws IOException, BuildException {
log("Doing Site Command: " + theCMD, Project.MSG_VERBOSE);
if (!ftp.sendSiteCommand(theCMD)) {
log("Failed to issue Site Command: " + theCMD, Project.MSG_WARN);
} else {
for (String reply : ftp.getReplyStrings()) {
if (reply != null && !reply.contains("200")) {
log(reply, Project.MSG_WARN);
}
}
}
}
/**
* Sends a single file to the remote host. <code>filename</code> may
* contain a relative path specification. When this is the case, <code>sendFile</code>
* will attempt to create any necessary parent directories before sending
* the file. The file will then be sent using the entire relative path
* spec - no attempt is made to change directories. It is anticipated that
* this may eventually cause problems with some FTP servers, but it
* simplifies the coding.
* @param ftp ftp client
* @param dir base directory of the file to be sent (local)
* @param filename relative path of the file to be send
* locally relative to dir
* remotely relative to the remotedir attribute
* @throws IOException in unknown circumstances
* @throws BuildException in unknown circumstances
*/
protected void sendFile(FTPClient ftp, String dir, String filename)
throws IOException, BuildException {
InputStream instream = null;
try {
// TODO - why not simply new File(dir, filename)?
File file = getProject().resolveFile(new File(dir, filename).getPath());
if (newerOnly && isUpToDate(ftp, file, resolveFile(filename))) {
return;
}
if (verbose) {
log("transferring " + file.getAbsolutePath());
}
instream = new BufferedInputStream(Files.newInputStream(file.toPath()));
createParents(ftp, filename);
ftp.storeFile(resolveFile(filename), instream);
boolean success = FTPReply.isPositiveCompletion(ftp.getReplyCode());
if (!success) {
String s = "could not put file: " + ftp.getReplyString();
if (skipFailedTransfers) {
log(s, Project.MSG_WARN);
skipped++;
} else {
throw new BuildException(s);
}
} else {
// see if we should issue a chmod command
if (chmod != null) {
doSiteCommand(ftp, "chmod " + chmod + " " + resolveFile(filename));
}
log("File " + file.getAbsolutePath() + " copied to " + server,
Project.MSG_VERBOSE);
transferred++;
}
} finally {
FileUtils.close(instream);
}
}
/**
* Delete a file from the remote host.
* @param ftp ftp client
* @param filename file to delete
* @throws IOException in unknown circumstances
* @throws BuildException if skipFailedTransfers is set to false
* and the deletion could not be done
*/
protected void delFile(FTPClient ftp, String filename)
throws IOException, BuildException {
if (verbose) {
log("deleting " + filename);
}
if (!ftp.deleteFile(resolveFile(filename))) {
String s = "could not delete file: " + ftp.getReplyString();
if (skipFailedTransfers) {
log(s, Project.MSG_WARN);
skipped++;
} else {
throw new BuildException(s);
}
} else {
log("File " + filename + " deleted from " + server,
Project.MSG_VERBOSE);
transferred++;
}
}
/**
* Delete a directory, if empty, from the remote host.
* @param ftp ftp client
* @param dirname directory to delete
* @throws IOException in unknown circumstances
* @throws BuildException if skipFailedTransfers is set to false
* and the deletion could not be done
*/
protected void rmDir(FTPClient ftp, String dirname)
throws IOException, BuildException {
if (verbose) {
log("removing " + dirname);
}
if (!ftp.removeDirectory(resolveFile(dirname))) {
String s = "could not remove directory: " + ftp.getReplyString();
if (skipFailedTransfers) {
log(s, Project.MSG_WARN);
skipped++;
} else {
throw new BuildException(s);
}
} else {
log("Directory " + dirname + " removed from " + server,
Project.MSG_VERBOSE);
transferred++;
}
}
/**
* Retrieve a single file from the remote host. <code>filename</code> may
* contain a relative path specification. <p>
*
* The file will then be retrieved using the entire relative path spec -
* no attempt is made to change directories. It is anticipated that this
* may eventually cause problems with some FTP servers, but it simplifies
* the coding.</p>
* @param ftp the ftp client
* @param dir local base directory to which the file should go back
* @param filename relative path of the file based upon the ftp remote directory
* and/or the local base directory (dir)
* @throws IOException in unknown circumstances
* @throws BuildException if skipFailedTransfers is false
* and the file cannot be retrieved.
*/
protected void getFile(FTPClient ftp, String dir, String filename)
throws IOException, BuildException {
File file = getProject().resolveFile(new File(dir, filename).getPath());
OutputStream outstream = null;
try {
if (newerOnly && isUpToDate(ftp, file, resolveFile(filename))) {
return;
}
if (verbose) {
log("transferring " + filename + " to "
+ file.getAbsolutePath());
}
File pdir = file.getParentFile();
if (!pdir.exists()) {
pdir.mkdirs();
}
outstream = new BufferedOutputStream(Files.newOutputStream(file.toPath()));
ftp.retrieveFile(resolveFile(filename), outstream);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
String s = "could not get file: " + ftp.getReplyString();
if (skipFailedTransfers) {
log(s, Project.MSG_WARN);
skipped++;
} else {
throw new BuildException(s);
}
} else {
log("File " + file.getAbsolutePath() + " copied from "
+ server, Project.MSG_VERBOSE);
transferred++;
if (preserveLastModified) {
outstream.close();
outstream = null;
FTPFile[] remote = ftp.listFiles(resolveFile(filename));
if (remote.length > 0) {
FILE_UTILS.setFileLastModified(file,
remote[0].getTimestamp()
.getTime().getTime());
}
}
}
} finally {
FileUtils.close(outstream);
}
}
/**
* List information about a single file from the remote host. <code>filename</code>
* may contain a relative path specification. <p>
*
* The file listing will then be retrieved using the entire relative path
* spec - no attempt is made to change directories. It is anticipated that
* this may eventually cause problems with some FTP servers, but it
* simplifies the coding.</p>
* @param ftp ftp client
* @param bw buffered writer
* @param filename the directory one wants to list
* @throws IOException in unknown circumstances
* @throws BuildException in unknown circumstances
*/
protected void listFile(FTPClient ftp, BufferedWriter bw, String filename)
throws IOException, BuildException {
if (verbose) {
log("listing " + filename);
}
FTPFile[] ftpfiles = ftp.listFiles(resolveFile(filename));
if (ftpfiles != null && ftpfiles.length > 0) {
bw.write(ftpfiles[0].toString());
bw.newLine();
transferred++;
}
}
/**
* Create the specified directory on the remote host.
*
* @param ftp The FTP client connection
* @param dir The directory to create (format must be correct for host
* type)
* @throws IOException in unknown circumstances
* @throws BuildException if ignoreNoncriticalErrors has not been set to true
* and a directory could not be created, for instance because it was
* already existing. Precisely, the codes 521, 550 and 553 will trigger
* a BuildException
*/
protected void makeRemoteDir(FTPClient ftp, String dir)
throws IOException, BuildException {
String workingDirectory = ftp.printWorkingDirectory();
boolean absolute = dir.startsWith("/");
if (verbose) {
if (absolute || workingDirectory == null) {
log("Creating directory: " + dir + " in /");
} else {
log("Creating directory: " + dir + " in " + workingDirectory);
}
}
if (absolute) {
ftp.changeWorkingDirectory("/");
}
StringTokenizer st = new StringTokenizer(dir, "/");
while (st.hasMoreTokens()) {
String subdir = st.nextToken();
log("Checking " + subdir, Project.MSG_DEBUG);
if (!ftp.changeWorkingDirectory(subdir)) {
if (ftp.makeDirectory(subdir)) {
if (verbose) {
log("Directory created OK");
}
ftp.changeWorkingDirectory(subdir);
} else {
// codes 521, 550 and 553 can be produced by FTP Servers
// to indicate that an attempt to create a directory has
// failed because the directory already exists.
int rc = ftp.getReplyCode();
if (!ignoreNoncriticalErrors
|| rc != CODE_550 && rc != CODE_553 && rc != CODE_521) {
throw new BuildException(
"could not create directory: %s",
ftp.getReplyString());
}
if (verbose) {
log("Directory already exists");
}
}
}
}
if (workingDirectory != null) {
ftp.changeWorkingDirectory(workingDirectory);
}
}
/**
* look at the response for a failed mkdir action, decide whether
* it matters or not. If it does, we throw an exception
* @param ftp current ftp connection
* @throws BuildException if this is an error to signal
*/
private void handleMkDirFailure(FTPClient ftp)
throws BuildException {
int rc = ftp.getReplyCode();
if (!ignoreNoncriticalErrors || rc != CODE_550 && rc != CODE_553 && rc != CODE_521) {
throw new BuildException("could not create directory: %s",
ftp.getReplyString());
}
}
/**
* checks if the wake up interval is expired
*/
private boolean wakeUpTransferIntervalExpired() {
boolean result = false;
// on the first call, initialize the keep-alive mechanism
// by storing the current date
if (lastWakeUpTime == 0) {
lastWakeUpTime = (new Date()).getTime();
} else {
long currentTime = (new Date()).getTime();
if (currentTime > (lastWakeUpTime + wakeUpTransferInterval * 1000)) {
lastWakeUpTime = currentTime;
result = true;
}
}
return result;
}
/**
* Runs the task.
*
* @throws BuildException if the task fails or is not configured
* correctly.
*/
@Override
public void execute() throws BuildException {
checkAttributes();
FTPClient ftp = null;
try {
log("Opening FTP connection to " + server, Project.MSG_VERBOSE);
ftp = new FTPClient();
if (this.isConfigurationSet) {
ftp = FTPConfigurator.configure(ftp, this);
}
ftp.setRemoteVerificationEnabled(enableRemoteVerification);
ftp.connect(server, port);
if (dataTimeout >= 0) {
ftp.setDataTimeout(dataTimeout);
log("Setting data timeout to " + dataTimeout, Project.MSG_VERBOSE);
}
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
throw new BuildException("FTP connection failed: %s",
ftp.getReplyString());
}
log("connected", Project.MSG_VERBOSE);
log("logging in to FTP server", Project.MSG_VERBOSE);
if ((this.account != null && !ftp.login(userid, password, account))
|| (this.account == null && !ftp.login(userid, password))) {
throw new BuildException("Could not login to FTP server");
}
log("login succeeded", Project.MSG_VERBOSE);
if (binary) {
ftp.setFileType(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
throw new BuildException("could not set transfer type: %s",
ftp.getReplyString());
}
} else {
ftp.setFileType(org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
throw new BuildException("could not set transfer type: %s",
ftp.getReplyString());
}
}
if (passive) {
log("entering passive mode", Project.MSG_VERBOSE);
ftp.enterLocalPassiveMode();
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
throw new BuildException(
"could not enter into passive mode: %s",
ftp.getReplyString());
}
}
// If an initial command was configured then send it.
// Some FTP servers offer different modes of operation,
// E.G. switching between a UNIX file system mode and
// a legacy file system.
if (this.initialSiteCommand != null) {
final FTPClient lftp = ftp;
executeRetryable(new RetryHandler(this.retriesAllowed, this),
() -> doSiteCommand(lftp, FTP.this.initialSiteCommand),
"initial site command: " + this.initialSiteCommand);
}
// For a unix ftp server you can set the default mask for all files
// created.
if (umask != null) {
final FTPClient lftp = ftp;
executeRetryable(new RetryHandler(this.retriesAllowed, this),
() -> doSiteCommand(lftp, "umask " + umask),
"umask " + umask);
}
// If the action is MK_DIR, then the specified remote
// directory is the directory to create.
if (action == MK_DIR) {
final FTPClient lftp = ftp;
executeRetryable(new RetryHandler(this.retriesAllowed, this),
() -> makeRemoteDir(lftp, remotedir), remotedir);
} else if (action == SITE_CMD) {
final FTPClient lftp = ftp;
executeRetryable(new RetryHandler(this.retriesAllowed, this),
() -> doSiteCommand(lftp, FTP.this.siteCommand),
"Site Command: " + this.siteCommand);
} else {
if (remotedir != null) {
log("changing the remote directory to " + remotedir,
Project.MSG_VERBOSE);
ftp.changeWorkingDirectory(remotedir);
if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
throw new BuildException(
"could not change remote directory: %s",
ftp.getReplyString());
}
}
if (newerOnly && timeDiffAuto) {
// in this case we want to find how much time span there is between local
// and remote
timeDiffMillis = getTimeDiff(ftp);
}
log(ACTION_STRS[action] + " " + ACTION_TARGET_STRS[action]);
transferFiles(ftp);
}
} catch (IOException ex) {
final Throwable cause = ex.getCause();
if (cause != null) {
final String msg = cause.toString();
if (msg != null && msg.contains("java.net.SocketTimeoutException")) {
// When a read timeout occurs, inform the server that it
// should abort.
// Note that the latest commons-net (3.6) still does not
// support sending urgent data, which is normally a
// prerequisite for ABORT command.
// As a consequence, it might not be taken in account immediately
try {
ftp.abort();
} catch (IOException ioe) {
// ignore it
}
}
}
throw new BuildException("error during FTP transfer: " + ex, ex);
} finally {
if (ftp != null && ftp.isConnected()) {
try {
log("disconnecting", Project.MSG_VERBOSE);
ftp.logout();
ftp.disconnect();
} catch (IOException ex) {
// ignore it
}
}
}
}
/**
* an action to perform, one of
* "send", "put", "recv", "get", "del", "delete", "list", "mkdir", "chmod",
* "rmdir"
*/
public static class Action extends EnumeratedAttribute {
private static final String[] VALID_ACTIONS = {
"send", "put", "recv", "get", "del", "delete", "list", "mkdir",
"chmod", "rmdir", "site"
};
/**
* Get the valid values
*
* @return an array of the valid FTP actions.
*/
@Override
public String[] getValues() {
return VALID_ACTIONS;
}
/**
* Get the symbolic equivalent of the action value.
*
* @return the SYMBOL representing the given action.
*/
public int getAction() {
switch (getValue().toLowerCase(Locale.ENGLISH)) {
case "send":
case "put":
return SEND_FILES;
case "recv":
case "get":
return GET_FILES;
case "del":
case "delete":
return DEL_FILES;
case "list":
return LIST_FILES;
case "chmod":
return CHMOD;
case "mkdir":
return MK_DIR;
case "rmdir":
return RM_DIR;
case "site":
return SITE_CMD;
default:
return SEND_FILES;
}
}
}
/**
* represents one of the valid timestamp adjustment values
* recognized by the <code>timestampGranularity</code> attribute.<p>
* A timestamp adjustment may be used in file transfers for checking
* uptodateness. MINUTE means to add one minute to the server
* timestamp. This is done because FTP servers typically list
* timestamps HH:mm and client FileSystems typically use HH:mm:ss.
*
* The default is to use MINUTE for PUT actions and NONE for GET
* actions, since GETs have the <code>preserveLastModified</code>
* option, which takes care of the problem in most use cases where
* this level of granularity is an issue.
*
*/
public static class Granularity extends EnumeratedAttribute {
private static final String[] VALID_GRANULARITIES = {
"", "MINUTE", "NONE"
};
/**
* Get the valid values.
* @return the list of valid Granularity values
*/
@Override
public String[] getValues() {
return VALID_GRANULARITIES;
}
/**
* returns the number of milliseconds associated with
* the attribute, which can vary in some cases depending
* on the value of the action parameter.
* @param action SEND_FILES or GET_FILES
* @return the number of milliseconds associated with
* the attribute, in the context of the supplied action
*/
public long getMilliseconds(int action) {
String granularityU = getValue().toUpperCase(Locale.ENGLISH);
if (granularityU.isEmpty()) {
if (action == SEND_FILES) {
return GRANULARITY_MINUTE;
}
} else if ("MINUTE".equals(granularityU)) {
return GRANULARITY_MINUTE;
}
return 0L;
}
static final Granularity getDefault() {
Granularity g = new Granularity();
g.setValue("");
return g;
}
}
/**
* one of the valid system type keys recognized by the systemTypeKey
* attribute.
*/
public static class FTPSystemType extends EnumeratedAttribute {
private static final String[] VALID_SYSTEM_TYPES = {
"", "UNIX", "VMS", "WINDOWS", "OS/2", "OS/400",
"MVS"
};
/**
* Get the valid values.
* @return the list of valid system types.
*/
@Override
public String[] getValues() {
return VALID_SYSTEM_TYPES;
}
static final FTPSystemType getDefault() {
FTPSystemType ftpst = new FTPSystemType();
ftpst.setValue("");
return ftpst;
}
}
/**
* Enumerated class for languages.
*/
public static class LanguageCode extends EnumeratedAttribute {
private static final String[] VALID_LANGUAGE_CODES =
getValidLanguageCodes();
private static String[] getValidLanguageCodes() {
Collection<String> c = FTPClientConfig.getSupportedLanguageCodes();
String[] ret = new String[c.size() + 1];
int i = 0;
ret[i++] = "";
for (String element : c) {
ret[i++] = element;
}
return ret;
}
/**
* Return the value values.
* @return the list of valid language types.
*/
@Override
public String[] getValues() {
return VALID_LANGUAGE_CODES;
}
static final LanguageCode getDefault() {
LanguageCode lc = new LanguageCode();
lc.setValue("");
return lc;
}
}
}