/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright (C) 2008,2009 Benjamin Otte <otte@gnome.org>
 *               2008,2009 Andreas Henriksson <andreas@fatal.se>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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 GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * Author: Benjamin Otte <otte@gnome.org>
 *         Andreas Henriksson <andreas@fatal.se>
 */


#include <config.h>

#include <errno.h> /* for strerror (EAGAIN) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gi18n.h>
#include <gio/gio.h>

#include "gvfsbackendftp.h"
#include "gvfsjobopenforread.h"
#include "gvfsjobread.h"
#include "gvfsjobseekread.h"
#include "gvfsjobopenforwrite.h"
#include "gvfsjobwrite.h"
#include "gvfsjobseekwrite.h"
#include "gvfsjobsetdisplayname.h"
#include "gvfsjobqueryinfo.h"
#include "gvfsjobqueryfsinfo.h"
#include "gvfsjobqueryattributes.h"
#include "gvfsjobenumerate.h"
#include "gvfsdaemonprotocol.h"
#include "gvfsdaemonutils.h"
#include "gvfskeyring.h"

#include "ParseFTPList.h"
#include "gvfsftpconnection.h"
#include "gvfsftpdircache.h"
#include "gvfsftpfile.h"
#include "gvfsftptask.h"

/*** GTK DOC ***/

/**
 * GVfsFtpMethod:
 * @G_VFS_FTP_METHOD_UNKNOWN: method has not yet been determined
 * @G_VFS_FTP_METHOD_EPSV: use EPSV command
 * @G_VFS_FTP_METHOD_PASV: use PASV command
 * @G_VFS_FTP_METHOD_PASV_ADDR: use PASV command, but ignore the returned 
 *                              address and only use it's port
 * @G_VFS_FTP_METHOD_EPRT: use the EPRT command
 * @G_VFS_FTP_METHOD_PORT: use the PORT command
 *
 * Possible methods for creating data connections to the ftp server. If the 
 * method is @G_VFS_FTP_METHOD_UNKNOWN, all possibilities are tried.
 */


/*
 * about filename interpretation in the ftp backend
 *
 * As GVfs composes paths using a slash character, we cannot allow a slash as
 * part of a basename. Other critical characters are \r \n and sometimes the
 * space. We therefore g_uri_escape_string() filenames by default and concatenate
 * paths using slashes. This should make GVfs happy.
 *
 * Luckily, TVFS (see RFC 3xxx for details) is a specification that does exactly
 * what we want. It disallows slashes, \r and \n in filenames, so we can happily
 * use it without the need to escape. We also can operate on full paths as our
 * paths exactly match those of a TVFS-using FTP server.
 */

/** CODE ***/

G_DEFINE_TYPE (GVfsBackendFtp, g_vfs_backend_ftp, G_VFS_TYPE_BACKEND)

static gboolean
gvfs_backend_ftp_determine_features (GVfsFtpTask *task)
{
  const struct {
    const char *        name;        	/* name of feature */
    GVfsFtpFeature      enable;                /* flags to enable with this feature */
  } features[] = {
    { "MDTM", G_VFS_FTP_FEATURE_MDTM },
    { "SIZE", G_VFS_FTP_FEATURE_SIZE },
    { "TVFS", G_VFS_FTP_FEATURE_TVFS },
    { "EPRT", G_VFS_FTP_FEATURE_EPRT },
    { "EPSV", G_VFS_FTP_FEATURE_EPSV },
    { "UTF8", G_VFS_FTP_FEATURE_UTF8 },
    { "AUTH TLS", G_VFS_FTP_FEATURE_AUTH_TLS },
    { "AUTH SSL", G_VFS_FTP_FEATURE_AUTH_SSL },
  };
  guint i, j;
  char **reply;

  if (!g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "FEAT"))
    return FALSE;

  task->backend->features = 0;
  for (i = 1; reply[i]; i++)
    {
      char *feature = reply[i];

      if (feature[0] != ' ')
        continue;
      feature++;

      /* There should just be one space according to RFC2389, but some
       * servers have more so we deal with any number of leading spaces.
       */
      while (g_ascii_isspace (feature[0]))
        feature++;

      for (j = 0; j < G_N_ELEMENTS (features); j++)
        {
          if (g_ascii_strcasecmp (feature, features[j].name) == 0)
            {
              g_debug ("# feature %s supported\n", features[j].name);
              task->backend->features |= 1 << features[j].enable;
            }
        }
    }

  g_strfreev (reply);

  return TRUE;
}

static void
gvfs_backend_ftp_determine_site_features (GVfsFtpTask *task)
{
  const struct {
    const char *        name;        	/* name of feature */
    GVfsFtpFeature      enable;         /* flags to enable with this feature */
  } features[] = {
    { "CHMOD", G_VFS_FTP_FEATURE_CHMOD },
    { "CHGRP", G_VFS_FTP_FEATURE_CHGRP },
  };
  guint i, j;
  char **reply;

  if (g_vfs_ftp_task_is_in_error (task))
    return;

  if (!g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "SITE HELP"))
    {
      g_vfs_ftp_task_clear_error (task);
      return;
    }

  if (g_strv_length (reply) == 1)
    {
      /* vsftpd returns just a single string, so we split it into multiple
       * and then treat it like a regular reply */
      char **split;

      split = g_strsplit (reply[0], " ", -1);
      g_strfreev (reply);
      reply = split;
    }

  for (i = 1; reply[i]; i++)
    {
      char *feature = reply[i];

      while (g_ascii_isspace (feature[0]))
        feature++;

      for (j = 0; j < G_N_ELEMENTS (features); j++)
        {
          if (g_ascii_strcasecmp (feature, features[j].name) == 0)
            {
              g_debug ("# site feature %s supported\n", features[j].name);
              task->backend->features |= 1 << features[j].enable;
            }
        }
    }

  g_strfreev (reply);

  return;
}

static void
gvfs_backend_ftp_determine_system (GVfsFtpTask *task)
{
  static const struct {
    const char *  id;
    GVfsFtpSystem system;
    const char *  debug_name;
  } known_systems[] = {
    /* NB: the first entry that matches is taken, so order matters */
    { "UNIX ", G_VFS_FTP_SYSTEM_UNIX, "Unix"},
    { "WINDOWS_NT ", G_VFS_FTP_SYSTEM_WINDOWS, "Windows NT" }
  };
  guint i;
  char *system_name;
  char **reply;

  if (g_vfs_ftp_task_is_in_error (task))
    return;

  if (!g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "SYST"))
    {
      g_vfs_ftp_task_clear_error (task);
      return;
    }

  system_name = reply[0] + 4;
  for (i = 0; i < G_N_ELEMENTS (known_systems); i++)
    {
      if (g_ascii_strncasecmp (system_name,
                               known_systems[i].id,
                	       strlen (known_systems[i].id)) == 0)
        {
          task->backend->system = known_systems[i].system;
          g_debug ("# system is %s\n", known_systems[i].debug_name);
          break;
        }
    }

  g_strfreev (reply);
}

