com.sshtools.j2ssh.SftpClient.java Source code

Java tutorial

Introduction

Here is the source code for com.sshtools.j2ssh.SftpClient.java

Source

// Changes (c) CCLRC/STFC 2007

/*
 *  SSHTools - Java SSH2 API
 *
 *  Copyright (C) 2002 Lee David Painter.
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public License
 *  as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *  You may also distribute it and/or modify it under the terms of the
 *  Apache style J2SSH Software License. A copy of which should have
 *  been provided with the distribution.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  License document supplied with your distribution for more details.
 *
 */
package com.sshtools.j2ssh;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

import java.text.SimpleDateFormat;

import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.Date;

import com.sshtools.j2ssh.connection.*;
import com.sshtools.j2ssh.sftp.*;
import com.sshtools.j2ssh.io.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <p>
 * Implements a Secure File Transfer (SFTP) client.
 * </p>
 *
 * @author Lee David Painter
 * @version $Revision: 1.4 $
 *
 * @since 0.2.0
 */
public class SftpClient {
    SftpSubsystemClient sftp;
    String cwd;
    String lcwd;
    private int BLOCKSIZE = 65535;

    // Default permissions is determined by default_permissions ^ umask
    int umask = 0022;
    int default_permissions = 0777;

    private boolean preserve = true;

    // Logger
    protected static Log log = LogFactory.getLog(SftpClient.class);

    public boolean getPreserve() {
        return preserve;
    }

    public void setPreserve(boolean preserve) {
        this.preserve = preserve;
    }

    /**
     * <p>
     * Constructs the SFTP client.
     * </p>
     *
     * @param ssh the <code>SshClient</code> instance
     *
     * @throws IOException if an IO error occurs
     */
    SftpClient(SshClient ssh) throws IOException {
        this(ssh, null);
    }

    /**
     * <p>
     * Constructs the SFTP client with a given channel event listener.
     * </p>
     *
     * @param ssh the <code>SshClient</code> instance
     * @param eventListener an event listener implementation
     *
     * @throws IOException if an IO error occurs
     */
    SftpClient(SshClient ssh, ChannelEventListener eventListener) throws IOException {
        if (!ssh.isConnected()) {
            throw new IOException("SshClient is not connected");
        }

        this.sftp = ssh.openSftpChannel(eventListener);

        // Get the users default directory
        cwd = sftp.getDefaultDirectory();
        lcwd = System.getProperty("user.home");
    }

    /**
     * Sets the umask used by this client.
     * @param umask
     * @return the previous umask value
     */
    public int umask(int umask) {
        int old = umask;
        this.umask = umask;
        return old;
    }

    /**
     * <p>
     * Changes the working directory on the remote server.
     * </p>
     *
     * @param dir the new working directory
     *
     * @throws IOException if an IO error occurs or the file does not exist
     * @throws FileNotFoundException
     *
     * @since 0.2.0
     */
    public void cd(String dir) throws IOException {
        try {
            String actual;

            if (dir.equals("")) {
                actual = sftp.getDefaultDirectory();
            } else {
                actual = resolveRemotePath(dir);
                actual = sftp.getAbsolutePath(actual);
            }

            FileAttributes attr = sftp.getAttributes(actual);

            if (!attr.isDirectory()) {
                throw new IOException(dir + " is not a directory");
            }

            cwd = actual;
        } catch (IOException ex) {
            throw new FileNotFoundException(dir + " could not be found");
        }
    }

    private File resolveLocalPath(String path) throws IOException {
        File f = new File(path);

        if (!f.isAbsolute()) {
            f = new File(lcwd, path);
        }

        return f;
    }

    private String resolveRemotePath(String path) throws IOException {
        verifyConnection();

        String actual;

        if (!path.startsWith("/")) {
            actual = cwd + (cwd.endsWith("/") ? "" : "/") + path;
        } else {
            actual = path;
        }

        return actual;
    }

    private void verifyConnection() throws SshException {
        if (sftp.isClosed()) {
            throw new SshException("The SFTP connection has been closed");
        }
    }

