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