static void
gvfs_backend_ftp_setup_directory_cache (GVfsBackendFtp *ftp)
{
  if (ftp->system == G_VFS_FTP_SYSTEM_UNIX)
    ftp->dir_funcs = &g_vfs_ftp_dir_cache_funcs_unix;
  else
    ftp->dir_funcs = &g_vfs_ftp_dir_cache_funcs_default;

  ftp->dir_cache = g_vfs_ftp_dir_cache_new (ftp->dir_funcs);
}

/* This parses a file according to RFC 959 Appendix II:
 *
 * the server should return a line of the form:
 *   257<space>"<directory-name>"<space><commentary>
 * That is, the server will tell the user what string to use when
 * referring to the created  directory.  The directory name can
 * contain any character; embedded double-quotes should be escaped by
 * double-quotes (the "quote-doubling" convention).
 *
 * Note that we actually accept (as regexp):
 * ....\s*"<directory-name>".*
 * so we're quite a bit more lax.
 *
 * If parsing this name fails, this function returns NULL.
 */
static GVfsFtpFile *
g_vfs_backend_create_file_from_reply (GVfsBackendFtp *ftp, const char *name)
{
  GVfsFtpFile *file;
  const char *quote;
  GString *unescaped;

  /* strip leading spaces */
  while (g_ascii_isspace (*name))
    name++;

  if (*name++ != '"')
    {
      g_debug ("# filename didn't start with a quote\n");
      return NULL;
    }

  unescaped = g_string_new (NULL);
  while ((quote = strchr (name, '"')))
    {
      g_string_append_len (unescaped, name, quote-name);
      name = quote + 2;
      if (quote[1] == '"')
        g_string_append_c (unescaped, '"');
      else
        break;
    }

  /* NB: The name variable may point into bad space here */
  if (quote == NULL)
    {
      g_debug ("# filename didn't end with a quote\n");
      g_string_free (unescaped, TRUE);
      return NULL;
    }
  else if (!g_ascii_isspace (quote[1]))
    {
      /* issue a warning here, just so we can guess right from debug logs */
      g_debug ("# warning: filename didn't contain space after final quote\n");
    }

  file = g_vfs_ftp_file_new_from_ftp (ftp, unescaped->str);

  g_string_free (unescaped, TRUE);
  return file;
}

static void
gvfs_backend_ftp_determine_default_location (GVfsFtpTask *task)
{
  char **reply;
  GVfsFtpFile *home;

  if (g_vfs_ftp_task_is_in_error (task))
    return;

  if (!g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "PWD"))
    {
      g_vfs_ftp_task_clear_error (task);
      return;
    }

  home = g_vfs_backend_create_file_from_reply (task->backend, reply[0] + 4);
  if (home != NULL)
    {
      g_vfs_backend_set_default_location (G_VFS_BACKEND (task->backend),
                                          g_vfs_ftp_file_get_gvfs_path (home));
      g_vfs_ftp_file_free (home);
    }

  g_strfreev (reply);
}

/*** COMMON FUNCTIONS WITH SPECIAL HANDLING ***/

static gboolean
g_vfs_ftp_task_cd (GVfsFtpTask *task, const GVfsFtpFile *file)
{
  guint response = g_vfs_ftp_task_send (task,
                			G_VFS_FTP_PASS_550,
                			"CWD %s", g_vfs_ftp_file_get_ftp_path (file));
  if (response == 550)
    {
      g_set_error_literal (&task->error,
                           G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY,
                	   _("The file is not a directory"));
      response = 0;
    }

  return response != 0;
}

static gboolean
g_vfs_ftp_task_try_cd (GVfsFtpTask *task, const GVfsFtpFile *file)
{
  if (g_vfs_ftp_task_is_in_error (task))
    return FALSE;

  if (!g_vfs_ftp_task_cd (task, file))
    {
      g_vfs_ftp_task_clear_error (task);
      return FALSE;
    }
 
  return TRUE;
}

/*** default directory reading ***/

static void
g_vfs_backend_ftp_finalize (GObject *object)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (object);

  if (ftp->addr)
    g_object_unref (ftp->addr);

  /* has been cleared on unmount */
  g_assert (ftp->queue == NULL);
  g_cond_free (ftp->cond);
  g_mutex_free (ftp->mutex);

  g_free (ftp->user);
  g_free (ftp->password);

  if (G_OBJECT_CLASS (g_vfs_backend_ftp_parent_class)->finalize)
    (*G_OBJECT_CLASS (g_vfs_backend_ftp_parent_class)->finalize) (object);
}

static void
g_vfs_backend_ftp_init (GVfsBackendFtp *ftp)
{
  ftp->mutex = g_mutex_new ();
  ftp->cond = g_cond_new ();
}

static void
do_mount (GVfsBackend *backend,
          GVfsJobMount *job,
          GMountSpec *mount_spec,
          GMountSource *mount_source,
          gboolean is_automount)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  char *prompt = NULL;
  char *username;
  char *password;
  char *display_name;
  gboolean aborted, anonymous, break_on_fail;
  GPasswordSave password_save = G_PASSWORD_SAVE_NEVER;
  GNetworkAddress *addr;
  guint port;