    /**
     * <p>
     * Creates a new directory on the remote server. This method will throw an
     * exception if the directory already exists. To create directories and
     * disregard any errors use the <code>mkdirs</code> method.
     * </p>
     *
     * @param dir the name of the new directory
     *
     * @throws IOException if an IO error occurs or if the directory already
     *         exists
     *
     * @since 0.2.0
     */
    public void mkdir(String dir) throws IOException {
        String actual = resolveRemotePath(dir);

        try {
            FileAttributes attrs = stat(actual);
            if (!attrs.isDirectory())
                throw new IOException("File already exists named " + dir);
        } catch (IOException ex) {
            sftp.makeDirectory(actual);
            chmod(default_permissions ^ umask, actual);
        }
    }

    /**
     * <p>
     * Create a directory or set of directories. This method will not fail even
     * if the directories exist. It is advisable to test whether the directory
     * exists before attempting an operation by using the <code>stat</code>
     * method to return the directories attributes.
     * </p>
     *
     * @param dir the path of directories to create.
     */
    public void mkdirs(String dir) {
        StringTokenizer tokens = new StringTokenizer(dir, "/");
        String path = dir.startsWith("/") ? "/" : "";

        while (tokens.hasMoreElements()) {
            path += (String) tokens.nextElement();

            try {
                stat(path);
            } catch (IOException ex) {
                try {
                    mkdir(path);
                } catch (IOException ex2) {
                }
            }

            path += "/";
        }
    }

    /**
     * <p>
     * Returns the absolute path name of the current remote working directory.
     * </p>
     *
     * @return the absolute path of the remote working directory.
     *
     * @since 0.2.0
     */
    public String pwd() {
        return cwd;
    }

    /**
     * <p>
     * List the contents of the current remote working directory.
     * </p>
     *
     * <p>
     * Returns a list of <code>SftpFile</code> instances for the current
     * working directory.
     * </p>
     *
     * @return a list of SftpFile for the current working directory
     *
     * @throws IOException if an IO error occurs
     *
     * @see com.sshtools.j2ssh.sftp.SftpFile
     * @since 0.2.0
     */
    public List ls() throws IOException {
        return ls(cwd);
    }

    /**
     * <p>
     * List the contents remote directory.
     * </p>
     *
     * <p>
     * Returns a list of <code>SftpFile</code> instances for the remote
     * directory.
     * </p>
     *
     * @param path the path on the remote server to list
     *
     * @return a list of SftpFile for the remote directory
     *
     * @throws IOException if an IO error occurs
     *
     * @see com.sshtools.j2ssh.sftp.SftpFile
     * @since 0.2.0
     */
    public List ls(String path) throws IOException {
        String actual = resolveRemotePath(path);

        FileAttributes attrs = sftp.getAttributes(actual);

        if (!attrs.isDirectory()) {
            throw new IOException(path + " is not a directory");
        }

        SftpFile file = sftp.openDirectory(actual);

        Vector children = new Vector();

        while (sftp.listChildren(file, children) > -1) {
            ;
        }

        file.close();

        return children;
    }

    /**
     * <p>
     * Changes the local working directory.
     * </p>
     *
     * @param path the path to the new working directory
     *
     * @throws IOException if an IO error occurs
     *
     * @since 0.2.0
     */
    public void lcd(String path) throws IOException {
        File actual;

        if (!isLocalAbsolutePath(path)) {
            actual = new File(lcwd, path);
        } else {
            actual = new File(path);
        }

        if (!actual.isDirectory()) {
            throw new IOException(path + " is not a directory");
        }

        lcwd = actual.getCanonicalPath();
    }

    private static boolean isLocalAbsolutePath(String path) {
        return (new File(path)).isAbsolute();
    }

    /**
     * <p>
     * Returns the absolute path to the local working directory.
     * </p>
     *
     * @return the absolute path of the local working directory.
     *
     * @since 0.2.0
     */
    public String lpwd() {
        return lcwd;
    }

