package org.apache.commons.vfs2.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.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileNotFolderException;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.RandomAccessContent;
import org.apache.commons.vfs2.provider.AbstractFileName;
import org.apache.commons.vfs2.provider.AbstractFileObject;
import org.apache.commons.vfs2.provider.UriParser;
import org.apache.commons.vfs2.util.FileObjectUtils;
import org.apache.commons.vfs2.util.Messages;
import org.apache.commons.vfs2.util.MonitorInputStream;
import org.apache.commons.vfs2.util.MonitorOutputStream;
import org.apache.commons.vfs2.util.RandomAccessMode;
public class FtpFileObject extends AbstractFileObject<FtpFileSystem> {
private static final long DEFAULT_TIMESTAMP = 0L;
private static final Map<String, FTPFile> EMPTY_FTP_FILE_MAP = Collections
.unmodifiableMap(new TreeMap<String, FTPFile>());
private static final FTPFile UNKNOWN = new FTPFile();
private static final Log log = LogFactory.getLog(FtpFileObject.class);
private final String relPath;
private volatile FTPFile fileInfo;
private volatile Map<String, FTPFile> children;
private volatile FileObject linkDestination;
private final AtomicBoolean inRefresh = new AtomicBoolean();
protected FtpFileObject(final AbstractFileName name, final FtpFileSystem fileSystem, final FileName rootName)
throws FileSystemException {
super(name, fileSystem);
final String relPath = UriParser.decode(rootName.getRelativeName(name));
if (".".equals(relPath)) {
this.relPath = null;
} else {
this.relPath = relPath;
}
}
private FTPFile getChildFile(final String name, final boolean flush) throws IOException {
if (flush && !inRefresh.get()) {
children = null;
}
doGetChildren();
return children != null ? children.get(name) : null;
}
private void doGetChildren() throws IOException {
if (children != null) {
return;
}
final FtpClient client = getAbstractFileSystem().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<>();
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",
Integer.valueOf(i), relPath));
}
continue;
}
if (!".".equals(child.getName()) && !"..".equals(child.getName())) {
children.put(child.getName(), child);
}
}
}
} finally {
getAbstractFileSystem().putClient(client);
}
}
@Override
protected void doAttach() throws IOException {
}
private void getInfo(final boolean flush) throws IOException {
synchronized (getFileSystem()) {
final FtpFileObject parent = (FtpFileObject) FileObjectUtils.getAbstractFileObject(getParent());
FTPFile newFileInfo;
if (parent != null) {
newFileInfo = parent.getChildFile(UriParser.decode(getName().getBaseName()), flush);
} else {
newFileInfo = new FTPFile();
newFileInfo.setType(FTPFile.DIRECTORY_TYPE);
}
if (newFileInfo == null) {
this.fileInfo = UNKNOWN;
} else {
this.fileInfo = newFileInfo;
}
}}
@Override
public void refresh() throws FileSystemException {
if (inRefresh.compareAndSet(false, true)) {
try {
super.refresh();
synchronized (getFileSystem()) {
this.fileInfo = null;
}
} finally {
inRefresh.set(false);
}
}
}
@Override
protected void doDetach() {
synchronized (getFileSystem()) {
this.fileInfo = null;
this.children = null;
}
}
@Override
protected void onChildrenChanged(final FileName child, final FileType newType) {
if (children != null && newType.equals(FileType.IMAGINARY)) {
try {
children.remove(UriParser.decode(child.getBaseName()));
} catch (final FileSystemException e) {
throw new RuntimeException(e.getMessage());
}
} else {
children = null;
}
}
@Override
protected void onChange() throws IOException {
children = null;
if (getType().equals(FileType.IMAGINARY)) {
synchronized (getFileSystem()) {
this.fileInfo = UNKNOWN;
}
return;
}
getInfo(true);
}
@Override
protected FileType doGetType() throws Exception {
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()) {
final FileObject linkDest = getLinkDestination();
if (this.isCircular(linkDest)) {
return FileType.IMAGINARY;
}
return linkDest.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 == null ? null : this.fileInfo.getLink();
}
final FileName parent = getName().getParent();
final FileName relativeTo = parent == null ? getName() : parent;
final FileName linkDestinationName = getFileSystem().getFileSystemManager().resolveName(relativeTo, path);
linkDestination = getFileSystem().resolveFile(linkDestinationName);
}
return linkDestination;
}
@Override
protected FileObject[] doListChildrenResolved() throws Exception {
synchronized (getFileSystem()) {
if (this.fileInfo != null && this.fileInfo.isSymbolicLink()) {
final FileObject linkDest = getLinkDestination();
if (this.isCircular(linkDest)) {
return null;
}
return linkDest.getChildren();
}
}
return null;
}
@Override
public FileObject[] getChildren() throws FileSystemException {
try {
if (doGetType() != FileType.FOLDER) {
throw new FileNotFolderException(getName());
}
} catch (final Exception ex) {
throw new FileNotFolderException(getName(), ex);
}
try {
this.inRefresh.set(true);
return super.getChildren();
} finally {
this.inRefresh.set(false);
}
}
@Override
protected String[] doListChildren() throws Exception {
doGetChildren();
if (children == null) {
return null;
}
final String[] childNames = new String[children.size()];
int childNum = -1;
final Iterator<FTPFile> iterChildren = children.values().iterator();
while (iterChildren.hasNext()) {
childNum++;
final FTPFile child = iterChildren.next();
childNames[childNum] = child.getName();
}
return UriParser.encode(childNames);
}
@Override
protected void doDelete() throws Exception {
synchronized (getFileSystem()) {
if (this.fileInfo != null) {
final boolean ok;
final FtpClient ftpClient = getAbstractFileSystem().getClient();
try {
if (this.fileInfo.isDirectory()) {
ok = ftpClient.removeDirectory(relPath);
} else {
ok = ftpClient.deleteFile(relPath);
}
} finally {
getAbstractFileSystem().putClient(ftpClient);
}
if (!ok) {
throw new FileSystemException("vfs.provider.ftp/delete-file.error", getName());
}
this.fileInfo = null;
}
this.children = EMPTY_FTP_FILE_MAP;
}
}
@Override
protected void doRename(final FileObject newFile) throws Exception {
synchronized (getFileSystem()) {
final boolean ok;
final FtpClient ftpClient = getAbstractFileSystem().getClient();
try {
final String oldName = relPath;
final String newName = ((FtpFileObject) FileObjectUtils.getAbstractFileObject(newFile)).getRelPath();
ok = ftpClient.rename(oldName, newName);
} finally {
getAbstractFileSystem().putClient(ftpClient);
}
if (!ok) {
throw new FileSystemException("vfs.provider.ftp/rename-file.error", getName().toString(), newFile);
}
this.fileInfo = null;
this.children = EMPTY_FTP_FILE_MAP;
}
}
@Override
protected void doCreateFolder() throws Exception {
final boolean ok;
final FtpClient client = getAbstractFileSystem().getClient();
try {
ok = client.makeDirectory(relPath);
} finally {
getAbstractFileSystem().putClient(client);
}
if (!ok) {
throw new FileSystemException("vfs.provider.ftp/create-folder.error", getName());
}
}
@Override
protected long doGetContentSize() throws Exception {
synchronized (getFileSystem()) {
if (this.fileInfo == null) {
return 0;
}
if (this.fileInfo.isSymbolicLink()) {
final FileObject linkDest = getLinkDestination();
if (this.isCircular(linkDest)) {
return this.fileInfo.getSize();
}
return linkDest.getContent().getSize();
}
return this.fileInfo.getSize();
}
}
@Override
protected long doGetLastModifiedTime() throws Exception {
synchronized (getFileSystem()) {
if (this.fileInfo == null) {
return DEFAULT_TIMESTAMP;
}
if (this.fileInfo.isSymbolicLink()) {
final FileObject linkDest = getLinkDestination();
if (this.isCircular(linkDest)) {
return getTimestamp();
}
return linkDest.getContent().getLastModifiedTime();
}
return getTimestamp();
}
}
@Override
protected InputStream doGetInputStream() throws Exception {
final FtpClient client = getAbstractFileSystem().getClient();
try {
final InputStream instr = client.retrieveFileStream(relPath);
if (instr == null) {
throw new FileNotFoundException(getName().toString());
}
return new FtpInputStream(client, instr);
} catch (final Exception e) {
getAbstractFileSystem().putClient(client);
throw e;
}
}
@Override
protected RandomAccessContent doGetRandomAccessContent(final RandomAccessMode mode) throws Exception {
return new FtpRandomAccessContent(this, mode);
}
@Override
protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
final FtpClient client = getAbstractFileSystem().getClient();
try {
OutputStream out = null;
if (bAppend) {
out = client.appendFileStream(relPath);
} else {
out = client.storeFileStream(relPath);
}
FileSystemException.requireNonNull(out, "vfs.provider.ftp/output-error.debug", this.getName(),
client.getReplyString());
return new FtpOutputStream(client, out);
} catch (final Exception e) {
getAbstractFileSystem().putClient(client);
throw e;
}
}
String getRelPath() {
return relPath;
}
private long getTimestamp() {
final Calendar timestamp = this.fileInfo != null ? this.fileInfo.getTimestamp() : null;
return timestamp == null ? DEFAULT_TIMESTAMP : timestamp.getTime().getTime();
}
private boolean isCircular(final FileObject linkDest) throws FileSystemException {
return linkDest.getName().getPathDecoded().equals(this.getName().getPathDecoded());
}
FtpInputStream getInputStream(final long filePointer) throws IOException {
final FtpClient client = getAbstractFileSystem().getClient();
try {
final InputStream instr = client.retrieveFileStream(relPath, filePointer);
FileSystemException.requireNonNull(instr, "vfs.provider.ftp/input-error.debug", this.getName(),
client.getReplyString());
return new FtpInputStream(client, instr);
} catch (final IOException e) {
getAbstractFileSystem().putClient(client);
throw e;
}
}
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();
}
@Override
protected void onClose() throws IOException {
final boolean ok;
try {
ok = client.completePendingCommand() || isTransferAbortedOkReplyCode();
} finally {
getAbstractFileSystem().putClient(client);
}
if (!ok) {
throw new FileSystemException("vfs.provider.ftp/finish-get.error", getName());
}
}
private boolean isTransferAbortedOkReplyCode() throws IOException {
final List<Integer> transferAbortedOkReplyCodes = FtpFileSystemConfigBuilder
.getInstance()
.getTransferAbortedOkReplyCodes(getAbstractFileSystem().getFileSystemOptions());
return transferAbortedOkReplyCodes != null && transferAbortedOkReplyCodes.contains(client.getReplyCode());
}
}
private class FtpOutputStream extends MonitorOutputStream {
private final FtpClient client;
public FtpOutputStream(final FtpClient client, final OutputStream outstr) {
super(outstr);
this.client = client;
}
@Override
protected void onClose() throws IOException {
final boolean ok;
try {
ok = client.completePendingCommand();
} finally {
getAbstractFileSystem().putClient(client);
}
if (!ok) {
throw new FileSystemException("vfs.provider.ftp/finish-put.error", getName());
}
}
}
}