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