restart:
  task.conn = g_vfs_ftp_connection_new (ftp->addr, task.cancellable, &task.error);
  /* fail fast here. No need to ask for a password if we know the hostname
   * doesn't exist or the given host/port doesn't have an ftp server running.
   */
  if (task.conn == NULL)
    {
      g_vfs_ftp_task_done (&task);
      return;
    }

  /* send pre-login commands */
  g_vfs_ftp_task_receive (&task, 0, NULL);
  if (!g_vfs_backend_ftp_uses_workaround (ftp, G_VFS_FTP_WORKAROUND_FEAT_AFTER_LOGIN) &&
      !gvfs_backend_ftp_determine_features (&task))
    {
      g_vfs_backend_ftp_use_workaround (ftp, G_VFS_FTP_WORKAROUND_FEAT_AFTER_LOGIN);
      ftp->features = 0;
      g_vfs_ftp_task_clear_error (&task);
      if (!g_vfs_ftp_connection_is_usable (task.conn))
        {
          g_vfs_ftp_connection_free (task.conn);
          goto restart;
        }
    }

  addr = G_NETWORK_ADDRESS (ftp->addr);
  g_object_ref (addr);
  port = g_network_address_get_port (addr);
  username = NULL;
  password = NULL;
  break_on_fail = FALSE;
 
  if (ftp->user != NULL && strcmp (ftp->user, "anonymous") == 0)
    {
      anonymous = TRUE;
      break_on_fail = TRUE;
      goto try_login;
    }
 
  if (g_vfs_keyring_lookup_password (ftp->user,
                		     g_network_address_get_hostname (addr),
                		     NULL,
                		     "ftp",
                		     NULL,
                		     NULL,
                		     port == 21 ? 0 : port,
                		     &username,
                		     NULL,
                		     &password) &&
      username != NULL &&
      password != NULL)
    {
      anonymous = FALSE;
      goto try_login;
    }
  g_free (username);
  g_free (password);
  username = NULL;
  password = NULL;

  while (TRUE)
    {
      GAskPasswordFlags flags;
      if (prompt == NULL)
        {
          if (ftp->has_initial_user)
            /* Translators: the first %s is the username, the second the host name */
            prompt = g_strdup_printf (_("Enter password for FTP as %s on %s"), ftp->user, ftp->host_display_name);
          else
            /* translators: %s here is the hostname */
            prompt = g_strdup_printf (_("Enter password for FTP on %s"), ftp->host_display_name);
        }
         
      flags = G_ASK_PASSWORD_NEED_PASSWORD;
       
      if (!ftp->has_initial_user)
        flags |= G_ASK_PASSWORD_NEED_USERNAME | G_ASK_PASSWORD_ANONYMOUS_SUPPORTED;
     
      if (g_vfs_keyring_is_available ())
        flags |= G_ASK_PASSWORD_SAVING_SUPPORTED;
     
      if (!g_mount_source_ask_password (
                	mount_source,
                        prompt,
                	ftp->user,
                        NULL,
                        flags,
                        &aborted,
                        &password,
                        &username,
                        NULL,
                	&anonymous,
                        &password_save) ||
          aborted)
        {
          g_set_error_literal (&task.error, G_IO_ERROR,
                               aborted ? G_IO_ERROR_FAILED_HANDLED : G_IO_ERROR_PERMISSION_DENIED,
                	       _("Password dialog cancelled"));
          break;
        }

      /* NEED_USERNAME wasn't set */
      if (ftp->has_initial_user)
        {
          g_free (username);
          username = g_strdup (ftp->user);
        }
     
try_login:
      g_free (ftp->user);
      g_free (ftp->password);
      if (anonymous)
        {
          g_free (username);
          g_free (password);
          ftp->user = g_strdup ("anonymous");
          ftp->password = g_strdup ("");
          if (g_vfs_ftp_task_login (&task, "anonymous", "") != 0)
            break;
          g_free (ftp->user);
          g_free (ftp->password);
          ftp->user = NULL;
          ftp->password = NULL;
        }
      else
        {
          ftp->user = username ? username : g_strdup ("");
          ftp->password = password;
          if (g_vfs_ftp_task_login (&task, ftp->user, ftp->password) != 0)
            break;
        }
     
      if (break_on_fail ||
          !g_error_matches (task.error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
        break;

      g_vfs_ftp_task_clear_error (&task);
    }
 
  /* send post-login commands */
  if (g_vfs_backend_ftp_uses_workaround (ftp, G_VFS_FTP_WORKAROUND_FEAT_AFTER_LOGIN) &&
      !g_vfs_ftp_task_is_in_error (&task))
    {
      if (!gvfs_backend_ftp_determine_features (&task))
        {
          g_vfs_ftp_task_clear_error (&task);
          ftp->features = G_VFS_FTP_FEATURES_DEFAULT;
        }
    }
  g_vfs_ftp_task_setup_connection (&task);
  gvfs_backend_ftp_determine_system (&task);
  gvfs_backend_ftp_determine_site_features (&task);
  gvfs_backend_ftp_setup_directory_cache (ftp);
  gvfs_backend_ftp_determine_default_location (&task);

  /* Save the address of the current connection, so that for future connections,
   * we are sure to connect to the same machine.
   * The idea here is to avoid using mirrors that have a different state, which
   * might cause Heisenbugs.
   */
  if (!g_vfs_ftp_task_is_in_error (&task))
    {
      g_object_unref (ftp->addr);
      ftp->addr = G_SOCKET_CONNECTABLE (g_vfs_ftp_connection_get_address (task.conn, &task.error));
      if (ftp->addr == NULL)
        {
          g_debug ("# error querying remote address: %s\nUsing original address instead.\n", task.error->message);
          g_vfs_ftp_task_clear_error (&task);
          ftp->addr = g_object_ref (addr);
        }
    }

  if (g_vfs_ftp_task_is_in_error (&task))
    {
      g_vfs_ftp_connection_free (task.conn);
      task.conn = NULL;
      g_vfs_ftp_task_done (&task);
      g_object_unref (addr);
      return;
    }

  if (prompt && !anonymous)
    {
      /* a prompt was created, so we have to save the password */
      g_vfs_keyring_save_password (ftp->user,
                                   g_network_address_get_hostname (addr),
                                   NULL,
                                   "ftp",
                                   NULL,
                                   NULL,
                                   port == 21 ? 0 : port,
                                   ftp->password,
                                   password_save);
      g_free (prompt);
    }

  mount_spec = g_mount_spec_new ("ftp");
  g_mount_spec_set (mount_spec, "host", g_network_address_get_hostname (addr));
  if (port != 21)
    {
      char *port_str = g_strdup_printf ("%u", port);
      g_mount_spec_set (mount_spec, "port", port_str);
      g_free (port_str);
    }

  if (ftp->has_initial_user)
    g_mount_spec_set (mount_spec, "user", ftp->user);
     
  if (g_str_equal (ftp->user, "anonymous"))
    display_name = g_strdup_printf (_("FTP on %s"), ftp->host_display_name);
  else
    {
      /* Translators: the first %s is the username, the second the host name */
      display_name = g_strdup_printf (_("FTP as %s on %s"), ftp->user, ftp->host_display_name);
    }
  g_vfs_backend_set_mount_spec (backend, mount_spec);
  g_mount_spec_unref (mount_spec);

  g_vfs_backend_set_display_name (backend, display_name);
  g_free (display_name);
  g_vfs_backend_set_icon_name (backend, "folder-remote");

  ftp->connections = 1;
  ftp->max_connections = G_MAXUINT;
  ftp->queue = g_queue_new ();
 
  g_object_unref (addr);
  g_vfs_ftp_task_done (&task);
}

static gboolean
try_mount (GVfsBackend *backend,
          GVfsJobMount *job,
          GMountSpec *mount_spec,
          GMountSource *mount_source,
          gboolean is_automount)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  const char *host, *port_str;
  guint port;

  host = g_mount_spec_get (mount_spec, "host");
  if (host == NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job),
                       G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                       _("No hostname specified"));
      return TRUE;
    }
  port_str = g_mount_spec_get (mount_spec, "port");
  if (port_str == NULL)
    {
      port = 21;
    }
  else
    {
      /* FIXME: error handling? */
      port = strtoul (port_str, NULL, 10);
    }

  ftp->addr = g_network_address_new (host, port);
  ftp->user = g_strdup (g_mount_spec_get (mount_spec, "user"));
  ftp->has_initial_user = ftp->user != NULL;
  if (port == 21)
    ftp->host_display_name = g_strdup (host);
  else
    ftp->host_display_name = g_strdup_printf ("%s:%u", host, port);

  return FALSE;
}

