| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.vfs.provider.ftp; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.TreeMap; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.commons.net.ftp.FTPFile; |
| import org.apache.commons.vfs.FileName; |
| import org.apache.commons.vfs.FileNotFolderException; |
| import org.apache.commons.vfs.FileObject; |
| import org.apache.commons.vfs.FileSystemException; |
| import org.apache.commons.vfs.FileType; |
| import org.apache.commons.vfs.RandomAccessContent; |
| import org.apache.commons.vfs.provider.AbstractFileObject; |
| import org.apache.commons.vfs.provider.UriParser; |
| import org.apache.commons.vfs.util.FileObjectUtils; |
| import org.apache.commons.vfs.util.Messages; |
| import org.apache.commons.vfs.util.MonitorInputStream; |
| import org.apache.commons.vfs.util.MonitorOutputStream; |
| import org.apache.commons.vfs.util.RandomAccessMode; |
| |
| /** |
| * An FTP file. |
| * |
| * @author <a href="mailto:adammurdoch@apache.org">Adam Murdoch</a> |
| * @version $Revision$ $Date$ |
| */ |
| public class FtpFileObject |
| extends AbstractFileObject |
| { |
| private static final Map EMPTY_FTP_FILE_MAP = Collections.unmodifiableMap(new TreeMap()); |
| private static final FTPFile UNKNOWN = new FTPFile(); |
| |
| private final Log log = LogFactory.getLog(FtpFileObject.class); |
| private final FtpFileSystem ftpFs; |
| private final String relPath; |
| |
| // Cached info |
| private FTPFile fileInfo; |
| private Map children; |
| private FileObject linkDestination; |
| |
| private boolean inRefresh; |
| |
| protected FtpFileObject(final FileName name, |
| final FtpFileSystem fileSystem, |
| final FileName rootName) |
| throws FileSystemException |
| { |
| super(name, fileSystem); |
| ftpFs = fileSystem; |
| String relPath = UriParser.decode(rootName.getRelativeName(name)); |
| if (".".equals(relPath)) |
| { |
| // do not use the "." as path against the ftp-server |
| // e.g. the uu.net ftp-server do a recursive listing then |
| // this.relPath = UriParser.decode(rootName.getPath()); |
| // this.relPath = "."; |
| this.relPath = null; |
| } |
| else |
| { |
| this.relPath = relPath; |
| } |
| } |
| |
| /** |
| * Called by child file objects, to locate their ftp file info. |
| * |
| * @param name the filename in its native form ie. without uri stuff (%nn) |
| * @param flush recreate children cache |
| */ |
| private FTPFile getChildFile(final String name, final boolean flush) throws IOException |
| { |
| /* If we should flush cached children, clear our children map unless |
| * we're in the middle of a refresh in which case we've just recently |
| * refreshed our children. No need to do it again when our children are |
| * refresh()ed, calling getChildFile() for themselves from within |
| * getInfo(). See getChildren(). */ |
| if (flush && !inRefresh) |
| { |
| children = null; |
| } |
| |
| // List the children of this file |
| doGetChildren(); |
| |
| // VFS-210 |
| if (children == null) |
| { |
| return null; |
| } |
| |
| // Look for the requested child |
| FTPFile ftpFile = (FTPFile) children.get(name); |
| return ftpFile; |
| } |
| |
| /** |
| * Fetches the children of this file, if not already cached. |
| */ |
| private void doGetChildren() throws IOException |
| { |
| if (children != null) |
| { |
| return; |
| } |
| |
| final FtpClient client = ftpFs.getClient(); |
| try |
| { |
| final String path = fileInfo != null && fileInfo.isSymbolicLink() |
| ? getFileSystem().getFileSystemManager(). |
| resolveName(getParent().getName(), fileInfo.getLink()).getPath() |
| : relPath; |
| final FTPFile[] tmpChildren = client.listFiles(path); |
| if (tmpChildren == null || tmpChildren.length == 0) |
| { |
| children = EMPTY_FTP_FILE_MAP; |
| } |
| else |
| { |
| children = new TreeMap(); |
| |
| // Remove '.' and '..' elements |
| for (int i = 0; i < tmpChildren.length; i++) |
| { |
| final FTPFile child = tmpChildren[i]; |
| if (child == null) |
| { |
| if (log.isDebugEnabled()) |
| { |
| log.debug(Messages.getString("vfs.provider.ftp/invalid-directory-entry.debug", |
| new Object[] |
| { |
| new Integer(i), relPath |
| })); |
| } |
| continue; |
| } |
| if (!".".equals(child.getName()) |
| && !"..".equals(child.getName())) |
| { |
| children.put(child.getName(), child); |
| } |
| } |
| } |
| } |
| finally |
| { |
| ftpFs.putClient(client); |
| } |
| } |
| |
| /** |
| * Attaches this file object to its file resource. |
| */ |
| protected void doAttach() |
| throws IOException |
| { |
| // Get the parent folder to find the info for this file |
| // VFS-210 getInfo(false); |
| } |
| |
| /** |
| * Fetches the info for this file. |
| */ |
| private void getInfo(boolean flush) throws IOException |
| { |
| final FtpFileObject parent = (FtpFileObject) FileObjectUtils.getAbstractFileObject(getParent()); |
| FTPFile newFileInfo; |
| if (parent != null) |
| { |
| newFileInfo = parent.getChildFile(UriParser.decode(getName().getBaseName()), flush); |
| } |
| else |
| { |
| // Assume the root is a directory and exists |
| newFileInfo = new FTPFile(); |
| newFileInfo.setType(FTPFile.DIRECTORY_TYPE); |
| } |
| |
| if (newFileInfo == null) |
| { |
| this.fileInfo = UNKNOWN; |
| } |
| else |
| { |
| this.fileInfo = newFileInfo; |
| } |
| } |
| |
| /** |
| * @throws FileSystemException if an error occurs. |
| */ |
| public void refresh() throws FileSystemException |
| { |
| if (!inRefresh) |
| { |
| try |
| { |
| inRefresh = true; |
| super.refresh(); |
| |
| synchronized (getFileSystem()) |
| { |
| this.fileInfo = null; |
| } |
| |
| /* VFS-210 |
| try |
| { |
| // this will tell the parent to recreate its children collection |
| getInfo(true); |
| } |
| catch (IOException e) |
| { |
| throw new FileSystemException(e); |
| } |
| */ |
| } |
| finally |
| { |
| inRefresh = false; |
| } |
| } |
| } |
| |
| /** |
| * Detaches this file object from its file resource. |
| */ |
| protected void doDetach() |
| { |
| synchronized (getFileSystem()) |
| { |
| this.fileInfo = null; |
| children = null; |
| } |
| } |
| |
| /** |
| * Called when the children of this file change. |
| */ |
| protected void onChildrenChanged(FileName child, FileType newType) |
| { |
| if (children != null && newType.equals(FileType.IMAGINARY)) |
| { |
| try |
| { |
| children.remove(UriParser.decode(child.getBaseName())); |
| } |
| catch (FileSystemException e) |
| { |
| throw new RuntimeException(e.getMessage()); |
| } |
| } |
| else |
| { |
| // if child was added we have to rescan the children |
| // TODO - get rid of this |
| children = null; |
| } |
| } |
| |
| /** |
| * Called when the type or content of this file changes. |
| */ |
| protected void onChange() throws IOException |
| { |
| children = null; |
| |
| if (getType().equals(FileType.IMAGINARY)) |
| { |
| // file is deleted, avoid server lookup |
| synchronized (getFileSystem()) |
| { |
| this.fileInfo = UNKNOWN; |
| } |
| return; |
| } |
| |
| getInfo(true); |
| } |
| |
| /** |
| * Determines the type of the file, returns null if the file does not |
| * exist. |
| */ |
| protected FileType doGetType() |
| throws Exception |
| { |
| // VFS-210 |
| synchronized (getFileSystem()) |
| { |
| if (this.fileInfo == null) |
| { |
| getInfo(false); |
| } |
| |
| if (this.fileInfo == UNKNOWN) |
| { |
| return FileType.IMAGINARY; |
| } |
| else if (this.fileInfo.isDirectory()) |
| { |
| return FileType.FOLDER; |
| } |
| else if (this.fileInfo.isFile()) |
| { |
| return FileType.FILE; |
| } |
| else if (this.fileInfo.isSymbolicLink()) |
| { |
| return getLinkDestination().getType(); |
| } |
| } |
| |
| throw new FileSystemException("vfs.provider.ftp/get-type.error", getName()); |
| } |
| |
| private FileObject getLinkDestination() throws FileSystemException |
| { |
| if (linkDestination == null) |
| { |
| final String path; |
| synchronized (getFileSystem()) |
| { |
| path = this.fileInfo.getLink(); |
| } |
| FileName relativeTo = getName().getParent(); |
| if (relativeTo == null) |
| { |
| relativeTo = getName(); |
| } |
| FileName linkDestinationName = getFileSystem().getFileSystemManager().resolveName(relativeTo, path); |
| linkDestination = getFileSystem().resolveFile(linkDestinationName); |
| } |
| |
| return linkDestination; |
| } |
| |
| protected FileObject[] doListChildrenResolved() throws Exception |
| { |
| synchronized (getFileSystem()) |
| { |
| if (this.fileInfo != null && this.fileInfo.isSymbolicLink()) |
| { |
| return getLinkDestination().getChildren(); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the file's list of children. |
| * |
| * @return The list of children |
| * @throws FileSystemException If there was a problem listing children |
| * @see AbstractFileObject#getChildren() |
| * @since 1.0 |
| */ |
| public FileObject[] getChildren() throws FileSystemException |
| { |
| try |
| { |
| if (doGetType() != FileType.FOLDER) |
| { |
| throw new FileNotFolderException(getName()); |
| } |
| } |
| catch (Exception ex) |
| { |
| throw new FileNotFolderException(getName(), ex); |
| } |
| |
| |
| try |
| { |
| /* Wrap our parent implementation, noting that we're refreshing so |
| * that we don't refresh() ourselves and each of our parents for |
| * each children. Note that refresh() will list children. Meaning, |
| * if if this file has C children, P parents, there will be (C * P) |
| * listings made with (C * (P + 1)) refreshes, when there should |
| * really only be 1 listing and C refreshes. */ |
| |
| this.inRefresh = true; |
| return super.getChildren(); |
| } |
| finally |
| { |
| this.inRefresh = false; |
| } |
| } |
| |
| /** |
| * Lists the children of the file. |
| */ |
| protected String[] doListChildren() |
| throws Exception |
| { |
| // List the children of this file |
| doGetChildren(); |
| |
| // VFS-210 |
| if (children == null) |
| { |
| return null; |
| } |
| |
| // TODO - get rid of this children stuff |
| final String[] childNames = new String[children.size()]; |
| int childNum = -1; |
| Iterator iterChildren = children.values().iterator(); |
| while (iterChildren.hasNext()) |
| { |
| childNum++; |
| final FTPFile child = (FTPFile) iterChildren.next(); |
| childNames[childNum] = child.getName(); |
| } |
| |
| return UriParser.encode(childNames); |
| } |
| |
| /** |
| * Deletes the file. |
| */ |
| protected void doDelete() throws Exception |
| { |
| synchronized (getFileSystem()) |
| { |
| final boolean ok; |
| final FtpClient ftpClient = ftpFs.getClient(); |
| try |
| { |
| if (this.fileInfo.isDirectory()) |
| { |
| ok = ftpClient.removeDirectory(relPath); |
| } |
| else |
| { |
| ok = ftpClient.deleteFile(relPath); |
| } |
| } |
| finally |
| { |
| ftpFs.putClient(ftpClient); |
| } |
| |
| if (!ok) |
| { |
| throw new FileSystemException("vfs.provider.ftp/delete-file.error", getName()); |
| } |
| this.fileInfo = null; |
| children = EMPTY_FTP_FILE_MAP; |
| } |
| } |
| |
| /** |
| * Renames the file |
| */ |
| protected void doRename(FileObject newfile) throws Exception |
| { |
| synchronized (getFileSystem()) |
| { |
| final boolean ok; |
| final FtpClient ftpClient = ftpFs.getClient(); |
| try |
| { |
| String oldName = getName().getPath(); |
| String newName = newfile.getName().getPath(); |
| ok = ftpClient.rename(oldName, newName); |
| } |
| finally |
| { |
| ftpFs.putClient(ftpClient); |
| } |
| |
| if (!ok) |
| { |
| throw new FileSystemException("vfs.provider.ftp/rename-file.error", |
| new Object[]{getName().toString(), newfile}); |
| } |
| this.fileInfo = null; |
| children = EMPTY_FTP_FILE_MAP; |
| } |
| } |
| |
| /** |
| * Creates this file as a folder. |
| */ |
| protected void doCreateFolder() |
| throws Exception |
| { |
| final boolean ok; |
| final FtpClient client = ftpFs.getClient(); |
| try |
| { |
| ok = client.makeDirectory(relPath); |
| } |
| finally |
| { |
| ftpFs.putClient(client); |
| } |
| |
| if (!ok) |
| { |
| throw new FileSystemException("vfs.provider.ftp/create-folder.error", getName()); |
| } |
| } |
| |
| /** |
| * Returns the size of the file content (in bytes). |
| */ |
| protected long doGetContentSize() throws Exception |
| { |
| synchronized (getFileSystem()) |
| { |
| if (this.fileInfo.isSymbolicLink()) |
| { |
| return getLinkDestination().getContent().getSize(); |
| } |
| else |
| { |
| return this.fileInfo.getSize(); |
| } |
| } |
| } |
| |
| /** |
| * get the last modified time on an ftp file |
| * |
| * @see org.apache.commons.vfs.provider.AbstractFileObject#doGetLastModifiedTime() |
| */ |
| protected long doGetLastModifiedTime() throws Exception |
| { |
| synchronized (getFileSystem()) |
| { |
| if (this.fileInfo.isSymbolicLink()) |
| { |
| return getLinkDestination().getContent().getLastModifiedTime(); |
| } |
| else |
| { |
| Calendar timestamp = this.fileInfo.getTimestamp(); |
| if (timestamp == null) |
| { |
| return 0L; |
| } |
| else |
| { |
| return (timestamp.getTime().getTime()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates an input stream to read the file content from. |
| */ |
| protected InputStream doGetInputStream() throws Exception |
| { |
| final FtpClient client = ftpFs.getClient(); |
| try |
| { |
| final InputStream instr = client.retrieveFileStream(relPath); |
| // VFS-210 |
| if (instr == null) |
| { |
| throw new FileNotFoundException(getName().toString()); |
| } |
| return new FtpInputStream(client, instr); |
| } |
| catch (Exception e) |
| { |
| ftpFs.putClient(client); |
| throw e; |
| } |
| } |
| |
| protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception |
| { |
| return new FtpRandomAccessContent(this, mode); |
| } |
| |
| /** |
| * Creates an output stream to write the file content to. |
| */ |
| protected OutputStream doGetOutputStream(boolean bAppend) |
| throws Exception |
| { |
| final FtpClient client = ftpFs.getClient(); |
| try |
| { |
| OutputStream out = null; |
| if (bAppend) |
| { |
| out = client.appendFileStream(relPath); |
| } |
| else |
| { |
| out = client.storeFileStream(relPath); |
| } |
| |
| if (out == null) |
| { |
| throw new FileSystemException("vfs.provider.ftp/output-error.debug", new Object[] |
| { |
| this.getName(), |
| client.getReplyString() |
| }); |
| } |
| |
| return new FtpOutputStream(client, out); |
| } |
| catch (Exception e) |
| { |
| ftpFs.putClient(client); |
| throw e; |
| } |
| } |
| |
| String getRelPath() |
| { |
| return relPath; |
| } |
| |
| FtpInputStream getInputStream(long filePointer) throws IOException |
| { |
| final FtpClient client = ftpFs.getClient(); |
| try |
| { |
| final InputStream instr = client.retrieveFileStream(relPath, filePointer); |
| if (instr == null) |
| { |
| throw new FileSystemException("vfs.provider.ftp/input-error.debug", new Object[] |
| { |
| this.getName(), |
| client.getReplyString() |
| }); |
| } |
| return new FtpInputStream(client, instr); |
| } |
| catch (IOException e) |
| { |
| ftpFs.putClient(client); |
| throw e; |
| } |
| } |
| |
| /** |
| * An InputStream that monitors for end-of-file. |
| */ |
| class FtpInputStream |
| extends MonitorInputStream |
| { |
| private final FtpClient client; |
| |
| public FtpInputStream(final FtpClient client, final InputStream in) |
| { |
| super(in); |
| this.client = client; |
| } |
| |
| void abort() throws IOException |
| { |
| client.abort(); |
| close(); |
| } |
| |
| /** |
| * Called after the stream has been closed. |
| */ |
| protected void onClose() throws IOException |
| { |
| final boolean ok; |
| try |
| { |
| ok = client.completePendingCommand(); |
| } |
| finally |
| { |
| ftpFs.putClient(client); |
| } |
| |
| if (!ok) |
| { |
| throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName()); |
| } |
| } |
| } |
| |
| /** |
| * An OutputStream that monitors for end-of-file. |
| */ |
| private class FtpOutputStream |
| extends MonitorOutputStream |
| { |
| private final FtpClient client; |
| |
| public FtpOutputStream(final FtpClient client, final OutputStream outstr) |
| { |
| super(outstr); |
| this.client = client; |
| } |
| |
| /** |
| * Called after this stream is closed. |
| */ |
| protected void onClose() throws IOException |
| { |
| final boolean ok; |
| try |
| { |
| ok = client.completePendingCommand(); |
| } |
| finally |
| { |
| ftpFs.putClient(client); |
| } |
| |
| if (!ok) |
| { |
| throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName()); |
| } |
| } |
| } |
| } |