blob: 7aa08ed39f0e87e3cce3f8ef3af5bf931d563529 [file] [log] [blame]
/*
* 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());
}
}
}
}