static void
do_unmount (GVfsBackend *   backend,
            GVfsJobUnmount *job,
            GMountUnmountFlags flags,
            GMountSource *mount_source)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpConnection *conn;

  g_mutex_lock (ftp->mutex);
  while ((conn = g_queue_pop_head (ftp->queue)))
    {
      /* FIXME: properly quit */
      g_vfs_ftp_connection_free (conn);
    }
  g_queue_free (ftp->queue);
  ftp->queue = NULL;
  g_cond_broadcast (ftp->cond);
  g_mutex_unlock (ftp->mutex);
  g_vfs_job_succeeded (G_VFS_JOB (job));
}

/* NB: sets EPERM if file exists, ENOENT if not - an error will _always_ be set */
static void
error_550_permission_or_not_found (GVfsFtpTask *task, gpointer file)
{
  GFileInfo *info;
  
  info = g_vfs_ftp_dir_cache_lookup_file (task->backend->dir_cache, task, file, FALSE);
  if (info)
    {
      g_object_unref (info);
      g_set_error_literal (&task->error,
                           G_IO_ERROR,
                           G_IO_ERROR_PERMISSION_DENIED,
                           _("Insufficient permissions"));
    }
  else
    {
      /* clear potential error from file lookup above */
      g_vfs_ftp_task_clear_error (task);
      g_set_error_literal (&task->error,
                           G_IO_ERROR,
                           G_IO_ERROR_NOT_FOUND,
                           _("File does not exist"));
    }
}

static void
error_550_exists (GVfsFtpTask *task, gpointer file)
{
  GFileInfo *info;
  
  info = g_vfs_ftp_dir_cache_lookup_file (task->backend->dir_cache, task, file, FALSE);
  if (info)
    {
      g_object_unref (info);
      g_set_error_literal (&task->error,
                           G_IO_ERROR,
                           G_IO_ERROR_EXISTS,
                           _("Target file already exists"));
    }
  else
    {
      /* clear potential error from file lookup above */
      g_vfs_ftp_task_clear_error (task);
    }
}

static void
error_550_is_directory (GVfsFtpTask *task, gpointer file)
{
  GFileInfo *info;
  
  /* need to resolve symlinks here to know if a link is a dir or not */
  info = g_vfs_ftp_dir_cache_lookup_file (task->backend->dir_cache, task, file, TRUE);
  if (info && g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
    {
      g_object_unref (info);
      g_set_error_literal (&task->error, G_IO_ERROR,
                           G_IO_ERROR_IS_DIRECTORY,
                           _("File is directory"));
      return;
    }
  else if (info)
    g_object_unref (info);
  
  /* clear potential error from file lookup above */
  g_vfs_ftp_task_clear_error (task);
}

static void
error_550_parent_not_found (GVfsFtpTask *task, gpointer file)
{
  GVfsFtpFile *dir = g_vfs_ftp_file_new_parent (file);

  if (!g_vfs_ftp_file_equal (file, dir) && !g_vfs_ftp_task_try_cd (task, dir))
    {
      /* Yes, this is a weird error for a missing parent directory */
      g_set_error_literal (&task->error, G_IO_ERROR,
                           G_IO_ERROR_NOT_FOUND,
                           _("No such file or directory"));
    }

  g_vfs_ftp_file_free (dir);
}

static void
do_open_for_read (GVfsBackend *backend,
                  GVfsJobOpenForRead *job,
                  const char *filename)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;
  static const GVfsFtpErrorFunc open_read_handlers[] = { error_550_is_directory, 
                                                         error_550_permission_or_not_found, 
                                                         NULL };

  g_vfs_ftp_task_setup_data_connection (&task);
  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);

  g_vfs_ftp_task_send_and_check (&task,
                                 G_VFS_FTP_PASS_100 | G_VFS_FTP_FAIL_200,
                                 open_read_handlers,
                                 file,
                                 NULL,
                                 "RETR %s", g_vfs_ftp_file_get_ftp_path (file));
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_open_data_connection (&task);

  if (!g_vfs_ftp_task_is_in_error (&task))
    {
      /* don't push the connection back, it's our handle now */
      GVfsFtpConnection *conn = g_vfs_ftp_task_take_connection (&task);

      g_vfs_job_open_for_read_set_handle (job, conn);
      g_vfs_job_open_for_read_set_can_seek (job, FALSE);
    }

  g_vfs_ftp_task_done (&task);
}

static void
do_close_read (GVfsBackend *     backend,
               GVfsJobCloseRead *job,
               GVfsBackendHandle handle)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpConnection *conn = handle;

  g_vfs_ftp_task_give_connection (&task, conn);
  g_vfs_ftp_task_close_data_connection (&task);
  g_vfs_ftp_task_receive (&task, 0, NULL);

  g_vfs_ftp_task_done (&task);
}

