001package dk.netarkivet.common.distribute;
002
003import java.io.IOException;
004import java.io.Serializable;
005import java.util.Calendar;
006
007import org.apache.commons.net.ftp.FTPClient;
008import org.apache.commons.net.io.CopyStreamException;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import dk.netarkivet.common.exceptions.IOFailure;
013import dk.netarkivet.common.utils.SystemUtils;
014import dk.netarkivet.common.utils.TimeUtils;
015
016public class FTPConnectionManager implements Serializable {
017
018        
019        /** A named logger for this class. */
020    private static final transient Logger log = LoggerFactory.getLogger(FTPConnectionManager.class);
021
022    /** The FTP client object for the current connection. */
023    private transient FTPClient currentFTPClient;
024
025    /**
026     * Ftp-connection information.
027     */
028    private String ftpServerName;
029    /** The ftp-server port. */
030    private final int ftpServerPort;
031    /** The username used to connect to the ftp-server. */
032    private final String ftpUserName;
033    /** The password used to connect to the ftp-server. */
034    private final String ftpUserPassword;
035
036        private int ftpRetries;
037
038        private int ftpDataTimeout;
039        
040        public FTPConnectionManager(String ftpUserName, String ftpUserPassword,
041            String ftpServerName, int ftpServerPort, int ftpRetries, int ftpDataTimeout) {
042                
043                this.ftpUserName = ftpUserName;
044                this.ftpUserPassword = ftpUserPassword;
045                this.ftpServerName = ftpServerName;
046                if (ftpServerName.equalsIgnoreCase("localhost")) {
047            this.ftpServerName = SystemUtils.getLocalHostName();
048            log.debug("ftpServerName set to localhost on machine: {}, resetting to {}",
049                    SystemUtils.getLocalHostName(), ftpServerName);
050        }
051                
052                this.ftpServerPort = ftpServerPort;
053                this.ftpRetries = ftpRetries;
054                this.ftpDataTimeout = ftpDataTimeout;
055        }
056        
057        /**
058     * Create FTPClient and log on to ftp-server, if not already connected to ftp-server. Attempts to set binary mode
059     * and passive mode. Will try to login up to FTP_RETRIES times, if login fails.
060     */
061    void logOn() {
062        if (currentFTPClient != null && currentFTPClient.isConnected()) {
063            return;
064        } else { // create new FTPClient object and connect to ftp-server
065            currentFTPClient = new FTPClient();
066        }
067
068        if (log.isDebugEnabled()) {
069            log.trace("Try to logon to ftp://{}:{}@{}:{}", ftpUserName, ftpUserPassword.replaceAll(".", "*"),
070                    ftpServerName, ftpServerPort);
071        }
072
073        int tries = 0;
074        boolean logOnSuccessful = false;
075        while (!logOnSuccessful && tries < ftpRetries) {
076            tries++;
077            try {
078                currentFTPClient.connect(ftpServerName, ftpServerPort);
079                currentFTPClient.setDataTimeout(ftpDataTimeout);
080                if (!currentFTPClient.login(ftpUserName, ftpUserPassword)) {
081                    final String message = "Could not log in [from host: " + SystemUtils.getLocalHostName() + "] to '"
082                            + ftpServerName + "' on port " + ftpServerPort + " with user '" + ftpUserName
083                            + "' password '" + ftpUserPassword.replaceAll(".", "*") + "': " + getFtpErrorMessage();
084                    log.warn(message);
085                    throw new IOFailure(message);
086                }
087
088                if (!currentFTPClient.setFileType(FTPClient.BINARY_FILE_TYPE)) {
089                    final String message = "Could not set binary on '" + ftpServerName
090                            + "', losing high bits.  Error: " + getFtpErrorMessage();
091                    log.warn(message);
092                    throw new IOFailure(message);
093                }
094
095                // This only means that PASV is sent before every transfer
096                // command.
097                currentFTPClient.enterLocalPassiveMode();
098
099                log.debug("w/ DataTimeout (ms): {}", currentFTPClient.getDefaultTimeout());
100                logOnSuccessful = true;
101            } catch (IOException e) {
102                final String msg = "Connect to " + ftpServerName + " from host: " + SystemUtils.getLocalHostName()
103                        + " failed";
104                if (tries < ftpRetries) {
105                    log.debug(
106                            "{}. Attempt #{} of max {}. Will sleep a while before trying to connect again. Exception: ",
107                            msg, tries, ftpRetries);
108                    TimeUtils.exponentialBackoffSleep(tries, Calendar.MINUTE);
109                } else {
110                    log.warn("{}. This was the last (#{}) connection attempt", msg, tries);
111                    throw new IOFailure(msg, e);
112                }
113            }
114        }
115
116        if (log.isDebugEnabled()) {
117            log.debug("Logged onto ftp://{}:{}@{}:{}", ftpUserName, ftpUserPassword.replaceAll(".", "*"),
118                    ftpServerName, ftpServerPort);
119        }
120    }
121
122    /**
123     * Get the reply code and string from the ftp client.
124     *
125     * @return A string with the FTP servers last reply code and message.
126     */
127    public String getFtpErrorMessage() {
128        return ("Error " + currentFTPClient.getReplyCode() + ": '" + currentFTPClient.getReplyString() + "'");
129    }
130
131    /**
132     * Log out from the FTP server.
133     */
134    void logOut() {
135        log.debug("Trying to log out.");
136        try {
137            if (currentFTPClient != null) {
138                currentFTPClient.disconnect();
139            }
140        } catch (IOException e) {
141            String msg = "Disconnect from '" + ftpServerName + "' failed ";
142            if (e instanceof CopyStreamException) {
143                CopyStreamException realException = (CopyStreamException) e;
144                msg += "(real cause = " + realException.getIOException() + ")";
145            }
146            log.warn(msg, e);
147        }
148    }
149
150        public FTPClient getFTPClient() {
151                return currentFTPClient;
152        }
153
154        public String getFtpServer() {
155                return ftpServerName;
156        }
157
158}