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.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.time.Instant;
23  import java.util.Calendar;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.TimeZone;
29  import java.util.TreeMap;
30  import java.util.concurrent.atomic.AtomicBoolean;
31  
32  import org.apache.commons.io.function.Uncheck;
33  import org.apache.commons.lang3.ArrayUtils;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.apache.commons.net.ftp.FTPFile;
37  import org.apache.commons.vfs2.FileName;
38  import org.apache.commons.vfs2.FileNotFolderException;
39  import org.apache.commons.vfs2.FileNotFoundException;
40  import org.apache.commons.vfs2.FileObject;
41  import org.apache.commons.vfs2.FileSystemException;
42  import org.apache.commons.vfs2.FileType;
43  import org.apache.commons.vfs2.RandomAccessContent;
44  import org.apache.commons.vfs2.provider.AbstractFileName;
45  import org.apache.commons.vfs2.provider.AbstractFileObject;
46  import org.apache.commons.vfs2.provider.UriParser;
47  import org.apache.commons.vfs2.util.FileObjectUtils;
48  import org.apache.commons.vfs2.util.Messages;
49  import org.apache.commons.vfs2.util.MonitorInputStream;
50  import org.apache.commons.vfs2.util.MonitorOutputStream;
51  import org.apache.commons.vfs2.util.RandomAccessMode;
52  
53  /**
54   * An FTP file.
55   */
56  public class FtpFileObject extends AbstractFileObject<FtpFileSystem> {
57  
58      /**
59       * An InputStream that monitors for end-of-file.
60       */
61      final class FtpInputStream extends MonitorInputStream {
62          private final FtpClient client;
63  
64          FtpInputStream(final FtpClient client, final InputStream in) {
65              super(in);
66              this.client = client;
67          }
68  
69          FtpInputStream(final FtpClient client, final InputStream in, final int bufferSize) {
70              super(in, bufferSize);
71              this.client = client;
72          }
73  
74          void abort() throws IOException {
75              client.abort();
76              close();
77          }
78  
79          private boolean isTransferAbortedOkReplyCode() throws IOException {
80              final List<Integer> transferAbortedOkReplyCodes = FtpFileSystemConfigBuilder
81                  .getInstance()
82                  .getTransferAbortedOkReplyCodes(getAbstractFileSystem().getFileSystemOptions());
83              return transferAbortedOkReplyCodes != null && transferAbortedOkReplyCodes.contains(client.getReplyCode());
84          }
85  
86          /**
87           * Called after the stream has been closed.
88           */
89          @Override
90          protected void onClose() throws IOException {
91              final boolean ok;
92              try {
93                  ok = client.completePendingCommand() || isTransferAbortedOkReplyCode();
94              } finally {
95                  getAbstractFileSystem().putClient(client);
96              }
97              if (!ok) {
98                  throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName());
99              }
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 }