static void
do_read (GVfsBackend *     backend,
         GVfsJobRead *     job,
         GVfsBackendHandle handle,
         char *            buffer,
         gsize             bytes_requested)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpConnection *conn = handle;
  GInputStream *input;
  gssize n_bytes;

  input = g_io_stream_get_input_stream (g_vfs_ftp_connection_get_data_stream (conn));
  n_bytes = g_input_stream_read (input,
                                 buffer,
                                 bytes_requested,
                                 task.cancellable,
                                 &task.error);

  if (n_bytes >= 0)
    g_vfs_job_read_set_size (job, n_bytes);

  g_vfs_ftp_task_done (&task);
}

static void
do_start_write (GVfsFtpTask *task,
                GFileCreateFlags flags,
                const char *format,
                ...) G_GNUC_PRINTF (3, 4);
static void
do_start_write (GVfsFtpTask *task,
                GFileCreateFlags flags,
                const char *format,
                ...)
{
  va_list varargs;

  /* FIXME: can we honour the flags? */

  g_vfs_ftp_task_setup_data_connection (task);

  va_start (varargs, format);
  g_vfs_ftp_task_sendv (task,
                        G_VFS_FTP_PASS_100 | G_VFS_FTP_FAIL_200,
                        NULL,
                        format,
                        varargs);
  va_end (varargs);

  g_vfs_ftp_task_open_data_connection (task);

  if (!g_vfs_ftp_task_is_in_error (task))
    {
      /* don't push the connection back, it's our handle now */
      GVfsFtpConnection *conn = g_vfs_ftp_task_take_connection (task);
      g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (task->job), conn);
      g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (task->job), FALSE);
    }
}

static void
do_create (GVfsBackend *backend,
           GVfsJobOpenForWrite *job,
           const char *filename,
           GFileCreateFlags flags)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GFileInfo *info;
  GVfsFtpFile *file;

  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  info = g_vfs_ftp_dir_cache_lookup_file (ftp->dir_cache, &task, file, FALSE);
  if (info)
    {
      g_object_unref (info);
      g_set_error_literal (&task.error,
                           G_IO_ERROR,
                	   G_IO_ERROR_EXISTS,
                	   _("Target file already exists"));
      g_vfs_ftp_file_free (file);
      g_vfs_ftp_task_done (&task);
      return;
    }
  do_start_write (&task, flags, "STOR %s", g_vfs_ftp_file_get_ftp_path (file));
  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_done (&task);
}

static void
do_append (GVfsBackend *backend,
           GVfsJobOpenForWrite *job,
           const char *filename,
           GFileCreateFlags flags)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;

  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  do_start_write (&task, flags, "APPE %s", g_vfs_ftp_file_get_ftp_path (file));
  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_done (&task);
}

static void
do_replace (GVfsBackend *backend,
            GVfsJobOpenForWrite *job,
            const char *filename,
            const char *etag,
            gboolean make_backup,
            GFileCreateFlags flags)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;

  if (make_backup)
    {
      /* FIXME: implement! */
      g_set_error_literal (&task.error,
                           G_IO_ERROR,
                           G_IO_ERROR_CANT_CREATE_BACKUP,
                           _("backups not supported yet"));
      g_vfs_ftp_task_done (&task);
      return;
    }

  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  do_start_write (&task, flags, "STOR %s", g_vfs_ftp_file_get_ftp_path (file));
  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_done (&task);
}

static void
do_close_write (GVfsBackend *backend,
                GVfsJobCloseWrite *job,
                GVfsBackendHandle handle)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));

  g_vfs_ftp_task_give_connection (&task, handle);

  g_vfs_ftp_task_close_data_connection (&task);
  g_vfs_ftp_task_receive (&task, 0, NULL);

  g_vfs_ftp_task_done (&task);
}

static void
do_write (GVfsBackend *backend,
          GVfsJobWrite *job,
          GVfsBackendHandle handle,
          char *buffer,
          gsize buffer_size)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpConnection *conn = handle;
  GOutputStream *output;
  gssize n_bytes;

  output = g_io_stream_get_output_stream (g_vfs_ftp_connection_get_data_stream (conn));

  /* FIXME: use write_all here? */
  n_bytes = g_output_stream_write (output,
                                   buffer,
                                   buffer_size,
                                   task.cancellable,
                                   &task.error);
           
  if (n_bytes >= 0)
    g_vfs_job_write_set_written_size (job, n_bytes);

  g_vfs_ftp_task_done (&task);
}

static void
do_query_info (GVfsBackend *backend,
               GVfsJobQueryInfo *job,
               const char *filename,
               GFileQueryInfoFlags query_flags,
               GFileInfo *info,
               GFileAttributeMatcher *matcher)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;
  GFileInfo *real;
 
  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  real = g_vfs_ftp_dir_cache_lookup_file (ftp->dir_cache,
                                          &task,
                                          file,
                                          query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? FALSE : TRUE);

  if (real)
    {
      g_file_info_copy_into (real, info);
      g_object_unref (real);
    }
  else if (!g_vfs_ftp_task_is_in_error (&task))
    {
      g_set_error_literal (&task.error,
                           G_IO_ERROR,
                           G_IO_ERROR_NOT_FOUND,
                           _("File doesn't exist"));
    }

  g_vfs_ftp_task_done (&task);
  g_vfs_ftp_file_free (file);
}

static gboolean
try_query_settable_attributes (GVfsBackend *backend,
			       GVfsJobQueryAttributes *job,
			       const char *filename)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GFileAttributeInfoList *list;

  if (!g_vfs_backend_ftp_has_feature (ftp, G_VFS_FTP_FEATURE_CHMOD))
    {    
      g_vfs_job_failed (G_VFS_JOB (job),
                        G_IO_ERROR,
                        G_IO_ERROR_NOT_SUPPORTED,
                        _("Operation unsupported"));
      return TRUE;
    }    

  list = g_file_attribute_info_list_new ();

  g_file_attribute_info_list_add (list,
				  G_FILE_ATTRIBUTE_UNIX_MODE,
				  G_FILE_ATTRIBUTE_TYPE_UINT32,
				  G_FILE_ATTRIBUTE_INFO_COPY_WITH_FILE |
				  G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED);
  
  g_vfs_job_query_attributes_set_list (job, list);
  g_vfs_job_succeeded (G_VFS_JOB (job));
  g_file_attribute_info_list_unref (list);
  
  return TRUE;
}

