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}