diff --git includes/filetransfer/drupalftp.inc includes/filetransfer/drupalftp.inc
new file mode 100644
index 0000000..9fb758f
--- /dev/null
+++ includes/filetransfer/drupalftp.inc
@@ -0,0 +1,1606 @@
+<?php
+// $id:$
+
+/**
+ * Ready to receive a username.
+ */
+define('DRUPAL_FTP_REPLY_READY_USER', '220');
+
+/**
+ * Ready to receive a password.
+ */
+define('DRUPAL_FTP_REPLY_READY_PASS', '331');
+
+/**
+ * Login was successful.
+ */
+define('DRUPAL_FTP_REPLY_LOGGED_IN', '230');
+
+/**
+ * Server supports SSL/TLS.
+ */
+define('DRUPAL_FTP_REPLY_SSL_OK', '234');
+
+/**
+ * Server has already opened a data connection.
+ */
+define('DRUPAL_FTP_REPLY_DATA_CONNECTION_ALREADY_OPENED', '125');
+
+/**
+ * Server has opened a data connection.
+ */
+define('DRUPAL_FTP_REPLY_DATA_CONNECTION_OPENED', '150');
+
+/**
+ * Server reports command was successful.
+ */
+define('DRUPAL_FTP_REPLY_COMMAND_OK', '200');
+
+/**
+ * A file status check was done.
+ */
+define('DRUPAL_FTP_REPLY_FILE_STATUS', '213');
+
+/**
+ * Logout was successful.
+ */
+define('DRUPAL_FTP_REPLY_LOGGED_OUT', '221');
+
+/**
+ * Server has closed a data connection.
+ */
+define('DRUPAL_FTP_REPLY_DATA_CONNECTION_CLOSED', '226');
+
+/**
+ * Server is entering passive mode.
+ */
+define('DRUPAL_FTP_REPLY_PASSIVE_MODE', '227');
+
+/**
+ * Server is entering extended passive mode.
+ */
+define('DRUPAL_FTP_REPLY_EXTENDED_PASSIVE_MODE', '229');
+
+/**
+ * An action has taken place on a file or directory.
+ */
+define('DRUPAL_FTP_REPLY_FILE_ACTION_OK', '250');
+
+/**
+ * Server has created a path. This is used when the server has
+ * literally created a path (created a directory) or when it has generated a
+ * path in response to a 'PWD' (print current working directory) command.
+ */
+define('DRUPAL_FTP_REPLY_PATHNAME_CREATED', '257');
+
+/**
+ * Server is ready to perform an action on a file or directory and
+ * requires further information before continuing. This is used when renaming a
+ * file.
+ */
+define('DRUPAL_FTP_REPLY_FILE_ACTION_OK_PENDING', '350');
+
+/**
+ * Binary type.
+ */
+define('DRUPAL_FTP_BINARY', 'I');
+
+/**
+ * ASCII type.
+ */
+define('DRUPAL_FTP_ASCII', 'A');
+
+/**
+ * A block size in bytes to use when performing read or write operations. Should
+ * be atleast 4k to maximize performance, and no more than 8k.
+ */
+define('DRUPAL_FTP_BLOCK_SIZE', 8196);
+
+class DrupalFTP {
+  /**
+   * An indexed array of error messages.
+   *
+   * @var Array
+   */
+  protected $error = array();
+
+  /**
+   * An indexed array of stored replies.
+   *
+   * @var Array
+   */
+  protected $stored_reply = array();
+
+  /**
+   * A control connection's socket descriptor.
+   *
+   * @var Resource
+   */
+  protected $control_socket = FALSE;
+
+  /**
+   * A data connection's socket descriptor.
+   *
+   * @var Resource
+   */
+  protected $data_socket = FALSE;
+
+  /** 
+   * A status flag to indicate a connection was established.
+   *
+   * @var boolean
+   */
+  protected $connected = FALSE;
+
+  /**
+   * A status flag to indicate whether data encryption was turned on.
+   *
+   * @var boolean
+   */
+  protected $encrypt_data_socket = FALSE;
+
+  /**
+   * Holds a value of the SSL method used when encryption is turned on for the
+   * control connection. If data encryption is also turned on, the same SSL
+   * method is used for it.
+   *
+   * @var int
+   */
+  protected $ssl_method = FALSE;
+
+  /** 
+   * A flag to indicate login status.
+   *
+   * @var boolean
+   */
+  protected $did_login = FALSE;
+
+  /**
+   * A stored IP address.
+   *
+   * @var string 
+   */
+  protected $ip;
+
+  /**
+   * An associative array of options.
+   *
+   * @var array
+   */
+  protected $options;
+
+  /**
+   * A flag to indicate the operating system is Windows.
+   *
+   * @var boolean
+   */
+  protected $os_is_windows;
+
+  /**
+   * Constructor for DrupalFTP.
+   *
+   * @param $options
+   *   An associative array of options as defined by
+   *   DrupalFTP::defaultOptions().
+   *
+   * @see DrupalFTP::defaultOptions()
+   */ 
+  public function __construct($options = array()) {
+    $this->options = $options += $this->defaultOptions();
+
+    if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
+      $this->os_is_windows = TRUE;
+    }
+    else {
+      $this->os_is_windows = FALSE;
+    }
+  }
+
+  /**
+   * Establishes a connection to the FTP server.
+   *
+   * @param $ip
+   *   An IP address or hostname.
+   * @param $port
+   *   A port number. Defaults to 21.
+   * @param $secure
+   *   Set to TRUE to encrypt the connection. Defaults to FALSE.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function connect($ip, $port = 21, $secure = FALSE) {
+    if (!is_resource($this->options['stream context'])) {
+      $this->control_socket = @stream_socket_client('tcp://' . $ip . ':' . $port, $errno, $errstr, $this->options['socket timeout'], STREAM_CLIENT_CONNECT);
+    }
+    else {
+      $this->control_socket = @stream_socket_client('tcp://' . $ip . ':' . $port, $errno, $errstr, $this->options['socket timeout'], STREAM_CLIENT_CONNECT, $this->options['stream context']);
+    }
+
+    if (!$this->control_socket) {
+      $this->setError(__METHOD__, t('Failed to open control socket: @error_message', array('@error_message' => $errstr)));
+      return FALSE;
+    }
+
+    stream_set_timeout($this->control_socket, $this->options['socket timeout']);
+
+    if (!$this->getReply(array(DRUPAL_FTP_REPLY_READY_USER), $error_result)) {
+      $this->setError(__METHOD__, $error_result);
+      return FALSE;
+    }
+
+    if ($secure && !$this->sslEnable($this->control_socket)) {
+      $this->setError(__METHOD__, t('Failed to enable SSL on control connection.'));
+      return FALSE;
+    }
+
+    $this->connected = TRUE;
+    $this->ip = $ip;
+
+    return TRUE;
+  }
+
+  /**
+   * Logs into the FTP server.
+   *
+   * @param $username
+   *   A username.
+   * @param $password
+   *   A password.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function login($username, $password) {
+    $this->checkDidConnect();
+
+    if ($this->did_login) {
+      return TRUE;
+    }
+
+    if (!$this->putCommand(__METHOD__, 'USER' . ' ' . $username, array(DRUPAL_FTP_REPLY_READY_PASS)) ||
+        !$this->putCommand(__METHOD__, 'PASS' . ' ' . $password, array(DRUPAL_FTP_REPLY_LOGGED_IN))) {
+
+      return FALSE;
+    }
+      
+    $this->did_login = TRUE;
+    return TRUE;
+  }
+
+  /**
+   * Logs out and disconnects from the FTP server.
+   */
+  public function logout() {
+    $this->putCommand(__METHOD__, 'QUIT', array(DRUPAL_FTP_REPLY_LOGGED_OUT));
+    // Close the control socket and reset state.
+    $this->disconnect();
+  }
+
+  /**
+   * Changes the working directory.
+   *
+   * @param $directory
+   *   A directory to switch to.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function chdir($directory) {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'CWD' . ' ' . $directory, array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Sets a new permission mode on a file or directory.
+   *
+   * @param $perm
+   *   A permission mode, must be integer or octal.
+   * @param $path
+   *   A path to a file or directory.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function chmod($perm, $path) {
+    $this->checkDidLogin();
+    // Convert $perm to its octal value. If already octal, $perm will remain
+    // unchanged.
+    $perm = sprintf("%o", $perm);
+
+    if (!$this->putCommand(__METHOD__, 'SITE CHMOD' . ' ' . $perm . ' ' . $path, array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK))) {
+      return FALSE;
+    }
+    
+    return TRUE;
+  }
+
+  /**
+   * Returns a list of filenames.
+   *
+   * @param $path
+   *   A path to a directory.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure. 
+   */
+  public function nlist($path) {
+    $this->checkDidLogin();
+
+    if (!$this->passiveStart('NLST' . ' ' . $path)) {
+      $this->setError(__METHOD__, t('Failed to open passive connection.'));
+      return FALSE;
+    }
+    // Get the list of filenames.
+    $result = $this->getDataReply();
+    // If $result is empty don't return yet because we still have to finish the
+    // passive connection.
+    $files = array();
+    if (!empty($result)) {
+      foreach($result as $file) {
+        // Skip '.' and '..' directories.
+        if ($file != '.' && $file != '..') {
+          $files[] = $file;
+        }
+      }
+    }
+
+    if (!$this->passiveFinish()) {
+      $this->setError(__METHOD__, t('Failed to close passive connection.'));
+      return FALSE;
+    }
+
+    return $files;
+  }
+
+  /**
+   * Uploads a file.
+   *
+   * If $transfer_mode is set to DRUPAL_FTP_BINARY this method will check if the
+   * uploaded file is complete. This check isn't done when $transfer_mode is set
+   * to DRUPAL_FTP_ASCII because the file's size may change due to end-of-line
+   * translations performed in ASCII mode.
+   * 
+   * @param $destination_path
+   *   A remote file path to save an uploaded file. 
+   * @param $source_path
+   *   A local file path to upload.
+   * @param $transfer_mode
+   *   A transfer mode. Either DRUPAL_FTP_ASCII or DRUPAL_FTP_BINARY. Defaults
+   *   to DRUPAL_FTP_BINARY.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function put($destination_path, $source_path, $transfer_mode = DRUPAL_FTP_BINARY) {
+    $this->checkDidLogin();
+
+    if (!is_file($source_path)) {
+      $this->setError(__METHOD__, t('File does not exist or is not a regular file: @file', array('@file' => $source_path)));
+      return FALSE;
+    }
+    elseif (!($fp_source = fopen($source_path, 'rb'))) {
+      $this->setError(__METHOD__, t('Failed to open file: @file', array('@file' => $source_path)));
+      return FALSE;
+    }
+
+    if (!$this->passiveStart('STOR' . ' ' . $destination_path, $transfer_mode)) {
+      $this->setError(__METHOD__, t('Failed to open passive connection.'));
+      return FALSE;
+    }
+
+    $ascii_mode = ($transfer_mode == DRUPAL_FTP_ASCII) ? TRUE : FALSE;
+    $file_size = filesize($source_path);
+    $write_failed = FALSE;
+    $total_written = 0;
+
+    do {
+      $data = $this->read($fp_source, $ascii_mode);
+      // Only write non-empty data.
+      if ($data !== FALSE) {
+        if ($ascii_mode) {
+          $data = $this->translateLine($data, TRUE);
+        }
+        $written = $this->write($this->data_socket, $data);
+        if ($written) {
+          $total_written += $written; 
+        }
+        else {
+          $write_failed = TRUE;
+        }
+      }
+    } while(!$write_failed && $data !== FALSE);
+
+    fclose($fp_source);
+    $return = TRUE;
+
+    if ($write_failed) {
+      // Don't return yet we still have to close the passive connection.
+      $this->setError(__METHOD__, t('Failure while writing to data socket.'));
+      $return = FALSE;
+    }
+
+    if (!$this->passiveFinish()) {
+      $this->setError(__METHOD__, t('Failed to close passive connection.'));
+      $return = FALSE;
+    }
+    // If the transfer type is binary then check to ensure we uploaded the
+    // complete file.
+    elseif (!$write_failed && !$ascii_mode && $total_written != $file_size) {
+      $this->setError(__METHOD__, t('Source file size doesn\'t match destination file size.'));
+      $return = FALSE;
+    }
+
+    return $return;
+  }
+
+  /**
+   * Downloads a file.
+   *
+   * If $transfer_mode is set to DRUPAL_FTP_BINARY this method will check if the
+   * downloaded file is complete. This check isn't done when $transfer_mode is
+   * set to DRUPAL_FTP_ASCII because the file's size may change due to
+   * end-of-line translations performed in ASCII mode.
+   *
+   * @param $destination_path
+   *   A local file path to save a downloaded file.
+   * @param $source_path
+   *   A remote file path to download.
+   * @param $transfer_mode
+   *   A transfer mode. Either DRUPAL_FTP_ASCII or DRUPAL_FTP_BINARY. Defaults
+   *   to DRUPAL_FTP_BINARY.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure. 
+   */
+  public function get($destination_path, $source_path, $transfer_mode = DRUPAL_FTP_BINARY) {
+    $this->checkDidLogin();
+
+    if (!($fp_dest = fopen($destination_path, 'wb'))) {
+      $this->setError(__METHOD__, t('Failed to open file: @file', array('@file' => $destination_path)));
+      return FALSE;
+    }
+
+    $ascii_mode = ($transfer_mode == DRUPAL_FTP_ASCII) ? TRUE : FALSE;
+    $file_size = $this->size($source_path);
+
+    if (!$this->passiveStart('RETR' . ' ' . $source_path, $transfer_mode)) {
+      $this->setError(__METHOD__, t('Failed to open passive connection.'));
+      return FALSE;
+    } 
+
+    $total_written = 0;
+    do {
+      $data = $this->read($this->data_socket, $ascii_mode);
+      // If $data is FALSE then the server is either finished sending or read
+      // timed out.
+      if ($data !== FALSE) {
+        if ($ascii_mode) {
+          $data = $this->translateLine($data, FALSE);
+        }
+        $total_written += fwrite($fp_dest, $data, DRUPAL_FTP_BLOCK_SIZE);
+      }
+    } while($data !== FALSE);
+
+    fclose($fp_dest);
+    $return = TRUE;
+
+    if (!$this->passiveFinish()) {
+      $this->setError(__METHOD__, t('Failed to close passive connection.'));
+      $return = FALSE;
+    } 
+    // If the transfer type is binary then check to ensure we downloaded the
+    // complete file.
+    if (!$ascii_mode && $file_size != $total_written) {
+      $this->setError(__METHOD__, t('Source file size doesn\'t match destination file size.'));
+      $return = FALSE;
+    }
+
+    return $return;
+  }
+
+  /**
+   * Creates a directory.
+   *
+   * @param $directory
+   *   A path to a directory. 
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function mkdir($directory) {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'MKD' . ' ' . $directory, array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_PATHNAME_CREATED))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Removes a directory.
+   *
+   * @param $directory
+   *   A path to a directory.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function rmdir($directory) {
+    $this->checkDidLogin();
+
+    // Don't allow removal attempts of '.', '..' directories, and '../' within
+    // the directory's path.
+    if ($directory == '.' || $directory == '..' || preg_match('/\.\.\//', $directory)) {
+      $this->setError(__METHOD__, t('Not allowed to remove directory: @directory', array('@directory' => $directory)));
+      return FALSE;
+    }
+
+    if (!$this->putCommand(__METHOD__, 'RMD' . ' ' . $directory, array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Deletes a file.
+   *
+   * @param $file
+   *   A path to a file.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function delete($file) {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'DELE' . ' ' . $file, array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Returns a file's size in bytes.
+   *
+   * @param $file
+   *   A path to a file.
+   *
+   * @return
+   *   The size of the file in bytes, or FALSE on failure.
+   */
+  public function size($file) {
+    $this->checkDidLogin();
+
+    $expect_reply = array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK, DRUPAL_FTP_REPLY_FILE_STATUS);
+    // ProFTPD requires the transfer mode to be binary for this command.
+    $this->setType(DRUPAL_FTP_BINARY);
+    if (!($result = $this->putCommand(__METHOD__, 'SIZE' . ' ' . $file, $expect_reply))) {
+      return FALSE;
+    }
+    // The file's size is returned in the final reply.
+    return $result['reply final'];
+  }
+
+  /**
+   * Changes to the current parent directory.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function cdup() {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'CDUP', array(DRUPAL_FTP_REPLY_COMMAND_OK))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Returns the current working directory.
+   *
+   * @return
+   *   A directory on success, or FALSE on failure.
+   */
+  public function pwd() {
+    $this->checkDidLogin(); 
+
+    if (!($result = $this->putCommand(__METHOD__, 'PWD', array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_PATHNAME_CREATED)))) {
+      return FALSE;
+    }
+    // The directory is found in the final reply and located between
+    // quotation marks.
+    $pwd = $result['reply final'];
+    $offset_start = strpos($pwd, '"') + 1;
+    $offset_end = strrpos($pwd, '"', $offset_start) - 1;
+
+    if ($offset_start === FALSE || $offset_end === FALSE) {
+      $this->setError(__METHOD__, t('Failed to parse PWD result.'));
+      return FALSE;
+    }
+
+    $pwd = substr($pwd, $offset_start, $offset_end);
+    return $pwd;
+  }
+
+  /**
+   * Sets the transfer mode.
+   *
+   * @param $transfer_mode
+   *   The type of transfer mode. Legal values are DRUPAL_FTP_ASCII or
+   *   DRUPAL_FTP_BINARY.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure. 
+   */
+  public function setType($transfer_mode) {
+    $this->checkDidLogin();
+
+    if ($transfer_mode != DRUPAL_FTP_BINARY && $transfer_mode != DRUPAL_FTP_ASCII) {
+      $this->setError(__METHOD__, t('Unsupported transfer mode specified.'));
+      return FALSE;
+    }
+
+    if (!$this->putCommand(__METHOD__, 'TYPE' . ' ' . $transfer_mode, array(DRUPAL_FTP_REPLY_COMMAND_OK))) {
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Renames a file or directory.
+   *
+   * @param $oldname.
+   *   The old name.
+   * @param $newname.
+   *   The new name.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function rename($oldname, $newname) {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'RNFR' . ' ' . $oldname, array(DRUPAL_FTP_REPLY_FILE_ACTION_OK_PENDING)) ||
+        !$this->putCommand(__METHOD__, 'RNTO' . ' ' . $newname, array(DRUPAL_FTP_REPLY_FILE_ACTION_OK))) {
+
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Issues a SITE command.
+   *
+   * @param $command
+   *   A command.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function site($command) {
+    $this->checkDidLogin();
+
+    if (!$this->putCommand(__METHOD__, 'SITE' . ' ' . $command)) {
+      return FALSE;
+    }
+
+    return TRUE; 
+  }
+
+  /**
+   * Performs a connection status check.
+   *
+   * Sends a 'NOOP' (No Operation) command to the server which simply instructs
+   * it to send back a command OK response. This method can be called at any
+   * point, even before logging in.
+   *
+   * @return
+   *   TRUE if connected, otherwise FALSE.
+   */
+  public function isConnected() {
+    if (!$this->connected || !$this->putCommand(__METHOD__, 'NOOP')) {
+        return FALSE;
+    }
+      
+    return TRUE;
+  }
+
+  /**
+   * Returns a parsed directory listing from the control connection.
+   *
+   * Same as getList() except the server sends the directory listing over the
+   * control connection for better effeciency. The server may not implement
+   * this command.
+   *
+   * @param $path
+   *   A path to a file or directory. Defaults to '.' (the current directory).
+   * @param $recursive
+   *   If TRUE will instruct the server to recurse over all directories found
+   *   under $path.
+   *
+   * @return
+   *   If successful, an array of file information as defined by
+   *   DrupalFTP::parseList(). Otherwise FALSE.
+   *
+   * @see DrupalFTP::parseList()
+   * @see DrupalFTP::getList()
+   * @see DrupalFTP::rawList()
+   */
+  public function stat($path = '.', $recursive = FALSE) {
+    $this->checkDidLogin();
+
+    $expect_reply = array(DRUPAL_FTP_REPLY_COMMAND_OK, DRUPAL_FTP_REPLY_FILE_ACTION_OK, DRUPAL_FTP_REPLY_FILE_STATUS);
+    $command = !$recursive ? 'STAT' : 'STAT -R';
+    if (!($result = $this->putCommand(__METHOD__, $command . ' ' . $path, $expect_reply))) {
+      return FALSE;
+    }
+
+    if (!$recursive) {
+      $result['reply list'] = $this->parseList($result['reply list']);
+    }
+    else {
+      $result['reply list'] = $this->parseList($result['reply list'], TRUE, $path);
+    }
+
+    return $result['reply list'];
+  }
+
+
+  /**
+   * Returns a parsed directory listing.
+   *
+   * @param $path
+   *   A path to a file or directory. Defaults to '.' (the current directory).
+   * @param $recursive
+   *   If TRUE will instruct the server to recurse over all directories found
+   *   under $path.
+   *
+   * @return
+   *   If successful, an array of file information as defined by
+   *   DrupalFTP::parseList(). Otherwise FALSE.
+   *
+   * @see DrupalFTP::parseList()
+   * @see DrupalFTP::stat()
+   * @see DrupalFTP::rawList()
+   */
+  public function getList($path = '.', $recursive = FALSE) {
+    $this->checkDidLogin();
+
+    $command = !$recursive ? 'LIST' : 'LIST -R';
+    if (!$this->passiveStart($command . ' ' . $path)) {
+      $this->setError(__METHOD__, t('Failed to open passive connection.'));
+      return FALSE;
+    }
+
+    $result = $this->getDataReply();
+    // $result will be FALSE if parsing failed.
+    if (!$recursive) {
+      $result = $this->parseList($result);
+    }
+    else {
+      $result = $this->parseList($result, TRUE, $path);
+    }
+
+    if (!$this->passiveFinish()) {
+      $this->setError(__METHOD__, t('Failed to close data connection.'));
+      return FALSE;
+    }
+    return $result;
+  } 
+
+  /**
+   * Returns a raw directory listing.
+   *
+   * @param $path
+   *   A path to a file or directory. Defaults to '.' (the current directory).
+   * @param $recursive
+   *   If TRUE will instruct the server to recurse over all directories found
+   *   under $path.
+   *
+   * @return
+   *   A directory listing as returned by the server on success, or FALSE on
+   *   failure.
+   *
+   * @see DrupalFTP::getList()
+   * @see DrupalFTP::stat()
+   */
+  public function rawList($path = '.', $recursive = FALSE) {
+    $this->checkDidLogin();
+
+    $command = !$recursive ? 'LIST' : 'LIST -R';
+    if (!$this->passiveStart($command . ' ' . $path)) {
+      $this->setError(__METHOD__, t('Failed to open data connection.'));
+      return FALSE;
+    }
+
+    $result = $this->getDataReply();
+
+    if (!$this->passiveFinish()) {
+      $this->setError(__METHOD__, t('Failed to close data connection.'));
+      return FALSE;
+    }
+
+    return $result;
+  }
+
+  /**
+   * Enables encryption on the control connection.
+   *
+    * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function sslEnable() {
+    // Try TLS first. Some newer servers only understand TLS while older servers
+    // only understand SSL.
+    if (!$this->putCommand(__METHOD__, 'AUTH TLS', array(DRUPAL_FTP_REPLY_SSL_OK))) {
+      if (!$this->putCommand(__METHOD__, 'AUTH SSL', array(DRUPAL_FTP_REPLY_SSL_OK))) {
+        return FALSE;
+      }
+    }
+
+    // Remember the SSL method in case data connection encryption gets turned
+    // on. These encryption methods are supported by both SSL and TLS protocols.
+    if (stream_socket_enable_crypto($this->control_socket, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT) !== FALSE) { 
+      $this->ssl_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
+    }
+    elseif (stream_socket_enable_crypto($this->control_socket, TRUE, STREAM_CRYPTO_METHOD_SSLv3_CLIENT) !== FALSE) {
+      $this->ssl_method = STREAM_CRYPTO_METHOD_SSLv3_CLIENT;
+    }
+    elseif (stream_socket_enable_crypto($this->control_socket, TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT) !== FALSE) {
+      $this->ssl_method = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
+    }
+    else {
+      $this->setError(__METHOD__, t('Failed to negotiate SSL encryption method.'));
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Enables encryption on a data connection.
+   *
+   * @return
+   *   TRUE on success, or FALSE on failure.
+   */
+  public function sslEnableData() {
+    $this->checkDidConnect();
+    // PBSZ - sets a buffer size for encrypted data. This is required and we
+    // also must only use a value of 0 here because SSL/TLS handles the
+    // buffering.
+    if (!$this->putCommand(__METHOD__, 'PBSZ 0', array(DRUPAL_FTP_REPLY_COMMAND_OK)) ||
+        !$this->putCommand(__METHOD__, 'PROT P', array(DRUPAL_FTP_REPLY_COMMAND_OK))) {
+
+      return FALSE;
+    }
+    // Remember that we turned on data encryption.
+    $this->encrypt_data_socket = TRUE;
+    return TRUE;
+  }
+
+  /**
+   * Gets a list of stored replies.
+   *
+   * @return
+   *   An indexed array of parsed replies as defined by
+   *   DrupalFTP::getReply().
+   */
+  public function getStoredReplies() {
+    return $this->stored_reply;
+  }
+
+  /**
+   * Empties a list of stored replies.
+   */
+  public function resetStoredReplies() {
+    $this->stored_reply = array();
+  }
+
+  /**
+   * Gets a list of errors.
+   *
+   * @return
+   *   An indexed array of errors with each index corresponding to
+   *   an associative array keyed by:
+   *   - error message: A string containing the error that occurred.
+   *   - method: A string of the method the error occurred in.
+   *   - result: If set, a parsed result of a reply as defined by 
+   *     DrupalFTP::getReply().
+   *   
+   * @see DrupalFTP::getReply()
+   */
+  public function getErrors() {
+    return $this->error;
+  }
+
+  /**
+   * Empties a list of errors.
+   */
+  public function resetErrors() {
+    $this->error = array();
+  }
+
+  /**
+   * Gets options.
+   *
+   * @return
+   *   An associative array of options as defined by
+   *   DrupalFTP::defaultOptions().
+   *
+   * @see DrupalFTP::defaultOptions()
+   * @see DrupalFTP::setOptions()
+   */
+  public function getOptions() {
+    return $this->options;
+  }
+
+  /**
+   * Sets options.
+   *
+   * @param
+   *   An associative array of options as defined by
+   *   DrupalFTP::defaultOptions().
+   *
+   * @see DrupalFTP::defaultOptions()
+   * @see DrupalFTP::getOptions()
+   */
+  public function setOptions(Array $options) {
+    $this->options += $options; 
+  }
+
+  /**
+   * Returns default options.
+   *
+   * @return
+   *   An associative array of default options keyed by:
+   *   - socket timeout: Amount of time in seconds that a read or write
+   *     attempt to a socket will wait before giving up.
+   *   - stream context: A resource to a context as created by
+   *     stream_context_create()
+   *   - store replies: Whether or not to store replies from the control
+   *     connection.
+   *
+  *  @see http://www.php.net/manual/en/function.stream-context-create.php
+   * @see DrupalFTP::setOptions()
+   * @see DrupalFTP::getOptions()
+   */
+  public function defaultOptions() {
+    return array(
+      'socket timeout' => 15,
+      'stream context' => FALSE,
+      'store replies' => FALSE,
+    );
+  }
+
+  /**
+   * Parses a server's response to the STAT and LIST commands.
+   *
+   * For file permissions this method only checks for read, write, and execute because
+   * these are the most common for web files.
+   *
+   * @param $file_list
+   *   A directory listing as returned by the STAT or LIST commands.
+   * @param $recursive
+   *   If TRUE this method will expect a recursive directory listing. Defaults
+   *   to FALSE.
+   * @param $path
+   *   Sets the path to the starting directory. Used only when $recursive is
+   *   TRUE.
+   *
+   * @return
+   *   If $recursive is FALSE an indexed array. If $recursive is TRUE an
+   *   associative array keyed by the recursed directories with each directory
+   *   key corresponding to an indexed array.
+   *
+   *   Each index in the indexed array corresponds to an associated array of
+   *   file information. Its keys depend on the type of directory listing
+   *   format used by the server. Values are strings unless otherwise specified:
+   *   - Windows format:
+   *     - type: A file's type. Value is either file or directory.
+   *     - size: A file's size in bytes, or FALSE if type is directory.
+   *     - date: The date a file was last modified.
+   *     - filename: The name of a file or directory.
+   *   - Unix format: 
+   *     - type: A file's type. Value is file, directory, or
+   *       symbolic link.
+   *     - rights: An associative array of permissions keyed by user,group, 
+   *       other, and their corresponding permission settings keyed by read,
+   *       write, and execute. If a permission setting is set it will evaluate
+   *       to TRUE, or FALSE if not set.
+   *     - hard link count: A count of a file's aliases.
+   *     - user: A file's owner.
+   *     - group: A file's group.
+   *     - size: A file's size in bytes.
+   *     - date: The date a file was last modified.
+   *     - filename: The name of a file or directory.
+   */
+  protected function parseList($file_list, $recursive = FALSE, $path = FALSE) {
+    $files = array();
+
+    if ($recursive) {
+      $files[$path] = array();
+    }
+
+    if (empty($file_list)) {
+      return FALSE;
+    }
+    
+    $is_windows = FALSE;
+    // Check if this directory listing is in the Windows style format (note:
+    // even if the operating system is Windows the FTP server may choose to use
+    // the Unix format). The first part of the Windows format is a numerical
+    // date. For Unix a string of file permissions come first. To determine
+    // which format it is we simply check if the first character in the first
+    // entry is a number.
+    if (is_numeric($file_list[0][0])) {
+      $is_windows = TRUE;
+    }
+
+    foreach($file_list as $line) {
+      // Each part of a directory list entry is delimited by space(s).
+      $parts = preg_split("/\s+/", $line);
+
+      // In recursive mode when the server is about to list files in a new
+      // directory it will output the absolute path to the directory followed
+      // by a colon.
+      if ($recursive && !isset($parts[1])) {
+        if (($pos = strpos($parts[0], ':')) !== FALSE) {
+          $path = substr($parts[0], 0, $pos - 1);
+          $files[$path] = array();
+          continue;
+        }
+      }
+
+      if ($is_windows) {
+        // Make sure we have atleast 4 parts.
+        if (!isset($parts[3])) {
+          return FALSE;
+        }
+        $file['date'] = $parts[0] . ' ' . $parts[1]; 
+        // If this is a file, $parts[2] will contain its size. If it's a
+        // directory then $parts[2] will equal '<DIR>'. We only need to check
+        // if the first character of $parts[2] is a number to determine if it's
+        // a file.
+        $file['size'] = FALSE;
+        if (is_numeric($parts[2][0])) { 
+          $file['size'] = $parts[2];
+          $file['type'] = 'file';
+        } 
+        else {
+          $file['type'] = 'directory'; 
+        }
+        // For Windows everything after the file's size or '<DIR>' is the
+        // filename. We can't get the filename from $parts because the filename
+        // itself may have spaces.
+        $filename_offset = strpos($line, $parts[2]) + strlen($parts[2]) + 1;
+        // Due to the format we need to trim the leading spaces.
+        $file['filename'] = ltrim(substr($line, $filename_offset));
+        // Nothing else to do for Windows.
+      }
+      else {
+        // This is a Unix style format.
+        if (!isset($parts[7])) {
+          return FALSE;
+        }
+
+        // Determine the file's type.
+        if ($parts[0][0] == 'd') {
+          $file['type'] = 'directory';
+        }
+        elseif ($parts[0][0] == 'l') {
+          $file['type'] = 'symbolic link';
+        }
+        else {
+          $file['type'] = 'file';
+        }
+
+        // Extract the user's permissions for this file.
+        $rights = substr($parts[0], 1, 3);
+        $file['rights']['user']['read'] = ($rights[0] == 'r') ? TRUE : FALSE;
+        $file['rights']['user']['write'] = ($rights[1] == 'w') ? TRUE : FALSE;
+        $file['rights']['user']['execute'] = ($rights[2] == 'x') ? TRUE : FALSE;
+
+        // Extract the group's permissions for this file.
+        $rights = substr($parts[0], 4, 6);
+        $file['rights']['group']['read'] = ($rights[0] == 'r') ? TRUE : FALSE;
+        $file['rights']['group']['write'] = ($rights[1] == 'w') ? TRUE : FALSE;
+        $file['rights']['group']['execute'] = ($rights[2] == 'x') ? TRUE : FALSE;
+
+        // Extract other's permissions for this file.
+        $rights = substr($parts[0], 7, 9);
+        $file['rights']['other']['read'] = ($rights[0] == 'r') ? TRUE : FALSE;
+        $file['rights']['other']['write'] = ($rights[1] == 'w') ? TRUE : FALSE;
+        $file['rights']['other']['execute'] = ($rights[2] == 'x') ? TRUE : FALSE;
+
+        $file['hard link count'] = $parts[1];
+        // The owner of this file.
+        $file['user'] = $parts[2];
+        // The group associated with this file.
+        $file['group'] = $parts[3];
+        // The size of this file.
+        $file['size'] = $parts[4];
+        // $parts[5] - Month, $parts[6] - Day, $parts[7] - Time.
+        $file['date'] = $parts[5] . ' ' . $parts[6] . ' ' . $parts[7];
+
+        // Everything after the date is the filename.
+        $filename_offset = strpos($line, $parts[7]) + strlen($parts[7]) + 1;
+        // No need to trim any leading spaces because there's only a single
+        // space separating the date from the filename.
+        $file['filename'] = substr($line, $filename_offset);
+      }
+
+      if (!$recursive) {
+        $files[] = $file;
+      }
+      else {
+        // Recursive mode.
+        $files[$path][] = $file;
+      }
+    }
+    return $files;
+  }
+
+  /**
+   * Closes a control socket used by a control connection.
+   *
+   * Called by DrupalFTP::logout(), DrupalFTP::getReply() on read failure, and
+   * DrupalFTP::putCommand() on write failure.
+   */
+  protected function disconnect() {
+    if (!$this->connected) {
+      return;
+    }
+    fclose($this->control_socket);
+    // Reset state.
+    $this->connected = FALSE;
+    $this->did_login = FALSE;
+    $this->encrypt_data_socket = FALSE;
+    $this->ssl_method = FALSE;
+  }
+
+  /**
+   * Checks if an established connection was made.
+   *
+   * This method is used for commands that require an established connection.
+   */
+  protected function checkDidConnect() {
+    if (!$this->connected) {
+      throw new DrupalFTPException('You must connect first.');
+    }
+  }
+
+  /**
+   * Checks if a successful login was made.
+   *
+   * This method is used for commands that require a successful login.
+   */
+  protected function checkDidLogin() {
+    $this->checkDidConnect();
+    if (!$this->did_login) {
+      throw new DrupalFTPException('You must login first.');
+    }
+  }
+
+  /**
+   * Sends a command to the server.
+   *
+   * @param $method
+   *   A calling method.
+   * @param $command
+   *   A command to send.
+   * @param $expect_reply
+   *   An indexed array of expected reply codes.
+   *
+   * @return
+   *   A parsed result of the reply, or FALSE on failure.
+   *
+   * @see DrupalFTP::getReply()
+   */
+  protected function putCommand($method, $command, $expect_reply = FALSE) {
+    // Writes to the control connection are always terminated by CRLF.
+    if (!$this->write($this->control_socket, $command . "\r\n")) {
+      $this->setError($method, t('Failure while writing to control socket.'));
+      // Abort the connection.
+      $this->disconnect();
+      return FALSE;
+    }
+    elseif (!($result = $this->getReply($expect_reply, $error_result))) {
+      $this->setError($method, $error_result);
+      return FALSE;
+    }
+
+    if ($this->options['store replies']) {
+      $this->storeReply($method, $result);
+    }
+
+    return $result;
+  }
+
+  /**
+   * Stores a parsed reply.
+   *
+   * By storing replies a caller can access information it otherwise can't get
+   * from values returned by FTP command methods.
+   *
+   * @param $method
+   *   A calling method.
+   * @param $reply
+   *   A parsed reply as returned by DrupalFTP::getReply().
+   *
+   * @see DrupalFTP::getReply()
+   */
+  protected function storeReply($method, $reply) {
+    $reply['method'] = $method;
+    $this->stored_reply[] = $reply;
+  }
+
+  /**
+   * Reads the server's response to a command on the data connection.
+   *
+   * This method is for commands that require the server to issue results over
+   * a data connection in ASCII. It is used when the server sends a list of
+   * file information.
+   *
+   * @return
+   *   An indexed array of file information.
+   *
+   * @see DrupalFTP::passiveStart()
+   * @see DrupalFTP::getReply()
+   */
+  protected function getDataReply() {
+    $result = array();
+    
+    do {
+      $reply = $this->readLineFromReply($this->data_socket);
+      // Only record non-empty results.
+      if (!empty($reply)) {
+        $result[] = $reply;
+      }
+      // The server is finished sending when $reply becomes FALSE.
+    } while($reply !== FALSE);
+
+    return $result;
+  }
+
+  /**
+   * Reads and parses the server's response to a command on the control
+   * connection.
+   *
+   * Upon receipt of a command the server acknowledges with one or more lines.
+   * The number of lines and type of text contained within the reply depend on
+   * the command. Possible types of reply lines are:
+   * - informational: (if present) always come before any other line types. It's
+   *   designated by a 3 digit code followed by a dash (i.e. 220-Welcome to my
+   *   server!). These messages are targeted at the end user and may contain the
+   *   server's MOTD, legal notices, and special instructions.
+   * - list: (if present) are for when the server is sending a list of something
+   *   (normally file information). It comes after the informational lines and
+   *   before the final line. It's designated by having no 3 digit code.
+   * - final line: (always present) designated by a 3 digit code followed by a
+   *   space. It shall be the same code found in the informational lines (if
+   *   applicable). Depending on the command it may also contain a message. The
+   *   message is either purely informational (i.e. if a command fails, it will
+   *   usually state the specific reason) or it could hold the result of a
+   *   command (i.e. for the PWD command, a directory).
+   *
+   * The type of reply is determined by the 3 digit code. Reply codes range from
+   * 100 to 500. Codes that begin with 4 or 5 indicate the command failed and
+   * the server took no action.
+   *
+   * @param $expect_reply
+   *   an indexed array of reply codes to compare against the server's response.
+   *   Defaults to FALSE. If a match is found the command is considered a
+   *   success. If set to FALSE and the first digit of the server's reply code
+   *   matches 4 or 5 the command is considered a failure.
+   * @param &$error_result
+   *   On failure, a reference to an associative array keyed by:
+   *   - error message: a detailed message containing the reason for failure.
+   *   - result: a parsed reply or unset if there was a failure while reading
+   *     the reply.
+   *
+   * @return
+   *   On success, an associative array of the parsed reply keyed by:
+   *   - reply informational: an indexed array of informational text.
+   *   - reply list: an indexed array of text.
+   *   - reply final: a string of the final message.
+   *   - reply code: a string of a 3 digit reply code.
+   *   On failure, FALSE.
+   *
+   * @see DrupalFTP::getDataReply()
+   */
+  protected function getReply($expect_reply = FALSE, &$error_result = array()) {
+    $result['reply informational'] = array();
+    $result['reply final'] = '';
+    $result['reply list'] = array();
+    $result['reply code'] = '';
+
+    do {
+      $reply = $this->readLineFromReply($this->control_socket);
+
+      // If reply is FALSE then the connection must've been lost.
+      if ($reply === FALSE) {
+        $error_result['error message'] = t('Failure while reading from control socket.');
+        // Abort the connection.
+        $this->disconnect();
+        return FALSE;
+      }
+
+      if (!empty($reply)) {
+        // Check to see if this is an informational or final reply.
+        // If the reply is '220-Welcome!' (an informational reply) then
+        // $reply_code_match[0] == '220-', $reply_code_match[1] == '220', and
+        // $reply_code_match[2] contains the dash.
+        if (preg_match('/^([0-9][0-9][0-9])(\s|-)/', $reply, $reply_code_match)) {
+          if ($reply_code_match[2] == '-') { 
+            // Found a dash, this is an informational reply.
+            $result['reply informational'][] = substr($reply, 4);
+          }
+          elseif ($reply_code_match[2] == ' ') {
+            // Found a space, this is the server's final reply.
+            $result['reply final'] = substr($reply, 4);
+            // Record the reply code so we can check it against the list of
+            // expected reply codes (if any).
+            $result['reply code'] = $reply_code_match[1];
+          } 
+        }
+        else {
+          // There was no reply code found. This means the server is sending a
+          // list. This is used by the stat() method.
+          $result['reply list'][] = $reply;
+        }
+      }
+    } while(empty($result['reply final']));
+
+    if (($expect_reply && !in_array($result['reply code'], $expect_reply, TRUE)) ||
+        (!$expect_reply && ($result['reply code'][0] == '4' || $result['reply code'][0] == '5'))) {
+
+      $error_result['error message'] = t('Received an unexpected reply (expected: [@expected] got: [@reply_code]). Server said: @server_message.', array('@expected' => implode(',', $expect_reply), '@reply_code' => $result['reply code'], '@server_message' => $result['reply final']));
+
+      $error_result['result'] = $result;
+      return FALSE;
+    }
+
+    return $result;
+  }
+
+  /**
+   * Starts a passive data connection.
+   *
+   * @param $command
+   *   A command to use for the data connection.
+   * @param $transfer_mode
+   *   A transfer mode. Defaults to DRUPAL_FTP_ASCII.
+   *
+   * @return
+   *   TRUE on success, otherwise FALSE.
+   */
+  protected function passiveStart($command, $transfer_mode = DRUPAL_FTP_ASCII) {
+
+    if (!$this->setType($transfer_mode)) {
+      return FALSE;
+    }
+
+    // Try extended passive mode first. This is needed for IPv6 and will work
+    // for IPv4 as well. Some servers don't support this, in that case we fall
+    // back to regular passive mode.
+    if ($result = $this->putCommand(__METHOD__, 'EPSV', array(DRUPAL_FTP_REPLY_EXTENDED_PASSIVE_MODE))) {
+      // The final reply of an extended passive mode request contains the port
+      // number. The format is: (|||port_number|) where the pipe character is
+      // the delimiter. Currently as per RFC the precededing fields must be
+      // empty.
+      if (!preg_match('/\(?+\|(\d+)\|\)/', $result['reply final'], $parts)) {
+        $this->setError(__METHOD__, t('Failed to parse epasv data'));
+        return FALSE;
+      }
+      $port = $parts[1];
+    }
+    elseif ($result = $this->putCommand(__METHOD__, 'PASV', array(DRUPAL_FTP_REPLY_PASSIVE_MODE))) {
+      // The final reply of a passive mode request will contain an IP and port
+      // number. The format is: (127,0,0,1,117,81) where the comma is the
+      // delimiter. The first four fields are composed of the IP address. The
+      // last two fields are used to construct the port number.
+      if (!preg_match('/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/', $result['reply final'], $parts)) {
+        $this->setError(__METHOD__, t('Failed to parse pasv data.'));
+        return FALSE;
+      }
+      // To get the port number we take the 5th number and multiply it by 256
+      // then add the resulting product to the 6th number.
+      $port = ($parts[5] * 256) + $parts[6];
+    }
+    else {
+      // If we got here then we failed to enter passive mode.
+      return FALSE;
+    }
+
+    if (!is_resource($this->options['stream context'])) {
+      $this->data_socket = @stream_socket_client('tcp://' . $this->ip . ':' . $port, $errno, $errstr, $this->options['socket timeout'], STREAM_CLIENT_CONNECT);
+    }
+    else {
+      $this->data_socket = @stream_socket_client('tcp://' . $this->ip . ':' . $port, $errno, $errstr, $this->options['socket timeout'], STREAM_CLIENT_CONNECT, $this->options['stream context']);
+    }
+
+    if (!$this->data_socket) {
+      $this->setError(__METHOD__, t('Failed to open socket for data connection: @error_message', array('@error_message' => $errstr)));
+      return FALSE;
+    }
+
+    stream_set_timeout($this->data_socket, $this->options['socket timeout']);
+    // Tell the server the command we'll be using for the data connection.
+    if (!$this->putCommand(__METHOD__, $command, array(DRUPAL_FTP_REPLY_DATA_CONNECTION_OPENED, DRUPAL_FTP_REPLY_DATA_CONNECTION_ALREADY_OPENED))) {
+      return FALSE;
+    }
+    // If encryption was turned on for data then attempt to enable it.
+    if ($this->encrypt_data_socket && (stream_socket_enable_crypto($this->data_socket, TRUE, $this->ssl_method) === FALSE)) {
+      $this->setError(__METHOD__, t('Failed to enable SSL encryption on the data connection.'));
+      return FALSE;
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Finishes a passive connection.
+   *
+   * @return
+   *   TRUE on success, otherwise FALSE.
+   */
+  protected function passiveFinish() {
+    // We must close our end of the connection first. This indicates to the
+    // server that we're finished and it can close its end.
+    fclose($this->data_socket);
+    if (!$this->getReply(array(DRUPAL_FTP_REPLY_DATA_CONNECTION_CLOSED), $error_result)) {
+      $this->setError(__METHOD__, $error_result);
+      return FALSE;
+    }
+    return TRUE;
+  }
+
+  /**
+   * Stores an error message.
+   *
+   * @param $method
+   *   A calling method.
+   * @param $error_message
+   *   An error message. Either an array or a string.
+   */
+  protected function setError($method, $error_message) {
+    if (!is_array($error_message)) {
+      $error_message = array('error message' => $error_message);
+    }
+
+    $error_message['method'] = $method;
+    $this->error[] = $error_message;
+  }
+
+  /**
+   * Provides end of line translation for ASCII mode file transfers.
+   *
+   * In ASCII mode lines are sent and received from a common EOL format (CRLF).
+   * The server (when client is uploading) or client (when downloading)
+   * transforms the common EOL into a suitable format for its respective
+   * operating system.
+   *
+   * When uploading this method only transforms LF's. CRLF's or CR's are left
+   * untouched. This method doesn't support old Mac OS, but does support
+   * Mac OS X.
+   *
+   * @param $line
+   *   A line to be translated.
+   * @param $to_server
+   *   Set to TRUE when uploading files, or FALSE when downloading.
+   *
+   * @return
+   *   A translated line.
+   */
+  protected function translateLine($line, $to_server) {
+    if ($to_server) {
+      // Skip CRLF, but not LF.
+      $line = preg_replace("/(?<!\r)\n/", "\r\n", $line);
+    }
+    else {
+      // Windows needs no translation since it uses the CRLF format.
+      if (!$this->os_is_windows) {
+        // For everything else CRLF is transformed to LF.
+        $line = str_replace("\r\n", "\n", $line);
+      }
+    }
+
+    return $line;
+  }
+
+  /**
+   * Returns a line from a reply on the control or data connection.
+   *
+   * @param $socket
+   *   A resource to a control or data socket.
+   *
+   * @return
+   *   A string on success, or FALSE on timeout or EOF.
+   */
+  protected function readLineFromReply($socket) {
+    $line = $this->read($socket, TRUE);
+
+    // Make sure CRLF exists.
+    if ($line === FALSE || strpos($line, "\r\n") === FALSE) {
+      return FALSE;
+    }
+    // Remove the ending CRLF characters. 
+    return rtrim($line);
+  }
+
+  /**
+   * Reads data from a stream.
+   *
+   * @param $fp
+   *   A resource to a stream.
+   * @param $get_line
+   *   If TRUE will read a single line. Defaults to FALSE.
+   *
+   * @return
+   *   A string on success, or FALSE on timeout or EOF.
+   */
+  protected function read($fp, $get_line = FALSE) {
+    if (!$get_line) {
+      $data = fread($fp, DRUPAL_FTP_BLOCK_SIZE);
+    }
+    else {
+      $data = fgets($fp, DRUPAL_FTP_BLOCK_SIZE);
+    }
+    // If fread() or fgets() returns an empty string it has either timed out or
+    // reached EOF.
+    return !empty($data) ? $data : FALSE;
+  }
+
+  /**
+   * Writes to a stream.
+   *
+   * @param $fp
+   *   A resource to a stream.
+   * @param $data
+   *   Data to write.
+   *
+   * @return
+   *   TRUE on success, otherwise FALSE.
+   */
+  protected function write($fp, $data) {
+    $len = strlen($data);
+    $write_failed = FALSE;
+
+    for($total_written = 0, $written = 0; $total_written < $len && !$write_failed; $total_written += $written) {
+      // Attempt to write in blocks specified by DRUPAL_FTP_BLOCK_SIZE. During
+      // certain conditions (slow connections, periods of high congestion)
+      // fwrite() may not be able to write the entire block at once. When this
+      // occurs PHP throws a notice stating that only x amount of bytes were
+      // successfully written. To handle this we loop until all data has been
+      // written.
+      if (!($written = @fwrite($fp, substr($data, $total_written, DRUPAL_FTP_BLOCK_SIZE), DRUPAL_FTP_BLOCK_SIZE))) {
+        // fwrite() timed out or the connection is broken.
+        $write_failed = TRUE;
+      }
+    }
+
+    return !$write_failed ? $total_written : FALSE;
+  }
+}
+
+class DrupalFTPException extends Exception {
+  public $arguments;
+
+  function __construct($message, $code = 0, $arguments = array()) {
+    parent::__construct($message, $code);
+    $this->arguments = $arguments;
+  }
+}
diff --git includes/filetransfer/filetransfer.inc includes/filetransfer/filetransfer.inc
index 4396de6..bfd73ee 100644
--- includes/filetransfer/filetransfer.inc
+++ includes/filetransfer/filetransfer.inc
@@ -210,11 +210,14 @@ abstract class FileTransfer {
     $this->createDirectory($destination);
     foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
       $relative_path = substr($filename, strlen($source));
-      if ($file->isDir()) {
-        $this->createDirectory($destination . $relative_path);
-      }
-      else {
-        $this->copyFile($file->getPathName(), $destination . $relative_path);
+      // Skip '.' and '..' directories.
+      if (strpos($relative_path, '/.') === FALSE) {
+        if ($file->isDir()) {
+          $this->createDirectory($destination . $relative_path);
+        }
+        else {
+          $this->copyFile($file->getPathName(), $destination . $relative_path);
+        }
       }
     }
   }
diff --git includes/filetransfer/ftp.inc includes/filetransfer/ftp.inc
index c3ee39b..b86acc0 100644
--- includes/filetransfer/ftp.inc
+++ includes/filetransfer/ftp.inc
@@ -5,7 +5,6 @@
  * Base class for FTP implementations.
  */
 abstract class FileTransferFTP extends FileTransfer {
-
   public function __construct($jail, $username, $password, $hostname, $port) {
     $this->username = $username;
     $this->password = $password;
@@ -31,8 +30,8 @@ abstract class FileTransferFTP extends FileTransfer {
     if (function_exists('ftp_connect')) {
       $class = 'FileTransferFTPExtension';
     }
-    elseif (ini_get('allow_url_fopen')) {
-      $class = 'FileTransferFTPWrapper';
+    elseif (function_exists('stream_socket_client')) {
+      $class = 'FileTransferDrupalFTP';
     }
     else {
       throw new FileTransferException('No FTP backend available.');
@@ -45,67 +44,94 @@ abstract class FileTransferFTP extends FileTransfer {
 /**
  * Connection class using the FTP URL wrapper.
  */
-class FileTransferFTPWrapper extends FileTransferFTP {
+class FileTransferDrupalFTP extends FileTransferFTP implements FileTransferChmodInterface {
 
-  function connect() {
-    $this->connection = 'ftp://' . urlencode($this->username) . ':' . urlencode($this->password) . '@' . $this->hostname . ':' . $this->port . '/';
-    if (!is_dir($this->connection)) {
-      throw new FileTransferException('FTP Connection failed.');
+  public function connect() {
+    $this->connection = new DrupalFTP();
+    if (!$this->connection->connect($this->hostname, $this->port)) {
+      throw new FileTransferException("Cannot connect to FTP Server, check settings");
+    }
+    if (!$this->connection->login($this->username, $this->password)) {
+      throw new FileTransferException("Cannot log in to FTP server. Check username and password");
     }
   }
 
-  function createDirectoryJailed($directory) {
-    if (!@drupal_mkdir($this->connection . $directory)) {
-      $exception = new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory));
-      throw $exception;
+  protected function copyFileJailed($source, $destination) {
+    if (!$this->connection->put($destination, $source, DRUPAL_FTP_BINARY)) {
+      throw new FileTransferException("Cannot move @source to @destination", NULL, array("@source" => $source, "@destination" => $destination));
     }
   }
 
-  function removeDirectoryJailed($directory) {
-    if (is_dir($this->connection . $directory)) {
-      $dh = opendir($this->connection . $directory);
-      while (($resource = readdir($dh)) !== FALSE) {
-        if ($resource == '.' || $resource == '..') {
-          continue;
-        }
-        $full_path = $directory . DIRECTORY_SEPARATOR . $resource;
-        if (is_file($this->connection . $full_path)) {
-          $this->removeFile($full_path);
-        }
-        elseif (is_dir($this->connection . $full_path)) {
-          $this->removeDirectory($full_path . '/');
-        }
-      }
-      closedir($dh);
-      if (!rmdir($this->connection . $directory)) {
-        $exception = new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory));
-        throw $exception;
-      }
+  protected function createDirectoryJailed($directory) {
+    if (!$this->connection->mkdir($directory)) {
+      throw new FileTransferException("Cannot create directory @directory", NULL, array("@directory" => $directory));
     }
   }
 
-  function copyFileJailed($source, $destination) {
-    if (!@copy($source, $this->connection . '/' . $destination)) {
-      throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination));
+  protected function removeDirectoryJailed($directory) {
+    $pwd = $this->connection->pwd();
+    if (!$this->connection->chdir($directory)) {
+      throw new FileTransferException("Unable to change to directory @directory", NULL, array('@directory' => $directory));
+    }
+    $list = $this->connection->nlist('.');
+    if (!$list) {
+      $list = array();
+    }
+    foreach ($list as $item){
+      if ($item == '.' || $item == '..') {
+        continue;
+      }
+      if ($this->connection->chdir($item)) {
+        $this->connection->cdup();
+        $this->removeDirectory($this->connection->pwd() . '/' . $item);
+      }
+      else {
+        $this->removeFile($this->connection->pwd() . '/' . $item);
+      }
+    }
+    $this->connection->chdir($pwd);
+    if (!$this->connection->rmdir($directory)) {
+      throw new FileTransferException("Unable to remove to directory @directory", NULL, array('@directory' => $directory));
     }
   }
 
-  function removeFileJailed($destination) {
-    if (!@unlink($this->connection . '/' .$destination)) {
-      throw new FileTransferException('Cannot remove @destination', NULL, array('@destination' => $destination));
+  protected function removeFileJailed($destination) {
+    if (!$this->connection->delete($destination)) {
+      throw new FileTransferException("Unable to remove to file @file", NULL, array('@file' => $destination));
     }
   }
 
-  function isDirectory($path) {
-    return is_dir($this->connection . '/' . $path);
+  public function isDirectory($path) {
+    $result = FALSE;
+    $curr = $this->connection->pwd();
+    if ($this->connection->chdir($path)) {
+      $result = TRUE;
+    }
+    $this->connection->chdir($curr);
+    return $result;
   }
 
   public function isFile($path) {
-    // This is stupid, but is_file and file_exists don't work! always return true.
-    return @fopen($this->connection . '/' . $path,'r');
+    return $this->connection->size($path) != -1;
+  }
+  function chmodJailed($path, $mode, $recursive) {
+    if (!$this->connection->chmod($mode, $path)) {
+      throw new FileTransferException("Unable to set permissions on %file", NULL, array ('%file' => $path));
+    }
+    if ($this->isDirectory($path) && $recursive) {
+      $filelist = $this->connection->nlist($path);
+      if (!$filelist) {
+        //empty directory - returns false
+        return;
+      }
+      foreach ($filelist as $file) {
+        $this->chmodJailed($file, $mode, $recursive);
+      }
+    }
   }
 }
 
+
 class FileTransferFTPExtension extends FileTransferFTP implements FileTransferChmodInterface {
 
   public function connect() {
