/* * 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.util.Vector; import org.dcache.dss.DssContextFactory; import org.dcache.ftp.client.exception.ClientException; import org.dcache.ftp.client.exception.FTPException; import org.dcache.ftp.client.exception.FTPReplyParseException; import org.dcache.ftp.client.exception.ServerException; import org.dcache.ftp.client.exception.UnexpectedReplyCodeException; import org.dcache.ftp.client.extended.GridFTPControlChannel; import org.dcache.ftp.client.extended.GridFTPServerFacade; import org.dcache.ftp.client.vanilla.Command; import org.dcache.ftp.client.vanilla.FTPControlChannel; import org.dcache.ftp.client.vanilla.FTPServerFacade; import org.dcache.ftp.client.vanilla.Reply; import org.dcache.util.PortRange; import org.dcache.util.Version; /** * This is the main user interface for GridFTP operations. * Use this class for client - server or third party transfers * with mode E, parallelism, markers, striping or GSI authentication. * 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()}, * {@link #setDataChannelProtection(int) setDataChannelProtection()}, * and {@link #setDataChannelAuthentication(DataChannelAuthentication) * setDataChannelAuthentication()} that affect data channel settings * <b>must</b> be called before passive or active data channel mode is set. **/ public class GridFTPClient extends FTPClient { private static Logger logger = LoggerFactory.getLogger(GridFTPClient.class); private static final Version VERSION = Version.of(GridFTPClient.class); //utility alias to session and localServer protected final GridFTPSession gSession; protected final String expectedHostName; protected GridFTPServerFacade gLocalServer; protected String usageString; /** * Constructs client and connects it to the remote server. * * @param host remote server host * @param port remote server port */ public GridFTPClient(String host, int port) throws IOException, ServerException { gSession = new GridFTPSession(); session = gSession; expectedHostName = host; controlChannel = new FTPControlChannel(host, port); controlChannel.open(); gLocalServer = new GridFTPServerFacade(controlChannel); localServer = gLocalServer; gLocalServer.authorize(); this.useAllo = true; setUsageInformation("dCache", VERSION.getVersion()); } /** * Establishes a secure and authenticated context with the server. * * @param factory factory for creating the DssContext of the secure session * @throws IOException on i/o error * @throws ServerException on server refusal or faulty server behavior */ public void authenticate(DssContextFactory factory) throws IOException, ServerException { authenticate(factory, null); } public void setUsageInformation( String appName, String appVer) { usageString = "CLIENTINFO appname=" + appName + ";appver=" + appVer + ";schema=gsiftp;"; } /** * Establishes a secure and authenticated context with the server. * * @param factory factory for creating the DssContext of the secure session * @param username specific username to authenticate as. * @throws IOException on i/o error * @throws ServerException on server refusal or faulty server behavior */ public void authenticate(DssContextFactory factory, String username) throws IOException, ServerException { try { // authenticate GridFTPControlChannel gridFTPControlChannel = new GridFTPControlChannel(controlChannel, factory, expectedHostName); //from now on, the commands and replies //are protected and pass through gsi wrapped socket // login try { Reply reply = gridFTPControlChannel.exchange(new Command("USER", (username == null) ? ":globus-mapping:" : username)); // wu-gsiftp sends intermediate code while // gssftp send completion reply code if (!Reply.isPositiveCompletion(reply) && !Reply.isPositiveIntermediate(reply)) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(reply), "User authorization failed."); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe, "Received faulty reply to USER command."); } try { Reply reply = gridFTPControlChannel.exchange(new Command("PASS", "dummy")); if (!Reply.isPositiveCompletion(reply)) { throw ServerException.embedUnexpectedReplyCodeException( new UnexpectedReplyCodeException(reply), "Bad password."); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe, "Received faulty reply to PASS command."); } this.gSession.authorized = true; this.username = username; this.controlChannel = gridFTPControlChannel; // quietly send version information to the server. // ignore errors try { this.site(usageString); } catch (Exception ex) { } } catch (ServerException | IOException e) { close(); throw e; } } /** * Performs remote directory listing like * {@link FTPClient#list(String, String) FTPClient.list()}. * <b>Note:</b> This method cannot be used * in conjunction with parallelism or striping; set parallelism to * 1 before calling it. Otherwise, use * {@link FTPClient#list(String, String, DataSink) FTPClient.list()}. * Unlike in vanilla FTP, here IMAGE mode is allowed. * For more documentation, look at FTPClient. */ @Override public Vector list(String filter, String modifier) throws ServerException, ClientException, IOException { if (gSession.parallel > 1) { throw new ClientException( ClientException.BAD_MODE, "list cannot be called with parallelism"); } return super.list(filter, modifier); } /** * Performs remote directory listing like * {@link FTPClient#nlist(String) FTPClient.nlist()}. * <b>Note:</b> This method cannot be used * in conjunction with parallelism or striping; set parallelism to * 1 before calling it. Otherwise, use * {@link FTPClient#nlist(String, DataSink) FTPClient.nlist()}. * Unlike in vanilla FTP, here IMAGE mode is allowed. * For more documentation, look at FTPClient. */ @Override public Vector nlist(String path) throws ServerException, ClientException, IOException { if (gSession.parallel > 1) { throw new ClientException( ClientException.BAD_MODE, "nlist cannot be called with parallelism"); } return super.nlist(path); } /** * Performs remote directory listing like * {@link FTPClient#mlsd(String) FTPClient.mlsd()}. * <b>Note:</b> This method cannot be used * in conjunction with parallelism or striping; set parallelism to * 1 before calling it. Otherwise, use * {@link FTPClient#mlsd(String, DataSink) FTPClient.mlsd()}. * Unlike in vanilla FTP, here IMAGE mode is allowed. * For more documentation, look at FTPClient. */ @Override public Vector mlsd(String filter) throws ServerException, ClientException, IOException { if (gSession.parallel > 1) { throw new ClientException( ClientException.BAD_MODE, "mlsd cannot be called with parallelism"); } return super.mlsd(filter); } @Override protected void listCheck() throws ClientException { // do nothing } @Override protected void checkTransferParamsGet() 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) { HostPort hp = setLocalPassive(); setActive(hp); } } @Override protected String getModeStr(int mode) { switch (mode) { case Session.MODE_STREAM: return "S"; case Session.MODE_BLOCK: return "B"; case GridFTPSession.MODE_EBLOCK: return "E"; default: throw new IllegalArgumentException("Bad mode: " + mode); } } /** * Sets remote server to striped passive server mode (SPAS). **/ public HostPortList setStripedPassive() throws IOException, ServerException { Command cmd = new Command("SPAS", (controlChannel.isIPv6()) ? "2" : null); Reply reply = null; try { reply = controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } this.gSession.serverMode = GridFTPSession.SERVER_EPAS; if (controlChannel.isIPv6()) { gSession.serverAddressList = HostPortList.parseIPv6Format(reply.getMessage()); int size = gSession.serverAddressList.size(); for (int i = 0; i < size; i++) { HostPort6 hp = (HostPort6) gSession.serverAddressList.get(i); if (hp.getHost() == null) { hp.setVersion(HostPort6.IPv6); hp.setHost(controlChannel.getHost()); } } } else { gSession.serverAddressList = HostPortList.parseIPv4Format(reply.getMessage()); } return gSession.serverAddressList; } /** * Sets remote server to striped active server mode (SPOR). **/ public void setStripedActive(HostPortList hpl) throws IOException, ServerException { Command cmd = new Command("SPOR", hpl.toFtpCmdArgument()); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } this.gSession.serverMode = GridFTPSession.SERVER_EACT; } /** * Starts local server in striped passive mode. Since the local server * is not distributed, it will only listen on one socket. * * @param range required server port * @param queue max size of queue of awaiting new data channel connection * requests * @return the HostPortList of 1 element representing the socket where the * local server is listening **/ public HostPortList setLocalStripedPassive(PortRange range, int queue) throws IOException { return gLocalServer.setStripedPassive(range, queue); } /** * Behaves like setLocalStripedPassive(FTPServerFacade.ANY_PORT, * FTPServerFacade.DEFAULT_QUEUE) **/ public HostPortList setLocalStripedPassive() throws IOException { return gLocalServer.setStripedPassive(portRange, FTPServerFacade.DEFAULT_QUEUE); } /** * Starts local server in striped active mode. * setStripedPassive() must be called before that. * This method takes no parameters. HostPortList of the remote * server, known from the last call of setStripedPassive(), is stored * internally and the local server will connect to this address. **/ public void setLocalStripedActive() throws ClientException, IOException { if (gSession.serverAddressList == null) { throw new ClientException(ClientException.CALL_PASSIVE_FIRST); } try { gLocalServer.setStripedActive(gSession.serverAddressList); } catch (UnknownHostException e) { throw new ClientException(ClientException.UNKNOWN_HOST); } } /** * Performs extended retrieve (partial retrieve mode starting * at offset 0). * * @param remoteFileName file to retrieve * @param size number of bytes of remote file to transmit * @param sink data sink to store the file * @param mListener marker listener **/ public void extendedGet(String remoteFileName, long size, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { extendedGet(remoteFileName, 0, size, sink, mListener); } /** * Performs extended retrieve (partial retrieve mode). * * @param remoteFileName file to retrieve * @param offset the staring offset in the remote file * @param size number of bytes of remote file to transmit * @param sink data sink to store the file * @param mListener marker listener **/ public void extendedGet(String remoteFileName, long offset, long size, DataSink sink, MarkerListener mListener) throws IOException, ClientException, ServerException { // servers support GridFTP? checkGridFTPSupport(); // all parameters set correctly (or still unset)? checkTransferParamsGet(); gLocalServer.store(sink); controlChannel.write(new Command("ERET", "P " + offset + " " + size + " " + remoteFileName)); transferRunSingleThread(localServer.getControlChannel(), mListener); } /** * Performs extended store (adujsted store mode with offset 0). * * @param remoteFileName file name to store * @param source source for the data to transfer * @param mListener marker listener **/ public void extendedPut(String remoteFileName, DataSource source, MarkerListener mListener) throws IOException, ServerException, ClientException { extendedPut(remoteFileName, 0, source, mListener); } /** * Performs extended store (adujsted store mode). * * @param remoteFileName file name to store * @param offset the offset added to the file pointer before storing * the blocks of the file. * @param source source for the data to transfer * @param mListener marker listener **/ public void extendedPut(String remoteFileName, long offset, DataSource source, MarkerListener mListener) throws IOException, ServerException, ClientException { // servers support GridFTP? checkGridFTPSupport(); // all parameters set correctly (or still unset)? checkTransferParamsPut(); localServer.retrieve(source); controlChannel.write(new Command("ESTO", "A " + offset + " " + remoteFileName)); transferRunSingleThread(localServer.getControlChannel(), mListener); } /* 3rd party transfer code */ /** * Performs a third-party transfer between two servers using extended * block mode. * If server modes are unset, source will be set to active * and destination to passive. * * @param remoteSrcFile source filename * @param destination destination server * @param remoteDstFile destination filename * @param mListener transer progress listener. * Can be set to null. */ public void extendedTransfer(String remoteSrcFile, GridFTPClient destination, String remoteDstFile, MarkerListener mListener) throws IOException, ServerException, ClientException { extendedTransfer(remoteSrcFile, 0, getSize(remoteSrcFile), destination, remoteDstFile, 0, mListener); } /** * Performs a third-party transfer between two servers using extended * block mode. * If server modes are unset, source will be set to active * and destination to passive. * * @param remoteSrcFile source filename * @param remoteSrcFileOffset source filename offset * @param remoteSrcFileLength source filename length to transfer * @param destination destination server * @param remoteDstFile destination filename * @param remoteDstFileOffset destination filename offset * @param mListener transer progress listener. * Can be set to null. */ public void extendedTransfer(String remoteSrcFile, long remoteSrcFileOffset, long remoteSrcFileLength, GridFTPClient destination, String remoteDstFile, long remoteDstFileOffset, MarkerListener mListener) throws IOException, ServerException, ClientException { // FIXME: ESTO & ERET do not require MODE E this needs to be fixed // servers support GridFTP? checkGridFTPSupport(); destination.checkGridFTPSupport(); // all parameters set correctly (or still unset)? gSession.matches(destination.gSession); //mode E if (gSession.transferMode != GridFTPSession.MODE_EBLOCK) { throw new ClientException(ClientException.BAD_MODE, "Extended transfer mode is necessary"); } // if transfer modes have not been defined, // set this (source) as active if (gSession.serverMode == Session.SERVER_DEFAULT) { HostPort hp = destination.setPassive(); this.setActive(hp); } Command estoCmd = new Command("ESTO", "A " + remoteDstFileOffset + " " + remoteDstFile); destination.controlChannel.write(estoCmd); Command eretCmd = new Command("ERET", "P " + remoteSrcFileOffset + " " + remoteSrcFileLength + " " + remoteSrcFile); controlChannel.write(eretCmd); transferRunSingleThread(destination.controlChannel, mListener); } public void extendedMultipleTransfer( long remoteSrcFileOffset[], long remoteSrcFileLength[], String remoteSrcFile[], GridFTPClient destination, long remoteDstFileOffset[], String remoteDstFile[], MarkerListener mListener, MultipleTransferCompleteListener doneListener) throws IOException, ServerException, ClientException { // servers support GridFTP? checkGridFTPSupport(); destination.checkGridFTPSupport(); // all parameters set correctly (or still unset)? gSession.matches(destination.gSession); //mode E if (gSession.transferMode != GridFTPSession.MODE_EBLOCK) { throw new ClientException(ClientException.BAD_MODE, "Extended transfer mode is necessary"); } if (remoteSrcFile.length != remoteDstFile.length) { throw new ClientException(ClientException.OTHER, "All array paremeters must be smae length"); } // if transfer modes have not been defined, // set this (source) as active if (gSession.serverMode == Session.SERVER_DEFAULT) { HostPort hp = destination.setPassive(); this.setActive(hp); } /* send them all down the pipe */ for (int i = 0; i < remoteSrcFile.length; i++) { Command estoCmd = new Command("ESTO", "A " + remoteDstFileOffset[i] + " " + remoteDstFile[i]); destination.controlChannel.write(estoCmd); Command eretCmd = new Command("ERET", "P " + remoteSrcFileOffset[i] + " " + remoteSrcFileLength[i] + " " + remoteSrcFile[i]); controlChannel.write(eretCmd); } for (int i = 0; i < remoteSrcFile.length; i++) { transferRunSingleThread(destination.controlChannel, mListener); if (doneListener != null) { MultipleTransferComplete mtc; mtc = new MultipleTransferComplete( remoteSrcFile[i], remoteDstFile[i], this, destination, i); doneListener.transferComplete(mtc); } } } public void extendedMultipleTransfer( String remoteSrcFile[], GridFTPClient destination, String remoteDstFile[], MarkerListener mListener, MultipleTransferCompleteListener doneListener) throws IOException, ServerException, ClientException { // servers support GridFTP? checkGridFTPSupport(); destination.checkGridFTPSupport(); // all parameters set correctly (or still unset)? gSession.matches(destination.gSession); //mode E if (gSession.transferMode != GridFTPSession.MODE_EBLOCK) { throw new ClientException(ClientException.BAD_MODE, "Extended transfer mode is necessary"); } if (remoteSrcFile.length != remoteDstFile.length) { throw new ClientException(ClientException.OTHER, "All array paremeters must be smae length"); } // if transfer modes have not been defined, // set this (source) as active if (gSession.serverMode == Session.SERVER_DEFAULT) { HostPort hp = destination.setPassive(); this.setActive(hp); } /* send them all down the pipe */ for (int i = 0; i < remoteSrcFile.length; i++) { Command estoCmd = new Command("STOR ", remoteDstFile[i]); destination.controlChannel.write(estoCmd); Command eretCmd = new Command("RETR", remoteSrcFile[i]); controlChannel.write(eretCmd); } for (int i = 0; i < remoteSrcFile.length; i++) { transferRunSingleThread(destination.controlChannel, mListener); if (doneListener != null) { MultipleTransferComplete mtc; mtc = new MultipleTransferComplete( remoteSrcFile[i], remoteDstFile[i], this, destination, i); doneListener.transferComplete(mtc); } } } /** * assure that the server supports extended transfer features; * throw exception if not **/ protected void checkGridFTPSupport() throws IOException, ServerException { FeatureList fl = getFeatureList(); if ( !(fl.contains(FeatureList.PARALLEL) && fl.contains(FeatureList.ESTO) && fl.contains(FeatureList.ERET) && fl.contains(FeatureList.SIZE)) ) { throw new ServerException(ServerException.UNSUPPORTED_FEATURE); } } /* end 3rd party transfer code */ /** * Sets data channel authentication mode (DCAU) * * @param type for 2-party transfer must be * DataChannelAuthentication.SELF or DataChannelAuthentication.NONE **/ public void setDataChannelAuthentication(DataChannelAuthentication type) throws IOException, ServerException { Command cmd = new Command("DCAU", type.toFtpCmdArgument()); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { if (!type.toFtpCmdArgument().equals("N")) { throw ServerException.embedUnexpectedReplyCodeException(urce); } } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } this.gSession.dataChannelAuthentication = type; gLocalServer.setDataChannelAuthentication(type); } /** * Sets compatibility mode with old GSIFTP server. * Locally sets data channel authentication to NONE * but does not send the command * to the remote server (the server wouldn't understand it) **/ public void setLocalNoDataChannelAuthentication() { gLocalServer.setDataChannelAuthentication(DataChannelAuthentication.NONE); } /** * Returns data channel authentication mode (DCAU). * * @return data channel authentication mode **/ public DataChannelAuthentication getDataChannelAuthentication() { return gSession.dataChannelAuthentication; } /** * Sets data channel protection level (PROT). * * @param protection should be * {@link GridFTPSession#PROTECTION_CLEAR CLEAR}, * {@link GridFTPSession#PROTECTION_SAFE SAFE}, or * {@link GridFTPSession#PROTECTION_PRIVATE PRIVATE}, or * {@link GridFTPSession#PROTECTION_CONFIDENTIAL CONFIDENTIAL}. **/ public void setDataChannelProtection(int protection) throws IOException, ServerException { String protectionStr = null; switch (protection) { case GridFTPSession.PROTECTION_CLEAR: protectionStr = "C"; break; case GridFTPSession.PROTECTION_SAFE: protectionStr = "S"; break; case GridFTPSession.PROTECTION_CONFIDENTIAL: protectionStr = "E"; break; case GridFTPSession.PROTECTION_PRIVATE: protectionStr = "P"; break; default: throw new IllegalArgumentException("Bad protection: " + protection); } Command cmd = new Command("PROT", protectionStr); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } this.gSession.dataChannelProtection = protection; gLocalServer.setDataChannelProtection(protection); } /** * Returns data channel protection level. * * @return data channel protection level: * {@link GridFTPSession#PROTECTION_CLEAR CLEAR}, * {@link GridFTPSession#PROTECTION_SAFE SAFE}, or * {@link GridFTPSession#PROTECTION_PRIVATE PRIVATE}, or * {@link GridFTPSession#PROTECTION_CONFIDENTIAL CONFIDENTIAL}. **/ public int getDataChannelProtection() { return gSession.dataChannelProtection; } // basic compatibility API @Override public void get(String remoteFileName, File localFile) throws IOException, ClientException, ServerException { if (gSession.transferMode == GridFTPSession.MODE_EBLOCK) { DataSink sink = new FileRandomIO(new RandomAccessFile(localFile, "rw")); get(remoteFileName, sink, null); } else { super.get(remoteFileName, localFile); } } @Override public void put(File localFile, String remoteFileName, boolean append) throws IOException, ServerException, ClientException { if (gSession.transferMode == GridFTPSession.MODE_EBLOCK) { DataSource source = new FileRandomIO(new RandomAccessFile(localFile, "r")); put(remoteFileName, source, null, append); } else { super.put(localFile, remoteFileName, append); } } /** * Sets the checksum values ahead of the transfer * * @param algorithm the checksume algorithm * @param value the checksum value as hexadecimal number * @return nothing * @throws ServerException if an error occured. */ public void setChecksum(ChecksumAlgorithm algorithm, String value) throws IOException, ServerException { String arguments = algorithm.toFtpCmdArgument() + " " + value; Command cmd = new Command("SCKS", arguments); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Computes and returns a checksum of a file. * transferred. * * @param algorithm the checksume algorithm * @param offset the offset * @param length the length * @param file file to compute checksum of * @return the computed checksum * @throws ServerException if an error occured. */ public String checksum(ChecksumAlgorithm algorithm, long offset, long length, String file) throws IOException, ServerException { String arguments = algorithm.toFtpCmdArgument() + " " + String.valueOf(offset) + " " + String.valueOf(length) + " " + file; Command cmd = new Command("CKSM", arguments); Reply reply = null; try { reply = controlChannel.execute(cmd); return reply.getMessage(); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Performs a recursive directory listing starting at the given path * (or, if path is null, at the current directory of the FTP server). * MlsxEntry instances for all of the files in the subtree will be * written through the passed MlsxEntryWriter. * * @param path path to begin recursive directory listing * @param writer sink for created MlsxEntry instances * @throws ServerException * @throws ClientException * @throws IOException */ public void mlsr(String path, MlsxEntryWriter writer) throws ServerException, ClientException, IOException { if (gSession.parallel > 1) { throw new ClientException( ClientException.BAD_MODE, "mlsr cannot be called with parallelism"); } Command cmd = (path == null) ? new Command("MLSR") : new Command("MLSR", path); MlsxParserDataSink sink = new MlsxParserDataSink(writer); performTransfer(cmd, sink); } private class MlsxParserDataSink implements DataSink { private final MlsxEntryWriter writer; private final byte[] buf = new byte[4096]; private int pos = 0; public MlsxParserDataSink(MlsxEntryWriter w) { writer = w; } @Override public void write(Buffer buffer) throws IOException { byte[] data = buffer.getBuffer(); int len = buffer.getLength(); int i = 0; while (i < len && pos < buf.length) { if (data[i] == '\r' || data[i] == '\n') { if (pos > 0) { try { writer.write(new MlsxEntry(new String(buf, 0, pos, StandardCharsets.UTF_8))); } catch (FTPException ex) { throw new IOException(); } } pos = 0; while (i < len && data[i] < ' ') ++i; } else { buf[pos++] = data[i++]; } } } @Override public void close() throws IOException { writer.close(); } } /** * Change the Unix group membership of a file. * * @param group the name or ID of the group * @param file the file whose group membership should be changed * @throws ServerException if an error occurred. */ public void changeGroup(String group, String file) throws IOException, ServerException { String arguments = group + " " + file; Command cmd = new Command("SITE CHGRP", arguments); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Change the modification time of a file. * * @param year Modifcation year * @param month Modification month (1-12) * @param day Modification day (1-31) * @param hour Modification hour (0-23) * @param min Modification minutes (0-59) * @param sec Modification seconds (0-59) * @param file file whose modification time should be changed * @throws IOException * @throws ServerException if an error occurred. */ public void changeModificationTime(int year, int month, int day, int hour, int min, int sec, String file) throws IOException, ServerException { DecimalFormat df2 = new DecimalFormat("00"); DecimalFormat df4 = new DecimalFormat("0000"); String arguments = df4.format(year) + df2.format(month) + df2.format(day) + df2.format(hour) + df2.format(min) + df2.format(sec) + " " + file; Command cmd = new Command("SITE UTIME", arguments); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } /** * Create a symbolic link on the FTP server. * * @param link_target the path to which the symbolic link should point * @param link_name the path of the symbolic link to create * @throws IOException * @throws ServerException if an error occurred. */ public void createSymbolicLink(String link_target, String link_name) throws IOException, ServerException { String arguments = link_target.replaceAll(" ", "%20") + " " + link_name; Command cmd = new Command("SITE SYMLINK", arguments); try { controlChannel.execute(cmd); } catch (UnexpectedReplyCodeException urce) { throw ServerException.embedUnexpectedReplyCodeException(urce); } catch (FTPReplyParseException rpe) { throw ServerException.embedFTPReplyParseException(rpe); } } }