/* * Copyright 1999-2006 University of Chicago * * Licensed 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.dcache.ftp.client; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.BufferedReader; import java.io.StringReader; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.StringTokenizer; import java.util.Vector; import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.util.TimeZone; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.text.SimpleDateFormat; import java.text.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.dcache.ftp.client.exception.ClientException; import org.dcache.ftp.client.exception.ServerException; import org.dcache.ftp.client.exception.FTPReplyParseException; import org.dcache.ftp.client.exception.UnexpectedReplyCodeException; import org.dcache.ftp.client.exception.FTPException; import org.dcache.ftp.client.vanilla.FTPControlChannel; import org.dcache.ftp.client.vanilla.FTPServerFacade; import org.dcache.ftp.client.vanilla.BasicClientControlChannel; import org.dcache.ftp.client.vanilla.Command; import org.dcache.ftp.client.vanilla.Reply; import org.dcache.ftp.client.vanilla.TransferMonitor; import org.dcache.ftp.client.vanilla.TransferState; import org.dcache.util.PortRange; /** * This is the main user interface for FTP operations. * Use this class for client - server or third party transfers * that do not require GridFTP extensions. * Consult the manual for general usage. * <br><b>Note:</b> If using with GridFTP servers operations like * {@link #setMode(int) setMode()}, {@link #setType(int) setType()} that * affect data channel settings <b>must</b> be called before passive * or active data channel mode is set. **/ public class FTPClient { private static final Logger logger = LoggerFactory.getLogger(FTPClient.class); // represents the state of interaction with remote server protected Session session; protected FTPControlChannel controlChannel; // the local server handles data channels protected FTPServerFacade localServer; /* needed for last modified command */ protected SimpleDateFormat dateFormat = null; protected String username = null; protected PortRange portRange = new PortRange(0); /** * Whether to use ALLO with put()/asyncPut() or not */ protected boolean useAllo; /** * List of the checksum algorithms supported by the server as described in * {@link http://www.ogf.org/documents/GFD.47.pdf [GridFTP v2 Protocol Description]} */ protected List<String> algorithms; /* for subclasses */ protected FTPClient() { } /** * Constructs client and connects it to the remote server. * * @param host remote server host * @param port remote server port */ public FTPClient(String host, int port) throws IOException, ServerException { session = new Session(); controlChannel = new FTPControlChannel(host, port); controlChannel.open(); localServer = new FTPServerFacade(controlChannel); localServer.authorize(); } public PortRange getPortRange() { return portRange; } public void setPortRange(PortRange portRange) { this.portRange = portRange; } /* * @return host */ public String getHost() { return this.controlChannel.getHost(); } /* * @return port */ public int getPort() { return this.controlChannel.getPort(); } /** * Returns the last reply received from the server. This could * be used immediately after the call to the constructor to * get the initial server reply */ public Reply getLastReply() { return this.controlChannel.getLastReply(); } /** * Returns the remote file size. * * @param filename filename get the size for. * @return size of the file. * @throws ServerException if the file does not exist or * an error occured. */ public long getSize(String filename) throws IOException, ServerException { if (filename == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("SIZE", filename); Reply reply = null; try { reply = controlChannel.execute(cmd); return Long.parseLong(reply.getMessage()); } catch (NumberFormatException e) { throw ServerException.embedFTPReplyParseException( new FTPReplyParseException( FTPReplyParseException.MESSAGE_UNPARSABLE, "Could not parse size: " + reply.getMessage())); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Returns last modification time of the specifed file. * * @param filename filename get the last modification time for. * @return the time and date of the last modification. * @throws ServerException if the file does not exist or * an error occured. */ public Date getLastModified(String filename) throws IOException, ServerException { if (filename == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("MDTM", filename); Reply reply = null; try { reply = controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing transfer mode"); } if (dateFormat == null) { dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } try { return dateFormat.parse(reply.getMessage()); } catch (ParseException e) { throw ServerException.embedFTPReplyParseException( new FTPReplyParseException( 0, "Invalid file modification time reply: " + reply)); } } /** * Checks if given file/directory exists on the server. * * @param filename file or directory name * @return true if the file exists, false otherwise. */ public boolean exists(String filename) throws IOException, ServerException { if (filename == null) { throw new IllegalArgumentException("Required argument missing"); } try { Reply reply = controlChannel.exchange(new Command("RNFR", filename)); if (Reply.isPositiveIntermediate(reply)) { controlChannel.execute(new Command("ABOR")); return true; } else { return false; } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Abort failed"); } } /** * Changes the remote current working directory. */ public void changeDir(String dir) throws IOException, ServerException { if (dir == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("CWD", dir); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing directory"); } } /** * Deletes the remote directory. */ public void deleteDir(String dir) throws IOException, ServerException { if (dir == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("RMD", dir); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused deleting directory"); } } /** * Deletes the remote file. */ public void deleteFile(String filename) throws IOException, ServerException { if (filename == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("DELE", filename); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused deleting file"); } } /** * Creates remote directory. */ public void makeDir(String dir) throws IOException, ServerException { if (dir == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("MKD", dir); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused creating directory"); } } /** * Renames remote directory. */ public void rename(String oldName, String newName) throws IOException, ServerException { if (oldName == null || newName == null) { throw new IllegalArgumentException("Required argument missing"); } Command cmd = new Command("RNFR", oldName); try { Reply reply = controlChannel.exchange(cmd); if (!Reply.isPositiveIntermediate(reply)) { throw new UnexpectedReplyCodeException(reply); } controlChannel.execute(new Command("RNTO", newName)); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused renaming file"); } } /** * Returns remote current working directory. * * @return remote current working directory. */ public String getCurrentDir() throws IOException, ServerException { Reply reply = null; try { reply = controlChannel.execute(Command.PWD); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused returning current directory"); } String strReply = reply.getMessage(); if (strReply.length() > 0 && strReply.charAt(0) == '"') { return strReply.substring(1, strReply.indexOf('"', 1)); } else { throw ServerException.embedFTPReplyParseException( new FTPReplyParseException( 0, "Cannot parse 'PWD' reply: " + reply)); } } /** * Changes remote current working directory to the higher level. */ public void goUpDir() throws IOException, ServerException { try { controlChannel.execute(Command.CDUP); // alternative: changeDir(".."); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing current directory"); } } private class ByteArrayDataSink implements DataSink { private final ByteArrayOutputStream received; public ByteArrayDataSink() { this.received = new ByteArrayOutputStream(1000); } @Override public void write(Buffer buffer) throws IOException { if (logger.isDebugEnabled()) { logger.debug( "received " + buffer.getLength() + " bytes of directory listing"); } this.received.write(buffer.getBuffer(), 0, buffer.getLength()); } @Override public void close() throws IOException { } public ByteArrayOutputStream getData() { return this.received; } } /** * Performs remote directory listing. Sends 'LIST -d *' command. * <p> * <br><b>Note</b>:<i> * This function can only parse Unix ls -d like output. Please * note that the LIST output is unspecified in the FTP standard and * each server might return slightly different output causing the * parsing to fail. * Also, if the ftp server does not accept -d option or support * wildcards, this method might fail. For example, this command will * fail on GridFTP server distributed with GT 4.0.0. * It is strongly recommended to use {@link #mlsd() mlsd()} * function instead.</i> * * @return Vector list of {@link FileInfo FileInfo} objects, representing * remote files * @see #mlsd() */ public Vector list() throws ServerException, ClientException, IOException { return list("*"); } /** * Performs remote directory listing with the specified filter. * Sends 'LIST -d <filter>' command. * <p> * <br><b>Note</b>: <i> * This function can only parse Unix ls -d like output. Please * note that the LIST output is unspecified in the FTP standard and * each server might return slightly different output causing the * parsing to fail. * Also, if the ftp server does not accept -d option or support * wildcards, this method might fail. For example, this command will * fail on GridFTP server distributed with GT 4.0.0. * It is strongly recommended to use {@link #mlsd(String) mlsd()} * function instead. </i> * * @param filter "*" for example, can be null. * @return Vector list of {@link FileInfo FileInfo} objects, representing * remote files * @see #mlsd(String) */ public Vector list(String filter) throws ServerException, ClientException, IOException { return list(filter, "-d"); } /** * Performs remote directory listing with the specified filter and * modifier. Sends 'LIST <modifier> <filter>' command. * <p> * <br><b>Note</b>: <i> * This function can only parse Unix ls -d like output. Please * note that the LIST output is unspecified in the FTP standard and * each server might return slightly different output causing the * parsing to fail. * Also, please keep in mind that the ftp server might not * recognize or support all the different modifiers or filters. * In fact, some servers such as GridFTP server distributed with * GT 4.0.0 does not support any modifiers or filters * (strict RFC 959 compliance). * It is strongly recommended to use {@link #mlsd(String) mlsd()} * function instead.</i> * * @param filter "*" for example, can be null. * @param modifier "-d" for example, can be null. * @return Vector list of {@link FileInfo FileInfo} objects, representing * remote files * @see #mlsd(String) */ public Vector list(String filter, String modifier) throws ServerException, ClientException, IOException { ByteArrayDataSink sink = new ByteArrayDataSink(); list(filter, modifier, sink); ByteArrayOutputStream received = sink.getData(); // transfer done. Data is in received stream. // convert it to a vector. BufferedReader reader = new BufferedReader(new StringReader(new String(received.toByteArray(), StandardCharsets.UTF_8))); Vector fileList = new Vector(); FileInfo fileInfo = null; String line = null; while ((line = reader.readLine()) != null) { line = line.trim(); if (logger.isDebugEnabled()) { logger.debug("line ->" + line); } if (line.equals("")) { continue; } if (line.startsWith("total")) continue; try { fileInfo = new FileInfo(line); } catch (FTPException e) { ClientException ce = new ClientException( ClientException.UNSPECIFIED, "Could not create FileInfo"); ce.setRootCause(e); throw ce; } fileList.addElement(fileInfo); } return fileList; } /** * Performs directory listing and writes the result * to the supplied data sink. * This method is allowed in ASCII mode only. * <p> * <br><b>Note</b>: <i> * Please keep in mind that the ftp server might not * recognize or support all the different modifiers or filters. * In fact, some servers such as GridFTP server distributed with * GT 4.0.0 does not support any modifiers or filters * (strict RFC 959 compliance). * It is strongly recommended to use {@link #mlsd(String, DataSink) * mlsd()} function instead.</i> * * @param filter remote list command file filter, eg. "*" * @param modifier remote list command modifier, eg. "-d" * @param sink data destination **/ public void list(String filter, String modifier, DataSink sink) throws ServerException, ClientException, IOException { String arg = null; if (modifier != null) { arg = modifier; } if (filter != null) { arg = (arg == null) ? filter : arg + " " + filter; } Command cmd = new Command("LIST", arg); performTransfer(cmd, sink); } /** * Performs remote directory listing of the current directory. * Sends 'NLST' command. * * @return Vector list of {@link FileInfo FileInfo} objects, representing * remote files */ public Vector nlist() throws ServerException, ClientException, IOException { return nlist(null); } /** * Performs remote directory listing on the given path. * Sends 'NLST <path>' command. * * @param path directory to perform listing of. If null, listing * of current directory will be performed. * @return Vector list of {@link FileInfo FileInfo} objects, representing * remote files */ public Vector nlist(String path) throws ServerException, ClientException, IOException { ByteArrayDataSink sink = new ByteArrayDataSink(); nlist(path, sink); ByteArrayOutputStream received = sink.getData(); // transfer done. Data is in received stream. // convert it to a vector. BufferedReader reader = new BufferedReader(new StringReader(new String(received.toByteArray(), StandardCharsets.UTF_8))); Vector fileList = new Vector(); FileInfo fileInfo = null; String line = null; while ((line = reader.readLine()) != null) { if (logger.isDebugEnabled()) { logger.debug("line ->" + line); } fileInfo = new FileInfo(); fileInfo.setName(line); fileInfo.setFileType(FileInfo.UNKNOWN_TYPE); fileList.addElement(fileInfo); } return fileList; } /** * Performs remote directory listing on the given path. * Sends 'NLST <path>' command. * * @param path directory to perform listing of. If null, listing * of current directory will be performed. * @param sink sink to which the listing data will be written. */ public void nlist(String path, DataSink sink) throws ServerException, ClientException, IOException { Command cmd = (path == null) ? new Command("NLST") : new Command("NLST", path); performTransfer(cmd, sink); } /** * Get info of a certain remote file in Mlsx format. */ public MlsxEntry mlst(String fileName) throws IOException, ServerException { try { Reply reply = controlChannel.execute(new Command("MLST", fileName)); String replyMessage = reply.getMessage(); StringTokenizer replyLines = new StringTokenizer( replyMessage, System.getProperty("line.separator")); if (replyLines.hasMoreElements()) { replyLines.nextElement(); } else { throw new FTPException(FTPException.UNSPECIFIED, "Expected multiline reply"); } if (replyLines.hasMoreElements()) { String line = (String) replyLines.nextElement(); return new MlsxEntry(line); } else { throw new FTPException(FTPException.UNSPECIFIED, "Expected multiline reply"); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused MLST command"); } catch (FTPException e) { ServerException ce = new ServerException( ClientException.UNSPECIFIED, "Could not create MlsxEntry"); ce.setRootCause(e); throw ce; } } /** * Performs remote directory listing of the current directory. * Sends 'MLSD' command. * * @return Vector list of {@link MlsxEntry MlsxEntry} objects, representing * remote files */ public Vector mlsd() throws ServerException, ClientException, IOException { return mlsd(null); } /** * Performs remote directory listing on the given path. * Sends 'MLSD <path>' command. * * @param path directory to perform listing of. If null, listing * of current directory will be performed. * @return Vector list of {@link MlsxEntry MlsxEntry} objects, representing * remote files */ public Vector mlsd(String path) throws ServerException, ClientException, IOException { ByteArrayDataSink sink = new ByteArrayDataSink(); mlsd(path, sink); ByteArrayOutputStream received = sink.getData(); // transfer done. Data is in received stream. // convert it to a vector. BufferedReader reader = new BufferedReader(new StringReader(new String(received.toByteArray(), StandardCharsets.UTF_8))); Vector fileList = new Vector(); MlsxEntry entry = null; String line = null; while ((line = reader.readLine()) != null) { if (logger.isDebugEnabled()) { logger.debug("line ->" + line); } try { entry = new MlsxEntry(line); } catch (FTPException e) { ClientException ce = new ClientException( ClientException.UNSPECIFIED, "Could not create MlsxEntry"); ce.setRootCause(e); throw ce; } fileList.addElement(entry); } return fileList; } /** * Performs remote directory listing on the given path. * Sends 'MLSD <path>' command. * * @param path directory to perform listing of. If null, listing * of current directory will be performed. * @param sink sink to which the listing data will be written. */ public void mlsd(String path, DataSink sink) throws ServerException, ClientException, IOException { Command cmd = (path == null) ? new Command("MLSD") : new Command("MLSD", path); performTransfer(cmd, sink); } /** * check performed at the beginning of list() **/ protected void listCheck() throws ClientException { if (session.transferType != Session.TYPE_ASCII) { throw new ClientException( ClientException.BAD_MODE, "list requires ASCII type"); } } protected void checkTransferParamsGet() throws ServerException, IOException, ClientException { checkTransferParams(); } protected void checkTransferParamsPut() throws ServerException, IOException, ClientException { checkTransferParams(); } protected void checkTransferParams() throws ServerException, IOException, ClientException { Session localSession = localServer.getSession(); session.matches(localSession); // if transfer modes have not been defined, // set this (dest) as active if (session.serverMode == Session.SERVER_DEFAULT) { // resulting HostPort stored in session setPassive(); // HostPort read from session setLocalActive(); } } protected void performTransfer(Command cmd, DataSink sink) throws ServerException, ClientException, IOException { listCheck(); checkTransferParamsGet(); controlChannel.write(cmd); localServer.store(sink); transferRunSingleThread(localServer.getControlChannel(), null); } /** * Sets transfer type. * * @param type should be {@link Session#TYPE_IMAGE TYPE_IMAGE}, * {@link Session#TYPE_ASCII TYPE_ASCII}, * {@link Session#TYPE_LOCAL TYPE_LOCAL}, * {@link Session#TYPE_EBCDIC TYPE_EBCDIC} **/ public void setType(int type) throws IOException, ServerException { localServer.setTransferType(type); String typeStr = null; switch (type) { case Session.TYPE_IMAGE: typeStr = "I"; break; case Session.TYPE_ASCII: typeStr = "A"; break; case Session.TYPE_LOCAL: typeStr = "E"; break; case Session.TYPE_EBCDIC: typeStr = "L"; break; default: throw new IllegalArgumentException("Bad type: " + type); } Command cmd = new Command("TYPE", typeStr); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing transfer mode"); } this.session.transferType = type; } protected String getModeStr(int mode) { switch (mode) { case Session.MODE_STREAM: return "S"; case Session.MODE_BLOCK: return "B"; default: throw new IllegalArgumentException("Bad mode: " + mode); } } /** * Sets transfer mode. * * @param mode should be {@link Session#MODE_STREAM MODE_STREAM}, * {@link Session#MODE_BLOCK MODE_BLOCK} **/ public void setMode(int mode) throws IOException, ServerException { actualSetMode(mode, getModeStr(mode)); } protected void actualSetMode(int mode, String modeStr) throws IOException, ServerException { localServer.setTransferMode(mode); Command cmd = new Command("MODE", modeStr); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing transfer mode"); } this.session.transferMode = mode; } /** * Sets protection buffer size (defined in RFC 2228) * * @param size the size of buffer */ public void setProtectionBufferSize(int size) throws IOException, ServerException { if (size <= 0) { throw new IllegalArgumentException("size <= 0"); } localServer.setProtectionBufferSize(size); try { Command cmd = new Command("PBSZ", Integer.toString(size)); controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused setting protection buffer size"); } this.session.protectionBufferSize = size; } /** * Aborts the current transfer. FTPClient is not thread * safe so be careful with using this procedure, which will * typically happen in multi threaded environment. * Especially during client-server two party transfer, * calling abort() may result with exceptions being thrown in the thread * that currently perform the transfer. **/ public void abort() throws IOException, ServerException { // TODO: This might need to be reimplemented to support // sending out of bounds urgent TCP messages try { controlChannel.execute(Command.ABOR); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused changing transfer mode"); } finally { localServer.abort(); } } /** * Closes connection. Sends QUIT command and closes connection * even if the server reply was not positive. Also, closes * the local server. This function will block until the server * sends a reply to the QUIT command. **/ public void close() throws IOException, ServerException { close(false); } /** * Closes connection. Sends QUIT and closes connection * even if the server reply was not positive. Also, closes * the local server. * * @param ignoreQuitReply if true the <code>QUIT</code> command * will be sent but the client will not wait for the * server's reply. If false, the client will block * for the server's reply. **/ public void close(boolean ignoreQuitReply) throws IOException, ServerException { try { if (ignoreQuitReply) { controlChannel.write(Command.QUIT); } else { controlChannel.execute(Command.QUIT); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused closing"); } finally { try { controlChannel.close(); } finally { localServer.close(); } } } /** * Returns true if the given feature is supported by remote server, * false otherwise. * * @return true if the given feature is supported by remote server, * false otherwise. */ public boolean isFeatureSupported(String feature) throws IOException, ServerException { return getFeatureList().contains(feature); } /** * Returns list of features supported by remote server. * * @return list of features supported by remote server. */ public FeatureList getFeatureList() throws IOException, ServerException { if (this.session.featureList != null) { return this.session.featureList; } // TODO: this can also be optimized. Instead of parsing the // reply after it is reveiced, we can parse it as it is // received. Reply featReply = null; try { featReply = controlChannel.execute(Command.FEAT); if (featReply.getCode() != 211) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(featReply), "Server refused returning features"); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused returning features"); } this.session.featureList = new FeatureList(featReply.getMessage()); return session.featureList; } /** * Sets remote server to passive server mode. * * @return the address at which the server is listening. */ public HostPort setPassive() throws IOException, ServerException { Reply reply = null; try { reply = controlChannel.execute( (controlChannel.isIPv6()) ? Command.EPSV : Command.PASV); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } String pasvReplyMsg = null; pasvReplyMsg = reply.getMessage(); int openBracket = pasvReplyMsg.indexOf("("); int closeBracket = pasvReplyMsg.indexOf(")", openBracket); String bracketContent = pasvReplyMsg.substring(openBracket + 1, closeBracket); this.session.serverMode = Session.SERVER_PASSIVE; HostPort hp = null; if (controlChannel.isIPv6()) { hp = new HostPort6(bracketContent); // since host information might be null // fill it it if (hp.getHost() == null) { ((HostPort6) hp).setVersion(HostPort6.IPv6); ((HostPort6) hp).setHost(controlChannel.getHost()); } } else { hp = new HostPort(bracketContent); } this.session.serverAddress = hp; return hp; } /** * Sets remote server active, telling it to connect to the given * address. * * @param hostPort the address to which the server should connect */ public void setActive(HostPort hostPort) throws IOException, ServerException { Command cmd = new Command((controlChannel.isIPv6()) ? "EPRT" : "PORT", hostPort.toFtpCmdArgument()); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } this.session.serverMode = Session.SERVER_ACTIVE; } /** * Sets remote server active, telling it to connect to the client. * setLocalPassive() must be called beforehand. **/ public void setActive() throws IOException, ServerException, ClientException { Session local = localServer.getSession(); if (local.serverAddress == null) { throw new ClientException(ClientException.CALL_PASSIVE_FIRST); } setActive(local.serverAddress); } /** * Starts local server in active server mode. **/ public void setLocalActive() throws ClientException, IOException { if (session.serverAddress == null) { throw new ClientException(ClientException.CALL_PASSIVE_FIRST); } try { localServer.setActive(session.serverAddress); } catch (java.net.UnknownHostException e) { throw new ClientException(ClientException.UNKNOWN_HOST); } } /** * Starts local server in passive server mode, with default parameters. * In other words, behaves like * setLocalPassive(FTPServerFacade.ANY_PORT, FTPServerFacade.DEFAULT_QUEUE) **/ public HostPort setLocalPassive() throws IOException { return localServer.setPassive(portRange, FTPServerFacade.DEFAULT_QUEUE); } /** * Starts the local server in passive server mode. * * @param range port range at which local server should be listening * @param queue max size of queue of awaiting new connection * requests * @return the server address **/ public HostPort setLocalPassive(PortRange range, int queue) throws IOException { return localServer.setPassive(range, queue); } /** * Changes the default client timeout parameters. * In the beginning of the transfer, the critical moment is the wait * for the initial server reply. If it does not arrive after timeout, * client assumes that the transfer could not start for some reason and * aborts the operation. Default timeout in miliseconds * is Session.DEFAULT_MAX_WAIT. During the waiting period, * client polls the control channel once a certain period, which is by * default set to Session.DEFAULT_WAIT_DELAY. * <br> * Use this method to change these parameters. * * @param maxWait timeout in miliseconds * @param waitDelay polling period **/ public void setClientWaitParams(int maxWait, int waitDelay) { if (maxWait <= 0 || waitDelay <= 0) { throw new IllegalArgumentException("Parameter is less than 0"); } this.session.maxWait = maxWait; this.session.waitDelay = waitDelay; } /** * Sets the supplied options to the server. */ public void setOptions(Options opts) throws IOException, ServerException { Command cmd = new Command("OPTS", opts.toFtpCmdArgument()); try { controlChannel.execute(cmd); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException( urce, "Server refused setting options"); } localServer.setOptions(opts); } /** * Sets restart parameter of the next transfer. * * @param restartData marker to use * @throws ServerException if the file does not exist or * an error occured. */ public void setRestartMarker(RestartData restartData) throws IOException, ServerException { Command cmd = new Command("REST", restartData.toFtpCmdArgument()); Reply reply = null; try { reply = controlChannel.exchange(cmd); } catch (FTPReplyParseException e) { throw ServerException.embedFTPReplyParseException(e); } if (!Reply.isPositiveIntermediate(reply)) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(reply)); } } /** * Performs user authorization with specified * user and password. * * @param user username * @param password user password * @throws ServerException on server refusal */ public void authorize(String user, String password) throws IOException, ServerException { Reply userReply = null; try { userReply = controlChannel.exchange(new Command("USER", user)); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } if (Reply.isPositiveIntermediate(userReply)) { Reply passReply = null; try { passReply = controlChannel.exchange(new Command("PASS", password)); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } if (!Reply.isPositiveCompletion(passReply)) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(passReply), "Bad password."); } // i'm logged in } else if (Reply.isPositiveCompletion(userReply)) { // i'm logged in } else { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(userReply), "Bad user."); } this.session.authorized = true; this.username = user; } public String getUserName() { return this.username; } /** * Retrieves the file from the remote server. * * @param remoteFileName remote file name * @param sink sink to which the data will be written * @param mListener restart marker listener (currently not used) */ public void get(String remoteFileName, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { checkTransferParamsGet(); localServer.store(sink); controlChannel.write(new Command("RETR", remoteFileName)); transferRunSingleThread(localServer.getControlChannel(), mListener); } /** * Retrieves the file from the remote server. * * @param remoteFileName remote file name * @param sink sink to which the data will be written * @param mListener restart marker listener (currently not used) */ public TransferState asynchGet(String remoteFileName, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { checkTransferParamsGet(); localServer.store(sink); controlChannel.write(new Command("RETR", remoteFileName)); return transferStart(localServer.getControlChannel(), mListener); } /** * Stores file at the remote server. * * @param remoteFileName remote file name * @param source data will be read from here * @param mListener restart marker listener (currently not used) */ public void put(String remoteFileName, DataSource source, MarkerListener mListener) throws IOException, ServerException, ClientException { put(remoteFileName, source, mListener, false); } /** * Stores file at the remote server. * * @param remoteFileName remote file name * @param source data will be read from here * @param mListener restart marker listener (currently not used) * @param append append to the end of file or overwrite */ public void put(String remoteFileName, DataSource source, MarkerListener mListener, boolean append) throws IOException, ServerException, ClientException { checkTransferParamsPut(); if (useAllo && source.totalSize() != -1) { allocate(source.totalSize()); } localServer.retrieve(source); if (append) { controlChannel.write(new Command("APPE", remoteFileName)); } else { controlChannel.write(new Command("STOR", remoteFileName)); } transferRunSingleThread(localServer.getControlChannel(), mListener); } /** * Stores file at the remote server. * * @param remoteFileName remote file name * @param source data will be read from here * @param mListener restart marker listener (currently not used) */ public TransferState asynchPut(String remoteFileName, DataSource source, MarkerListener mListener) throws IOException, ServerException, ClientException { return asynchPut(remoteFileName, source, mListener, false); } /** * Stores file at the remote server. * * @param remoteFileName remote file name * @param source data will be read from here * @param mListener restart marker listener (currently not used) * @param append append to the end of file or overwrite */ public TransferState asynchPut(String remoteFileName, DataSource source, MarkerListener mListener, boolean append) throws IOException, ServerException, ClientException { checkTransferParamsPut(); if (useAllo && source.totalSize() != -1) { allocate(source.totalSize()); } localServer.retrieve(source); if (append) { controlChannel.write(new Command("APPE", remoteFileName)); } else { controlChannel.write(new Command("STOR", remoteFileName)); } return transferStart(localServer.getControlChannel(), mListener); } /** * Performs third-party transfer between two servers. * * @param remoteSrcFile source filename * @param destination another client connected to destination server * @param remoteDstFile destination filename * @param append enables append mode; if true, * data will be appened to the remote file, otherwise * file will be overwritten. * @param mListener marker listener. * Can be set to null. */ public void transfer(String remoteSrcFile, FTPClient destination, String remoteDstFile, boolean append, MarkerListener mListener) throws IOException, ServerException, ClientException { session.matches(destination.session); // if transfer modes have not been defined, // set this (source) as active if (session.serverMode == Session.SERVER_DEFAULT) { HostPort hp = destination.setPassive(); setActive(hp); } destination.controlChannel.write( new Command((append) ? "APPE" : "STOR", remoteDstFile)); controlChannel.write(new Command("RETR", remoteSrcFile)); transferRunSingleThread(destination.controlChannel, mListener); } /** * Actual transfer management. * Transfer is controlled by two new threads listening * to the two servers. **/ protected void transferRun(BasicClientControlChannel other, MarkerListener mListener) throws IOException, ServerException, ClientException { TransferState transferState = transferBegin(other, mListener); transferWait(transferState); } protected TransferState transferBegin(BasicClientControlChannel other, MarkerListener mListener) { // this structure will contain up to date information // about the state of transfer at both sides TransferState transferState = new TransferState(); // thread monitoring our server during transfer // (that is, the server associated with this FTPClient) TransferMonitor ourMonitor = new TransferMonitor( controlChannel, transferState, mListener, session.maxWait, session.waitDelay, TransferMonitor.LOCAL); // thread monitoring other server during transfer // (that is, the server associated with the other FTPClient) TransferMonitor otherMonitor = new TransferMonitor( other, transferState, mListener, session.maxWait, session.waitDelay, TransferMonitor.REMOTE); ourMonitor.setOther(otherMonitor); otherMonitor.setOther(ourMonitor); // start two threads controling the transfer ourMonitor.start(false); otherMonitor.start(false); return transferState; } protected TransferState transferStart(BasicClientControlChannel other, MarkerListener mListener) throws IOException, ServerException, ClientException { TransferState transferState = transferBegin(other, mListener); transferState.waitForStart(); return transferState; } protected void transferWait(TransferState transferState) throws IOException, ServerException, ClientException { transferState.waitForEnd(); } protected void transferRunSingleThread(BasicClientControlChannel other, MarkerListener mListener) throws IOException, ServerException, ClientException { // this structure will contain up to date information // about the state of transfer at both sides TransferState transferState = new TransferState(); // thread monitoring our server during transfer // (that is, the server associated with this FTPClient) TransferMonitor ourMonitor = new TransferMonitor( controlChannel, transferState, mListener, session.maxWait, session.waitDelay, TransferMonitor.LOCAL); // thread monitoring other server during transfer // (that is, the server associated with the other FTPClient) TransferMonitor otherMonitor = new TransferMonitor( other, transferState, mListener, session.maxWait, session.waitDelay, TransferMonitor.REMOTE); ourMonitor.setOther(otherMonitor); otherMonitor.setOther(ourMonitor); // controling the other connection - non-blocking otherMonitor.start(false); // control this connection - blocking ourMonitor.start(true); transferState.waitForEnd(); } /** * Executes arbitrary operation on the server. * <p> * <br><b>Note</b>: <i>This is potentially dangerous operation. * Depending on the command executed it might put the server in a * different state from the state the client is expecting.</i> * * @param command command to execute * @return the Reply to the operation. * @throws IOException in case of I/O error. * @throws ServerException if operation failed. */ public Reply quote(String command) throws IOException, ServerException { Command cmd = new Command(command); return doCommand(cmd); } /** * Executes site-specific operation (using the SITE command). * <p> * <br><b>Note</b>: <i>This is potentially dangerous operation. * Depending on the command executed it might put the server in a * different state from the state the client is expecting.</i> * * @param args parameters for the SITE operation. * @return the Reply to the operation. * @throws IOException in case of I/O error * @throws ServerException if operation failed. */ public Reply site(String args) throws IOException, ServerException { Command cmd = new Command("SITE", args); return doCommand(cmd); } private Reply doCommand(Command cmd) throws IOException, ServerException { try { return controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Reserve sufficient storage to accommodate the new file to be * transferred. * * @param size the amount of space to reserve * @throws ServerException if an error occured. */ public void allocate(long size) throws IOException, ServerException { Command cmd = new Command("ALLO", String.valueOf(size)); Reply reply = null; try { reply = controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } // basic compatibility API public long size(String filename) throws IOException, ServerException { return getSize(filename); } public Date lastModified(String filename) throws IOException, ServerException { return getLastModified(filename); } public void get(String remoteFileName, File localFile) throws IOException, ClientException, ServerException { DataSink sink = new DataSinkStream(new FileOutputStream(localFile)); get(remoteFileName, sink, null); } public void put(File localFile, String remoteFileName, boolean append) throws IOException, ServerException, ClientException { DataSource source = new DataSourceStream(new FileInputStream(localFile)); put(remoteFileName, source, null, append); } /** * Enables/disables passive data connections. * * @param passiveMode if true passive connections will be * established. If false, they will not. */ public void setPassiveMode(boolean passiveMode) throws IOException, ClientException, ServerException { if (passiveMode) { setPassive(); setLocalActive(); } else { setLocalPassive(); setActive(); } } public boolean isPassiveMode() { return (this.session.serverMode == Session.SERVER_PASSIVE); } ////////////////////////////////////////////////////////////////////// // Implementation of GFD.47 compliant GETPUT support. The reason // why this is implemented in FTPClient rather than GridFTPClient // is, that GFD.47 support is detected via feature strings and is // thus independent of GSI authentication. /** * Throws ServerException if GFD.47 GETPUT is not supported or * cannot be used. */ protected void checkGETPUTSupport() throws ServerException, IOException { if (!isFeatureSupported(FeatureList.GETPUT)) { throw new ServerException(ServerException.UNSUPPORTED_FEATURE); } if (controlChannel.isIPv6()) { throw new ServerException(ServerException.UNSUPPORTED_FEATURE, "Cannot use GridFTP2 with IP 6"); } } /** * Regular expression for matching the port information of a * GFD.47 127 reply. */ public static final Pattern portPattern = Pattern.compile("\\d+,\\d+,\\d+,\\d+,\\d+,\\d+"); /** * Reads a GFD.47 compliant 127 reply and extracts the port * information from it. */ protected HostPort get127Reply() throws ServerException, IOException, FTPReplyParseException { Reply reply = controlChannel.read(); if (Reply.isTransientNegativeCompletion(reply) || Reply.isPermanentNegativeCompletion(reply)) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(reply), reply.getMessage()); } if (reply.getCode() != 127) { throw new ServerException(ServerException.WRONG_PROTOCOL, reply.getMessage()); } Matcher matcher = portPattern.matcher(reply.getMessage()); if (!matcher.find()) { throw new ServerException(ServerException.WRONG_PROTOCOL, "Cannot parse 127 reply: " + reply.getMessage()); } return new HostPort(matcher.group()); } /** * Writes a GFD.47 compliant GET or PUT command to the control * channel. * * @param command Either "GET" or "PUT", depending on the command to issue * @param passive True if the "pasv" parameter should be used * @param port If passive is false, this is the port for * the "port" parameter * @param mode The value for the "mode" parameter, or 0 if the * parameter should not be specified * @param path The value for the "path" parameter */ private void issueGETPUT(String command, boolean passive, HostPort port, int mode, String path) throws IOException { Command cmd = new Command(command, (passive ? "pasv" : ("port=" + port.toFtpCmdArgument()) ) + ";" + "path=" + path + ";" + (mode > 0 ? "mode=" + getModeStr(mode) + ";" : "")); controlChannel.write(cmd); } /** * Retrieves a file using the GFD.47 (a.k.a GridFTP2) GET command. * <p> * Notice that as a side effect this method may change the local * server facade passive/active mode setting. The caller should * not rely on this setting after call to get2. * <p> * Even though the active/passive status of the current session is * ignored for the actual transfer, it still has to be in a * consistent state prior to calling gridftp2Get. * * @param remoteFileName file to retrieve * @param passive whether to configure the server to be passive * @param sink data sink to store the file * @param mListener marker listener **/ public void get2(String remoteFileName, boolean passive, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { int serverMode = session.serverMode; HostPort serverAddress = session.serverAddress; try { // Can we use GETPUT? checkGETPUTSupport(); // Check sanity of arguments if (session.transferMode == GridFTPSession.MODE_EBLOCK && passive) { throw new IllegalArgumentException("Sender must be active in extended block mode"); } // All parameters set correctly (or still unset)? Session localSession = localServer.getSession(); session.matches(localSession); // Connection setup depends a lot on whether we use // passive or active mode. The passive party needs to be // configured before the active party. if (passive) { issueGETPUT("GET", true, null, 0, remoteFileName); session.serverMode = Session.SERVER_PASSIVE; session.serverAddress = get127Reply(); setLocalActive(); localServer.store(sink); } else { HostPort hp = setLocalPassive(); localServer.store(sink); issueGETPUT("GET", false, hp, 0, remoteFileName); session.serverMode = Session.SERVER_ACTIVE; } transferRunSingleThread(localServer.getControlChannel(), mListener); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } finally { session.serverMode = serverMode; session.serverAddress = serverAddress; } } /** * Retrieves a file asynchronously using the GFD.47 (a.k.a * GridFTP2) GET command. * <p> * Notice that as a side effect this method may change the local * server facade passive/active mode setting. The caller should * not rely on this setting after call to gridftp2Get. * <p> * Even though the active/passive status of the current session is * ignored for the actual transfer, it still has to be in a * consistent state prior to calling gridftp2Get. * * @param remoteFileName file to retrieve * @param passive whether to configure the server to be passive * @param sink data sink to store the file * @param mListener marker listener **/ public TransferState asynchGet2(String remoteFileName, boolean passive, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { int serverMode = session.serverMode; HostPort serverAddress = session.serverAddress; try { // Can we use GETPUT? checkGETPUTSupport(); // Check sanity of arguments if (session.transferMode == GridFTPSession.MODE_EBLOCK && passive) { throw new IllegalArgumentException("Sender must be active in extended block mode"); } // All parameters set correctly (or still unset)? Session localSession = localServer.getSession(); session.matches(localSession); // Connection setup depends a lot on whether we use // passive or active mode. The passive party needs to be // configured before the active party. if (passive) { issueGETPUT("GET", true, null, 0, remoteFileName); session.serverMode = Session.SERVER_PASSIVE; session.serverAddress = get127Reply(); setLocalActive(); localServer.store(sink); } else { HostPort hp = setLocalPassive(); localServer.store(sink); issueGETPUT("GET", false, hp, 0, remoteFileName); session.serverMode = Session.SERVER_ACTIVE; } return transferStart(localServer.getControlChannel(), mListener); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } finally { // This might not be the most elegant or correct // solution. On the other hand, these parameters do not // seem to be used after transferStart() and it is much // easier to restore the old values now rather than when // the transfer completes. session.serverMode = serverMode; session.serverAddress = serverAddress; } } /** * Stores a file at the remote server using the GFD.47 (a.k.a * GridFTP2) PUT command. * <p> * Notice that as a side effect this method may change the local * server facade passive/active mode setting. The caller should * not rely on this setting after call to gridftp2Get. * <p> * Even though the active/passive status of the current session is * ignored for the actual transfer, it still has to be in a * consistent state prior to calling gridftp2Get. * * @param remoteFileName file to retrieve * @param passive whether to configure the server to be passive * @param source data will be read from here * @param mListener marker listener **/ public void put2(String remoteFileName, boolean passive, DataSource source, MarkerListener mListener) throws IOException, ClientException, ServerException { int serverMode = session.serverMode; HostPort serverAddress = session.serverAddress; try { // Can we use GETPUT? checkGETPUTSupport(); // Check sanity of arguments if (session.transferMode == GridFTPSession.MODE_EBLOCK && !passive) { throw new IllegalArgumentException("Sender must be active in extended block mode"); } // All parameters set correctly (or still unset)? Session localSession = localServer.getSession(); session.matches(localSession); // Connection setup depends a lot on whether we use // passive or active mode. The passive party needs to be // configured before the active party. if (passive) { issueGETPUT("PUT", true, null, 0, remoteFileName); session.serverMode = Session.SERVER_PASSIVE; session.serverAddress = get127Reply(); setLocalActive(); localServer.retrieve(source); } else { HostPort hp = setLocalPassive(); localServer.retrieve(source); issueGETPUT("PUT", false, hp, 0, remoteFileName); session.serverMode = Session.SERVER_ACTIVE; } transferRunSingleThread(localServer.getControlChannel(), mListener); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } finally { session.serverMode = serverMode; session.serverAddress = serverAddress; } } /** * Stores a file at the remote server using the GFD.47 (a.k.a * GridFTP2) PUT command. * <p> * Notice that as a side effect this method may change the local * server facade passive/active mode setting. The caller should * not rely on this setting after call to gridftp2Get. * <p> * Even though the active/passive status of the current session is * ignored for the actual transfer, it still has to be in a * consistent state prior to calling gridftp2Get. * * @param remoteFileName file to retrieve * @param passive whether to configure the server to be passive * @param source data will be read from here * @param mListener marker listener **/ public TransferState asynchPut2(String remoteFileName, boolean passive, DataSource source, MarkerListener mListener) throws IOException, ClientException, ServerException { int serverMode = session.serverMode; HostPort serverAddress = session.serverAddress; try { // Can we use GETPUT? checkGETPUTSupport(); // Check sanity of arguments if (session.transferMode == GridFTPSession.MODE_EBLOCK && !passive) { throw new IllegalArgumentException("Sender must be active in extended block mode"); } // All parameters set correctly (or still unset)? Session localSession = localServer.getSession(); session.matches(localSession); // Connection setup depends a lot on whether we use // passive or active mode. The passive party needs to be // configured before the active party. if (passive) { issueGETPUT("PUT", true, null, 0, remoteFileName); session.serverMode = Session.SERVER_PASSIVE; session.serverAddress = get127Reply(); setLocalActive(); localServer.retrieve(source); } else { HostPort hp = setLocalPassive(); localServer.retrieve(source); issueGETPUT("PUT", false, hp, 0, remoteFileName); session.serverMode = Session.SERVER_ACTIVE; } return transferStart(localServer.getControlChannel(), mListener); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } finally { // This might not be the most elegant or correct // solution. On the other hand, these parameters do not // seem to be used after transferStart() and it is much // easier to restore the old values now rather than when // the transfer completes. session.serverMode = serverMode; session.serverAddress = serverAddress; } } /** * Performs third-party transfer between two servers. If possibly, * GFD.47 (a.k.a GridFTP2) GET and PUT commands are used. * * @param destination client connected to source server * @param remoteSrcFile source filename * @param destination client connected to destination server * @param remoteDstFile destination filename * @param mode data channel mode or 0 to use the current mode * @param mListener marker listener. * Can be set to null. */ public static void transfer(FTPClient source, String remoteSrcFile, FTPClient destination, String remoteDstFile, int mode, MarkerListener mListener) throws IOException, ServerException, ClientException { try { // Although neither mode nor passive setting from in the // session is used, we still perform this check, since // other things may be checked as well. source.session.matches(destination.session); HostPort hp; if (destination.isFeatureSupported(FeatureList.GETPUT)) { destination.issueGETPUT("PUT", true, null, mode, remoteDstFile); hp = destination.get127Reply(); } else { if (mode > 0) { destination.setMode(mode); } hp = destination.setPassive(); destination.controlChannel.write(new Command("STOR", remoteDstFile)); } if (source.isFeatureSupported(FeatureList.GETPUT)) { source.issueGETPUT("GET", false, hp, mode, remoteSrcFile); } else { if (mode > 0) { source.setMode(mode); } source.setActive(hp); source.controlChannel.write(new Command("RETR", remoteSrcFile)); } source.transferRunSingleThread(destination.controlChannel, mListener); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } public boolean isActiveMode() { return (this.session.serverMode == Session.SERVER_ACTIVE); } /** * Controls whether the client attempts to send an ALLO command * before a STOR request during the put/asyncPut calls. This is * disabled by default in the FTP client and enabled by default * in the GridFTP client. This setting will apply to all * subsequent transfers. * * @param useAllo <code>true</code> if the client should try * to send an ALLO command before a STOR request */ public void setUseAllo(boolean useAllo) { this.useAllo = useAllo; } /** * Determines whether this client is configured to send an ALLO * command before a STOR request in the put/asyncPut methods. */ public boolean getUseAllo() { return this.useAllo; } /** * According to * {@link http://www.ogf.org/documents/GFD.47.pdf [GridFTP v2 Protocol Description]} * checksum feature has the following syntax: * <pre> * CKSUM <algorithm>[, …] * </pre> * getSupportedCksumAlgorithms parses checsum feauture parms and form a * list of checksum algorithms supported by the server * * @return a list of checksum algorithms supported by the server in the order * specified by the server * @throws ClientException * @throws org.dcache.ftp.client.exception.ServerException * @throws java.io.IOException */ public List<String> getSupportedCksumAlgorithms() throws ClientException, ServerException, IOException { if (algorithms != null) { return algorithms; } // check if the CKSUM algorithm is supported by the server List<FeatureList.Feature> cksumFeature = getFeatureList().getFeature(FeatureList.CKSUM); if (cksumFeature == null) { algorithms = Collections.emptyList(); return algorithms; } algorithms = new ArrayList(); for (FeatureList.Feature feature : cksumFeature) { String[] parms = feature.getParms().split(","); Collections.addAll(algorithms, parms); } return algorithms; } public boolean isCksumAlgorithmSupported(String algorithm) throws ClientException, ServerException, IOException { return getSupportedCksumAlgorithms().contains(algorithm.toUpperCase()); } private void checkCksumSupport(String algorithm) throws ClientException, ServerException, IOException { // check if the CKSUM is supported by the server if (!isFeatureSupported(FeatureList.CKSUM)) { throw new ClientException( ClientException.OTHER, FeatureList.CKSUM + " is not supported by server"); } // check if the CKSUM algorithm is supported by the server if (!isCksumAlgorithmSupported(algorithm)) { throw new ClientException( ClientException.OTHER, FeatureList.CKSUM + " algorithm " + algorithm + " is not supported by server"); } } /** * implement GridFTP v2 CKSM command from * {@link http://www.ogf.org/documents/GFD.47.pdf [GridFTP v2 Protocol Description]} * <pre> * 5.1 CKSM * This command is used by the client to request checksum calculation over a portion or * whole file existing on the server. The syntax is: * CKSM <algorithm> <offset> <length> <path> CRLF * Server executes this command by calculating specified type of checksum over * portion of the file starting at the offset and of the specified length. If length is –1, * the checksum will be calculated through the end of the file. On success, the server * replies with * 2xx <checksum value> * Actual format of checksum value depends on the algorithm used, but generally, * hexadecimal representation should be used. * </pre> * * @param algorithm ckeckum alorithm * @param offset * @param length * @param path * @return ckecksum value returned by the server * @throws ClientException * @throws org.dcache.ftp.client.exception.ServerException * @throws java.io.IOException */ public String getChecksum(String algorithm, long offset, long length, String path) throws ClientException, ServerException, IOException { // check if we the cksum commands and specific algorithm are supported checkCksumSupport(algorithm); // form CKSM command String parameters = String.format("%s %d %d %s", algorithm, offset, length, path); Command cmd = new Command("CKSM", parameters); // transfer command, obtain reply Reply cksumReply = doCommand(cmd); // check for error if (!Reply.isPositiveCompletion(cksumReply)) { throw new ServerException(ServerException.SERVER_REFUSED, cksumReply.getMessage()); } return cksumReply.getMessage(); } /** * GridFTP v2 CKSM command for the whole file * * @param algorithm ckeckum alorithm * @param path * @return ckecksum value returned by the server * @throws ClientException * @throws org.dcache.ftp.client.exception.ServerException * @throws java.io.IOException */ public String getChecksum(String algorithm, String path) throws ClientException, ServerException, IOException { return getChecksum(algorithm, 0, -1, path); } /** * implement GridFTP v2 SCKS command as described in * {@link http://www.ogf.org/documents/GFD.47.pdf [GridFTP v2 Protocol Description]} * <pre> * 5.2 SCKS * This command is sent prior to upload command such as STOR, ESTO, PUT. It is used * to convey to the server that the checksum value for the file which is about to be * uploaded. At the end of transfer, server will calculate checksum for the received file, * and if it does not match, will consider the transfer to have failed. Syntax of the * command is: * SCKS <algorithm> <value> CRLF * Actual format of checksum value depends on the algorithm used, but generally, * hexadecimal representation should be used. * </pre> * * @param algorithm * @param value * @throws ClientException * @throws org.dcache.ftp.client.exception.ServerException * @throws java.io.IOException */ public void setChecksum(String algorithm, String value) throws ClientException, ServerException, IOException { // check if we the cksum commands and specific algorithm are supported checkCksumSupport(algorithm); // form CKSM command String parameters = String.format("%s %s", algorithm, value); Command cmd = new Command("SCKS", parameters); // transfer command, obtain reply Reply cksumReply = doCommand(cmd); // check for error if (!Reply.isPositiveCompletion(cksumReply)) { throw new ServerException(ServerException.SERVER_REFUSED, cksumReply.getMessage()); } } } //FTPClient