static void
do_set_attribute (GVfsBackend *backend,
                  GVfsJobSetAttribute *job,
                  const char *filename,
                  const char *attribute,
                  GFileAttributeType type,
                  gpointer value_p,
                  GFileQueryInfoFlags flags)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;

  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);

  if (strcmp (attribute, G_FILE_ATTRIBUTE_UNIX_MODE) == 0)
    {
      if (type != G_FILE_ATTRIBUTE_TYPE_UINT32) 
        {
          g_set_error_literal (&task.error,
                               G_IO_ERROR,
                               G_IO_ERROR_INVALID_ARGUMENT,
                               _("Invalid attribute type (uint32 expected)"));
        }
      else if (!g_vfs_backend_ftp_has_feature (ftp, G_VFS_FTP_FEATURE_CHMOD))
        {
          g_set_error_literal (&task.error,
                               G_IO_ERROR,
                               G_IO_ERROR_NOT_SUPPORTED,
                               _("Operation unsupported"));
        }
      else
        {
          guint mode = *(guint32 *)value_p;

          if (g_vfs_ftp_task_send (&task,
                                   0,
                                   "SITE CHMOD %04o %s",
                                   mode,
                                   g_vfs_ftp_file_get_ftp_path (file)))
            {
              g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
            }
        }
    }
  else
    {
      g_set_error_literal (&task.error,
                           G_IO_ERROR,
                           G_IO_ERROR_NOT_SUPPORTED,
                           _("Operation unsupported"));
    }

  g_vfs_ftp_task_done (&task);
  g_vfs_ftp_file_free (file);
}

static void
do_enumerate (GVfsBackend *backend,
              GVfsJobEnumerate *job,
              const char *dirname,
              GFileAttributeMatcher *matcher,
              GFileQueryInfoFlags query_flags)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *dir;
  GList *list, *walk;

  dir = g_vfs_ftp_file_new_from_gvfs (ftp, dirname);
  list = g_vfs_ftp_dir_cache_lookup_dir (ftp->dir_cache,
                                         &task,
                                         dir,
                                         TRUE,
                                         query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? FALSE : TRUE);
  g_vfs_ftp_file_free (dir);
  if (g_vfs_ftp_task_is_in_error (&task))
    {
      g_assert (list == NULL);
      g_vfs_ftp_task_done (&task);
      return;
    }

  g_vfs_ftp_task_done (&task);

  for (walk = list; walk; walk = walk->next)
    {
      GFileInfo *matched_info = g_file_info_new ();

      /* copy into a new GFileInfo as g_vfs_job_enumerate_add_info()
       * modifies the given GFileInfo */
      g_file_info_copy_into (walk->data, matched_info);
      g_vfs_job_enumerate_add_info (job, matched_info);
      g_object_unref (matched_info);
      g_object_unref (walk->data);
    }
  g_vfs_job_enumerate_done (job);

  g_list_free (list);
}

static void
do_set_display_name (GVfsBackend *backend,
                     GVfsJobSetDisplayName *job,
                     const char *filename,
                     const char *display_name)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *original, *dir, *now;

  original = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  dir = g_vfs_ftp_file_new_parent (original);
  now = g_vfs_ftp_file_new_child (dir, display_name, &task.error);
  g_vfs_ftp_task_send (&task,
                       G_VFS_FTP_PASS_300 | G_VFS_FTP_FAIL_200,
                       "RNFR %s", g_vfs_ftp_file_get_ftp_path (original));
  g_vfs_ftp_task_send (&task,
                       0,
                       "RNTO %s", g_vfs_ftp_file_get_ftp_path (now));

  /* FIXME: parse result of RNTO here? */
  g_vfs_job_set_display_name_set_new_path (job, g_vfs_ftp_file_get_gvfs_path (now));
  g_vfs_ftp_dir_cache_purge_dir (ftp->dir_cache, dir);
  g_vfs_ftp_file_free (now);
  g_vfs_ftp_file_free (dir);
  g_vfs_ftp_file_free (original);

  g_vfs_ftp_task_done (&task);
}

static void
do_delete (GVfsBackend *backend,
           GVfsJobDelete *job,
           const char *filename)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;
  guint response;

  /* We try file deletion first. If that fails, we try directory deletion.
   * The file-first-then-directory order has been decided by coin-toss. */
  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  response = g_vfs_ftp_task_send (&task,
                		  G_VFS_FTP_PASS_500,
                		  "DELE %s", g_vfs_ftp_file_get_ftp_path (file));
  if (G_VFS_FTP_RESPONSE_GROUP (response) == 5)
    {
      response = g_vfs_ftp_task_send (&task,
                		      G_VFS_FTP_PASS_550,
                		      "RMD %s", g_vfs_ftp_file_get_ftp_path (file));
      if (response == 550)
        {
          GList *list = g_vfs_ftp_dir_cache_lookup_dir (ftp->dir_cache,
                                                        &task,
                                                        file,
                                                        FALSE,
                                                        FALSE);
          if (list)
            {
              g_set_error_literal (&task.error,
                		   G_IO_ERROR,
                		   G_IO_ERROR_NOT_EMPTY,
                		   g_strerror (ENOTEMPTY));
              g_list_foreach (list, (GFunc) g_object_unref, NULL);
              g_list_free (list);
            }
          else
            {
              g_vfs_ftp_task_clear_error (&task);
              g_vfs_ftp_task_set_error_from_response (&task, response);
            }
        }
    }

  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_done (&task);
}

static void
do_make_directory (GVfsBackend *backend,
                   GVfsJobMakeDirectory *job,
                   const char *filename)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *file;
  static const GVfsFtpErrorFunc make_directory_handlers[] = { error_550_exists, error_550_parent_not_found, NULL };

  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
  g_vfs_ftp_task_send_and_check (&task,
                                 0,
                                 make_directory_handlers,
                                 file,
                                 NULL,
                                 "MKD %s", g_vfs_ftp_file_get_ftp_path (file));

  /* FIXME: Compare created file with name from server result to be sure
   * it's correct and otherwise fail. */
  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
  g_vfs_ftp_file_free (file);

  g_vfs_ftp_task_done (&task);
}

