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