001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.ftp;
018
019import java.io.FileNotFoundException;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.util.Calendar;
024import java.util.Collections;
025import java.util.Iterator;
026import java.util.Map;
027import java.util.TreeMap;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.apache.commons.net.ftp.FTPFile;
032import org.apache.commons.vfs2.FileName;
033import org.apache.commons.vfs2.FileNotFolderException;
034import org.apache.commons.vfs2.FileObject;
035import org.apache.commons.vfs2.FileSystemException;
036import org.apache.commons.vfs2.FileType;
037import org.apache.commons.vfs2.RandomAccessContent;
038import org.apache.commons.vfs2.provider.AbstractFileName;
039import org.apache.commons.vfs2.provider.AbstractFileObject;
040import org.apache.commons.vfs2.provider.UriParser;
041import org.apache.commons.vfs2.util.FileObjectUtils;
042import org.apache.commons.vfs2.util.Messages;
043import org.apache.commons.vfs2.util.MonitorInputStream;
044import org.apache.commons.vfs2.util.MonitorOutputStream;
045import org.apache.commons.vfs2.util.RandomAccessMode;
046
047/**
048 * An FTP file.
049 */
050public class FtpFileObject extends AbstractFileObject<FtpFileSystem> {
051    private static final Map<String, FTPFile> EMPTY_FTP_FILE_MAP = Collections
052            .unmodifiableMap(new TreeMap<String, FTPFile>());
053    private static final FTPFile UNKNOWN = new FTPFile();
054    private static final Log log = LogFactory.getLog(FtpFileObject.class);
055
056    private final String relPath;
057
058    // Cached info
059    private FTPFile fileInfo;
060    private Map<String, FTPFile> children;
061    private FileObject linkDestination;
062
063    private boolean inRefresh;
064
065    protected FtpFileObject(final AbstractFileName name, final FtpFileSystem fileSystem, final FileName rootName)
066            throws FileSystemException {
067        super(name, fileSystem);
068        final String relPath = UriParser.decode(rootName.getRelativeName(name));
069        if (".".equals(relPath)) {
070            // do not use the "." as path against the ftp-server
071            // e.g. the uu.net ftp-server do a recursive listing then
072            // this.relPath = UriParser.decode(rootName.getPath());
073            // this.relPath = ".";
074            this.relPath = null;
075        } else {
076            this.relPath = relPath;
077        }
078    }
079
080    /**
081     * Called by child file objects, to locate their ftp file info.
082     *
083     * @param name the filename in its native form ie. without uri stuff (%nn)
084     * @param flush recreate children cache
085     */
086    private FTPFile getChildFile(final String name, final boolean flush) throws IOException {
087        /*
088         * If we should flush cached children, clear our children map unless we're in the middle of a refresh in which
089         * case we've just recently refreshed our children. No need to do it again when our children are refresh()ed,
090         * calling getChildFile() for themselves from within getInfo(). See getChildren().
091         */
092        if (flush && !inRefresh) {
093            children = null;
094        }
095
096        // List the children of this file
097        doGetChildren();
098
099        // VFS-210
100        if (children == null) {
101            return null;
102        }
103
104        // Look for the requested child
105        final FTPFile ftpFile = children.get(name);
106        return ftpFile;
107    }
108
109    /**
110     * Fetches the children of this file, if not already cached.
111     */
112    private void doGetChildren() throws IOException {
113        if (children != null) {
114            return;
115        }
116
117        final FtpClient client = getAbstractFileSystem().getClient();
118        try {
119            final String path = fileInfo != null && fileInfo.isSymbolicLink()
120                    ? getFileSystem().getFileSystemManager().resolveName(getParent().getName(), fileInfo.getLink())
121                            .getPath()
122                    : relPath;
123            final FTPFile[] tmpChildren = client.listFiles(path);
124            if (tmpChildren == null || tmpChildren.length == 0) {
125                children = EMPTY_FTP_FILE_MAP;
126            } else {
127                children = new TreeMap<>();
128
129                // Remove '.' and '..' elements
130                for (int i = 0; i < tmpChildren.length; i++) {
131                    final FTPFile child = tmpChildren[i];
132                    if (child == null) {
133                        if (log.isDebugEnabled()) {
134                            log.debug(Messages.getString("vfs.provider.ftp/invalid-directory-entry.debug",
135                                    Integer.valueOf(i), relPath));
136                        }
137                        continue;
138                    }
139                    if (!".".equals(child.getName()) && !"..".equals(child.getName())) {
140                        children.put(child.getName(), child);
141                    }
142                }
143            }
144        } finally {
145            getAbstractFileSystem().putClient(client);
146        }
147    }
148
149    /**
150     * Attaches this file object to its file resource.
151     */
152    @Override
153    protected void doAttach() throws IOException {
154        // Get the parent folder to find the info for this file
155        // VFS-210 getInfo(false);
156    }
157
158    /**
159     * Fetches the info for this file.
160     */
161    private void getInfo(final boolean flush) throws IOException {
162        final FtpFileObject parent = (FtpFileObject) FileObjectUtils.getAbstractFileObject(getParent());
163        FTPFile newFileInfo;
164        if (parent != null) {
165            newFileInfo = parent.getChildFile(UriParser.decode(getName().getBaseName()), flush);
166        } else {
167            // Assume the root is a directory and exists
168            newFileInfo = new FTPFile();
169            newFileInfo.setType(FTPFile.DIRECTORY_TYPE);
170        }
171
172        if (newFileInfo == null) {
173            this.fileInfo = UNKNOWN;
174        } else {
175            this.fileInfo = newFileInfo;
176        }
177    }
178
179    /**
180     * @throws FileSystemException if an error occurs.
181     */
182    @Override
183    public void refresh() throws FileSystemException {
184        if (!inRefresh) {
185            try {
186                inRefresh = true;
187                super.refresh();
188
189                synchronized (getFileSystem()) {
190                    this.fileInfo = null;
191                }
192
193                /*
194                 * VFS-210 try { // this will tell the parent to recreate its children collection getInfo(true); } catch
195                 * (IOException e) { throw new FileSystemException(e); }
196                 */
197            } finally {
198                inRefresh = false;
199            }
200        }
201    }
202
203    /**
204     * Detaches this file object from its file resource.
205     */
206    @Override
207    protected void doDetach() {
208        synchronized (getFileSystem()) {
209            this.fileInfo = null;
210            children = null;
211        }
212    }
213
214    /**
215     * Called when the children of this file change.
216     */
217    @Override
218    protected void onChildrenChanged(final FileName child, final FileType newType) {
219        if (children != null && newType.equals(FileType.IMAGINARY)) {
220            try {
221                children.remove(UriParser.decode(child.getBaseName()));
222            } catch (final FileSystemException e) {
223                throw new RuntimeException(e.getMessage());
224            }
225        } else {
226            // if child was added we have to rescan the children
227            // TODO - get rid of this
228            children = null;
229        }
230    }
231
232    /**
233     * Called when the type or content of this file changes.
234     */
235    @Override
236    protected void onChange() throws IOException {
237        children = null;
238
239        if (getType().equals(FileType.IMAGINARY)) {
240            // file is deleted, avoid server lookup
241            synchronized (getFileSystem()) {
242                this.fileInfo = UNKNOWN;
243            }
244            return;
245        }
246
247        getInfo(true);
248    }
249
250    /**
251     * Determines the type of the file, returns null if the file does not exist.
252     */
253    @Override
254    protected FileType doGetType() throws Exception {
255        // VFS-210
256        synchronized (getFileSystem()) {
257            if (this.fileInfo == null) {
258                getInfo(false);
259            }
260
261            if (this.fileInfo == UNKNOWN) {
262                return FileType.IMAGINARY;
263            } else if (this.fileInfo.isDirectory()) {
264                return FileType.FOLDER;
265            } else if (this.fileInfo.isFile()) {
266                return FileType.FILE;
267            } else if (this.fileInfo.isSymbolicLink()) {
268                final FileObject linkDest = getLinkDestination();
269                // VFS-437: We need to check if the symbolic link links back to the symbolic link itself
270                if (this.isCircular(linkDest)) {
271                    // If the symbolic link links back to itself, treat it as an imaginary file to prevent following
272                    // this link. If the user tries to access the link as a file or directory, the user will end up with
273                    // a FileSystemException warning that the file cannot be accessed. This is to prevent the infinite
274                    // call back to doGetType() to prevent the StackOverFlow
275                    return FileType.IMAGINARY;
276                }
277                return linkDest.getType();
278
279            }
280        }
281        throw new FileSystemException("vfs.provider.ftp/get-type.error", getName());
282    }
283
284    private FileObject getLinkDestination() throws FileSystemException {
285        if (linkDestination == null) {
286            final String path;
287            synchronized (getFileSystem()) {
288                path = this.fileInfo.getLink();
289            }
290            FileName relativeTo = getName().getParent();
291            if (relativeTo == null) {
292                relativeTo = getName();
293            }
294            final FileName linkDestinationName = getFileSystem().getFileSystemManager().resolveName(relativeTo, path);
295            linkDestination = getFileSystem().resolveFile(linkDestinationName);
296        }
297
298        return linkDestination;
299    }
300
301    @Override
302    protected FileObject[] doListChildrenResolved() throws Exception {
303        synchronized (getFileSystem()) {
304            if (this.fileInfo != null && this.fileInfo.isSymbolicLink()) {
305                final FileObject linkDest = getLinkDestination();
306                // VFS-437: Try to avoid a recursion loop.
307                if (this.isCircular(linkDest)) {
308                    return null;
309                }
310                return linkDest.getChildren();
311            }
312        }
313        return null;
314    }
315
316    /**
317     * Returns the file's list of children.
318     *
319     * @return The list of children
320     * @throws FileSystemException If there was a problem listing children
321     * @see AbstractFileObject#getChildren()
322     * @since 2.0
323     */
324    @Override
325    public FileObject[] getChildren() throws FileSystemException {
326        try {
327            if (doGetType() != FileType.FOLDER) {
328                throw new FileNotFolderException(getName());
329            }
330        } catch (final Exception ex) {
331            throw new FileNotFolderException(getName(), ex);
332        }
333
334        try {
335            /*
336             * Wrap our parent implementation, noting that we're refreshing so that we don't refresh() ourselves and
337             * each of our parents for each children. Note that refresh() will list children. Meaning, if if this file
338             * has C children, P parents, there will be (C * P) listings made with (C * (P + 1)) refreshes, when there
339             * should really only be 1 listing and C refreshes.
340             */
341
342            this.inRefresh = true;
343            return super.getChildren();
344        } finally {
345            this.inRefresh = false;
346        }
347    }
348
349    /**
350     * Lists the children of the file.
351     */
352    @Override
353    protected String[] doListChildren() throws Exception {
354        // List the children of this file
355        doGetChildren();
356
357        // VFS-210
358        if (children == null) {
359            return null;
360        }
361
362        // TODO - get rid of this children stuff
363        final String[] childNames = new String[children.size()];
364        int childNum = -1;
365        final Iterator<FTPFile> iterChildren = children.values().iterator();
366        while (iterChildren.hasNext()) {
367            childNum++;
368            final FTPFile child = iterChildren.next();
369            childNames[childNum] = child.getName();
370        }
371
372        return UriParser.encode(childNames);
373    }
374
375    /**
376     * Deletes the file.
377     */
378    @Override
379    protected void doDelete() throws Exception {
380        synchronized (getFileSystem()) {
381            final boolean ok;
382            final FtpClient ftpClient = getAbstractFileSystem().getClient();
383            try {
384                if (this.fileInfo.isDirectory()) {
385                    ok = ftpClient.removeDirectory(relPath);
386                } else {
387                    ok = ftpClient.deleteFile(relPath);
388                }
389            } finally {
390                getAbstractFileSystem().putClient(ftpClient);
391            }
392
393            if (!ok) {
394                throw new FileSystemException("vfs.provider.ftp/delete-file.error", getName());
395            }
396            this.fileInfo = null;
397            children = EMPTY_FTP_FILE_MAP;
398        }
399    }
400
401    /**
402     * Renames the file
403     */
404    @Override
405    protected void doRename(final FileObject newFile) throws Exception {
406        synchronized (getFileSystem()) {
407            final boolean ok;
408            final FtpClient ftpClient = getAbstractFileSystem().getClient();
409            try {
410                final String oldName = relPath;
411                final String newName = ((FtpFileObject) FileObjectUtils.getAbstractFileObject(newFile)).getRelPath();
412                ok = ftpClient.rename(oldName, newName);
413            } finally {
414                getAbstractFileSystem().putClient(ftpClient);
415            }
416
417            if (!ok) {
418                throw new FileSystemException("vfs.provider.ftp/rename-file.error", getName().toString(), newFile);
419            }
420            this.fileInfo = null;
421            children = EMPTY_FTP_FILE_MAP;
422        }
423    }
424
425    /**
426     * Creates this file as a folder.
427     */
428    @Override
429    protected void doCreateFolder() throws Exception {
430        final boolean ok;
431        final FtpClient client = getAbstractFileSystem().getClient();
432        try {
433            ok = client.makeDirectory(relPath);
434        } finally {
435            getAbstractFileSystem().putClient(client);
436        }
437
438        if (!ok) {
439            throw new FileSystemException("vfs.provider.ftp/create-folder.error", getName());
440        }
441    }
442
443    /**
444     * Returns the size of the file content (in bytes).
445     */
446    @Override
447    protected long doGetContentSize() throws Exception {
448        synchronized (getFileSystem()) {
449            if (this.fileInfo.isSymbolicLink()) {
450                final FileObject linkDest = getLinkDestination();
451                // VFS-437: Try to avoid a recursion loop.
452                if (this.isCircular(linkDest)) {
453                    return this.fileInfo.getSize();
454                }
455                return linkDest.getContent().getSize();
456            }
457            return this.fileInfo.getSize();
458        }
459    }
460
461    /**
462     * get the last modified time on an ftp file
463     *
464     * @see org.apache.commons.vfs2.provider.AbstractFileObject#doGetLastModifiedTime()
465     */
466    @Override
467    protected long doGetLastModifiedTime() throws Exception {
468        synchronized (getFileSystem()) {
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            if (out == null) {
520                throw new FileSystemException("vfs.provider.ftp/output-error.debug", this.getName(),
521                        client.getReplyString());
522            }
523
524            return new FtpOutputStream(client, out);
525        } catch (final Exception e) {
526            getAbstractFileSystem().putClient(client);
527            throw e;
528        }
529    }
530
531    String getRelPath() {
532        return relPath;
533    }
534
535    private long getTimestamp() {
536        final Calendar timestamp = this.fileInfo.getTimestamp();
537        return timestamp == null ? 0L : timestamp.getTime().getTime();
538    }
539
540    /**
541     * This is an over simplistic implementation for VFS-437.
542     */
543    private boolean isCircular(final FileObject linkDest) throws FileSystemException {
544        return linkDest.getName().getPathDecoded().equals(this.getName().getPathDecoded());
545    }
546
547    FtpInputStream getInputStream(final long filePointer) throws IOException {
548        final FtpClient client = getAbstractFileSystem().getClient();
549        try {
550            final InputStream instr = client.retrieveFileStream(relPath, filePointer);
551            if (instr == null) {
552                throw new FileSystemException("vfs.provider.ftp/input-error.debug", this.getName(),
553                        client.getReplyString());
554            }
555            return new FtpInputStream(client, instr);
556        } catch (final IOException e) {
557            getAbstractFileSystem().putClient(client);
558            throw e;
559        }
560    }
561
562    /**
563     * An InputStream that monitors for end-of-file.
564     */
565    class FtpInputStream extends MonitorInputStream {
566        private final FtpClient client;
567
568        public FtpInputStream(final FtpClient client, final InputStream in) {
569            super(in);
570            this.client = client;
571        }
572
573        void abort() throws IOException {
574            client.abort();
575            close();
576        }
577
578        /**
579         * Called after the stream has been closed.
580         */
581        @Override
582        protected void onClose() throws IOException {
583            final boolean ok;
584            try {
585                ok = client.completePendingCommand();
586            } finally {
587                getAbstractFileSystem().putClient(client);
588            }
589
590            if (!ok) {
591                throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName());
592            }
593        }
594    }
595
596    /**
597     * An OutputStream that monitors for end-of-file.
598     */
599    private class FtpOutputStream extends MonitorOutputStream {
600        private final FtpClient client;
601
602        public FtpOutputStream(final FtpClient client, final OutputStream outstr) {
603            super(outstr);
604            this.client = client;
605        }
606
607        /**
608         * Called after this stream is closed.
609         */
610        @Override
611        protected void onClose() throws IOException {
612            final boolean ok;
613            try {
614                ok = client.completePendingCommand();
615            } finally {
616                getAbstractFileSystem().putClient(client);
617            }
618
619            if (!ok) {
620                throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName());
621            }
622        }
623    }
624}