static void
do_move (GVfsBackend *backend,
         GVfsJobMove *job,
         const char *source,
         const char *destination,
         GFileCopyFlags flags,
         GFileProgressCallback progress_callback,
         gpointer progress_callback_data)
{
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *srcfile, *destfile;

  /* FIXME: what about G_FILE_COPY_NOFOLLOW_SYMLINKS and G_FILE_COPY_ALL_METADATA? */

  if (flags & G_FILE_COPY_BACKUP)
    {
      /* FIXME: implement? */
      g_set_error_literal (&task.error,
                           G_IO_ERROR,
                           G_IO_ERROR_CANT_CREATE_BACKUP,
                           _("backups not supported yet"));
      g_vfs_ftp_task_done (&task);
      return;
    }

  srcfile = g_vfs_ftp_file_new_from_gvfs (ftp, source);
  destfile = g_vfs_ftp_file_new_from_gvfs (ftp, destination);
  if (g_vfs_ftp_task_try_cd (&task, destfile))
    {
      char *basename = g_path_get_basename (source);
      GVfsFtpFile *real = g_vfs_ftp_file_new_child (destfile, basename, &task.error);

      g_free (basename);
      if (real == NULL)
        {
          goto out;
        }
      else
        {
          g_vfs_ftp_file_free (destfile);
          destfile = real;
        }
    }

  if (!(flags & G_FILE_COPY_OVERWRITE))
    {
      GFileInfo *info = g_vfs_ftp_dir_cache_lookup_file (ftp->dir_cache,
                                                         &task,
                                                         destfile,
                                                         FALSE);

      if (info)
        {
          g_object_unref (info);
          g_set_error_literal (&task.error,
                	       G_IO_ERROR,
                               G_IO_ERROR_EXISTS,
                	       _("Target file already exists"));
          goto out;
        }
    }

  g_vfs_ftp_task_send (&task,
                       G_VFS_FTP_PASS_300 | G_VFS_FTP_FAIL_200,
                       "RNFR %s", g_vfs_ftp_file_get_ftp_path (srcfile));
  g_vfs_ftp_task_send (&task,
                       0,
                       "RNTO %s", g_vfs_ftp_file_get_ftp_path (destfile));

  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, srcfile);
  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, destfile);
out:
  g_vfs_ftp_file_free (srcfile);
  g_vfs_ftp_file_free (destfile);

  g_vfs_ftp_task_done (&task);
}

static void
cancel_timer_cb (GCancellable *orig, GCancellable *to_cancel)
{
  g_cancellable_cancel (to_cancel);
}

static gboolean
cancel_cancellable (gpointer cancellable)
{
  g_cancellable_cancel (cancellable);
  return FALSE;
}

static gssize
ftp_output_stream_splice (GOutputStream *output,
                          GInputStream *input,
                          goffset total_size,
                          GFileProgressCallback progress_callback,
                          gpointer progress_callback_data,
                          GCancellable *cancellable,
                          GError **error)
{
  gssize n_read, n_written;
  gssize bytes_copied;
  char buffer[8192], *p;
  GCancellable *current, *timer_cancel;
  gulong cancel_cb_id;

  timer_cancel = NULL;
  cancel_cb_id = 0;

  bytes_copied = 0;
  if (progress_callback)
    {
      timer_cancel = g_cancellable_new ();
      cancel_cb_id = g_cancellable_connect (cancellable, 
                                            G_CALLBACK (cancel_timer_cb),
                                            timer_cancel,
                                            NULL);
    }
  current = cancellable;
  for (;;) 
    {
      n_read = g_input_stream_read (input, buffer, sizeof (buffer), current, error);
      if (n_read == -1)
        {
          if (g_cancellable_is_cancelled (timer_cancel) &&
              !g_cancellable_is_cancelled (cancellable))
            {
              g_cancellable_reset (timer_cancel);
              current = cancellable;
              g_clear_error (error);
              if (progress_callback)
                progress_callback (bytes_copied, total_size, progress_callback_data);
              continue;
            }
          else
            {
              bytes_copied = -1;
              break;
            }
          g_assert_not_reached();
        }
      if (n_read == 0)
        break;

      p = buffer;
      while (n_read > 0)
        {
          n_written = g_output_stream_write (output, p, n_read, current, error);
          if (n_written == -1)
            {
              if (g_cancellable_is_cancelled (timer_cancel) &&
                  !g_cancellable_is_cancelled (cancellable))
                {
                  g_cancellable_reset (timer_cancel);
                  current = cancellable;
                  g_clear_error (error);
                  if (progress_callback)
                    progress_callback (bytes_copied, total_size, progress_callback_data);
                  continue;
                }
              else
                {
                  bytes_copied = -1;
                  break;
                }
              g_assert_not_reached();
            }

          p += n_written;
          n_read -= n_written;
          bytes_copied += n_written;
          if (progress_callback && current != timer_cancel)
            {
              g_object_ref (timer_cancel);
              g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
                                          1,
                                          cancel_cancellable,
                                          timer_cancel,
                                          g_object_unref);
              current = timer_cancel;
            }
        }
    }

  if (timer_cancel != NULL)
    {
      /* no need to remove the timeout, it'll remove itself fine and we
       * don't get into races that way */
      g_cancellable_disconnect (cancellable, cancel_cb_id);
      g_object_unref (timer_cancel);
    }
  if (bytes_copied >= 0 && progress_callback)
    progress_callback (bytes_copied, total_size, progress_callback_data);

  return bytes_copied;
}

static void
do_pull_improve_error_message (GVfsFtpTask *task,
		               GFile       *dest,
                               gboolean     overwrite)
{
  GFileInfo *info;
  GFileType file_type;
  
  /* There was an error opening the source, try to set a good error for it.
   * Code taken from glib's gio/gfile.c:open_source_for_copy() function */

  if (g_vfs_ftp_task_error_matches (task, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY))
    {
      /* The source is a directory, don't fail with WOULD_RECURSE immediately, 
       * as that is less useful to the app. Better check for errors on the 
       * target instead. 
       */
      g_vfs_ftp_task_clear_error (task);
      
      info = g_file_query_info (dest, G_FILE_ATTRIBUTE_STANDARD_TYPE,
				G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
				task->cancellable, &task->error);
      if (info != NULL)
	{
	  file_type = g_file_info_get_file_type (info);
	  g_object_unref (info);
	  
	  if (overwrite)
	    {
	      if (file_type == G_FILE_TYPE_DIRECTORY)
		{
		  g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_WOULD_MERGE,
                                       _("Can't copy directory over directory"));
		  return;
		}
	      /* continue to would_recurse error */
	    }
	  else
	    {
	      g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_EXISTS,
                                   _("Target file already exists"));
	      return;
	    }
	}
      else
	{
	  /* Error getting info from target, return that error 
           * (except for NOT_FOUND, which is no error here) 
           */
	  if (!g_vfs_ftp_task_error_matches (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
            return;
          g_vfs_ftp_task_clear_error (task);
	}
      
      g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_WOULD_RECURSE,
                           _("Can't recursively copy directory"));
    }
}

