/*
* Copyright 2002-2005 The Apache Software Foundation.
*
* Licensed 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 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.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.Messages;
import org.apache.commons.vfs.util.MonitorInputStream;
import org.apache.commons.vfs.util.MonitorOutputStream;
import org.apache.commons.vfs.util.RandomAccessMode;
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;
/**
* An FTP file.
*
* @author <a href="mailto:adammurdoch@apache.org">Adam Murdoch</a>
*/
public class FtpFileObject
extends AbstractFileObject
{
private Log log = LogFactory.getLog(FtpFileObject.class);
private static final Map EMPTY_FTP_FILE_MAP = Collections.unmodifiableMap(new TreeMap());
private final FtpFileSystem ftpFs;
private final String relPath;
// Cached info
private FTPFile fileInfo;
private Map children;
private FileObject linkDestination;
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 (flush)
{
children = null;
}
// List the children of this file
doGetChildren();
// 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
{
String key = FtpFileSystemConfigBuilder.getInstance().getEntryParser(getFileSystem().getFileSystemOptions());
final FTPFile[] tmpChildren = client.listFiles(key, relPath);
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
getInfo(false);
}
/**
* Fetches the info for this file.
*/
private void getInfo(boolean flush) throws IOException
{
final FtpFileObject parent = (FtpFileObject) 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);
}
this.fileInfo = newFileInfo;
}
/**
* Detaches this file object from its file resource.
*/
protected void doDetach()
{
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
this.fileInfo = null;
return;
}
getInfo(true);
}
/**
* Determines the type of the file, returns null if the file does not
* exist.
*/
protected FileType doGetType()
throws Exception
{
if (this.fileInfo == null)
{
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 = 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
{
if (this.fileInfo.isSymbolicLink())
{
return getLinkDestination().getChildren();
}
return null;
}
/**
* Lists the children of the file.
*/
protected String[] doListChildren()
throws Exception
{
// List the children of this file
doGetChildren();
// 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
{
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
{
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
{
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
{
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();
final InputStream instr = client.retrieveFileStream(relPath);
return new FtpInputStream(client, instr);
}
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();
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);
}
String getRelPath()
{
return relPath;
}
FtpInputStream getInputStream(long filePointer) throws IOException
{
final FtpClient client = ftpFs.getClient();
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);
}
/**
* 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());
}
}
}
}