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