001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.fs.ftp;
019
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.InputStream;
023import java.net.ConnectException;
024import java.net.URI;
025
026import com.google.common.annotations.VisibleForTesting;
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.apache.commons.net.ftp.FTP;
030import org.apache.commons.net.ftp.FTPClient;
031import org.apache.commons.net.ftp.FTPFile;
032import org.apache.commons.net.ftp.FTPReply;
033import org.apache.hadoop.classification.InterfaceAudience;
034import org.apache.hadoop.classification.InterfaceStability;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.fs.FSDataInputStream;
037import org.apache.hadoop.fs.FSDataOutputStream;
038import org.apache.hadoop.fs.FileAlreadyExistsException;
039import org.apache.hadoop.fs.FileStatus;
040import org.apache.hadoop.fs.FileSystem;
041import org.apache.hadoop.fs.ParentNotDirectoryException;
042import org.apache.hadoop.fs.Path;
043import org.apache.hadoop.fs.permission.FsAction;
044import org.apache.hadoop.fs.permission.FsPermission;
045import org.apache.hadoop.net.NetUtils;
046import org.apache.hadoop.util.Progressable;
047
048/**
049 * <p>
050 * A {@link FileSystem} backed by an FTP client provided by <a
051 * href="http://commons.apache.org/net/">Apache Commons Net</a>.
052 * </p>
053 */
054@InterfaceAudience.Public
055@InterfaceStability.Stable
056public class FTPFileSystem extends FileSystem {
057
058  public static final Log LOG = LogFactory
059      .getLog(FTPFileSystem.class);
060
061  public static final int DEFAULT_BUFFER_SIZE = 1024 * 1024;
062
063  public static final int DEFAULT_BLOCK_SIZE = 4 * 1024;
064  public static final String FS_FTP_USER_PREFIX = "fs.ftp.user.";
065  public static final String FS_FTP_HOST = "fs.ftp.host";
066  public static final String FS_FTP_HOST_PORT = "fs.ftp.host.port";
067  public static final String FS_FTP_PASSWORD_PREFIX = "fs.ftp.password.";
068  public static final String FS_FTP_DATA_CONNECTION_MODE =
069      "fs.ftp.data.connection.mode";
070  public static final String FS_FTP_TRANSFER_MODE = "fs.ftp.transfer.mode";
071  public static final String E_SAME_DIRECTORY_ONLY =
072      "only same directory renames are supported";
073
074  private URI uri;
075
076  /**
077   * Return the protocol scheme for the FileSystem.
078   * <p/>
079   *
080   * @return <code>ftp</code>
081   */
082  @Override
083  public String getScheme() {
084    return "ftp";
085  }
086
087  /**
088   * Get the default port for this FTPFileSystem.
089   *
090   * @return the default port
091   */
092  @Override
093  protected int getDefaultPort() {
094    return FTP.DEFAULT_PORT;
095  }
096
097  @Override
098  public void initialize(URI uri, Configuration conf) throws IOException { // get
099    super.initialize(uri, conf);
100    // get host information from uri (overrides info in conf)
101    String host = uri.getHost();
102    host = (host == null) ? conf.get(FS_FTP_HOST, null) : host;
103    if (host == null) {
104      throw new IOException("Invalid host specified");
105    }
106    conf.set(FS_FTP_HOST, host);
107
108    // get port information from uri, (overrides info in conf)
109    int port = uri.getPort();
110    port = (port == -1) ? FTP.DEFAULT_PORT : port;
111    conf.setInt("fs.ftp.host.port", port);
112
113    // get user/password information from URI (overrides info in conf)
114    String userAndPassword = uri.getUserInfo();
115    if (userAndPassword == null) {
116      userAndPassword = (conf.get("fs.ftp.user." + host, null) + ":" + conf
117          .get("fs.ftp.password." + host, null));
118      if (userAndPassword == null) {
119        throw new IOException("Invalid user/passsword specified");
120      }
121    }
122    String[] userPasswdInfo = userAndPassword.split(":");
123    conf.set(FS_FTP_USER_PREFIX + host, userPasswdInfo[0]);
124    if (userPasswdInfo.length > 1) {
125      conf.set(FS_FTP_PASSWORD_PREFIX + host, userPasswdInfo[1]);
126    } else {
127      conf.set(FS_FTP_PASSWORD_PREFIX + host, null);
128    }
129    setConf(conf);
130    this.uri = uri;
131  }
132
133  /**
134   * Connect to the FTP server using configuration parameters *
135   * 
136   * @return An FTPClient instance
137   * @throws IOException
138   */
139  private FTPClient connect() throws IOException {
140    FTPClient client = null;
141    Configuration conf = getConf();
142    String host = conf.get(FS_FTP_HOST);
143    int port = conf.getInt(FS_FTP_HOST_PORT, FTP.DEFAULT_PORT);
144    String user = conf.get(FS_FTP_USER_PREFIX + host);
145    String password = conf.get(FS_FTP_PASSWORD_PREFIX + host);
146    client = new FTPClient();
147    client.connect(host, port);
148    int reply = client.getReplyCode();
149    if (!FTPReply.isPositiveCompletion(reply)) {
150      throw NetUtils.wrapException(host, port,
151                   NetUtils.UNKNOWN_HOST, 0,
152                   new ConnectException("Server response " + reply));
153    } else if (client.login(user, password)) {
154      client.setFileTransferMode(getTransferMode(conf));
155      client.setFileType(FTP.BINARY_FILE_TYPE);
156      client.setBufferSize(DEFAULT_BUFFER_SIZE);
157      setDataConnectionMode(client, conf);
158    } else {
159      throw new IOException("Login failed on server - " + host + ", port - "
160          + port + " as user '" + user + "'");
161    }
162
163    return client;
164  }
165
166  /**
167   * Set FTP's transfer mode based on configuration. Valid values are
168   * STREAM_TRANSFER_MODE, BLOCK_TRANSFER_MODE and COMPRESSED_TRANSFER_MODE.
169   * <p/>
170   * Defaults to BLOCK_TRANSFER_MODE.
171   *
172   * @param conf
173   * @return
174   */
175  @VisibleForTesting
176  int getTransferMode(Configuration conf) {
177    final String mode = conf.get(FS_FTP_TRANSFER_MODE);
178    // FTP default is STREAM_TRANSFER_MODE, but Hadoop FTPFS's default is
179    // FTP.BLOCK_TRANSFER_MODE historically.
180    int ret = FTP.BLOCK_TRANSFER_MODE;
181    if (mode == null) {
182      return ret;
183    }
184    final String upper = mode.toUpperCase();
185    if (upper.equals("STREAM_TRANSFER_MODE")) {
186      ret = FTP.STREAM_TRANSFER_MODE;
187    } else if (upper.equals("COMPRESSED_TRANSFER_MODE")) {
188      ret = FTP.COMPRESSED_TRANSFER_MODE;
189    } else {
190      if (!upper.equals("BLOCK_TRANSFER_MODE")) {
191        LOG.warn("Cannot parse the value for " + FS_FTP_TRANSFER_MODE + ": "
192            + mode + ". Using default.");
193      }
194    }
195    return ret;
196  }
197
198  /**
199   * Set the FTPClient's data connection mode based on configuration. Valid
200   * values are ACTIVE_LOCAL_DATA_CONNECTION_MODE,
201   * PASSIVE_LOCAL_DATA_CONNECTION_MODE and PASSIVE_REMOTE_DATA_CONNECTION_MODE.
202   * <p/>
203   * Defaults to ACTIVE_LOCAL_DATA_CONNECTION_MODE.
204   *
205   * @param client
206   * @param conf
207   * @throws IOException
208   */
209  @VisibleForTesting
210  void setDataConnectionMode(FTPClient client, Configuration conf)
211      throws IOException {
212    final String mode = conf.get(FS_FTP_DATA_CONNECTION_MODE);
213    if (mode == null) {
214      return;
215    }
216    final String upper = mode.toUpperCase();
217    if (upper.equals("PASSIVE_LOCAL_DATA_CONNECTION_MODE")) {
218      client.enterLocalPassiveMode();
219    } else if (upper.equals("PASSIVE_REMOTE_DATA_CONNECTION_MODE")) {
220      client.enterRemotePassiveMode();
221    } else {
222      if (!upper.equals("ACTIVE_LOCAL_DATA_CONNECTION_MODE")) {
223        LOG.warn("Cannot parse the value for " + FS_FTP_DATA_CONNECTION_MODE
224            + ": " + mode + ". Using default.");
225      }
226    }
227  }
228
229  /**
230   * Logout and disconnect the given FTPClient. *
231   * 
232   * @param client
233   * @throws IOException
234   */
235  private void disconnect(FTPClient client) throws IOException {
236    if (client != null) {
237      if (!client.isConnected()) {
238        throw new FTPException("Client not connected");
239      }
240      boolean logoutSuccess = client.logout();
241      client.disconnect();
242      if (!logoutSuccess) {
243        LOG.warn("Logout failed while disconnecting, error code - "
244            + client.getReplyCode());
245      }
246    }
247  }
248
249  /**
250   * Resolve against given working directory. *
251   * 
252   * @param workDir
253   * @param path
254   * @return
255   */
256  private Path makeAbsolute(Path workDir, Path path) {
257    if (path.isAbsolute()) {
258      return path;
259    }
260    return new Path(workDir, path);
261  }
262
263  @Override
264  public FSDataInputStream open(Path file, int bufferSize) throws IOException {
265    FTPClient client = connect();
266    Path workDir = new Path(client.printWorkingDirectory());
267    Path absolute = makeAbsolute(workDir, file);
268    FileStatus fileStat = getFileStatus(client, absolute);
269    if (fileStat.isDirectory()) {
270      disconnect(client);
271      throw new FileNotFoundException("Path " + file + " is a directory.");
272    }
273    client.allocate(bufferSize);
274    Path parent = absolute.getParent();
275    // Change to parent directory on the
276    // server. Only then can we read the
277    // file
278    // on the server by opening up an InputStream. As a side effect the working
279    // directory on the server is changed to the parent directory of the file.
280    // The FTP client connection is closed when close() is called on the
281    // FSDataInputStream.
282    client.changeWorkingDirectory(parent.toUri().getPath());
283    InputStream is = client.retrieveFileStream(file.getName());
284    FSDataInputStream fis = new FSDataInputStream(new FTPInputStream(is,
285        client, statistics));
286    if (!FTPReply.isPositivePreliminary(client.getReplyCode())) {
287      // The ftpClient is an inconsistent state. Must close the stream
288      // which in turn will logout and disconnect from FTP server
289      fis.close();
290      throw new IOException("Unable to open file: " + file + ", Aborting");
291    }
292    return fis;
293  }
294
295  /**
296   * A stream obtained via this call must be closed before using other APIs of
297   * this class or else the invocation will block.
298   */
299  @Override
300  public FSDataOutputStream create(Path file, FsPermission permission,
301      boolean overwrite, int bufferSize, short replication, long blockSize,
302      Progressable progress) throws IOException {
303    final FTPClient client = connect();
304    Path workDir = new Path(client.printWorkingDirectory());
305    Path absolute = makeAbsolute(workDir, file);
306    FileStatus status;
307    try {
308      status = getFileStatus(client, file);
309    } catch (FileNotFoundException fnfe) {
310      status = null;
311    }
312    if (status != null) {
313      if (overwrite && !status.isDirectory()) {
314        delete(client, file, false);
315      } else {
316        disconnect(client);
317        throw new FileAlreadyExistsException("File already exists: " + file);
318      }
319    }
320    
321    Path parent = absolute.getParent();
322    if (parent == null || !mkdirs(client, parent, FsPermission.getDirDefault())) {
323      parent = (parent == null) ? new Path("/") : parent;
324      disconnect(client);
325      throw new IOException("create(): Mkdirs failed to create: " + parent);
326    }
327    client.allocate(bufferSize);
328    // Change to parent directory on the server. Only then can we write to the
329    // file on the server by opening up an OutputStream. As a side effect the
330    // working directory on the server is changed to the parent directory of the
331    // file. The FTP client connection is closed when close() is called on the
332    // FSDataOutputStream.
333    client.changeWorkingDirectory(parent.toUri().getPath());
334    FSDataOutputStream fos = new FSDataOutputStream(client.storeFileStream(file
335        .getName()), statistics) {
336      @Override
337      public void close() throws IOException {
338        super.close();
339        if (!client.isConnected()) {
340          throw new FTPException("Client not connected");
341        }
342        boolean cmdCompleted = client.completePendingCommand();
343        disconnect(client);
344        if (!cmdCompleted) {
345          throw new FTPException("Could not complete transfer, Reply Code - "
346              + client.getReplyCode());
347        }
348      }
349    };
350    if (!FTPReply.isPositivePreliminary(client.getReplyCode())) {
351      // The ftpClient is an inconsistent state. Must close the stream
352      // which in turn will logout and disconnect from FTP server
353      fos.close();
354      throw new IOException("Unable to create file: " + file + ", Aborting");
355    }
356    return fos;
357  }
358
359  /** This optional operation is not yet supported. */
360  @Override
361  public FSDataOutputStream append(Path f, int bufferSize,
362      Progressable progress) throws IOException {
363    throw new IOException("Not supported");
364  }
365  
366  /**
367   * Convenience method, so that we don't open a new connection when using this
368   * method from within another method. Otherwise every API invocation incurs
369   * the overhead of opening/closing a TCP connection.
370   * @throws IOException on IO problems other than FileNotFoundException
371   */
372  private boolean exists(FTPClient client, Path file) throws IOException {
373    try {
374      return getFileStatus(client, file) != null;
375    } catch (FileNotFoundException fnfe) {
376      return false;
377    }
378  }
379
380  @Override
381  public boolean delete(Path file, boolean recursive) throws IOException {
382    FTPClient client = connect();
383    try {
384      boolean success = delete(client, file, recursive);
385      return success;
386    } finally {
387      disconnect(client);
388    }
389  }
390
391  /**
392   * Convenience method, so that we don't open a new connection when using this
393   * method from within another method. Otherwise every API invocation incurs
394   * the overhead of opening/closing a TCP connection.
395   */
396  private boolean delete(FTPClient client, Path file, boolean recursive)
397      throws IOException {
398    Path workDir = new Path(client.printWorkingDirectory());
399    Path absolute = makeAbsolute(workDir, file);
400    String pathName = absolute.toUri().getPath();
401    try {
402      FileStatus fileStat = getFileStatus(client, absolute);
403      if (fileStat.isFile()) {
404        return client.deleteFile(pathName);
405      }
406    } catch (FileNotFoundException e) {
407      //the file is not there
408      return false;
409    }
410    FileStatus[] dirEntries = listStatus(client, absolute);
411    if (dirEntries != null && dirEntries.length > 0 && !(recursive)) {
412      throw new IOException("Directory: " + file + " is not empty.");
413    }
414    if (dirEntries != null) {
415      for (int i = 0; i < dirEntries.length; i++) {
416        delete(client, new Path(absolute, dirEntries[i].getPath()), recursive);
417      }
418    }
419    return client.removeDirectory(pathName);
420  }
421
422  private FsAction getFsAction(int accessGroup, FTPFile ftpFile) {
423    FsAction action = FsAction.NONE;
424    if (ftpFile.hasPermission(accessGroup, FTPFile.READ_PERMISSION)) {
425      action.or(FsAction.READ);
426    }
427    if (ftpFile.hasPermission(accessGroup, FTPFile.WRITE_PERMISSION)) {
428      action.or(FsAction.WRITE);
429    }
430    if (ftpFile.hasPermission(accessGroup, FTPFile.EXECUTE_PERMISSION)) {
431      action.or(FsAction.EXECUTE);
432    }
433    return action;
434  }
435
436  private FsPermission getPermissions(FTPFile ftpFile) {
437    FsAction user, group, others;
438    user = getFsAction(FTPFile.USER_ACCESS, ftpFile);
439    group = getFsAction(FTPFile.GROUP_ACCESS, ftpFile);
440    others = getFsAction(FTPFile.WORLD_ACCESS, ftpFile);
441    return new FsPermission(user, group, others);
442  }
443
444  @Override
445  public URI getUri() {
446    return uri;
447  }
448
449  @Override
450  public FileStatus[] listStatus(Path file) throws IOException {
451    FTPClient client = connect();
452    try {
453      FileStatus[] stats = listStatus(client, file);
454      return stats;
455    } finally {
456      disconnect(client);
457    }
458  }
459
460  /**
461   * Convenience method, so that we don't open a new connection when using this
462   * method from within another method. Otherwise every API invocation incurs
463   * the overhead of opening/closing a TCP connection.
464   */
465  private FileStatus[] listStatus(FTPClient client, Path file)
466      throws IOException {
467    Path workDir = new Path(client.printWorkingDirectory());
468    Path absolute = makeAbsolute(workDir, file);
469    FileStatus fileStat = getFileStatus(client, absolute);
470    if (fileStat.isFile()) {
471      return new FileStatus[] { fileStat };
472    }
473    FTPFile[] ftpFiles = client.listFiles(absolute.toUri().getPath());
474    FileStatus[] fileStats = new FileStatus[ftpFiles.length];
475    for (int i = 0; i < ftpFiles.length; i++) {
476      fileStats[i] = getFileStatus(ftpFiles[i], absolute);
477    }
478    return fileStats;
479  }
480
481  @Override
482  public FileStatus getFileStatus(Path file) throws IOException {
483    FTPClient client = connect();
484    try {
485      FileStatus status = getFileStatus(client, file);
486      return status;
487    } finally {
488      disconnect(client);
489    }
490  }
491
492  /**
493   * Convenience method, so that we don't open a new connection when using this
494   * method from within another method. Otherwise every API invocation incurs
495   * the overhead of opening/closing a TCP connection.
496   */
497  private FileStatus getFileStatus(FTPClient client, Path file)
498      throws IOException {
499    FileStatus fileStat = null;
500    Path workDir = new Path(client.printWorkingDirectory());
501    Path absolute = makeAbsolute(workDir, file);
502    Path parentPath = absolute.getParent();
503    if (parentPath == null) { // root dir
504      long length = -1; // Length of root dir on server not known
505      boolean isDir = true;
506      int blockReplication = 1;
507      long blockSize = DEFAULT_BLOCK_SIZE; // Block Size not known.
508      long modTime = -1; // Modification time of root dir not known.
509      Path root = new Path("/");
510      return new FileStatus(length, isDir, blockReplication, blockSize,
511          modTime, root.makeQualified(this));
512    }
513    String pathName = parentPath.toUri().getPath();
514    FTPFile[] ftpFiles = client.listFiles(pathName);
515    if (ftpFiles != null) {
516      for (FTPFile ftpFile : ftpFiles) {
517        if (ftpFile.getName().equals(file.getName())) { // file found in dir
518          fileStat = getFileStatus(ftpFile, parentPath);
519          break;
520        }
521      }
522      if (fileStat == null) {
523        throw new FileNotFoundException("File " + file + " does not exist.");
524      }
525    } else {
526      throw new FileNotFoundException("File " + file + " does not exist.");
527    }
528    return fileStat;
529  }
530
531  /**
532   * Convert the file information in FTPFile to a {@link FileStatus} object. *
533   * 
534   * @param ftpFile
535   * @param parentPath
536   * @return FileStatus
537   */
538  private FileStatus getFileStatus(FTPFile ftpFile, Path parentPath) {
539    long length = ftpFile.getSize();
540    boolean isDir = ftpFile.isDirectory();
541    int blockReplication = 1;
542    // Using default block size since there is no way in FTP client to know of
543    // block sizes on server. The assumption could be less than ideal.
544    long blockSize = DEFAULT_BLOCK_SIZE;
545    long modTime = ftpFile.getTimestamp().getTimeInMillis();
546    long accessTime = 0;
547    FsPermission permission = getPermissions(ftpFile);
548    String user = ftpFile.getUser();
549    String group = ftpFile.getGroup();
550    Path filePath = new Path(parentPath, ftpFile.getName());
551    return new FileStatus(length, isDir, blockReplication, blockSize, modTime,
552        accessTime, permission, user, group, filePath.makeQualified(this));
553  }
554
555  @Override
556  public boolean mkdirs(Path file, FsPermission permission) throws IOException {
557    FTPClient client = connect();
558    try {
559      boolean success = mkdirs(client, file, permission);
560      return success;
561    } finally {
562      disconnect(client);
563    }
564  }
565
566  /**
567   * Convenience method, so that we don't open a new connection when using this
568   * method from within another method. Otherwise every API invocation incurs
569   * the overhead of opening/closing a TCP connection.
570   */
571  private boolean mkdirs(FTPClient client, Path file, FsPermission permission)
572      throws IOException {
573    boolean created = true;
574    Path workDir = new Path(client.printWorkingDirectory());
575    Path absolute = makeAbsolute(workDir, file);
576    String pathName = absolute.getName();
577    if (!exists(client, absolute)) {
578      Path parent = absolute.getParent();
579      created = (parent == null || mkdirs(client, parent, FsPermission
580          .getDirDefault()));
581      if (created) {
582        String parentDir = parent.toUri().getPath();
583        client.changeWorkingDirectory(parentDir);
584        created = created && client.makeDirectory(pathName);
585      }
586    } else if (isFile(client, absolute)) {
587      throw new ParentNotDirectoryException(String.format(
588          "Can't make directory for path %s since it is a file.", absolute));
589    }
590    return created;
591  }
592
593  /**
594   * Convenience method, so that we don't open a new connection when using this
595   * method from within another method. Otherwise every API invocation incurs
596   * the overhead of opening/closing a TCP connection.
597   */
598  private boolean isFile(FTPClient client, Path file) {
599    try {
600      return getFileStatus(client, file).isFile();
601    } catch (FileNotFoundException e) {
602      return false; // file does not exist
603    } catch (IOException ioe) {
604      throw new FTPException("File check failed", ioe);
605    }
606  }
607
608  /*
609   * Assuming that parent of both source and destination is the same. Is the
610   * assumption correct or it is suppose to work like 'move' ?
611   */
612  @Override
613  public boolean rename(Path src, Path dst) throws IOException {
614    FTPClient client = connect();
615    try {
616      boolean success = rename(client, src, dst);
617      return success;
618    } finally {
619      disconnect(client);
620    }
621  }
622
623  /**
624   * Probe for a path being a parent of another
625   * @param parent parent path
626   * @param child possible child path
627   * @return true if the parent's path matches the start of the child's
628   */
629  private boolean isParentOf(Path parent, Path child) {
630    URI parentURI = parent.toUri();
631    String parentPath = parentURI.getPath();
632    if (!parentPath.endsWith("/")) {
633      parentPath += "/";
634    }
635    URI childURI = child.toUri();
636    String childPath = childURI.getPath();
637    return childPath.startsWith(parentPath);
638  }
639
640  /**
641   * Convenience method, so that we don't open a new connection when using this
642   * method from within another method. Otherwise every API invocation incurs
643   * the overhead of opening/closing a TCP connection.
644   * 
645   * @param client
646   * @param src
647   * @param dst
648   * @return
649   * @throws IOException
650   */
651  private boolean rename(FTPClient client, Path src, Path dst)
652      throws IOException {
653    Path workDir = new Path(client.printWorkingDirectory());
654    Path absoluteSrc = makeAbsolute(workDir, src);
655    Path absoluteDst = makeAbsolute(workDir, dst);
656    if (!exists(client, absoluteSrc)) {
657      throw new FileNotFoundException("Source path " + src + " does not exist");
658    }
659    if (isDirectory(absoluteDst)) {
660      // destination is a directory: rename goes underneath it with the
661      // source name
662      absoluteDst = new Path(absoluteDst, absoluteSrc.getName());
663    }
664    if (exists(client, absoluteDst)) {
665      throw new FileAlreadyExistsException("Destination path " + dst
666          + " already exists");
667    }
668    String parentSrc = absoluteSrc.getParent().toUri().toString();
669    String parentDst = absoluteDst.getParent().toUri().toString();
670    if (isParentOf(absoluteSrc, absoluteDst)) {
671      throw new IOException("Cannot rename " + absoluteSrc + " under itself"
672      + " : "+ absoluteDst);
673    }
674
675    if (!parentSrc.equals(parentDst)) {
676      throw new IOException("Cannot rename source: " + absoluteSrc
677          + " to " + absoluteDst
678          + " -"+ E_SAME_DIRECTORY_ONLY);
679    }
680    String from = absoluteSrc.getName();
681    String to = absoluteDst.getName();
682    client.changeWorkingDirectory(parentSrc);
683    boolean renamed = client.rename(from, to);
684    return renamed;
685  }
686
687  @Override
688  public Path getWorkingDirectory() {
689    // Return home directory always since we do not maintain state.
690    return getHomeDirectory();
691  }
692
693  @Override
694  public Path getHomeDirectory() {
695    FTPClient client = null;
696    try {
697      client = connect();
698      Path homeDir = new Path(client.printWorkingDirectory());
699      return homeDir;
700    } catch (IOException ioe) {
701      throw new FTPException("Failed to get home directory", ioe);
702    } finally {
703      try {
704        disconnect(client);
705      } catch (IOException ioe) {
706        throw new FTPException("Failed to disconnect", ioe);
707      }
708    }
709  }
710
711  @Override
712  public void setWorkingDirectory(Path newDir) {
713    // we do not maintain the working directory state
714  }
715}