summaryrefslogtreecommitdiff
path: root/trunk/daemon/gvfsbackendftp.c
diff options
context:
space:
mode:
Diffstat (limited to 'trunk/daemon/gvfsbackendftp.c')
-rw-r--r--trunk/daemon/gvfsbackendftp.c2573
1 files changed, 2573 insertions, 0 deletions
diff --git a/trunk/daemon/gvfsbackendftp.c b/trunk/daemon/gvfsbackendftp.c
new file mode 100644
index 00000000..164a03e5
--- /dev/null
+++ b/trunk/daemon/gvfsbackendftp.c
@@ -0,0 +1,2573 @@
+/* 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 <libsoup/soup.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"
+
+#define PRINT_DEBUG
+
+#ifdef PRINT_DEBUG
+#define DEBUG g_print
+#else
+#define DEBUG(...)
+#endif
+
+/* timeout for network connect/send/receive (use 0 for none) */
+#define TIMEOUT_IN_SECONDS 30
+
+/*
+ * 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.
+ */
+
+/* unsinged char is on purpose, so we get warnings when we misuse them */
+typedef unsigned char FtpFile;
+typedef struct _FtpConnection FtpConnection;
+
+typedef struct FtpDirReader FtpDirReader;
+struct FtpDirReader {
+ void (* init_data) (FtpConnection *conn,
+ const FtpFile *dir);
+ GFileInfo * (* get_root) (FtpConnection *conn);
+ gpointer (* iter_new) (FtpConnection *conn);
+ GFileInfo * (* iter_process)(gpointer iter,
+ FtpConnection *conn,
+ const FtpFile *dirname,
+ const FtpFile *must_match_file,
+ const char *line,
+ char **symlink);
+ void (* iter_free) (gpointer iter);
+};
+
+typedef enum {
+ FTP_FEATURE_MDTM = (1 << 0),
+ FTP_FEATURE_SIZE = (1 << 1),
+ FTP_FEATURE_TVFS = (1 << 2),
+ FTP_FEATURE_EPSV = (1 << 3),
+ FTP_FEATURE_UTF8 = (1 << 4),
+} FtpFeatures;
+#define FTP_FEATURES_DEFAULT (FTP_FEATURE_EPSV)
+
+typedef enum {
+ FTP_SYSTEM_UNKNOWN = 0,
+ FTP_SYSTEM_UNIX,
+ FTP_SYSTEM_WINDOWS
+} FtpSystem;
+
+typedef enum {
+ /* Server advertises support for EPSV (or we assume that it supports it),
+ * but it does fail to do so, we set this flag so we can fall back to
+ * PASV. */
+ FTP_WORKAROUND_BROKEN_EPSV = (1 << 0),
+ /* Server replies with a wrong address in PASV, we use connection IP
+ * instead */
+ FTP_WORKAROUND_PASV_ADDR = (1 << 1),
+ /* server does not allow querying features before login, so we try after
+ * logging in instead. */
+ FTP_WORKAROUND_FEAT_AFTER_LOGIN = (1 << 2),
+} FtpWorkarounds;
+
+struct _GVfsBackendFtp
+{
+ GVfsBackend backend;
+
+ SoupAddress * addr;
+ char * user;
+ gboolean has_initial_user;
+ char * password; /* password or NULL for anonymous */
+
+ /* vfuncs */
+ const FtpDirReader * dir_ops;
+
+ /* connection collection */
+ GQueue * queue;
+ GMutex * mutex;
+ GCond * cond;
+ guint connections;
+ guint max_connections;
+
+ /* caching results from dir queries */
+ GStaticRWLock directory_cache_lock;
+ GHashTable * directory_cache;
+};
+
+G_DEFINE_TYPE (GVfsBackendFtp, g_vfs_backend_ftp, G_VFS_TYPE_BACKEND)
+
+#define STATUS_GROUP(status) ((status) / 100)
+
+typedef void (* Ftp550Handler) (FtpConnection *conn, const FtpFile *file);
+
+/*** FTP CONNECTION ***/
+
+struct _FtpConnection
+{
+ /* per-job data */
+ GError * error;
+ GVfsJob * job;
+
+ FtpFeatures features;
+ FtpSystem system;
+ FtpWorkarounds workarounds;
+
+ SoupSocket * commands;
+ gchar * read_buffer;
+ gsize read_buffer_size;
+ gsize read_bytes;
+
+ SoupSocket * data;
+};
+
+static void
+ftp_connection_free (FtpConnection *conn)
+{
+ g_assert (conn->job == NULL);
+
+ if (conn->commands)
+ g_object_unref (conn->commands);
+ if (conn->data)
+ g_object_unref (conn->data);
+
+ g_slice_free (FtpConnection, conn);
+}
+
+#define ftp_connection_in_error(conn) ((conn)->error != NULL)
+#define ftp_connection_clear_error(conn) (g_clear_error (&(conn)->error))
+
+static gboolean
+ftp_connection_pop_job (FtpConnection *conn)
+{
+ gboolean result;
+ GError *error;
+ GVfsJob *job;
+
+ g_return_val_if_fail (conn->job != NULL, FALSE);
+
+ /* sending a reply is racy after the reply is sent. The connection may be
+ * reused in a different thread before the reply sending returns. This is
+ * racy in particular when the connection is used as a read/write handle.
+ */
+ error = conn->error;
+ conn->error = NULL;
+ job = conn->job;
+ conn->job = NULL;
+
+ if (error)
+ {
+ g_vfs_job_failed_from_error (job, error);
+ g_clear_error (&error);
+ result = FALSE;
+ }
+ else
+ {
+ g_vfs_job_succeeded (job);
+ result = TRUE;
+ }
+
+ return result;
+}
+
+static void
+ftp_connection_push_job (FtpConnection *conn, GVfsJob *job)
+{
+ g_return_if_fail (conn->job == NULL);
+
+ /* FIXME: ref the job? */
+ conn->job = job;
+}
+
+/**
+ * ftp_error_set_from_response:
+ * @error: pointer to an error to be set or %NULL
+ * @response: an FTP response code to use as the error message
+ *
+ * Sets an error based on an FTP response code.
+ **/
+static void
+ftp_connection_set_error_from_response (FtpConnection *conn, guint response)
+{
+ const char *msg;
+ int code;
+
+ /* Please keep this list ordered by response code,
+ * but group responses with the same message. */
+ switch (response)
+ {
+ case 332: /* Need account for login. */
+ case 532: /* Need account for storing files. */
+ /* FIXME: implement a sane way to handle accounts. */
+ code = G_IO_ERROR_NOT_SUPPORTED;
+ msg = _("Accounts are unsupported");
+ break;
+ case 421: /* Service not available, closing control connection. */
+ code = G_IO_ERROR_FAILED;
+ msg = _("Host closed connection");
+ break;
+ case 425: /* Can't open data connection. */
+ code = G_IO_ERROR_CLOSED;
+ msg = _("Cannot open data connection. Maybe your firewall prevents this?");
+ break;
+ case 426: /* Connection closed; transfer aborted. */
+ code = G_IO_ERROR_CLOSED;
+ msg = _("Data connection closed");
+ break;
+ case 450: /* Requested file action not taken. File unavailable (e.g., file busy). */
+ case 550: /* Requested action not taken. File unavailable (e.g., file not found, no access). */
+ /* FIXME: This is a lot of different errors. So we have to pretend to
+ * be smart here. */
+ code = G_IO_ERROR_FAILED;
+ msg = _("Operation failed");
+ break;
+ case 451: /* Requested action aborted: local error in processing. */
+ code = G_IO_ERROR_FAILED;
+ msg = _("Operation failed");
+ break;
+ case 452: /* Requested action not taken. Insufficient storage space in system. */
+ case 552:
+ code = G_IO_ERROR_NO_SPACE;
+ msg = _("No space left on server");
+ break;
+ case 500: /* Syntax error, command unrecognized. */
+ case 501: /* Syntax error in parameters or arguments. */
+ case 502: /* Command not implemented. */
+ case 503: /* Bad sequence of commands. */
+ case 504: /* Command not implemented for that parameter. */
+ code = G_IO_ERROR_NOT_SUPPORTED;
+ msg = _("Operation unsupported");
+ break;
+ case 530: /* Not logged in. */
+ code = G_IO_ERROR_PERMISSION_DENIED;
+ msg = _("Permission denied");
+ break;
+ case 551: /* Requested action aborted: page type unknown. */
+ code = G_IO_ERROR_FAILED;
+ msg = _("Page type unknown");
+ break;
+ case 553: /* Requested action not taken. File name not allowed. */
+ code = G_IO_ERROR_INVALID_FILENAME;
+ msg = _("Invalid filename");
+ break;
+ default:
+ code = G_IO_ERROR_FAILED;
+ msg = _("Invalid reply");
+ break;
+ }
+
+ DEBUG ("error: %s\n", msg);
+ g_set_error_literal (&conn->error, G_IO_ERROR, code, msg);
+}
+
+/**
+ * ResponseFlags:
+ * RESPONSE_PASS_100: Don't treat 1XX responses, but return them
+ * RESPONSE_PASS_300: Don't treat 3XX responses, but return them
+ * RESPONSE_PASS_400: Don't treat 4XX responses, but return them
+ * RESPONSE_PASS_500: Don't treat 5XX responses, but return them
+ * RESPONSE_PASS_550: Don't treat 550 responses, but return them
+ * RESPONSE_FAIL_200: Fail on a 2XX response
+ */
+
+typedef enum {
+ RESPONSE_PASS_100 = (1 << 0),
+ RESPONSE_PASS_300 = (1 << 1),
+ RESPONSE_PASS_400 = (1 << 2),
+ RESPONSE_PASS_500 = (1 << 3),
+ RESPONSE_PASS_550 = (1 << 4),
+ RESPONSE_FAIL_200 = (1 << 5)
+} ResponseFlags;
+
+/**
+ * ftp_connection_receive:
+ * @conn: connection to receive from
+ * @flags: flags for handling the response
+ * @error: pointer to error message
+ *
+ * Reads a command and stores it in @conn->read_buffer. The read buffer will be
+ * null-terminated and contain @conn->read_bytes bytes. Afterwards, the response
+ * will be parsed and processed according to @flags. By default, all responses
+ * but 2xx will cause an error.
+ *
+ * Returns: 0 on error, the ftp code otherwise
+ **/
+static guint
+ftp_connection_receive (FtpConnection *conn,
+ ResponseFlags flags)
+{
+ SoupSocketIOStatus status;
+ gboolean got_boundary;
+ char *last_line;
+ enum {
+ FIRST_LINE,
+ MULTILINE,
+ DONE
+ } reply_state = FIRST_LINE;
+ guint response = 0;
+
+ g_assert (conn->job != NULL);
+
+ if (ftp_connection_in_error (conn))
+ return 0;
+
+ conn->read_bytes = 0;
+ while (reply_state != DONE)
+ {
+ last_line = conn->read_buffer + conn->read_bytes;
+ do {
+ gsize n_bytes;
+
+ /* the available size must at least allow for boundary size (2)
+ * bytes to be available for reading */
+ if (conn->read_buffer_size - conn->read_bytes < 128)
+ {
+ gsize new_size = conn->read_buffer_size + 1024;
+ /* FIXME: upper limit for size? */
+ gchar *new = g_try_realloc (conn->read_buffer, new_size);
+ if (new)
+ {
+ /* make last line relative to new allocation */
+ last_line = new + (last_line - conn->read_buffer);
+ conn->read_buffer = new;
+ conn->read_buffer_size = new_size;
+ }
+ else
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return 0;
+ }
+ }
+ status = soup_socket_read_until (conn->commands,
+ conn->read_buffer + conn->read_bytes,
+ /* -1 byte for nul-termination */
+ conn->read_buffer_size - conn->read_bytes - 1,
+ "\r\n",
+ 2,
+ &n_bytes,
+ &got_boundary,
+ conn->job->cancellable,
+ &conn->error);
+
+ conn->read_bytes += n_bytes;
+ conn->read_buffer[conn->read_bytes] = 0;
+
+ g_assert (status != SOUP_SOCKET_WOULD_BLOCK);
+ if (status == SOUP_SOCKET_ERROR)
+ return 0;
+
+ if (n_bytes == 0)
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return 0;
+ }
+ } while (!got_boundary);
+
+ DEBUG ("<-- %s", last_line);
+
+ if (reply_state == FIRST_LINE)
+ {
+ if (last_line[0] <= '0' || last_line[0] > '5' ||
+ last_line[1] < '0' || last_line[1] > '9' ||
+ last_line[2] < '0' || last_line[2] > '9')
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return 0;
+ }
+ response = 100 * (last_line[0] - '0') +
+ 10 * (last_line[1] - '0') +
+ (last_line[2] - '0');
+ if (last_line[3] == ' ')
+ reply_state = DONE;
+ else if (last_line[3] == '-')
+ reply_state = MULTILINE;
+ else
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return 0;
+ }
+ }
+ else
+ {
+ if (last_line[0] == conn->read_buffer[0] &&
+ last_line[1] == conn->read_buffer[1] &&
+ last_line[2] == conn->read_buffer[2] &&
+ last_line[3] == ' ')
+ reply_state = DONE;
+ }
+ }
+
+ switch (STATUS_GROUP (response))
+ {
+ case 0:
+ return 0;
+ case 1:
+ if (flags & RESPONSE_PASS_100)
+ break;
+ ftp_connection_set_error_from_response (conn, response);
+ return 0;
+ case 2:
+ if (flags & RESPONSE_FAIL_200)
+ {
+ ftp_connection_set_error_from_response (conn, response);
+ return 0;
+ }
+ break;
+ case 3:
+ if (flags & RESPONSE_PASS_300)
+ break;
+ ftp_connection_set_error_from_response (conn, response);
+ return 0;
+ case 4:
+ if (flags & RESPONSE_PASS_400)
+ break;
+ ftp_connection_set_error_from_response (conn, response);
+ return 0;
+ break;
+ case 5:
+ if ((flags & RESPONSE_PASS_500) || (response == 550 && (flags & RESPONSE_PASS_550)))
+ break;
+ ftp_connection_set_error_from_response (conn, response);
+ return 0;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+
+ return response;
+}
+
+/**
+ * ftp_connection_send:
+ * @conn: the connection to send to
+ * @flags: #ResponseFlags to use
+ * @error: pointer to take an error
+ * @format: format string to construct command from
+ * (without trailing \r\n)
+ * @...: arguments to format string
+ *
+ * Takes a command, waits for an answer and parses it. Without any @flags, FTP
+ * codes other than 2xx cause an error. The last read ftp command will be put
+ * into @conn->read_buffer.
+ *
+ * Returns: 0 on error or the receied FTP code otherwise.
+ *
+ **/
+static guint
+ftp_connection_sendv (FtpConnection *conn,
+ ResponseFlags flags,
+ const char * format,
+ va_list varargs)
+{
+ GString *command;
+ SoupSocketIOStatus status;
+ gsize n_bytes;
+ guint response;
+
+ g_assert (conn->job != NULL);
+
+ if (ftp_connection_in_error (conn))
+ return 0;
+
+ command = g_string_new ("");
+ g_string_append_vprintf (command, format, varargs);
+#ifdef PRINT_DEBUG
+ if (g_str_has_prefix (command->str, "PASS"))
+ DEBUG ("--> PASS ***\n");
+ else
+ DEBUG ("--> %s\n", command->str);
+#endif
+ g_string_append (command, "\r\n");
+ status = soup_socket_write (conn->commands,
+ command->str,
+ command->len,
+ &n_bytes,
+ conn->job->cancellable,
+ &conn->error);
+ switch (status)
+ {
+ case SOUP_SOCKET_OK:
+ case SOUP_SOCKET_EOF:
+ if (n_bytes == command->len)
+ break;
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("broken transmission"));
+ /* fall through */
+ case SOUP_SOCKET_ERROR:
+ g_string_free (command, TRUE);
+ return 0;
+ case SOUP_SOCKET_WOULD_BLOCK:
+ default:
+ g_assert_not_reached ();
+ }
+ g_string_free (command, TRUE);
+
+ response = ftp_connection_receive (conn, flags);
+ return response;
+}
+
+static guint
+ftp_connection_send (FtpConnection *conn,
+ ResponseFlags flags,
+ const char * format,
+ ...) G_GNUC_PRINTF (3, 4);
+static guint
+ftp_connection_send (FtpConnection *conn,
+ ResponseFlags flags,
+ const char * format,
+ ...)
+{
+ va_list varargs;
+ guint response;
+
+ va_start (varargs, format);
+ response = ftp_connection_sendv (conn,
+ flags,
+ format,
+ varargs);
+ va_end (varargs);
+ return response;
+}
+
+static void
+ftp_connection_check_file (FtpConnection *conn,
+ const Ftp550Handler *handlers,
+ const FtpFile *file)
+{
+ while (*handlers && !ftp_connection_in_error (conn))
+ {
+ (*handlers) (conn, file);
+ handlers++;
+ }
+}
+
+static guint
+ftp_connection_send_and_check (FtpConnection *conn,
+ ResponseFlags flags,
+ const Ftp550Handler *handlers,
+ const FtpFile *file,
+ const char *format,
+ ...) G_GNUC_PRINTF (5, 6);
+static guint
+ftp_connection_send_and_check (FtpConnection *conn,
+ ResponseFlags flags,
+ const Ftp550Handler *handlers,
+ const FtpFile *file,
+ const char *format,
+ ...)
+{
+ va_list varargs;
+ guint response;
+
+ /* check that there's no 550 handling used - don't allow bad use of API */
+ g_return_val_if_fail ((flags & RESPONSE_PASS_550) == 0, 0);
+ g_return_val_if_fail (handlers != NULL, 0);
+ g_return_val_if_fail (file != NULL, 0);
+
+ va_start (varargs, format);
+ response = ftp_connection_sendv (conn,
+ flags | RESPONSE_PASS_550,
+ format,
+ varargs);
+ va_end (varargs);
+ if (response == 550)
+ {
+ ftp_connection_check_file (conn, handlers, file);
+ if (!ftp_connection_in_error (conn))
+ ftp_connection_set_error_from_response (conn, response);
+ response = 0;
+ }
+ return response;
+}
+
+static void
+ftp_connection_parse_features (FtpConnection *conn)
+{
+ struct {
+ const char * name; /* name of feature */
+ FtpFeatures enable; /* flags to enable with this feature */
+ } features[] = {
+ { "MDTM", FTP_FEATURE_MDTM },
+ { "SIZE", FTP_FEATURE_SIZE },
+ { "TVFS", FTP_FEATURE_TVFS },
+ { "EPSV", FTP_FEATURE_EPSV },
+ { "UTF8", FTP_FEATURE_UTF8 },
+ };
+ char **supported;
+ guint i, j;
+
+ /* The "\n" should really be "\r\n" but we deal with both to handle broken
+ * servers. We strip off '\r' later, if it exists. */
+ supported = g_strsplit (conn->read_buffer, "\n", -1);
+
+ for (i = 1; supported[i]; i++)
+ {
+ char *feature = supported[i];
+ int len;
+
+ 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++;
+
+ /* strip off trailing '\r', if it exists. */
+ len = strlen(feature);
+ if (len > 0 && feature[len-1] == '\r')
+ feature[len-1] = '\0';
+
+ for (j = 0; j < G_N_ELEMENTS (features); j++)
+ {
+ if (g_ascii_strcasecmp (feature, features[j].name) == 0)
+ {
+ DEBUG ("feature %s supported\n", features[j].name);
+ conn->features |= features[j].enable;
+ }
+ }
+ }
+
+ g_strfreev (supported);
+}
+
+/* NB: you must free the connection if it's in error returning from here */
+static FtpConnection *
+ftp_connection_create (SoupAddress * addr,
+ GVfsJob * job)
+{
+ FtpConnection *conn;
+ guint status;
+
+ conn = g_slice_new0 (FtpConnection);
+ ftp_connection_push_job (conn, job);
+
+ conn->commands = soup_socket_new ("non-blocking", FALSE,
+ "remote-address", addr,
+ "timeout", TIMEOUT_IN_SECONDS,
+ NULL);
+ status = soup_socket_connect_sync (conn->commands, job->cancellable);
+ if (!SOUP_STATUS_IS_SUCCESSFUL (status))
+ {
+ /* FIXME: better error messages depending on status please */
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_HOST_NOT_FOUND,
+ _("Could not connect to host"));
+ }
+
+ ftp_connection_receive (conn, 0);
+ return conn;
+}
+
+static guint
+ftp_connection_login (FtpConnection *conn,
+ const char * username,
+ const char * password)
+{
+ guint status;
+
+ if (ftp_connection_in_error (conn))
+ return 0;
+
+ status = ftp_connection_send (conn, RESPONSE_PASS_300,
+ "USER %s", username);
+
+ if (STATUS_GROUP (status) == 3)
+ {
+ /* rationale for choosing the default password:
+ * - some ftp servers expect something that looks like an email address
+ * - we don't want to send the user's name or address, as that would be
+ * a privacy problem
+ * - we want to give ftp server administrators a chance to notify us of
+ * problems with our client.
+ * - we don't want to drown in spam.
+ */
+ if (password == NULL || password[0] == 0)
+ password = "gvfsd-ftp-" VERSION "@example.com";
+ status = ftp_connection_send (conn, 0,
+ "PASS %s", password);
+ }
+
+ return status;
+}
+
+static void
+ftp_connection_parse_system (FtpConnection *conn)
+{
+ static const struct {
+ const char *id;
+ FtpSystem system;
+ } known_systems[] = {
+ /* NB: the first entry that matches is taken, so order matters */
+ { "UNIX ", FTP_SYSTEM_UNIX },
+ { "WINDOWS_NT ", FTP_SYSTEM_WINDOWS }
+ };
+ guint i;
+ char *system_name = conn->read_buffer + 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)
+ {
+ conn->system = known_systems[i].system;
+ DEBUG ("system is %u\n", conn->system);
+ break;
+ }
+ }
+}
+
+static void
+ftp_connection_prepare (FtpConnection *conn)
+{
+ /* check supported features */
+ if (ftp_connection_send (conn, 0, "FEAT") != 0) {
+ ftp_connection_parse_features (conn);
+ } else {
+ ftp_connection_clear_error(conn);
+ conn->workarounds |= FTP_WORKAROUND_FEAT_AFTER_LOGIN;
+ conn->features = 0;
+ }
+
+ /* instruct server that we'll give and assume we get utf8 */
+ if (conn->features & FTP_FEATURE_UTF8)
+ ftp_connection_send (conn, 0, "OPTS UTF8 ON");
+}
+
+static gboolean
+ftp_connection_use (FtpConnection *conn)
+{
+ /* only binary transfers please */
+ ftp_connection_send (conn, 0, "TYPE I");
+ if (ftp_connection_in_error (conn))
+ return FALSE;
+
+#if 0
+ /* RFC 2428 suggests to send this to make NAT routers happy */
+ /* XXX: Disabled for the following reasons:
+ * - most ftp clients don't use it
+ * - lots of broken ftp servers can't see the difference between
+ * "EPSV" and "EPSV ALL"
+ * - impossible to dynamically fall back to regular PASV in case
+ * EPSV doesn't work for some reason.
+ * If this makes your ftp connection fail, please file a bug and we will
+ * try to invent a way to make this all work. Until then, we'll just
+ * ignore the RFC.
+ */
+ if (conn->features & FTP_FEATURE_EPSV)
+ ftp_connection_send (conn, 0, "EPSV ALL");
+ ftp_connection_clear_error(conn);
+#endif
+
+ if (conn->workarounds & FTP_WORKAROUND_FEAT_AFTER_LOGIN) {
+ if (ftp_connection_send (conn, 0, "FEAT") != 0) {
+ ftp_connection_parse_features (conn);
+ } else {
+ ftp_connection_clear_error(conn);
+ conn->features = FTP_FEATURE_EPSV;
+ }
+ }
+
+ if (ftp_connection_send (conn, 0, "SYST"))
+ ftp_connection_parse_system (conn);
+ ftp_connection_clear_error(conn);
+
+ return TRUE;
+}
+
+static gboolean
+ftp_connection_open_data_connection (FtpConnection *conn, SoupAddress *addr)
+{
+ guint status;
+
+ conn->data = soup_socket_new ("non-blocking", FALSE,
+ "remote-address", addr,
+ "timeout", TIMEOUT_IN_SECONDS,
+ NULL);
+ g_object_unref (addr);
+ status = soup_socket_connect_sync (conn->data, conn->job->cancellable);
+ if (!SOUP_STATUS_IS_SUCCESSFUL (status))
+ {
+ /* FIXME: better error messages depending on status please */
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_HOST_NOT_FOUND,
+ _("Could not connect to host"));
+ g_object_unref (conn->data);
+ conn->data = NULL;
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+ftp_connection_ensure_data_connection_epsv (FtpConnection *conn)
+{
+ const char *s;
+ guint port;
+ SoupAddress *addr;
+ guint status;
+ gboolean connected;
+
+ if ((conn->features & FTP_FEATURE_EPSV) == 0)
+ return FALSE;
+
+ if (conn->workarounds & FTP_WORKAROUND_BROKEN_EPSV)
+ return FALSE;
+
+ status = ftp_connection_send (conn, RESPONSE_PASS_500, "EPSV");
+ if (STATUS_GROUP (status) != 2)
+ return FALSE;
+
+ s = strrchr (conn->read_buffer, '(');
+ if (!s)
+ return FALSE;
+
+ s += 4;
+ port = strtoul (s, NULL, 10);
+ if (port == 0)
+ return FALSE;
+
+ addr = soup_address_new (
+ soup_address_get_name (soup_socket_get_remote_address (conn->commands)),
+ port);
+
+ connected = ftp_connection_open_data_connection (conn, addr);
+ if (!connected)
+ {
+ DEBUG ("Successful EPSV response code, but data connection failed. Enabling FTP_WORKAROUND_BROKEN_EPSV.\n");
+ conn->workarounds |= FTP_WORKAROUND_BROKEN_EPSV;
+ ftp_connection_clear_error (conn);
+ }
+ return connected;
+}
+
+static gboolean
+ftp_connection_ensure_data_connection_pasv (FtpConnection *conn)
+{
+ guint ip1, ip2, ip3, ip4, port1, port2;
+ const char *s;
+ SoupAddress *addr;
+ guint status;
+
+ /* only binary transfers please */
+ status = ftp_connection_send (conn, 0, "PASV");
+ if (status == 0)
+ return FALSE;
+
+ /* parse response and try to find the address to connect to.
+ * This code does the same as curl.
+ */
+ for (s = conn->read_buffer; *s; s++)
+ {
+ if (sscanf (s, "%u,%u,%u,%u,%u,%u",
+ &ip1, &ip2, &ip3, &ip4,
+ &port1, &port2) == 6)
+ break;
+ }
+ if (*s == 0)
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Invalid reply"));
+ return FALSE;
+ }
+
+ if (!(conn->workarounds & FTP_WORKAROUND_PASV_ADDR))
+ {
+ char *ip;
+
+ ip = g_strdup_printf ("%u.%u.%u.%u", ip1, ip2, ip3, ip4);
+ addr = soup_address_new (ip, port1 << 8 | port2);
+ g_free (ip);
+
+ if (ftp_connection_open_data_connection (conn, addr))
+ return TRUE;
+
+ /* set workaround flag (see below), so we don't try this again */
+ DEBUG ("Successfull PASV response but data connection failed. Enabling FTP_WORKAROUND_PASV_ADDR.\n");
+ conn->workarounds |= FTP_WORKAROUND_PASV_ADDR;
+ ftp_connection_clear_error (conn);
+ }
+
+ /* Workaround code:
+ * Various ftp servers aren;t setup correctly when behind a NAT. They report
+ * their own IP address (like 10.0.0.4) and not the address in front of the
+ * NAT. But this is likely the same address that we connected to with our
+ * command connetion. So if the address given by PASV fails, we fall back
+ * to the address of the command stream.
+ */
+ addr = soup_address_new (soup_address_get_name (soup_socket_get_remote_address (conn->commands)),
+ port1 << 8 | port2);
+ return ftp_connection_open_data_connection (conn, addr);
+}
+
+static gboolean
+ftp_connection_ensure_data_connection (FtpConnection *conn)
+{
+ if (ftp_connection_ensure_data_connection_epsv (conn))
+ return TRUE;
+
+ if (ftp_connection_in_error (conn))
+ return FALSE;
+
+ if (ftp_connection_ensure_data_connection_pasv (conn))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+ftp_connection_close_data_connection (FtpConnection *conn)
+{
+ if (conn == NULL || conn->data == NULL)
+ return;
+
+ g_object_unref (conn->data);
+ conn->data = NULL;
+}
+
+/*** FILE MAPPINGS ***/
+
+/* FIXME: This most likely needs adaption to non-unix like directory structures.
+ * There's at least the case of multiple roots (Netware) plus probably a shitload
+ * of weird old file systems (starting with MS-DOS)
+ * But we first need a way to detect that.
+ */
+
+/**
+ * FtpFile:
+ *
+ * Byte string used to identify a file on the FTP server. It's typedef'ed to
+ * make it easy to distinguish from GVfs paths.
+ */
+
+static FtpFile *
+ftp_filename_from_gvfs_path (FtpConnection *conn, const char *pathname)
+{
+ return (FtpFile *) g_strdup (pathname);
+}
+
+static char *
+ftp_filename_to_gvfs_path (FtpConnection *conn, const FtpFile *filename)
+{
+ return g_strdup ((const char *) filename);
+}
+
+/* Takes an FTP dirname and a basename (as used in RNTO or as result from LIST
+ * or similar) and gets the new ftp filename from it.
+ *
+ * Returns: the filename or %NULL if filename construction wasn't possible.
+ */
+/* let's hope we can live without a connection here, or we have to rewrite LIST */
+static FtpFile *
+ftp_filename_construct (FtpConnection *conn, const FtpFile *dirname, const char *basename)
+{
+ if (strpbrk (basename, "/\r\n"))
+ return NULL;
+
+ return (FtpFile *) g_build_path ("/", (char *) dirname, basename, NULL);
+}
+
+#define ftp_filename_equal g_str_equal
+
+/*** COMMON FUNCTIONS WITH SPECIAL HANDLING ***/
+
+static gboolean
+ftp_connection_cd (FtpConnection *conn, const FtpFile *file)
+{
+ guint response = ftp_connection_send (conn,
+ RESPONSE_PASS_500,
+ "CWD %s", file);
+ if (response == 550)
+ {
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY,
+ _("The file is not a directory"));
+ response = 0;
+ }
+ else if (STATUS_GROUP (response) == 5)
+ {
+ ftp_connection_set_error_from_response (conn, response);
+ response = 0;
+ }
+
+ return response != 0;
+}
+
+static gboolean
+ftp_connection_try_cd (FtpConnection *conn, const FtpFile *file)
+{
+ if (ftp_connection_in_error (conn))
+ return FALSE;
+
+ if (!ftp_connection_cd (conn, file))
+ {
+ ftp_connection_clear_error (conn);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/*** default directory reading ***/
+
+static void
+dir_default_init_data (FtpConnection *conn, const FtpFile *dir)
+{
+ ftp_connection_cd (conn, dir);
+ ftp_connection_ensure_data_connection (conn);
+
+ ftp_connection_send (conn,
+ RESPONSE_PASS_100 | RESPONSE_FAIL_200,
+ (conn->system == FTP_SYSTEM_UNIX) ? "LIST -a" : "LIST");
+}
+
+static GFileInfo *
+dir_default_get_root (FtpConnection *conn)
+{
+ GFileInfo *info;
+ GIcon *icon;
+ char *display_name;
+
+ info = g_file_info_new ();
+ g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
+
+ g_file_info_set_name (info, "/");
+ display_name = g_strdup_printf (_("/ on %s"),
+ soup_address_get_name (soup_socket_get_remote_address (conn->commands)));
+ g_file_info_set_display_name (info, display_name);
+ g_free (display_name);
+ g_file_info_set_edit_name (info, "/");
+
+ g_file_info_set_content_type (info, "inode/directory");
+ g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, "inode/directory");
+
+ icon = g_themed_icon_new ("folder-remote");
+ g_file_info_set_icon (info, icon);
+ g_object_unref (icon);
+
+ return info;
+}
+
+static gpointer
+dir_default_iter_new (FtpConnection *conn)
+{
+ return g_slice_new0 (struct list_state);
+}
+
+static GFileInfo *
+dir_default_iter_process (gpointer iter,
+ FtpConnection *conn,
+ const FtpFile *dirname,
+ const FtpFile *must_match_file,
+ const char *line,
+ char **symlink)
+{
+ struct list_state *state = iter;
+ struct list_result result = { 0, };
+ GTimeVal tv = { 0, 0 };
+ GFileInfo *info;
+ int type;
+ FtpFile *name;
+ char *s, *t;
+
+ type = ParseFTPList (line, state, &result);
+ if (type != 'd' && type != 'f' && type != 'l')
+ return NULL;
+
+ /* don't list . and .. directories
+ * Let's hope they're not important files on some ftp servers
+ */
+ if (type == 'd')
+ {
+ if (result.fe_fnlen == 1 &&
+ result.fe_fname[0] == '.')
+ return NULL;
+ if (result.fe_fnlen == 2 &&
+ result.fe_fname[0] == '.' &&
+ result.fe_fname[1] == '.')
+ return NULL;
+ }
+
+ s = g_strndup (result.fe_fname, result.fe_fnlen);
+ if (dirname)
+ {
+ name = ftp_filename_construct (conn, dirname, s);
+ g_free (s);
+ }
+ else
+ name = (FtpFile *) s;
+ if (name == NULL)
+ return NULL;
+
+ if (must_match_file && !ftp_filename_equal (name, must_match_file))
+ {
+ g_free (name);
+ return NULL;
+ }
+
+ info = g_file_info_new ();
+
+ s = ftp_filename_to_gvfs_path (conn, name);
+
+ t = g_path_get_basename (s);
+ g_file_info_set_name (info, t);
+ g_free (t);
+
+ if (type == 'l')
+ {
+ char *link;
+
+ link = g_strndup (result.fe_lname, result.fe_lnlen);
+
+ /* FIXME: this whole stuff is not FtpFile save */
+ g_file_info_set_symlink_target (info, link);
+ g_file_info_set_is_symlink (info, TRUE);
+
+ if (symlink)
+ {
+ char *str = g_path_get_dirname (s);
+ char *symlink_file = g_build_path ("/", str, link, NULL);
+
+ g_free (str);
+ while ((str = strstr (symlink_file, "/../")))
+ {
+ char *end = str + 4;
+ char *start;
+ start = str - 1;
+ while (start >= symlink_file && *start != '/')
+ start--;
+
+ if (start < symlink_file) {
+ *symlink_file = '/';
+ start = symlink_file;
+ }
+
+ memmove (start + 1, end, strlen (end) + 1);
+ }
+ str = symlink_file + strlen (symlink_file) - 1;
+ while (*str == '/' && str > symlink_file)
+ *str-- = 0;
+ *symlink = symlink_file;
+ }
+ g_free (link);
+ }
+ else if (symlink)
+ *symlink = NULL;
+
+ g_file_info_set_size (info, strtoul (result.fe_size, NULL, 10));
+
+ gvfs_file_info_populate_default (info, s,
+ type == 'f' ? G_FILE_TYPE_REGULAR :
+ type == 'l' ? G_FILE_TYPE_SYMBOLIC_LINK :
+ G_FILE_TYPE_DIRECTORY);
+
+ if (conn->system == FTP_SYSTEM_UNIX)
+ g_file_info_set_is_hidden (info, result.fe_fnlen > 0 &&
+ result.fe_fname[0] == '.');
+
+ g_free (s);
+ g_free (name);
+
+ /* Workaround:
+ * result.fetime.tm_year contains actual year instead of offset-from-1900,
+ * which mktime expects.
+ */
+ if (result.fe_time.tm_year >= 1900)
+ result.fe_time.tm_year -= 1900;
+
+ tv.tv_sec = mktime (&result.fe_time);
+ if (tv.tv_sec != -1)
+ g_file_info_set_modification_time (info, &tv);
+
+ return info;
+}
+
+static void
+dir_default_iter_free (gpointer iter)
+{
+ g_slice_free (struct list_state, iter);
+}
+
+static const FtpDirReader dir_default = {
+ dir_default_init_data,
+ dir_default_get_root,
+ dir_default_iter_new,
+ dir_default_iter_process,
+ dir_default_iter_free
+};
+
+/*** BACKEND ***/
+
+static void
+g_vfs_backend_ftp_push_connection (GVfsBackendFtp *ftp, FtpConnection *conn)
+{
+ /* we allow conn == NULL to ease error cases */
+ if (conn == NULL)
+ return;
+
+ if (conn->job)
+ ftp_connection_pop_job (conn);
+
+ g_mutex_lock (ftp->mutex);
+ if (ftp->queue)
+ {
+ g_queue_push_tail (ftp->queue, conn);
+ g_cond_signal (ftp->cond);
+ }
+ else
+ ftp_connection_free (conn);
+ g_mutex_unlock (ftp->mutex);
+}
+
+static void
+do_broadcast (GCancellable *cancellable, GCond *cond)
+{
+ g_cond_broadcast (cond);
+}
+
+static FtpConnection *
+g_vfs_backend_ftp_pop_connection (GVfsBackendFtp *ftp,
+ GVfsJob * job)
+{
+ FtpConnection *conn = NULL;
+ GTimeVal now;
+ guint id;
+
+ g_mutex_lock (ftp->mutex);
+ id = g_signal_connect (job->cancellable,
+ "cancelled",
+ G_CALLBACK (do_broadcast),
+ ftp->cond);
+ while (conn == NULL && ftp->queue != NULL)
+ {
+ if (g_cancellable_is_cancelled (job->cancellable))
+ break;
+ conn = g_queue_pop_head (ftp->queue);
+
+ if (conn != NULL)
+ {
+ /* Figure out if this connection had a timeout sent. If so, skip it. */
+ g_mutex_unlock (ftp->mutex);
+ ftp_connection_push_job (conn, job);
+ if (ftp_connection_send (conn, 0, "NOOP"))
+ break;
+
+ ftp_connection_clear_error (conn);
+ conn->job = NULL;
+ ftp_connection_free (conn);
+ conn = NULL;
+ g_mutex_lock (ftp->mutex);
+ ftp->connections--;
+ continue;
+ }
+
+ if (ftp->connections < ftp->max_connections)
+ {
+ /* Save current number of connections here, so we can limit maximum
+ * connections later.
+ * This is necessary for threading reasons (connections can be
+ * opened or closed while we are still in the opening process. */
+ guint maybe_max_connections = ftp->connections;
+
+ ftp->connections++;
+ g_mutex_unlock (ftp->mutex);
+ conn = ftp_connection_create (ftp->addr, job);
+ ftp_connection_prepare (conn);
+ ftp_connection_login (conn, ftp->user, ftp->password);
+ ftp_connection_use (conn);
+ if (G_LIKELY (!ftp_connection_in_error (conn)))
+ break;
+
+ ftp_connection_clear_error (conn);
+ /* Don't call ftp_connection_pop_job () here, the job isn't done yet */
+ conn->job = NULL;
+ ftp_connection_free (conn);
+ conn = NULL;
+ g_mutex_lock (ftp->mutex);
+ ftp->connections--;
+ ftp->max_connections = MIN (ftp->max_connections, maybe_max_connections);
+ if (ftp->max_connections == 0)
+ {
+ DEBUG ("no more connections left, exiting...");
+ /* FIXME: shut down properly */
+ exit (0);
+ }
+
+ continue;
+ }
+
+ g_get_current_time (&now);
+ g_time_val_add (&now, TIMEOUT_IN_SECONDS * 1000 * 1000);
+ if (!g_cond_timed_wait (ftp->cond, ftp->mutex, &now))
+ {
+ g_vfs_job_failed (G_VFS_JOB (job),
+ G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK,
+ /* defeat string freeze! */
+ /* _("Resource temporarily unavailable")); */
+ "%s", g_strerror (EAGAIN));
+ break;
+ }
+ }
+ g_signal_handler_disconnect (job->cancellable, id);
+
+ return conn;
+}
+
+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_hash_table_destroy (ftp->directory_cache);
+ g_static_rw_lock_free (&ftp->directory_cache_lock);
+
+ 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
+list_free (gpointer list)
+{
+ g_list_foreach (list, (GFunc) g_free, NULL);
+ g_list_free (list);
+}
+
+static void
+g_vfs_backend_ftp_init (GVfsBackendFtp *ftp)
+{
+ ftp->mutex = g_mutex_new ();
+ ftp->cond = g_cond_new ();
+
+ ftp->directory_cache = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ list_free);
+ g_static_rw_lock_init (&ftp->directory_cache_lock);
+
+ ftp->dir_ops = &dir_default;
+}
+
+static void
+do_mount (GVfsBackend *backend,
+ GVfsJobMount *job,
+ GMountSpec *mount_spec,
+ GMountSource *mount_source,
+ gboolean is_automount)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ char *host;
+ char *prompt = NULL;
+ char *username;
+ char *password;
+ char *display_name;
+ gboolean aborted, anonymous, break_on_fail;
+ GPasswordSave password_save = G_PASSWORD_SAVE_NEVER;
+ guint port;
+
+ conn = ftp_connection_create (ftp->addr,
+ G_VFS_JOB (job));
+ /* 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 (ftp_connection_in_error (conn))
+ {
+ ftp_connection_pop_job (conn);
+ ftp_connection_free (conn);
+ return;
+ }
+
+ ftp_connection_prepare (conn);
+
+ port = soup_address_get_port (ftp->addr);
+ /* FIXME: need to translate this? */
+ if (port == 21)
+ host = g_strdup (soup_address_get_name (ftp->addr));
+ else
+ host = g_strdup_printf ("%s:%u",
+ soup_address_get_name (ftp->addr),
+ port);
+
+ 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,
+ soup_address_get_name (ftp->addr),
+ NULL,
+ "ftp",
+ NULL,
+ NULL,
+ port == 21 ? 0 : port,
+ &username,
+ NULL,
+ &password))
+ {
+ anonymous = FALSE;
+ goto try_login;
+ }
+
+ 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, host);
+ else
+ /* translators: %s here is the hostname */
+ prompt = g_strdup_printf (_("Enter password for ftp on %s"), host);
+ }
+
+ 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 (&conn->error, G_IO_ERROR, 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)
+ {
+ if (ftp_connection_login (conn, "anonymous", "") != 0)
+ {
+ ftp->user = g_strdup ("anonymous");
+ ftp->password = g_strdup ("");
+ break;
+ }
+ ftp->user = NULL;
+ ftp->password = NULL;
+ }
+ else
+ {
+ ftp->user = username ? g_strdup (username) : g_strdup ("");
+ ftp->password = g_strdup (password);
+ if (ftp_connection_login (conn, username, password) != 0)
+ break;
+ }
+ g_free (username);
+ g_free (password);
+
+ if (break_on_fail ||
+ !g_error_matches (conn->error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
+ break;
+
+ ftp_connection_clear_error (conn);
+ }
+
+ ftp_connection_use (conn);
+
+ if (ftp_connection_in_error (conn))
+ {
+ ftp_connection_pop_job (conn);
+ ftp_connection_free (conn);
+ }
+ else
+ {
+ if (prompt && !anonymous)
+ {
+ /* a prompt was created, so we have to save the password */
+ g_vfs_keyring_save_password (ftp->user,
+ soup_address_get_name (ftp->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", soup_address_get_name (ftp->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"), host);
+ 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, host);
+ }
+ 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_vfs_backend_ftp_push_connection (ftp, conn);
+ }
+
+ g_free (host);
+}
+
+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 = soup_address_new (host, port);
+ ftp->user = g_strdup (g_mount_spec_get (mount_spec, "user"));
+ ftp->has_initial_user = ftp->user != NULL;
+
+ return FALSE;
+}
+
+static void
+do_unmount (GVfsBackend * backend,
+ GVfsJobUnmount *job)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+
+ g_mutex_lock (ftp->mutex);
+ while ((conn = g_queue_pop_head (ftp->queue)))
+ {
+ /* FIXME: properly quit */
+ 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));
+}
+
+static void
+error_550_is_directory (FtpConnection *conn, const FtpFile *file)
+{
+ guint response = ftp_connection_send (conn,
+ RESPONSE_PASS_550,
+ "CWD %s", file);
+
+ if (STATUS_GROUP (response) == 2)
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR,
+ G_IO_ERROR_IS_DIRECTORY,
+ _("File is directory"));
+ }
+}
+
+static void
+do_open_for_read (GVfsBackend *backend,
+ GVfsJobOpenForRead *job,
+ const char *filename)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ FtpFile *file;
+ static const Ftp550Handler open_read_handlers[] = { error_550_is_directory, NULL };
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (!conn)
+ return;
+
+ ftp_connection_ensure_data_connection (conn);
+
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ ftp_connection_send_and_check (conn,
+ RESPONSE_PASS_100 | RESPONSE_FAIL_200,
+ &open_read_handlers[0],
+ file,
+ "RETR %s", file);
+ g_free (file);
+
+ if (ftp_connection_in_error (conn))
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+ else
+ {
+ /* don't push the connection back, it's our handle now */
+ g_vfs_job_open_for_read_set_handle (job, conn);
+ g_vfs_job_open_for_read_set_can_seek (job, FALSE);
+ ftp_connection_pop_job (conn);
+ }
+}
+
+static void
+do_close_read (GVfsBackend * backend,
+ GVfsJobCloseRead *job,
+ GVfsBackendHandle handle)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn = handle;
+
+ ftp_connection_push_job (conn, G_VFS_JOB (job));
+ ftp_connection_close_data_connection (conn);
+ ftp_connection_receive (conn, 0);
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_read (GVfsBackend * backend,
+ GVfsJobRead * job,
+ GVfsBackendHandle handle,
+ char * buffer,
+ gsize bytes_requested)
+{
+ FtpConnection *conn = handle;
+ gsize n_bytes;
+
+ ftp_connection_push_job (conn, G_VFS_JOB (job));
+
+ soup_socket_read (conn->data,
+ buffer,
+ bytes_requested,
+ &n_bytes,
+ conn->job->cancellable,
+ &conn->error);
+ /* no need to check return value, code will just do the right thing
+ * depenging on wether conn->error is set */
+
+ g_vfs_job_read_set_size (job, n_bytes);
+ ftp_connection_pop_job (conn);
+}
+
+static void
+do_start_write (GVfsBackendFtp *ftp,
+ FtpConnection *conn,
+ GFileCreateFlags flags,
+ const char *format,
+ ...) G_GNUC_PRINTF (4, 5);
+static void
+do_start_write (GVfsBackendFtp *ftp,
+ FtpConnection *conn,
+ GFileCreateFlags flags,
+ const char *format,
+ ...)
+{
+ va_list varargs;
+ guint status;
+
+ /* FIXME: can we honour the flags? */
+
+ ftp_connection_ensure_data_connection (conn);
+
+ va_start (varargs, format);
+ status = ftp_connection_sendv (conn,
+ RESPONSE_PASS_100 | RESPONSE_FAIL_200,
+ format,
+ varargs);
+ va_end (varargs);
+
+ if (ftp_connection_in_error (conn))
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+ else
+ {
+ /* don't push the connection back, it's our handle now */
+ g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (conn->job), conn);
+ g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (conn->job), FALSE);
+ ftp_connection_pop_job (conn);
+ }
+}
+
+static void
+gvfs_backend_ftp_purge_cache_directory (GVfsBackendFtp *ftp,
+ const FtpFile * dir)
+{
+ g_static_rw_lock_writer_lock (&ftp->directory_cache_lock);
+ g_hash_table_remove (ftp->directory_cache, dir);
+ g_static_rw_lock_writer_unlock (&ftp->directory_cache_lock);
+}
+
+static void
+gvfs_backend_ftp_purge_cache_of_file (GVfsBackendFtp *ftp,
+ FtpConnection * conn,
+ const FtpFile * file)
+{
+ char *dirname, *filename;
+ FtpFile *dir;
+
+ filename = ftp_filename_to_gvfs_path (conn, file);
+ dirname = g_path_get_dirname (filename);
+ dir = ftp_filename_from_gvfs_path (conn, dirname);
+
+ gvfs_backend_ftp_purge_cache_directory (ftp, dir);
+
+ g_free (dir);
+ g_free (filename);
+ g_free (dirname);
+}
+
+/* forward declaration */
+static GFileInfo *
+create_file_info (GVfsBackendFtp *ftp, FtpConnection *conn, const char *filename, char **symlink);
+
+static void
+do_create (GVfsBackend *backend,
+ GVfsJobOpenForWrite *job,
+ const char *filename,
+ GFileCreateFlags flags)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ GFileInfo *info;
+ FtpFile *file;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ info = create_file_info (ftp, conn, filename, NULL);
+ if (info)
+ {
+ g_object_unref (info);
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_EXISTS,
+ _("Target file already exists"));
+ goto error;
+ }
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ do_start_write (ftp, conn, flags, "STOR %s", file);
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, file);
+ g_free (file);
+ return;
+
+error:
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_append (GVfsBackend *backend,
+ GVfsJobOpenForWrite *job,
+ const char *filename,
+ GFileCreateFlags flags)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ FtpFile *file;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ do_start_write (ftp, conn, flags, "APPE %s", filename);
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, file);
+ g_free (file);
+ return;
+}
+
+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);
+ FtpConnection *conn;
+ FtpFile *file;
+
+ if (make_backup)
+ {
+ /* FIXME: implement! */
+ g_vfs_job_failed (G_VFS_JOB (job),
+ G_IO_ERROR,
+ G_IO_ERROR_CANT_CREATE_BACKUP,
+ _("backups not supported yet"));
+ return;
+ }
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ do_start_write (ftp, conn, flags, "STOR %s", file);
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, file);
+ g_free (file);
+ return;
+}
+
+static void
+do_close_write (GVfsBackend *backend,
+ GVfsJobCloseWrite *job,
+ GVfsBackendHandle handle)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn = handle;
+
+ ftp_connection_push_job (conn, G_VFS_JOB (job));
+
+ ftp_connection_close_data_connection (conn);
+ ftp_connection_receive (conn, 0);
+
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_write (GVfsBackend *backend,
+ GVfsJobWrite *job,
+ GVfsBackendHandle handle,
+ char *buffer,
+ gsize buffer_size)
+{
+ FtpConnection *conn = handle;
+ gsize n_bytes;
+
+ ftp_connection_push_job (conn, G_VFS_JOB (job));
+
+ soup_socket_write (conn->data,
+ buffer,
+ buffer_size,
+ &n_bytes,
+ G_VFS_JOB (job)->cancellable,
+ &conn->error);
+
+ g_vfs_job_write_set_written_size (job, n_bytes);
+ ftp_connection_pop_job (conn);
+}
+
+static GList *
+do_enumerate_directory (FtpConnection *conn)
+{
+ gsize size, n_bytes, bytes_read;
+ SoupSocketIOStatus status;
+ gboolean got_boundary;
+ char *name;
+ GList *list = NULL;
+
+ if (ftp_connection_in_error (conn))
+ return NULL;
+
+ size = 128;
+ bytes_read = 0;
+ name = g_malloc (size);
+
+ do
+ {
+ if (bytes_read + 3 >= size)
+ {
+ if (size >= 16384)
+ {
+ g_set_error_literal (&conn->error, G_IO_ERROR, G_IO_ERROR_FILENAME_TOO_LONG,
+ _("filename too long"));
+ break;
+ }
+ size += 128;
+ name = g_realloc (name, size);
+ }
+ status = soup_socket_read_until (conn->data,
+ name + bytes_read,
+ size - bytes_read - 1,
+ "\n",
+ 1,
+ &n_bytes,
+ &got_boundary,
+ conn->job->cancellable,
+ &conn->error);
+
+ bytes_read += n_bytes;
+ switch (status)
+ {
+ case SOUP_SOCKET_EOF:
+ case SOUP_SOCKET_OK:
+ if (n_bytes == 0)
+ {
+ status = SOUP_SOCKET_EOF;
+ break;
+ }
+ if (got_boundary)
+ {
+ name[bytes_read - 1] = 0;
+ if (bytes_read >= 2 && name[bytes_read - 2] == '\r')
+ name[bytes_read - 2] = 0;
+ DEBUG ("--- %s\n", name);
+ list = g_list_prepend (list, g_strdup (name));
+ bytes_read = 0;
+ }
+ break;
+ case SOUP_SOCKET_ERROR:
+ goto error2;
+ case SOUP_SOCKET_WOULD_BLOCK:
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+ }
+ while (status == SOUP_SOCKET_OK);
+
+ if (bytes_read)
+ {
+ name[bytes_read] = 0;
+ DEBUG ("--- %s\n", name);
+ list = g_list_prepend (list, name);
+ }
+ else
+ g_free (name);
+
+ ftp_connection_close_data_connection (conn);
+ ftp_connection_receive (conn, 0);
+ if (ftp_connection_in_error (conn))
+ goto error;
+
+ return g_list_reverse (list);
+
+error2:
+ ftp_connection_close_data_connection (conn);
+ ftp_connection_receive (conn, 0);
+error:
+ ftp_connection_close_data_connection (conn);
+ g_list_foreach (list, (GFunc) g_free, NULL);
+ g_list_free (list);
+ return NULL;
+}
+
+/* IMPORTANT: SUCK ALARM!
+ * locks ftp->directory_cache_lock but only iff it returns !NULL */
+static const GList *
+enumerate_directory (GVfsBackendFtp *ftp,
+ FtpConnection * conn,
+ const FtpFile * dir,
+ gboolean use_cache)
+{
+ GList *files;
+
+ g_static_rw_lock_reader_lock (&ftp->directory_cache_lock);
+ do {
+ if (use_cache)
+ files = g_hash_table_lookup (ftp->directory_cache, dir);
+ else
+ {
+ use_cache = TRUE;
+ files = NULL;
+ }
+ if (files == NULL)
+ {
+ g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
+ ftp->dir_ops->init_data (conn, dir);
+ files = do_enumerate_directory (conn);
+ if (files == NULL)
+ {
+ return NULL;
+ }
+ g_static_rw_lock_writer_lock (&ftp->directory_cache_lock);
+ g_hash_table_insert (ftp->directory_cache, g_strdup ((const char *) dir), files);
+ g_static_rw_lock_writer_unlock (&ftp->directory_cache_lock);
+ files = NULL;
+ g_static_rw_lock_reader_lock (&ftp->directory_cache_lock);
+ }
+ } while (files == NULL);
+
+ return files;
+}
+
+/* NB: This gets a file info for the given object, no matter if it's a dir
+ * or a file */
+static GFileInfo *
+create_file_info (GVfsBackendFtp *ftp, FtpConnection *conn, const char *filename, char **symlink)
+{
+ const GList *walk, *files;
+ char *dirname;
+ FtpFile *dir, *file;
+ GFileInfo *info;
+ gpointer iter;
+
+ if (symlink)
+ *symlink = NULL;
+
+ if (g_str_equal (filename, "/"))
+ return ftp->dir_ops->get_root (conn);
+
+ dirname = g_path_get_dirname (filename);
+ dir = ftp_filename_from_gvfs_path (conn, dirname);
+ g_free (dirname);
+
+ files = enumerate_directory (ftp, conn, dir, TRUE);
+ if (files == NULL)
+ {
+ g_free (dir);
+ return NULL;
+ }
+
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ iter = ftp->dir_ops->iter_new (conn);
+ for (walk = files; walk; walk = walk->next)
+ {
+ info = ftp->dir_ops->iter_process (iter,
+ conn,
+ dir,
+ file,
+ walk->data,
+ symlink);
+ if (info)
+ break;
+ }
+ ftp->dir_ops->iter_free (iter);
+ g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
+ g_free (dir);
+ g_free (file);
+ return info;
+}
+
+static GFileInfo *
+resolve_symlink (GVfsBackendFtp *ftp, FtpConnection *conn, GFileInfo *original, const char *filename)
+{
+ GFileInfo *info = NULL;
+ char *symlink, *newlink;
+ guint i;
+ static const char *copy_attributes[] = {
+ G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK,
+ G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
+ G_FILE_ATTRIBUTE_STANDARD_NAME,
+ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+ G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME,
+ G_FILE_ATTRIBUTE_STANDARD_COPY_NAME,
+ G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET
+ };
+
+ if (ftp_connection_in_error (conn))
+ return original;
+
+ /* How many symlinks should we follow?
+ * <alex> maybe 8?
+ */
+ symlink = g_strdup (filename);
+ for (i = 0; i < 8 && symlink; i++)
+ {
+ info = create_file_info (ftp,
+ conn,
+ symlink,
+ &newlink);
+ if (!newlink)
+ break;
+
+ g_free (symlink);
+ symlink = newlink;
+ }
+ g_free (symlink);
+
+ if (ftp_connection_in_error (conn))
+ {
+ g_assert (info == NULL);
+ ftp_connection_clear_error (conn);
+ return original;
+ }
+ if (info == NULL)
+ return original;
+
+ for (i = 0; i < G_N_ELEMENTS (copy_attributes); i++)
+ {
+ GFileAttributeType type;
+ gpointer value;
+
+ if (!g_file_info_get_attribute_data (original,
+ copy_attributes[i],
+ &type,
+ &value,
+ NULL))
+ continue;
+
+ g_file_info_set_attribute (info,
+ copy_attributes[i],
+ type,
+ value);
+ }
+ g_object_unref (original);
+
+ return info;
+}
+
+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);
+ FtpConnection *conn;
+ GFileInfo *real;
+ char *symlink;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ if (query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
+ {
+ real = create_file_info (ftp,
+ conn,
+ filename,
+ NULL);
+ }
+ else
+ {
+ real = create_file_info (ftp,
+ conn,
+ filename,
+ &symlink);
+ if (symlink)
+ {
+ real = resolve_symlink (ftp, conn, real, symlink);
+ g_free (symlink);
+ }
+ }
+
+ if (real)
+ {
+ g_file_info_copy_into (real, info);
+ g_object_unref (real);
+ }
+ else if (!ftp_connection_in_error (conn))
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_FOUND,
+ _("File doesn't exist"));
+
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_enumerate (GVfsBackend *backend,
+ GVfsJobEnumerate *job,
+ const char *dirname,
+ GFileAttributeMatcher *matcher,
+ GFileQueryInfoFlags query_flags)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ const GList *walk, *files;
+ FtpFile *dir;
+ gpointer iter;
+ GSList *symlink_targets = NULL;
+ GSList *symlink_fileinfos = NULL;
+ GSList *twalk, *fwalk;
+ GFileInfo *info;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ /* no need to check for IS_DIR, because the enumeration code will return that
+ * automatically.
+ */
+
+ dir = ftp_filename_from_gvfs_path (conn, dirname);
+ files = enumerate_directory (ftp, conn, dir, FALSE);
+ if (ftp_connection_pop_job (conn))
+ {
+ ftp_connection_push_job (conn, G_VFS_JOB (job));
+ if (files != NULL)
+ {
+ iter = ftp->dir_ops->iter_new (conn);
+ for (walk = files; walk; walk = walk->next)
+ {
+ char *symlink = NULL;
+ info = ftp->dir_ops->iter_process (iter,
+ conn,
+ dir,
+ NULL,
+ walk->data,
+ query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? NULL : &symlink);
+ if (symlink)
+ {
+ /* This is necessary due to our locking.
+ * And we must not unlock here because it might invalidate the list we iterate */
+ symlink_targets = g_slist_prepend (symlink_targets, symlink);
+ symlink_fileinfos = g_slist_prepend (symlink_fileinfos, info);
+ }
+ else if (info)
+ {
+ g_vfs_job_enumerate_add_info (job, info);
+ g_object_unref (info);
+ }
+ }
+ ftp->dir_ops->iter_free (iter);
+ g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
+ for (twalk = symlink_targets, fwalk = symlink_fileinfos; twalk;
+ twalk = twalk->next, fwalk = fwalk->next)
+ {
+ info = resolve_symlink (ftp, conn, fwalk->data, twalk->data);
+ g_free (twalk->data);
+ g_vfs_job_enumerate_add_info (job, info);
+ g_object_unref (info);
+ }
+ g_slist_free (symlink_targets);
+ g_slist_free (symlink_fileinfos);
+ }
+
+ g_vfs_job_enumerate_done (job);
+ conn->job = NULL;
+ ftp_connection_clear_error (conn);
+ }
+ else
+ g_assert (files == NULL);
+
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+ g_free (dir);
+}
+
+static void
+do_set_display_name (GVfsBackend *backend,
+ GVfsJobSetDisplayName *job,
+ const char *filename,
+ const char *display_name)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ char *name;
+ FtpFile *original, *dir, *now;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ original = ftp_filename_from_gvfs_path (conn, filename);
+ name = g_path_get_dirname (filename);
+ dir = ftp_filename_from_gvfs_path (conn, name);
+ g_free (name);
+ now = ftp_filename_construct (conn, dir, display_name);
+ if (now == NULL)
+ {
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_FILENAME,
+ _("Invalid filename"));
+ }
+ ftp_connection_send (conn,
+ RESPONSE_PASS_300 | RESPONSE_FAIL_200,
+ "RNFR %s", original);
+ g_free (original);
+ ftp_connection_send (conn,
+ 0,
+ "RNTO %s", now);
+
+ name = ftp_filename_to_gvfs_path (conn, now);
+ g_free (now);
+ g_vfs_job_set_display_name_set_new_path (job, name);
+ g_free (name);
+ gvfs_backend_ftp_purge_cache_directory (ftp, dir);
+ g_free (dir);
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_delete (GVfsBackend *backend,
+ GVfsJobDelete *job,
+ const char *filename)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ FtpFile *file;
+ guint response;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ /* 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 = ftp_filename_from_gvfs_path (conn, filename);
+ response = ftp_connection_send (conn,
+ RESPONSE_PASS_500,
+ "DELE %s", file);
+ if (STATUS_GROUP (response) == 5)
+ {
+ response = ftp_connection_send (conn,
+ RESPONSE_PASS_500,
+ "RMD %s", file);
+ if (response == 550)
+ {
+ const GList *files = enumerate_directory (ftp, conn, file, FALSE);
+ if (files)
+ {
+ g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_EMPTY,
+ g_strerror (ENOTEMPTY));
+ }
+ else
+ ftp_connection_set_error_from_response (conn, response);
+ }
+ else if (STATUS_GROUP (response) == 5)
+ {
+ ftp_connection_set_error_from_response (conn, response);
+ }
+ }
+
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, file);
+ g_free (file);
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+static void
+do_make_directory (GVfsBackend *backend,
+ GVfsJobMakeDirectory *job,
+ const char *filename)
+{
+ GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
+ FtpConnection *conn;
+ FtpFile *file;
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ file = ftp_filename_from_gvfs_path (conn, filename);
+ ftp_connection_send (conn,
+ 0,
+ "MKD %s", file);
+ /* FIXME: Compare created file with name from server result to be sure
+ * it's correct and otherwise fail. */
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, file);
+ g_free (file);
+
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+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);
+ FtpConnection *conn;
+ FtpFile *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_vfs_job_failed (G_VFS_JOB (job),
+ G_IO_ERROR,
+ G_IO_ERROR_CANT_CREATE_BACKUP,
+ _("backups not supported yet"));
+ return;
+ }
+
+ conn = g_vfs_backend_ftp_pop_connection (ftp, G_VFS_JOB (job));
+ if (conn == NULL)
+ return;
+
+ srcfile = ftp_filename_from_gvfs_path (conn, source);
+ destfile = ftp_filename_from_gvfs_path (conn, destination);
+ if (ftp_connection_try_cd (conn, destfile))
+ {
+ char *basename = g_path_get_basename (source);
+ FtpFile *real = ftp_filename_construct (conn, destfile, basename);
+
+ g_free (basename);
+ if (real == NULL)
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
+ _("Invalid destination filename"));
+ else
+ {
+ g_free (destfile);
+ destfile = real;
+ }
+ }
+
+ if (!(flags & G_FILE_COPY_OVERWRITE))
+ {
+ char *destfilename = ftp_filename_to_gvfs_path (conn, destfile);
+ GFileInfo *info = create_file_info (ftp, conn, destfilename, NULL);
+
+ g_free (destfilename);
+ if (info)
+ {
+ g_object_unref (info);
+ g_set_error_literal (&conn->error,
+ G_IO_ERROR,
+ G_IO_ERROR_EXISTS,
+ _("Target file already exists"));
+ goto out;
+ }
+ }
+
+ ftp_connection_send (conn,
+ RESPONSE_PASS_300 | RESPONSE_FAIL_200,
+ "RNFR %s", srcfile);
+ ftp_connection_send (conn,
+ 0,
+ "RNTO %s", destfile);
+
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, srcfile);
+ gvfs_backend_ftp_purge_cache_of_file (ftp, conn, destfile);
+out:
+ g_free (srcfile);
+ g_free (destfile);
+ g_vfs_backend_ftp_push_connection (ftp, conn);
+}
+
+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;
+}