static void
do_pull (GVfsBackend *         backend,
         GVfsJobPull *         job,
         const char *          source,
         const char *          local_path,
         GFileCopyFlags        flags,
         gboolean              remove_source,
         GFileProgressCallback progress_callback,
         gpointer              progress_callback_data)
{
  static const GVfsFtpErrorFunc open_read_handlers[] = { error_550_is_directory,
                                                         error_550_permission_or_not_found, 
                                                         NULL };
  GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
  GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
  GVfsFtpFile *src;
  GFile *dest;
  GInputStream *input;
  GOutputStream *output;
  goffset total_size = 0;
  
  src = g_vfs_ftp_file_new_from_gvfs (ftp, source);
  dest = g_file_new_for_path (local_path);

  if (progress_callback)
    {
      GFileInfo *info = g_vfs_ftp_dir_cache_lookup_file (ftp->dir_cache, &task, src, TRUE);
      if (info)
        {
          total_size = g_file_info_get_size (info);
          g_object_unref (info);
        }
    }

  g_vfs_ftp_task_setup_data_connection (&task);
  g_vfs_ftp_task_send_and_check (&task,
                                 G_VFS_FTP_PASS_100 | G_VFS_FTP_FAIL_200,
                                 open_read_handlers,
                                 src,
                                 NULL,
                                 "RETR %s", g_vfs_ftp_file_get_ftp_path (src));
  g_vfs_ftp_task_open_data_connection (&task);
  if (g_vfs_ftp_task_is_in_error (&task))
    {
      do_pull_improve_error_message (&task, dest, flags & G_FILE_COPY_OVERWRITE);
      goto out;
    }

  if (flags & G_FILE_COPY_OVERWRITE)
    output = G_OUTPUT_STREAM (g_file_replace (dest,
                                              NULL,
                                              flags & G_FILE_COPY_BACKUP ? TRUE : FALSE,
                                              G_FILE_CREATE_REPLACE_DESTINATION,
                                              task.cancellable,
                                              &task.error));
  else
    output = G_OUTPUT_STREAM (g_file_create (dest,
                                             0,
                                             task.cancellable,
                                             &task.error));
  if (output == NULL)
    {
      g_vfs_ftp_task_close_data_connection (&task);
      g_vfs_ftp_task_receive (&task, 0, NULL);
      goto out;
    }

  input = g_io_stream_get_input_stream (g_vfs_ftp_connection_get_data_stream (task.conn));
  ftp_output_stream_splice (output,
                            input,
                            total_size,
                            progress_callback,
                            progress_callback_data,
                            task.cancellable,
                            &task.error);
  g_vfs_ftp_task_close_data_connection (&task);
  g_vfs_ftp_task_receive (&task, 0, NULL);
  g_object_unref (output);

  if (remove_source)
    {
      g_vfs_ftp_task_send (&task,
                	   G_VFS_FTP_PASS_500,
                           "DELE %s", g_vfs_ftp_file_get_ftp_path (src));
    }

out:
  g_object_unref (dest);
  g_vfs_ftp_file_free (src);
  g_vfs_ftp_task_done (&task);
}

static void
g_vfs_backend_ftp_class_init (GVfsBackendFtpClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass);
 
  gobject_class->finalize = g_vfs_backend_ftp_finalize;

  backend_class->mount = do_mount;
  backend_class->try_mount = try_mount;
  backend_class->unmount = do_unmount;
  backend_class->open_for_read = do_open_for_read;
  backend_class->close_read = do_close_read;
  backend_class->read = do_read;
  backend_class->create = do_create;
  backend_class->append_to = do_append;
  backend_class->replace = do_replace;
  backend_class->close_write = do_close_write;
  backend_class->write = do_write;
  backend_class->query_info = do_query_info;
  backend_class->enumerate = do_enumerate;
  backend_class->set_display_name = do_set_display_name;
  backend_class->delete = do_delete;
  backend_class->make_directory = do_make_directory;
  backend_class->move = do_move;
  backend_class->try_query_settable_attributes = try_query_settable_attributes;
  backend_class->set_attribute = do_set_attribute;
  backend_class->pull = do_pull;
}

/*** PUBLIC API ***/

/**
 * g_vfs_backend_ftp_has_feature:
 * @ftp: the backend
 * @feature: feature to check
 *
 * Checks if the FTP server supports a given @feature. Features are determined
 * once during the mount phase and are not queried again.
 *
 * Returns: %TRUE if @feature is supported.
 **/
gboolean
g_vfs_backend_ftp_has_feature (GVfsBackendFtp *ftp,
                               GVfsFtpFeature  feature)
{
  g_return_val_if_fail (G_VFS_IS_BACKEND_FTP (ftp), FALSE);
  g_return_val_if_fail (feature < 32, FALSE);

  return (ftp->features & (1 << feature)) != 0;
}

/**
 * g_vfs_backend_ftp_uses_workaround:
 * @ftp: the backend
 * @workaround: workaround to check
 *
 * Checks if the given @workaround was enabled previously using
 * g_vfs_backend_ftp_use_workaround(). See that function for a discussion
 * of the purpose of workarounds.
 *
 * Returns: %TRUE if the workaround is enabled
 **/
gboolean
g_vfs_backend_ftp_uses_workaround (GVfsBackendFtp *  ftp,
                                  GVfsFtpWorkaround workaround)
{
  g_return_val_if_fail (G_VFS_IS_BACKEND_FTP (ftp), FALSE);
  g_return_val_if_fail (workaround < 32, FALSE);

  return (g_atomic_int_get (&ftp->workarounds) & (1 << workaround)) != 0;
}

/**
 * g_vfs_backend_ftp_use_workaround:
 * @ftp: the backend
 * @workaround: workaround to set
 *
 * Sets the given @workaround to be used on the backend. Workarounds are flags
 * set on the backend to ensure a special behavior in the client to work
 * around problems with servers. See the existing workarounds for examples.
 **/
void
g_vfs_backend_ftp_use_workaround (GVfsBackendFtp *  ftp,
                                  GVfsFtpWorkaround workaround)
{
  int cur, set;

  g_return_if_fail (G_VFS_IS_BACKEND_FTP (ftp));
  g_return_if_fail (workaround < 32);

  set = 1 << workaround;
  while (((cur = g_atomic_int_get (&ftp->workarounds)) & set) == 0 &&
         !g_atomic_int_compare_and_exchange (&ftp->workarounds, cur, cur | set));
   
}

