/* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2008,2009 Benjamin Otte * 2008,2009 Andreas Henriksson * * 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 * Andreas Henriksson */ #include #include /* for strerror (EAGAIN) */ #include #include #include #include #include #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"" * 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*"".* * 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 FALSE; } 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)); }