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