    /**
     * <p>
     * Download the remote file to the local computer.
     * </p>
     *
     * @param path the path to the remote file
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs of the file does not exist
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public FileAttributes get(String path, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        String localfile;

        if (path.lastIndexOf("/") > -1) {
            localfile = path.substring(path.lastIndexOf("/") + 1);
        } else {
            localfile = path;
        }

        return get(path, localfile, progress);
    }

    /**
     *
     *
     * @param path
     *
     * @return
     *
     * @throws IOException
     */
    public FileAttributes get(String path) throws IOException {
        return get(path, (FileTransferProgress) null);
    }

    private void transferFile(InputStream in, OutputStream out) throws IOException, TransferCancelledException {
        transferFile(in, out, null);
    }

    private void transferFile(InputStream in, OutputStream out, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        try {
            long bytesSoFar = 0;
            byte[] buffer = new byte[BLOCKSIZE];
            int read;

            while ((read = in.read(buffer)) > -1) {
                if ((progress != null) && progress.isCancelled()) {
                    throw new TransferCancelledException();
                }

                if (read > 0) {
                    out.write(buffer, 0, read);
                    //out.flush();
                    bytesSoFar += read;

                    if (progress != null) {
                        progress.progressed(bytesSoFar);
                    }
                }
            }
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException ex) {
            }
        }
    }

    /**
     * <p>
     * Download the remote file to the local computer. If the paths provided
     * are not absolute the current working directory is used.
     * </p>
     *
     * @param remote the path/name of the remote file
     * @param local the path/name to place the file on the local computer
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs or the file does not exist
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public FileAttributes get(String remote, String local, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        File localPath = resolveLocalPath(local);

        if (!localPath.exists()) {
            localPath.getParentFile().mkdirs();
            localPath.createNewFile();
        }

        FileOutputStream out = new FileOutputStream(localPath);

        FileAttributes attrs = get(remote, out, progress);

        if (preserve) {
            setLocalAttrs(localPath.getCanonicalPath(), attrs);
        }

        return attrs;
    }

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmm.ss");

    public void setLocalAttrs(String localPath, FileAttributes attrs) {
        try {
            Date mtime = new Date(attrs.getModifiedTime().longValue() * 1000L);
            Date atime = new Date(attrs.getAccessedTime().longValue() * 1000L);
            Runtime.getRuntime().exec("touch -m -t " + sdf.format(mtime) + " " + localPath).waitFor();
            Runtime.getRuntime().exec("touch -a -t " + sdf.format(atime) + " " + localPath).waitFor();
            String s = attrs.getMaskString();
            Runtime.getRuntime().exec("chmod " + s + " " + localPath).waitFor();
        } catch (Exception e) {
            System.err.println("Problem preserving attributes: " + e.getMessage());
        }

    }

    private String run(String cmd) throws IOException, InterruptedException {
        Process p = Runtime.getRuntime().exec(cmd);
        String res = (new BufferedReader(new InputStreamReader(p.getInputStream()))).readLine();
        p.waitFor();
        return res;
    }

    public void setRemoteAttrs(FileAttributes attrs, String localPath) {
        try {
            String octal = run("stat -c %a " + localPath);
            String mtimeS = run("stat -c %Y " + localPath);
            String atimeS = run("stat -c %X " + localPath);
            long atime = Long.parseLong(atimeS);
            long mtime = Long.parseLong(mtimeS);
            attrs.setPermissionsFromMaskString("0" + octal);
            attrs.setTimes(new UnsignedInteger32(atime), new UnsignedInteger32(mtime));
        } catch (Exception e) {
            System.err.println("Problem preserving attributes: " + e.getMessage());
        }
    }

    public FileAttributes setRemoteAttrs2(String localPath) {
        try {
            FileAttributes attrs = new FileAttributes();
            String octal = run("stat -c %a " + localPath);
            String mtimeS = run("stat -c %Y " + localPath);
            String atimeS = run("stat -c %X " + localPath);
            long atime = Long.parseLong(atimeS);
            long mtime = Long.parseLong(mtimeS);
            attrs.setPermissionsFromMaskString("0" + octal);
            attrs.setTimes(new UnsignedInteger32(atime), new UnsignedInteger32(mtime));
            return attrs;
        } catch (Exception e) {
            System.err.println("Problem preserving attributes: " + e.getMessage());
            return null;
        }
    }

    /**
     *
     *
     * @param remote
     * @param local
     *
     * @return
     *
     * @throws IOException
     */
    public FileAttributes get(String remote, String local) throws IOException {
        return get(remote, local, null);
    }

    /**
     * <p>
     * Download the remote file writing it to the specified
     * <code>OutputStream</code>. The OutputStream is closed by this mehtod
     * even if the operation fails.
     * </p>
     *
     * @param remote the path/name of the remote file
     * @param local the OutputStream to write
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs or the file does not exist
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public FileAttributes get(String remote, OutputStream local, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        String remotePath = resolveRemotePath(remote);

        FileAttributes attrs = stat(remotePath);

        if (progress != null) {
            progress.started(attrs.getSize().longValue(), remotePath);
        }

        SftpFileInputStream in = new SftpFileInputStream(sftp.openFile(remotePath, SftpSubsystemClient.OPEN_READ));

        transferFile(in, local, progress);

        if (progress != null) {
            progress.completed();
        }

        return attrs;
    }

    /**
     *
     *
     * @param remote
     * @param local
     *
     * @return
     *
     * @throws IOException
     */
    public FileAttributes get(String remote, OutputStream local) throws IOException {
        return get(remote, local, null);
    }

    /**
     * <p>
     * Returns the state of the SFTP client. The client is closed if the
     * underlying session channel is closed. Invoking the <code>quit</code>
     * method of this object will close the underlying session channel.
     * </p>
     *
     * @return true if the client is still connected, otherwise false
     *
     * @since 0.2.0
     */
    public boolean isClosed() {
        return sftp.isClosed();
    }

    /**
     * <p>
     * Upload a file to the remote computer.
     * </p>
     *
     * @param local the path/name of the local file
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs or the file does not exist
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public void put(String local, FileTransferProgress progress) throws IOException, TransferCancelledException {
        File f = new File(local);
        put(local, f.getName(), progress);
    }

    /**
     *
     *
     * @param local
     *
     * @return
     *
     * @throws IOException
     */
    public void put(String local) throws IOException {
        put(local, (FileTransferProgress) null);
    }

    /**
     * <p>
     * Upload a file to the remote computer. If the paths provided are not
     * absolute the current working directory is used.
     * </p>
     *
     * @param local the path/name of the local file
     * @param remote the path/name of the destination file
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs or the file does not exist
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public void put(String local, String remote, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        File localPath = resolveLocalPath(local);

        FileInputStream in = new FileInputStream(localPath);

        try {
            FileAttributes attrs = stat(remote);
            if (attrs.isDirectory()) {
                File f = new File(local);
                remote += (remote.endsWith("/") ? "" : "/") + f.getName();
            }
        } catch (IOException ex) {
        }

        put(in, remote, progress);
        if (preserve) {
            FileAttributes attrs = stat(remote);
            setRemoteAttrs(attrs, localPath.getCanonicalPath());
            sftp.setAttributes(remote, attrs);
        }
    }

    /**
     *
     *
     * @param local
     * @param remote
     *
     * @return
     *
     * @throws IOException
     */
    public void put(String local, String remote) throws IOException {
        put(local, remote, null);

    }

    /**
     * <p>
     * Upload a file to the remote computer reading from the specified <code>
     * InputStream</code>. The InputStream is closed, even if the operation
     * fails.
     * </p>
     *
     * @param in the InputStream being read
     * @param remote the path/name of the destination file
     * @param progress
     *
     * @return
     *
     * @throws IOException if an IO error occurs
     * @throws TransferCancelledException
     *
     * @since 0.2.0
     */
    public void put(InputStream in, String remote, FileTransferProgress progress)
            throws IOException, TransferCancelledException {
        String remotePath = resolveRemotePath(remote);

        SftpFileOutputStream out;

        FileAttributes attrs;
        boolean newfile = false;
        try {
            attrs = stat(remotePath);

            out = new SftpFileOutputStream(sftp.openFile(remotePath, SftpSubsystemClient.OPEN_CREATE
                    | SftpSubsystemClient.OPEN_TRUNCATE | SftpSubsystemClient.OPEN_WRITE));
        } catch (IOException ex) {

            attrs = new FileAttributes();
            newfile = true;
            attrs.setPermissions(new UnsignedInteger32(default_permissions ^ umask));

            out = new SftpFileOutputStream(sftp.openFile(remotePath,
                    SftpSubsystemClient.OPEN_CREATE | SftpSubsystemClient.OPEN_WRITE, attrs));
        }

        if (progress != null) {
            progress.started(in.available(), remotePath);
        }

        transferFile(in, out, progress);

        if (progress != null) {
            progress.completed();
        }

        // Set the permissions here since at creation they dont always work
        if (newfile)
            chmod(default_permissions ^ umask, remotePath);
    }

    /**
     *
     *
     * @param in
     * @param remote
     *
     * @return
     *
     * @throws IOException
     */
    public void put(InputStream in, String remote) throws IOException {
        put(in, remote, null);
    }

    /**
     * <p>
     * Sets the user ID to owner for the file or directory.
     * </p>
     *
     * @param uid numeric user id of the new owner
     * @param path the path to the remote file/directory
     *
     * @throws IOException if an IO error occurs or the file does not exist
     *
     * @since 0.2.0
     */
    public void chown(int uid, String path) throws IOException {
        String actual = resolveRemotePath(path);

        FileAttributes attrs = sftp.getAttributes(actual);

        attrs.setUID(new UnsignedInteger32(uid));

        sftp.setAttributes(actual, attrs);
    }

    /**
     * <p>
     * Sets the group ID for the file or directory.
     * </p>
     *
     * @param gid the numeric group id for the new group
     * @param path the path to the remote file/directory
     *
     * @throws IOException if an IO error occurs or the file does not exist
     *
     * @since 0.2.0
     */
    public void chgrp(int gid, String path) throws IOException {
        String actual = resolveRemotePath(path);

        FileAttributes attrs = sftp.getAttributes(actual);

        attrs.setGID(new UnsignedInteger32(gid));

        sftp.setAttributes(actual, attrs);
    }

    /**
     * <p>
     * Changes the access permissions or modes of the specified file or
     * directory.
     * </p>
     *
     * <p>
     * Modes determine who can read, change or execute a file.
     * </p>
     * <blockquote><pre>Absolute modes are octal numbers specifying the complete list of
     * attributes for the files; you specify attributes by OR'ing together
     * these bits.
     *
     * 0400       Individual read
     * 0200       Individual write
     * 0100       Individual execute (or list directory)
     * 0040       Group read
     * 0020       Group write
     * 0010       Group execute
     * 0004       Other read
     * 0002       Other write
     * 0001       Other execute </pre></blockquote>
     *
     * @param permissions the absolute mode of the file/directory
     * @param path the path to the file/directory on the remote server
     *
     * @throws IOException if an IO error occurs or the file if not found
     *
     * @since 0.2.0
     */
    public void chmod(int permissions, String path) throws IOException {
        String actual = resolveRemotePath(path);
        sftp.changePermissions(actual, permissions);
    }

    public void umask(String umask) throws IOException {
        try {
            this.umask = Integer.parseInt(umask, 8);
        } catch (NumberFormatException ex) {
            throw new IOException("umask must be 4 digit octal number e.g. 0022");
        }
    }

    /**
     * <p>
     * Rename a file on the remote computer.
     * </p>
     *
     * @param oldpath the old path
     * @param newpath the new path
     *
     * @throws IOException if an IO error occurs
     *
     * @since 0.2.0
     */
    public void rename(String oldpath, String newpath) throws IOException {
        String from = resolveRemotePath(oldpath);
        String to = resolveRemotePath(newpath);

        sftp.renameFile(from, to);
    }

    /**
     * <p>
     * Remove a file or directory from the remote computer.
     * </p>
     *
     * @param path the path of the remote file/directory
     *
     * @throws IOException if an IO error occurs
     *
     * @since 0.2.0
     */
    public void rm(String path) throws IOException {
        String actual = resolveRemotePath(path);

        FileAttributes attrs = sftp.getAttributes(actual);

        if (attrs.isDirectory()) {
            sftp.removeDirectory(actual);
        } else {
            sftp.removeFile(actual);
        }
    }

    /**
     *
     *
     * @param path
     * @param force
     * @param recurse
     *
     * @throws IOException
     */
    public void rm(String path, boolean force, boolean recurse) throws IOException {
        String actual = resolveRemotePath(path);

        FileAttributes attrs = sftp.getAttributes(actual);

        SftpFile file;

        if (attrs.isDirectory()) {
            List list = ls(path);

            if (!force && (list.size() > 0)) {
                throw new IOException("You cannot delete non-empty directory, use force=true to overide");
            } else {
                for (Iterator it = list.iterator(); it.hasNext();) {
                    file = (SftpFile) it.next();

                    if (file.isDirectory() && !file.getFilename().equals(".") && !file.getFilename().equals("..")) {
                        if (recurse) {
                            rm(file.getAbsolutePath(), force, recurse);
                        } else {
                            throw new IOException("Directory has contents, cannot delete without recurse=true");
                        }
                    } else if (file.isFile()) {
                        sftp.removeFile(file.getAbsolutePath());
                    }
                }
            }

            sftp.removeDirectory(actual);
        } else {
            sftp.removeFile(actual);
        }
    }

    /**
     * <p>
     * Create a symbolic link on the remote computer.
     * </p>
     *
     * @param path the path to the existing file
     * @param link the new link
     *
     * @throws IOException if an IO error occurs or the operation is not
     *         supported on the remote platform
     *
     * @since 0.2.0
     */
    public void symlink(String path, String link) throws IOException {
        String actualPath = resolveRemotePath(path);
        String actualLink = resolveRemotePath(link);

        sftp.createSymbolicLink(actualPath, actualLink);
    }

    /**
     * <p>
     * Find the target of a symbolic link on the remote computer.
     * </p>
     *
     * @param path the path to the symbolic link
     *
     * @throws IOException if an IO error occurs or the operation is not
     *         supported on the remote platform
     *
     * @since 0.2.0
     */
    public String symlinkTarget(String path) throws IOException {
        String actualPath = resolveRemotePath(path);

        return sftp.getSymbolicLinkTarget(actualPath);
    }

    /**
     * <p>
     * Returns the attributes of the file from the remote computer.
     * </p>
     *
     * @param path the path of the file on the remote computer
     *
     * @return the attributes
     *
     * @throws IOException if an IO error occurs or the file does not exist
     *
     * @see com.sshtools.j2ssh.sftp.FileAttributes
     * @since 0.2.0
     */
    public FileAttributes stat(String path) throws IOException {
        String actual = resolveRemotePath(path);

        return sftp.getAttributes(actual);
    }

    /**
     *
     *
     * @param path
     *
     * @return
     *
     * @throws IOException
     */
    public String getAbsolutePath(String path) throws IOException {
        String actual = resolveRemotePath(path);

        return sftp.getAbsolutePath(path);
    }

    /**
     * <p>
     * Close the SFTP client.
     * </p>
     *
     * @throws IOException
     *
     * @since 0.2.0
     */
    public void quit() throws IOException {
        sftp.close();
    }

    /**
     *
     *
     * @param localdir
     * @param remotedir
     * @param recurse
     * @param sync
     * @param commit
     * @param progress
     *
     * @return
     *
     * @throws IOException
     */
    public DirectoryOperation copyLocalDirectory(String localdir, String remotedir, boolean recurse, boolean sync,
            boolean commit, FileTransferProgress progress) throws IOException {
        DirectoryOperation op = new DirectoryOperation();

        File local = resolveLocalPath(localdir);

        remotedir = resolveRemotePath(remotedir);
        remotedir += (remotedir.endsWith("/") ? "" : "/");
        remotedir += local.getName();
        remotedir += (remotedir.endsWith("/") ? "" : "/");

        // Setup the remote directory if were committing
        if (commit) {
            try {
                FileAttributes attrs = stat(remotedir);
            } catch (IOException ex) {
                mkdir(remotedir);
            }
        }

        // List the local files and verify against the remote server
        File[] ls = local.listFiles();

        if (ls != null) {
            for (int i = 0; i < ls.length; i++) {
                boolean copy = false;
                if (ls[i].isDirectory() && !ls[i].getName().equals(".") && !ls[i].getName().equals("..")) {
                    if (recurse) {
                        File f = new File(local, ls[i].getName());
                        op.addDirectoryOperation(
                                copyLocalDirectory(f.getAbsolutePath(), remotedir, recurse, sync, commit, progress),
                                f);
                    }
                } else if (ls[i].isFile()) {
                    try {
                        FileAttributes attrs = stat(remotedir + ls[i].getName());

                        if ((ls[i].length() == attrs.getSize().longValue())
                                && ((ls[i].lastModified() / 1000) == attrs.getModifiedTime().longValue())) {
                            op.addUnchangedFile(ls[i]);
                        } else {
                            op.addUpdatedFile(ls[i]);
                            copy = true;
                        }
                    } catch (IOException ex1) {
                        op.addNewFile(ls[i]);
                        copy = true;
                    }

                    if (commit && copy) {
                        put(ls[i].getAbsolutePath(), remotedir + ls[i].getName(), progress);
                        /*FileAttributes attrs = stat(remotedir + ls[i].getName());
                                    attrs.setTimes(new UnsignedInteger32(
                            ls[i].lastModified() / 1000),
                        new UnsignedInteger32(ls[i].lastModified() / 1000));
                         sftp.setAttributes(remotedir + ls[i].getName(), attrs);*/
                    }
                }
            }
        }

        if (sync) {
            // List the contents of the new local directory and remove any
            // files/directories that were not updated
            try {
                List files = ls(remotedir);
                SftpFile file;

                File f;

                for (Iterator it = files.iterator(); it.hasNext();) {
                    file = (SftpFile) it.next();

                    // Create a local file object to test for its existence
                    f = new File(local, file.getFilename());

                    if (!op.containsFile(file) && !file.getFilename().equals(".")
                            && !file.getFilename().equals("..")) {
                        op.addDeletedFile(file);

                        if (commit) {
                            if (file.isDirectory()) {
                                // Recurse through the directory, deleting stuff
                                recurseMarkForDeletion(file, op);

                                if (commit) {
                                    rm(file.getAbsolutePath(), true, true);
                                }
                            } else if (file.isFile()) {
                                rm(file.getAbsolutePath());
                            }
                        }
                    }
                }
            } catch (IOException ex2) {
                // Ignorew since if it does not exist we cant delete it
            }
        }

        if (preserve && commit) {
            FileAttributes attrs = setRemoteAttrs2(local.getCanonicalPath());
            if (attrs != null)
                sftp.setAttributes(remotedir, attrs);
        }
        // Return the operation details
        return op;
    }

    /**
     *
     *
     * @param eventListener
     */
    public void addEventListener(ChannelEventListener eventListener) {
        sftp.addEventListener(eventListener);
    }

    private void recurseMarkForDeletion(SftpFile file, DirectoryOperation op) throws IOException {
        List list = ls(file.getAbsolutePath());
        op.addDeletedFile(file);

        for (Iterator it = list.iterator(); it.hasNext();) {
            file = (SftpFile) it.next();

            if (file.isDirectory() && !file.getFilename().equals(".") && !file.getFilename().equals("..")) {
                recurseMarkForDeletion(file, op);
            } else if (file.isFile()) {
                op.addDeletedFile(file);
            }
        }
    }

    private void recurseMarkForDeletion(File file, DirectoryOperation op) throws IOException {
        File[] list = file.listFiles();
        op.addDeletedFile(file);

        if (list != null) {
            for (int i = 0; i < list.length; i++) {
                file = list[i];

                if (file.isDirectory() && !file.getName().equals(".") && !file.getName().equals("..")) {
                    recurseMarkForDeletion(file, op);
                } else if (file.isFile()) {
                    op.addDeletedFile(file);
                }
            }
        }
    }

    /**
     *
     *
     * @param remotedir
     * @param localdir
     * @param recurse
     * @param sync
     * @param commit
     * @param progress
     *
     * @return
     *
     * @throws IOException
     */
    public DirectoryOperation copyRemoteDirectory(String remotedir, String localdir, boolean recurse, boolean sync,
            boolean commit, FileTransferProgress progress) throws IOException {
        // Create an operation object to hold the information
        DirectoryOperation op = new DirectoryOperation();

        String actual;
        if (remotedir.equals("")) {
            actual = sftp.getDefaultDirectory();
        } else {
            actual = resolveRemotePath(remotedir);
            actual = sftp.getAbsolutePath(actual);
        }

        FileAttributes attr = sftp.getAttributes(actual);

        if (!attr.isDirectory()) {
            throw new IOException(remotedir + " is not a directory");
        }

        String cwd = actual;

        // Setup the local cwd
        String base = remotedir;
        int idx = base.lastIndexOf('/');

        if (idx != -1) {
            base = base.substring(idx + 1);
        }

        File local = new File(localdir, base);

        if (!local.isAbsolute()) {
            local = new File(lpwd(), localdir);
        }

        if (!local.exists() && commit) {
            local.mkdir();
        }

        if (commit && preserve) {
            setLocalAttrs(local.getCanonicalPath(), attr);
        }

        List files = ls(cwd);
        SftpFile file;
        File f;

        for (Iterator it = files.iterator(); it.hasNext();) {
            file = (SftpFile) it.next();

            if (file.isDirectory() && !file.getFilename().equals(".") && !file.getFilename().equals("..")) {
                if (recurse) {
                    f = new File(local, file.getFilename());
                    op.addDirectoryOperation(copyRemoteDirectory(file.getAbsolutePath(), local.getAbsolutePath(),
                            recurse, sync, commit, progress), f);
                }
            } else if (file.isFile()) {
                f = new File(local, file.getFilename());

                if (f.exists() && (f.length() == file.getAttributes().getSize().longValue())
                        && ((f.lastModified() / 1000) == file.getAttributes().getModifiedTime().longValue())) {
                    if (commit) {
                        op.addUnchangedFile(f);
                    } else {
                        op.addUnchangedFile(file);
                    }

                    continue;
                }

                if (f.exists()) {
                    if (commit) {
                        op.addUpdatedFile(f);
                    } else {
                        op.addUpdatedFile(file);
                    }
                } else {
                    if (commit) {
                        op.addNewFile(f);
                    } else {
                        op.addNewFile(file);
                    }
                }

                if (commit) {
                    FileAttributes attrs = get(file.getAbsolutePath(), f.getAbsolutePath(), progress);
                    //  f.setLastModified(attrs.getModifiedTime().longValue() * 1000);
                }
            }
        }

        if (sync) {
            // List the contents of the new local directory and remove any
            // files/directories that were not updated
            File[] contents = local.listFiles();

            if (contents != null) {
                for (int i = 0; i < contents.length; i++) {
                    if (!op.containsFile(contents[i])) {
                        op.addDeletedFile(contents[i]);

                        if (contents[i].isDirectory() && !contents[i].getName().equals(".")
                                && !contents[i].getName().equals("..")) {
                            recurseMarkForDeletion(contents[i], op);

                            if (commit) {
                                IOUtil.recurseDeleteDirectory(contents[i]);
                            }
                        } else if (commit) {
                            contents[i].delete();
                        }
                    }
                }
            }
        }

        return op;
    }
}