001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.ftp; 018 019import java.io.FileNotFoundException; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.OutputStream; 023import java.util.Calendar; 024import java.util.Collections; 025import java.util.Iterator; 026import java.util.Map; 027import java.util.TreeMap; 028 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.apache.commons.net.ftp.FTPFile; 032import org.apache.commons.vfs2.FileName; 033import org.apache.commons.vfs2.FileNotFolderException; 034import org.apache.commons.vfs2.FileObject; 035import org.apache.commons.vfs2.FileSystemException; 036import org.apache.commons.vfs2.FileType; 037import org.apache.commons.vfs2.RandomAccessContent; 038import org.apache.commons.vfs2.provider.AbstractFileName; 039import org.apache.commons.vfs2.provider.AbstractFileObject; 040import org.apache.commons.vfs2.provider.UriParser; 041import org.apache.commons.vfs2.util.FileObjectUtils; 042import org.apache.commons.vfs2.util.Messages; 043import org.apache.commons.vfs2.util.MonitorInputStream; 044import org.apache.commons.vfs2.util.MonitorOutputStream; 045import org.apache.commons.vfs2.util.RandomAccessMode; 046 047/** 048 * An FTP file. 049 */ 050public class FtpFileObject extends AbstractFileObject<FtpFileSystem> { 051 private static final Map<String, FTPFile> EMPTY_FTP_FILE_MAP = Collections 052 .unmodifiableMap(new TreeMap<String, FTPFile>()); 053 private static final FTPFile UNKNOWN = new FTPFile(); 054 private static final Log log = LogFactory.getLog(FtpFileObject.class); 055 056 private final String relPath; 057 058 // Cached info 059 private FTPFile fileInfo; 060 private Map<String, FTPFile> children; 061 private FileObject linkDestination; 062 063 private boolean inRefresh; 064 065 protected FtpFileObject(final AbstractFileName name, final FtpFileSystem fileSystem, final FileName rootName) 066 throws FileSystemException { 067 super(name, fileSystem); 068 final String relPath = UriParser.decode(rootName.getRelativeName(name)); 069 if (".".equals(relPath)) { 070 // do not use the "." as path against the ftp-server 071 // e.g. the uu.net ftp-server do a recursive listing then 072 // this.relPath = UriParser.decode(rootName.getPath()); 073 // this.relPath = "."; 074 this.relPath = null; 075 } else { 076 this.relPath = relPath; 077 } 078 } 079 080 /** 081 * Called by child file objects, to locate their ftp file info. 082 * 083 * @param name the filename in its native form ie. without uri stuff (%nn) 084 * @param flush recreate children cache 085 */ 086 private FTPFile getChildFile(final String name, final boolean flush) throws IOException { 087 /* 088 * If we should flush cached children, clear our children map unless we're in the middle of a refresh in which 089 * case we've just recently refreshed our children. No need to do it again when our children are refresh()ed, 090 * calling getChildFile() for themselves from within getInfo(). See getChildren(). 091 */ 092 if (flush && !inRefresh) { 093 children = null; 094 } 095 096 // List the children of this file 097 doGetChildren(); 098 099 // VFS-210 100 if (children == null) { 101 return null; 102 } 103 104 // Look for the requested child 105 final FTPFile ftpFile = children.get(name); 106 return ftpFile; 107 } 108 109 /** 110 * Fetches the children of this file, if not already cached. 111 */ 112 private void doGetChildren() throws IOException { 113 if (children != null) { 114 return; 115 } 116 117 final FtpClient client = getAbstractFileSystem().getClient(); 118 try { 119 final String path = fileInfo != null && fileInfo.isSymbolicLink() 120 ? getFileSystem().getFileSystemManager().resolveName(getParent().getName(), fileInfo.getLink()) 121 .getPath() 122 : relPath; 123 final FTPFile[] tmpChildren = client.listFiles(path); 124 if (tmpChildren == null || tmpChildren.length == 0) { 125 children = EMPTY_FTP_FILE_MAP; 126 } else { 127 children = new TreeMap<>(); 128 129 // Remove '.' and '..' elements 130 for (int i = 0; i < tmpChildren.length; i++) { 131 final FTPFile child = tmpChildren[i]; 132 if (child == null) { 133 if (log.isDebugEnabled()) { 134 log.debug(Messages.getString("vfs.provider.ftp/invalid-directory-entry.debug", 135 Integer.valueOf(i), relPath)); 136 } 137 continue; 138 } 139 if (!".".equals(child.getName()) && !"..".equals(child.getName())) { 140 children.put(child.getName(), child); 141 } 142 } 143 } 144 } finally { 145 getAbstractFileSystem().putClient(client); 146 } 147 } 148 149 /** 150 * Attaches this file object to its file resource. 151 */ 152 @Override 153 protected void doAttach() throws IOException { 154 // Get the parent folder to find the info for this file 155 // VFS-210 getInfo(false); 156 } 157 158 /** 159 * Fetches the info for this file. 160 */ 161 private void getInfo(final boolean flush) throws IOException { 162 final FtpFileObject parent = (FtpFileObject) FileObjectUtils.getAbstractFileObject(getParent()); 163 FTPFile newFileInfo; 164 if (parent != null) { 165 newFileInfo = parent.getChildFile(UriParser.decode(getName().getBaseName()), flush); 166 } else { 167 // Assume the root is a directory and exists 168 newFileInfo = new FTPFile(); 169 newFileInfo.setType(FTPFile.DIRECTORY_TYPE); 170 } 171 172 if (newFileInfo == null) { 173 this.fileInfo = UNKNOWN; 174 } else { 175 this.fileInfo = newFileInfo; 176 } 177 } 178 179 /** 180 * @throws FileSystemException if an error occurs. 181 */ 182 @Override 183 public void refresh() throws FileSystemException { 184 if (!inRefresh) { 185 try { 186 inRefresh = true; 187 super.refresh(); 188 189 synchronized (getFileSystem()) { 190 this.fileInfo = null; 191 } 192 193 /* 194 * VFS-210 try { // this will tell the parent to recreate its children collection getInfo(true); } catch 195 * (IOException e) { throw new FileSystemException(e); } 196 */ 197 } finally { 198 inRefresh = false; 199 } 200 } 201 } 202 203 /** 204 * Detaches this file object from its file resource. 205 */ 206 @Override 207 protected void doDetach() { 208 synchronized (getFileSystem()) { 209 this.fileInfo = null; 210 children = null; 211 } 212 } 213 214 /** 215 * Called when the children of this file change. 216 */ 217 @Override 218 protected void onChildrenChanged(final FileName child, final FileType newType) { 219 if (children != null && newType.equals(FileType.IMAGINARY)) { 220 try { 221 children.remove(UriParser.decode(child.getBaseName())); 222 } catch (final FileSystemException e) { 223 throw new RuntimeException(e.getMessage()); 224 } 225 } else { 226 // if child was added we have to rescan the children 227 // TODO - get rid of this 228 children = null; 229 } 230 } 231 232 /** 233 * Called when the type or content of this file changes. 234 */ 235 @Override 236 protected void onChange() throws IOException { 237 children = null; 238 239 if (getType().equals(FileType.IMAGINARY)) { 240 // file is deleted, avoid server lookup 241 synchronized (getFileSystem()) { 242 this.fileInfo = UNKNOWN; 243 } 244 return; 245 } 246 247 getInfo(true); 248 } 249 250 /** 251 * Determines the type of the file, returns null if the file does not exist. 252 */ 253 @Override 254 protected FileType doGetType() throws Exception { 255 // VFS-210 256 synchronized (getFileSystem()) { 257 if (this.fileInfo == null) { 258 getInfo(false); 259 } 260 261 if (this.fileInfo == UNKNOWN) { 262 return FileType.IMAGINARY; 263 } else if (this.fileInfo.isDirectory()) { 264 return FileType.FOLDER; 265 } else if (this.fileInfo.isFile()) { 266 return FileType.FILE; 267 } else if (this.fileInfo.isSymbolicLink()) { 268 final FileObject linkDest = getLinkDestination(); 269 // VFS-437: We need to check if the symbolic link links back to the symbolic link itself 270 if (this.isCircular(linkDest)) { 271 // If the symbolic link links back to itself, treat it as an imaginary file to prevent following 272 // this link. If the user tries to access the link as a file or directory, the user will end up with 273 // a FileSystemException warning that the file cannot be accessed. This is to prevent the infinite 274 // call back to doGetType() to prevent the StackOverFlow 275 return FileType.IMAGINARY; 276 } 277 return linkDest.getType(); 278 279 } 280 } 281 throw new FileSystemException("vfs.provider.ftp/get-type.error", getName()); 282 } 283 284 private FileObject getLinkDestination() throws FileSystemException { 285 if (linkDestination == null) { 286 final String path; 287 synchronized (getFileSystem()) { 288 path = this.fileInfo.getLink(); 289 } 290 FileName relativeTo = getName().getParent(); 291 if (relativeTo == null) { 292 relativeTo = getName(); 293 } 294 final FileName linkDestinationName = getFileSystem().getFileSystemManager().resolveName(relativeTo, path); 295 linkDestination = getFileSystem().resolveFile(linkDestinationName); 296 } 297 298 return linkDestination; 299 } 300 301 @Override 302 protected FileObject[] doListChildrenResolved() throws Exception { 303 synchronized (getFileSystem()) { 304 if (this.fileInfo != null && this.fileInfo.isSymbolicLink()) { 305 final FileObject linkDest = getLinkDestination(); 306 // VFS-437: Try to avoid a recursion loop. 307 if (this.isCircular(linkDest)) { 308 return null; 309 } 310 return linkDest.getChildren(); 311 } 312 } 313 return null; 314 } 315 316 /** 317 * Returns the file's list of children. 318 * 319 * @return The list of children 320 * @throws FileSystemException If there was a problem listing children 321 * @see AbstractFileObject#getChildren() 322 * @since 2.0 323 */ 324 @Override 325 public FileObject[] getChildren() throws FileSystemException { 326 try { 327 if (doGetType() != FileType.FOLDER) { 328 throw new FileNotFolderException(getName()); 329 } 330 } catch (final Exception ex) { 331 throw new FileNotFolderException(getName(), ex); 332 } 333 334 try { 335 /* 336 * Wrap our parent implementation, noting that we're refreshing so that we don't refresh() ourselves and 337 * each of our parents for each children. Note that refresh() will list children. Meaning, if if this file 338 * has C children, P parents, there will be (C * P) listings made with (C * (P + 1)) refreshes, when there 339 * should really only be 1 listing and C refreshes. 340 */ 341 342 this.inRefresh = true; 343 return super.getChildren(); 344 } finally { 345 this.inRefresh = false; 346 } 347 } 348 349 /** 350 * Lists the children of the file. 351 */ 352 @Override 353 protected String[] doListChildren() throws Exception { 354 // List the children of this file 355 doGetChildren(); 356 357 // VFS-210 358 if (children == null) { 359 return null; 360 } 361 362 // TODO - get rid of this children stuff 363 final String[] childNames = new String[children.size()]; 364 int childNum = -1; 365 final Iterator<FTPFile> iterChildren = children.values().iterator(); 366 while (iterChildren.hasNext()) { 367 childNum++; 368 final FTPFile child = iterChildren.next(); 369 childNames[childNum] = child.getName(); 370 } 371 372 return UriParser.encode(childNames); 373 } 374 375 /** 376 * Deletes the file. 377 */ 378 @Override 379 protected void doDelete() throws Exception { 380 synchronized (getFileSystem()) { 381 final boolean ok; 382 final FtpClient ftpClient = getAbstractFileSystem().getClient(); 383 try { 384 if (this.fileInfo.isDirectory()) { 385 ok = ftpClient.removeDirectory(relPath); 386 } else { 387 ok = ftpClient.deleteFile(relPath); 388 } 389 } finally { 390 getAbstractFileSystem().putClient(ftpClient); 391 } 392 393 if (!ok) { 394 throw new FileSystemException("vfs.provider.ftp/delete-file.error", getName()); 395 } 396 this.fileInfo = null; 397 children = EMPTY_FTP_FILE_MAP; 398 } 399 } 400 401 /** 402 * Renames the file 403 */ 404 @Override 405 protected void doRename(final FileObject newFile) throws Exception { 406 synchronized (getFileSystem()) { 407 final boolean ok; 408 final FtpClient ftpClient = getAbstractFileSystem().getClient(); 409 try { 410 final String oldName = relPath; 411 final String newName = ((FtpFileObject) FileObjectUtils.getAbstractFileObject(newFile)).getRelPath(); 412 ok = ftpClient.rename(oldName, newName); 413 } finally { 414 getAbstractFileSystem().putClient(ftpClient); 415 } 416 417 if (!ok) { 418 throw new FileSystemException("vfs.provider.ftp/rename-file.error", getName().toString(), newFile); 419 } 420 this.fileInfo = null; 421 children = EMPTY_FTP_FILE_MAP; 422 } 423 } 424 425 /** 426 * Creates this file as a folder. 427 */ 428 @Override 429 protected void doCreateFolder() throws Exception { 430 final boolean ok; 431 final FtpClient client = getAbstractFileSystem().getClient(); 432 try { 433 ok = client.makeDirectory(relPath); 434 } finally { 435 getAbstractFileSystem().putClient(client); 436 } 437 438 if (!ok) { 439 throw new FileSystemException("vfs.provider.ftp/create-folder.error", getName()); 440 } 441 } 442 443 /** 444 * Returns the size of the file content (in bytes). 445 */ 446 @Override 447 protected long doGetContentSize() throws Exception { 448 synchronized (getFileSystem()) { 449 if (this.fileInfo.isSymbolicLink()) { 450 final FileObject linkDest = getLinkDestination(); 451 // VFS-437: Try to avoid a recursion loop. 452 if (this.isCircular(linkDest)) { 453 return this.fileInfo.getSize(); 454 } 455 return linkDest.getContent().getSize(); 456 } 457 return this.fileInfo.getSize(); 458 } 459 } 460 461 /** 462 * get the last modified time on an ftp file 463 * 464 * @see org.apache.commons.vfs2.provider.AbstractFileObject#doGetLastModifiedTime() 465 */ 466 @Override 467 protected long doGetLastModifiedTime() throws Exception { 468 synchronized (getFileSystem()) { 469 if (this.fileInfo.isSymbolicLink()) { 470 final FileObject linkDest = getLinkDestination(); 471 // VFS-437: Try to avoid a recursion loop. 472 if (this.isCircular(linkDest)) { 473 return getTimestamp(); 474 } 475 return linkDest.getContent().getLastModifiedTime(); 476 } 477 return getTimestamp(); 478 } 479 } 480 481 /** 482 * Creates an input stream to read the file content from. 483 */ 484 @Override 485 protected InputStream doGetInputStream() throws Exception { 486 final FtpClient client = getAbstractFileSystem().getClient(); 487 try { 488 final InputStream instr = client.retrieveFileStream(relPath); 489 // VFS-210 490 if (instr == null) { 491 throw new FileNotFoundException(getName().toString()); 492 } 493 return new FtpInputStream(client, instr); 494 } catch (final Exception e) { 495 getAbstractFileSystem().putClient(client); 496 throw e; 497 } 498 } 499 500 @Override 501 protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception { 502 return new FtpRandomAccessContent(this, mode); 503 } 504 505 /** 506 * Creates an output stream to write the file content to. 507 */ 508 @Override 509 protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception { 510 final FtpClient client = getAbstractFileSystem().getClient(); 511 try { 512 OutputStream out = null; 513 if (bAppend) { 514 out = client.appendFileStream(relPath); 515 } else { 516 out = client.storeFileStream(relPath); 517 } 518 519 if (out == null) { 520 throw new FileSystemException("vfs.provider.ftp/output-error.debug", this.getName(), 521 client.getReplyString()); 522 } 523 524 return new FtpOutputStream(client, out); 525 } catch (final Exception e) { 526 getAbstractFileSystem().putClient(client); 527 throw e; 528 } 529 } 530 531 String getRelPath() { 532 return relPath; 533 } 534 535 private long getTimestamp() { 536 final Calendar timestamp = this.fileInfo.getTimestamp(); 537 return timestamp == null ? 0L : timestamp.getTime().getTime(); 538 } 539 540 /** 541 * This is an over simplistic implementation for VFS-437. 542 */ 543 private boolean isCircular(final FileObject linkDest) throws FileSystemException { 544 return linkDest.getName().getPathDecoded().equals(this.getName().getPathDecoded()); 545 } 546 547 FtpInputStream getInputStream(final long filePointer) throws IOException { 548 final FtpClient client = getAbstractFileSystem().getClient(); 549 try { 550 final InputStream instr = client.retrieveFileStream(relPath, filePointer); 551 if (instr == null) { 552 throw new FileSystemException("vfs.provider.ftp/input-error.debug", this.getName(), 553 client.getReplyString()); 554 } 555 return new FtpInputStream(client, instr); 556 } catch (final IOException e) { 557 getAbstractFileSystem().putClient(client); 558 throw e; 559 } 560 } 561 562 /** 563 * An InputStream that monitors for end-of-file. 564 */ 565 class FtpInputStream extends MonitorInputStream { 566 private final FtpClient client; 567 568 public FtpInputStream(final FtpClient client, final InputStream in) { 569 super(in); 570 this.client = client; 571 } 572 573 void abort() throws IOException { 574 client.abort(); 575 close(); 576 } 577 578 /** 579 * Called after the stream has been closed. 580 */ 581 @Override 582 protected void onClose() throws IOException { 583 final boolean ok; 584 try { 585 ok = client.completePendingCommand(); 586 } finally { 587 getAbstractFileSystem().putClient(client); 588 } 589 590 if (!ok) { 591 throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName()); 592 } 593 } 594 } 595 596 /** 597 * An OutputStream that monitors for end-of-file. 598 */ 599 private class FtpOutputStream extends MonitorOutputStream { 600 private final FtpClient client; 601 602 public FtpOutputStream(final FtpClient client, final OutputStream outstr) { 603 super(outstr); 604 this.client = client; 605 } 606 607 /** 608 * Called after this stream is closed. 609 */ 610 @Override 611 protected void onClose() throws IOException { 612 final boolean ok; 613 try { 614 ok = client.completePendingCommand(); 615 } finally { 616 getAbstractFileSystem().putClient(client); 617 } 618 619 if (!ok) { 620 throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName()); 621 } 622 } 623 } 624}