/* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2006-2007 Red Hat, Inc. * * 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., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * Author: Alexander Larsson */ #include #include #include #include #include #include #include #include #include #include #include #include "gvfsbackendsmb.h" #include "gvfsbackendsmbprivate.h" #include "gvfsjobopenforread.h" #include "gvfsjobread.h" #include "gvfsjobseekread.h" #include "gvfsjobopenforwrite.h" #include "gvfsjobwrite.h" #include "gvfsjobclosewrite.h" #include "gvfsjobseekwrite.h" #include "gvfsjobtruncate.h" #include "gvfsjobsetdisplayname.h" #include "gvfsjobqueryinfo.h" #include "gvfsjobqueryfsinfo.h" #include "gvfsjobqueryattributes.h" #include "gvfsjobenumerate.h" #include "gvfsjobmove.h" #include "gvfsdaemonprotocol.h" #include "gvfsdaemonutils.h" #include "gvfsutils.h" #include "gvfskeyring.h" #include struct _GVfsBackendSmb { GVfsBackend parent_instance; char *server; char *share; char *user; char *domain; char *path; char *default_workgroup; int port; SMBCCTX *smb_context; char *last_user; char *last_domain; char *last_password; GMountSource *mount_source; /* Only used/set during mount */ int mount_try; gboolean mount_cancelled; gboolean password_in_keyring; GPasswordSave password_save; }; G_DEFINE_TYPE (GVfsBackendSmb, g_vfs_backend_smb, G_VFS_TYPE_BACKEND) static void set_info_from_stat (GVfsBackendSmb *backend, GFileInfo *info, struct stat *statbuf, const char *basename, GFileAttributeMatcher *matcher); static void g_vfs_backend_smb_finalize (GObject *object) { GVfsBackendSmb *backend; backend = G_VFS_BACKEND_SMB (object); g_free (backend->share); g_free (backend->server); g_free (backend->user); g_free (backend->domain); g_free (backend->path); g_free (backend->default_workgroup); if (G_OBJECT_CLASS (g_vfs_backend_smb_parent_class)->finalize) (*G_OBJECT_CLASS (g_vfs_backend_smb_parent_class)->finalize) (object); } static void g_vfs_backend_smb_init (GVfsBackendSmb *backend) { char *workgroup; GSettings *settings; /* Get default workgroup name */ settings = g_settings_new ("org.gnome.system.smb"); workgroup = g_settings_get_string (settings, "workgroup"); if (workgroup && workgroup[0]) backend->default_workgroup = workgroup; else g_free (workgroup); g_object_unref (settings); g_debug ("g_vfs_backend_smb_init: default workgroup = '%s'\n", backend->default_workgroup ? backend->default_workgroup : "NULL"); } /** * Authentication callback function type (method that includes context) * * Type for the the authentication function called by the library to * obtain authentication credentals * * @param context Pointer to the smb context * @param srv Server being authenticated to * @param shr Share being authenticated to * @param wg Pointer to buffer containing a "hint" for the * workgroup to be authenticated. Should be filled in * with the correct workgroup if the hint is wrong. * @param wglen The size of the workgroup buffer in bytes * @param un Pointer to buffer containing a "hint" for the * user name to be use for authentication. Should be * filled in with the correct workgroup if the hint is * wrong. * @param unlen The size of the username buffer in bytes * @param pw Pointer to buffer containing to which password * copied * @param pwlen The size of the password buffer in bytes * */ static void auth_callback (SMBCCTX *context, const char *server_name, const char *share_name, char *domain_out, int domainmaxlen, char *username_out, int unmaxlen, char *password_out, int pwmaxlen) { GVfsBackendSmb *backend; char *ask_password, *ask_user, *ask_domain; gboolean handled, abort, anonymous = FALSE; backend = smbc_getOptionUserData (context); strncpy (password_out, "", pwmaxlen); if (backend->domain) strncpy (domain_out, backend->domain, domainmaxlen); if (backend->user) strncpy (username_out, backend->user, unmaxlen); if (backend->mount_cancelled) { /* Don't prompt for credentials, let smbclient finish the mount loop */ strncpy (username_out, "ABORT", unmaxlen); strncpy (password_out, "", pwmaxlen); g_debug ("auth_callback - mount_cancelled\n"); return; } if (backend->mount_source == NULL) { /* Not during mount, use last password */ if (backend->last_user) strncpy (username_out, backend->last_user, unmaxlen); if (backend->last_domain) strncpy (domain_out, backend->last_domain, domainmaxlen); if (backend->last_password) strncpy (password_out, backend->last_password, pwmaxlen); return; } if (backend->mount_try == 0 && backend->user == NULL && backend->domain == NULL) { g_debug ("auth_callback - kerberos pass\n"); } else if (backend->mount_try == 1 && backend->user == NULL && backend->domain == NULL) { g_debug ("auth_callback - ccache pass\n"); } else { gboolean in_keyring = FALSE; g_debug ("auth_callback - normal pass\n"); if (!backend->password_in_keyring) { in_keyring = g_vfs_keyring_lookup_password (backend->user, backend->server, backend->domain, "smb", NULL, NULL, backend->port != -1 ? backend->port : 0, &ask_user, &ask_domain, &ask_password); backend->password_in_keyring = in_keyring; if (in_keyring) g_debug ("auth_callback - reusing keyring credentials: user = '%s', domain = '%s'\n", ask_user ? ask_user : "NULL", ask_domain ? ask_domain : "NULL"); } if (!in_keyring) { GAskPasswordFlags flags = G_ASK_PASSWORD_NEED_PASSWORD; char *message; if (g_vfs_keyring_is_available ()) flags |= G_ASK_PASSWORD_SAVING_SUPPORTED; if (backend->domain == NULL) flags |= G_ASK_PASSWORD_NEED_DOMAIN; if (backend->user == NULL) flags |= G_ASK_PASSWORD_NEED_USERNAME; if (backend->user == NULL && backend->domain == NULL) flags |= G_ASK_PASSWORD_ANONYMOUS_SUPPORTED; g_debug ("auth_callback - asking for password...\n"); if (backend->user) { /* Translators: First %s is a share name, second is a server name */ message = g_strdup_printf (_("Authentication Required\nEnter password for share “%s” on “%s”:"), share_name, server_name); } else { /* Translators: First %s is a share name, second is a server name */ message = g_strdup_printf (_("Authentication Required\nEnter user and password for share “%s” on “%s”:"), share_name, server_name); } handled = g_mount_source_ask_password (backend->mount_source, message, username_out, domain_out, flags, &abort, &ask_password, &ask_user, &ask_domain, &anonymous, &(backend->password_save)); g_free (message); if (!handled) goto out; if (abort) { strncpy (username_out, "ABORT", unmaxlen); strncpy (password_out, "", pwmaxlen); backend->mount_cancelled = TRUE; goto out; } } smbc_setOptionNoAutoAnonymousLogin (backend->smb_context, !anonymous); if (anonymous) { backend->password_save = FALSE; g_debug ("auth_callback - anonymous enabled\n"); } else { strncpy (password_out, ask_password, pwmaxlen); if (ask_user && *ask_user) strncpy (username_out, ask_user, unmaxlen); if (ask_domain && *ask_domain) strncpy (domain_out, ask_domain, domainmaxlen); } out: g_free (ask_password); g_free (ask_user); g_free (ask_domain); } backend->last_user = g_strdup (username_out); backend->last_domain = g_strdup (domain_out); backend->last_password = g_strdup (password_out); g_debug ("auth_callback - out: last_user = '%s', last_domain = '%s'\n", backend->last_user, backend->last_domain); } #define SUB_DELIM_CHARS "!$&'()*+,;=" static GString * create_smb_uri_string (const char *server, int port, const char *share, const char *path) { GString *uri; uri = g_string_new ("smb://"); if (server == NULL) return uri; /* IPv6 server includes brackets in GMountSpec, smbclient doesn't */ if (gvfs_is_ipv6 (server)) { g_string_append_uri_escaped (uri, server + 1, NULL, FALSE); g_string_truncate (uri, uri->len - 3); } else g_string_append_uri_escaped (uri, server, NULL, FALSE); if (port != -1) g_string_append_printf (uri, ":%d", port); g_string_append_c (uri, '/'); if (share != NULL) g_string_append_uri_escaped (uri, share, NULL, FALSE); if (path != NULL) { if (*path != '/') g_string_append_c (uri, '/'); g_string_append_uri_escaped (uri, path, SUB_DELIM_CHARS ":@/", FALSE); } while (uri->len > 0 && uri->str[uri->len - 1] == '/') g_string_erase (uri, uri->len - 1, 1); return uri; } char * create_smb_uri (const char *server, int port, const char *share, const char *path) { GString *uri; uri = create_smb_uri_string (server, port, share, path); return g_string_free (uri, FALSE); } static void do_mount (GVfsBackend *backend, GVfsJobMount *job, GMountSpec *mount_spec, GMountSource *mount_source, gboolean is_automount) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); SMBCCTX *smb_context; struct stat st; char *uri; int res; char *display_name; const char *debug; int debug_val; gchar *port_str; GMountSpec *smb_mount_spec; smbc_stat_fn smbc_stat; int errsv; smb_context = smbc_new_context (); if (smb_context == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Internal Error (%s)"), "Failed to allocate smb context"); return; } smbc_setOptionUserData (smb_context, backend); debug = g_getenv ("GVFS_SMB_DEBUG"); if (debug) debug_val = atoi (debug); else debug_val = 0; smbc_setDebug (smb_context, debug_val); smbc_setFunctionAuthDataWithContext (smb_context, auth_callback); if (op_backend->default_workgroup != NULL) smbc_setWorkgroup (smb_context, op_backend->default_workgroup); smbc_setOptionUseKerberos (smb_context, 1); smbc_setOptionFallbackAfterKerberos (smb_context, op_backend->user != NULL); smbc_setOptionNoAutoAnonymousLogin (smb_context, TRUE); smbc_setOptionUseCCache (smb_context, 1); if (!smbc_init_context (smb_context)) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Internal Error (%s)"), "Failed to initialize smb context"); smbc_free_context (smb_context, FALSE); return; } op_backend->smb_context = smb_context; /* Set the mountspec according to original uri, no matter whether user changes credentials during mount loop. Nautilus and other gio clients depend on correct mountspec, setting it to real (different) credentials would lead to G_IO_ERROR_NOT_MOUNTED errors */ /* Translators: This is " on " and is used as name for an SMB share */ display_name = g_strdup_printf (_("%s on %s"), op_backend->share, op_backend->server); g_vfs_backend_set_display_name (backend, display_name); g_free (display_name); g_vfs_backend_set_icon_name (backend, "folder-remote"); g_vfs_backend_set_symbolic_icon_name (backend, "folder-remote-symbolic"); smb_mount_spec = g_mount_spec_new ("smb-share"); g_mount_spec_set (smb_mount_spec, "share", op_backend->share); g_mount_spec_set (smb_mount_spec, "server", op_backend->server); if (op_backend->user) g_mount_spec_set (smb_mount_spec, "user", op_backend->user); if (op_backend->domain) g_mount_spec_set (smb_mount_spec, "domain", op_backend->domain); if (op_backend->port != -1) { port_str = g_strdup_printf ("%d", op_backend->port); g_mount_spec_set (smb_mount_spec, "port", port_str); g_free (port_str); } g_vfs_backend_set_mount_spec (backend, smb_mount_spec); g_mount_spec_unref (smb_mount_spec); /* FIXME: we're stat()-ing user-specified path here, not the root. Ideally we would like to fallback to root when first mount attempt fails, though it would be tough to actually say if it was an authentication failure or the particular path problem. */ uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, op_backend->path); g_debug ("do_mount - URI = %s\n", uri); /* Samba mount loop */ op_backend->mount_source = mount_source; op_backend->mount_try = 0; op_backend->password_save = G_PASSWORD_SAVE_NEVER; errsv = 0; /* If user is not specified, first and second iteration is kerberos resp. * ccache only (this is necessary in order to prevent redundant password * prompts). Consequently, credentials from keyring are tried if available. * Finally, user is prompted over GMountOperation if available. Anonymous is * tried only if explicitely requested over GMountOperation. */ do { op_backend->mount_cancelled = FALSE; g_debug ("do_mount - try #%d \n", op_backend->mount_try); smbc_stat = smbc_getFunctionStat (smb_context); res = smbc_stat (smb_context, uri, &st); errsv = errno; g_debug ("do_mount - [%s; %d] res = %d, cancelled = %d, errno = [%d] '%s' \n", uri, op_backend->mount_try, res, op_backend->mount_cancelled, errsv, g_strerror (errsv)); if (res == 0) break; if (errsv == EINVAL && op_backend->mount_try <= 1 && op_backend->user == NULL) { /* EINVAL is "expected" when kerberos/ccache is misconfigured, see: * https://gitlab.gnome.org/GNOME/gvfs/-/issues/611 */ } else if (op_backend->mount_cancelled || (errsv != EACCES && errsv != EPERM)) { g_debug ("do_mount - (errno != EPERM && errno != EACCES), cancelled = %d, breaking\n", op_backend->mount_cancelled); break; } /* The first round is Kerberos-only. Only if this fails do we enable * NTLMSSP fallback. */ if (op_backend->mount_try == 0 && op_backend->user == NULL) { g_debug ("do_mount - enabling NTLMSSP fallback\n"); smbc_setOptionFallbackAfterKerberos (op_backend->smb_context, 1); } op_backend->mount_try ++; } while (TRUE); g_free (uri); op_backend->mount_source = NULL; if (res != 0) { if (op_backend->mount_cancelled) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, _("Password dialog cancelled")); else g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, /* translators: We tried to mount a windows (samba) share, but failed */ _("Failed to mount Windows share: %s"), g_strerror (errsv)); return; } /* Mount was successful */ g_debug ("do_mount - login successful\n"); g_vfs_backend_set_default_location (backend, op_backend->path); g_vfs_keyring_save_password (op_backend->last_user, op_backend->server, op_backend->last_domain, "smb", NULL, NULL, op_backend->port != -1 ? op_backend->port : 0, op_backend->last_password, op_backend->password_save); g_vfs_job_succeeded (G_VFS_JOB (job)); } static gboolean try_mount (GVfsBackend *backend, GVfsJobMount *job, GMountSpec *mount_spec, GMountSource *mount_source, gboolean is_automount) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); const char *server, *share, *user, *domain, *path, *port; int port_num; server = g_mount_spec_get (mount_spec, "server"); share = g_mount_spec_get (mount_spec, "share"); if (server == NULL || share == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Invalid mount spec")); return TRUE; } user = g_mount_spec_get (mount_spec, "user"); domain = g_mount_spec_get (mount_spec, "domain"); port = g_mount_spec_get (mount_spec, "port"); path = mount_spec->mount_prefix; op_backend->server = g_strdup (server); op_backend->share = g_strdup (share); op_backend->user = g_strdup (user); op_backend->domain = g_strdup (domain); op_backend->path = g_strdup (path); if (port && (port_num = atoi (port))) op_backend->port = port_num; else op_backend->port = -1; return FALSE; } static void do_unmount (GVfsBackend *backend, GVfsJobUnmount *job, GMountUnmountFlags flags, GMountSource *mount_source) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); int res; if (op_backend->smb_context == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Internal Error (%s)"), "SMB context has not been initialized"); return; } /* shutdown_ctx = TRUE, "all connections and files will be closed even if they are busy" */ res = smbc_free_context (op_backend->smb_context, TRUE); op_backend->smb_context = NULL; if (res != 0) { g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); return; } g_vfs_job_succeeded (G_VFS_JOB (job)); } static int fixup_open_errno (int err) { /* samba has a bug (#6228) where it doesn't set errno if path resolving failed */ if (err == 0) err = ENOTDIR; return err; } static void do_open_for_read (GVfsBackend *backend, GVfsJobOpenForRead *job, const char *filename) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *uri; SMBCFILE *file; struct stat st; smbc_open_fn smbc_open; smbc_stat_fn smbc_stat; int res; int olderr; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_open = smbc_getFunctionOpen (op_backend->smb_context); errno = 0; file = smbc_open (op_backend->smb_context, uri, O_RDONLY, 0); if (file == NULL) { olderr = fixup_open_errno (errno); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); res = smbc_stat (op_backend->smb_context, uri, &st); if ((res == 0) && (S_ISDIR (st.st_mode))) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("Can’t open directory")); else g_vfs_job_failed_from_errno (G_VFS_JOB (job), olderr); } else { g_vfs_job_open_for_read_set_can_seek (job, TRUE); g_vfs_job_open_for_read_set_handle (job, file); g_vfs_job_succeeded (G_VFS_JOB (job)); } g_free (uri); } static void do_read (GVfsBackend *backend, GVfsJobRead *job, GVfsBackendHandle handle, char *buffer, gsize bytes_requested) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); ssize_t res; smbc_read_fn smbc_read; smbc_read = smbc_getFunctionRead (op_backend->smb_context); res = smbc_read (op_backend->smb_context, (SMBCFILE *)handle, buffer, bytes_requested); if (res == -1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else { g_vfs_job_read_set_size (job, res); g_vfs_job_succeeded (G_VFS_JOB (job)); } } static void do_seek_on_read (GVfsBackend *backend, GVfsJobSeekRead *job, GVfsBackendHandle handle, goffset offset, GSeekType type) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); int whence; off_t res; smbc_lseek_fn smbc_lseek; if ((whence = gvfs_seek_type_to_lseek (type)) == -1) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Unsupported seek type")); return; } smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context); res = smbc_lseek (op_backend->smb_context, (SMBCFILE *)handle, offset, whence); if (res == (off_t)-1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else { g_vfs_job_seek_read_set_offset (job, res); g_vfs_job_succeeded (G_VFS_JOB (job)); } return; } static void do_query_info_on_read (GVfsBackend *backend, GVfsJobQueryInfoRead *job, GVfsBackendHandle handle, GFileInfo *info, GFileAttributeMatcher *matcher) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat st = {0}; int res, saved_errno; smbc_fstat_fn smbc_fstat; smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context); res = smbc_fstat (op_backend->smb_context, (SMBCFILE *)handle, &st); saved_errno = errno; if (res == 0) { set_info_from_stat (op_backend, info, &st, NULL, matcher); g_vfs_job_succeeded (G_VFS_JOB (job)); } else g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno); } static void do_close_read (GVfsBackend *backend, GVfsJobCloseRead *job, GVfsBackendHandle handle) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); ssize_t res; smbc_close_fn smbc_close; smbc_close = smbc_getFunctionClose (op_backend->smb_context); res = smbc_close (op_backend->smb_context, (SMBCFILE *)handle); if (res == -1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else g_vfs_job_succeeded (G_VFS_JOB (job)); } typedef struct { SMBCFILE *file; char *uri; char *tmp_uri; char *backup_uri; } SmbWriteHandle; static void smb_write_handle_free (SmbWriteHandle *handle) { g_free (handle->uri); g_free (handle->tmp_uri); g_free (handle->backup_uri); g_free (handle); } static void do_create (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, GFileCreateFlags flags) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *uri; SMBCFILE *file; SmbWriteHandle *handle; smbc_open_fn smbc_open; int errsv; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_open = smbc_getFunctionOpen (op_backend->smb_context); errno = 0; file = smbc_open (op_backend->smb_context, uri, O_CREAT|O_RDWR|O_EXCL, 0666); g_free (uri); if (file == NULL) { errsv = fixup_open_errno (errno); /* We guarantee EEXIST on create on existing dir */ if (errsv == EISDIR) errsv = EEXIST; g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); } else { handle = g_new0 (SmbWriteHandle, 1); handle->file = file; g_vfs_job_open_for_write_set_can_seek (job, TRUE); g_vfs_job_open_for_write_set_can_truncate (job, TRUE); g_vfs_job_open_for_write_set_handle (job, handle); g_vfs_job_succeeded (G_VFS_JOB (job)); } } static void do_append_to (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, GFileCreateFlags flags) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *uri; SMBCFILE *file; SmbWriteHandle *handle; off_t initial_offset; smbc_open_fn smbc_open; smbc_lseek_fn smbc_lseek; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_open = smbc_getFunctionOpen (op_backend->smb_context); errno = 0; file = smbc_open (op_backend->smb_context, uri, O_CREAT|O_RDWR|O_APPEND, 0666); g_free (uri); if (file == NULL) g_vfs_job_failed_from_errno (G_VFS_JOB (job), fixup_open_errno (errno)); else { handle = g_new0 (SmbWriteHandle, 1); handle->file = file; smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context); initial_offset = smbc_lseek (op_backend->smb_context, file, 0, SEEK_CUR); if (initial_offset == (off_t) -1) g_vfs_job_open_for_write_set_can_seek (job, FALSE); else { g_vfs_job_open_for_write_set_initial_offset (job, initial_offset); g_vfs_job_open_for_write_set_can_seek (job, TRUE); g_vfs_job_open_for_write_set_can_truncate (job, TRUE); } g_vfs_job_open_for_write_set_handle (job, handle); g_vfs_job_succeeded (G_VFS_JOB (job)); } } static char * get_dir_from_uri (const char *uri) { const char *prefix_end; prefix_end = uri + strlen (uri); /* Skip slashes at end */ while (prefix_end > uri && *(prefix_end - 1) == '/') prefix_end--; /* Skip to next slash */ while (prefix_end > uri && *(prefix_end - 1) != '/') prefix_end--; return g_strndup (uri, prefix_end - uri); } static SMBCFILE * open_tmpfile (GVfsBackendSmb *backend, const char *uri, char **tmp_uri_out) { char *dir_uri, *tmp_uri; char filename[] = "~gvfXXXX.tmp"; SMBCFILE *file; smbc_open_fn smbc_open; dir_uri = get_dir_from_uri (uri); do { gvfs_randomize_string (filename + 4, 4); tmp_uri = g_strconcat (dir_uri, filename, NULL); smbc_open = smbc_getFunctionOpen (backend->smb_context); errno = 0; file = smbc_open (backend->smb_context, tmp_uri, O_CREAT|O_RDWR|O_EXCL, 0666); } while (file == NULL && errno == EEXIST); g_free (dir_uri); if (file) { *tmp_uri_out = tmp_uri; return file; } else { g_free (tmp_uri); return NULL; } } static gboolean copy_file (GVfsBackendSmb *backend, GVfsJob *job, const char *from_uri, const char *to_uri) { SMBCFILE *from_file, *to_file; char buffer[4096]; size_t buffer_size; ssize_t res; char *p; gboolean succeeded; smbc_open_fn smbc_open; smbc_read_fn smbc_read; smbc_write_fn smbc_write; smbc_close_fn smbc_close; from_file = NULL; to_file = NULL; succeeded = FALSE; smbc_open = smbc_getFunctionOpen (backend->smb_context); smbc_read = smbc_getFunctionRead (backend->smb_context); smbc_write = smbc_getFunctionWrite (backend->smb_context); smbc_close = smbc_getFunctionClose (backend->smb_context); from_file = smbc_open (backend->smb_context, from_uri, O_RDONLY, 0666); if (from_file == NULL || g_vfs_job_is_cancelled (job)) goto out; to_file = smbc_open (backend->smb_context, to_uri, O_CREAT|O_WRONLY|O_TRUNC, 0666); if (from_file == NULL || g_vfs_job_is_cancelled (job)) goto out; while (1) { res = smbc_read (backend->smb_context, from_file, buffer, sizeof(buffer)); if (res < 0 || g_vfs_job_is_cancelled (job)) goto out; if (res == 0) break; /* Succeeded */ buffer_size = res; p = buffer; while (buffer_size > 0) { res = smbc_write (backend->smb_context, to_file, p, buffer_size); if (res < 0 || g_vfs_job_is_cancelled (job)) goto out; buffer_size -= res; p += res; } } succeeded = TRUE; out: if (to_file) smbc_close (backend->smb_context, to_file); if (from_file) smbc_close (backend->smb_context, from_file); return succeeded; } static char * create_etag (struct stat *statbuf) { return g_strdup_printf ("%lu", (long unsigned int)statbuf->st_mtime); } static void do_replace (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, const char *etag, gboolean make_backup, GFileCreateFlags flags) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat original_stat; int res; char *uri, *tmp_uri, *backup_uri, *current_etag; SMBCFILE *file; GError *error = NULL; SmbWriteHandle *handle; smbc_open_fn smbc_open; smbc_stat_fn smbc_stat; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); tmp_uri = NULL; if (make_backup) backup_uri = g_strconcat (uri, "~", NULL); else backup_uri = NULL; smbc_open = smbc_getFunctionOpen (op_backend->smb_context); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); errno = 0; file = smbc_open (op_backend->smb_context, uri, O_CREAT|O_RDWR|O_EXCL, 0); if (file == NULL && errno != EEXIST) { int errsv = fixup_open_errno (errno); g_set_error_literal (&error, G_IO_ERROR, g_io_error_from_errno (errsv), g_strerror (errsv)); goto error; } else if (file == NULL && errno == EEXIST) { if (etag != NULL) { res = smbc_stat (op_backend->smb_context, uri, &original_stat); if (res == 0) { current_etag = create_etag (&original_stat); if (strcmp (etag, current_etag) != 0) { g_free (current_etag); g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_WRONG_ETAG, _("The file was externally modified")); goto error; } g_free (current_etag); } } /* Backup strategy: * * By default we: * 1) save to a tmp file (that doesn't exist already) * 2) rename orig file to backup file * (or delete it if no backup) * 3) rename tmp file to orig file * * However, this can fail if we can't write to the directory. * In that case we just truncate the file, after having * copied directly to the backup filename. */ file = open_tmpfile (op_backend, uri, &tmp_uri); if (file == NULL) { if (make_backup) { if (!copy_file (op_backend, G_VFS_JOB (job), uri, backup_uri)) { if (g_vfs_job_is_cancelled (G_VFS_JOB (job))) g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was cancelled")); else g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP, _("Backup file creation failed")); goto error; } g_free (backup_uri); backup_uri = NULL; } errno = 0; file = smbc_open (op_backend->smb_context, uri, O_CREAT|O_RDWR|O_TRUNC, 0); if (file == NULL) { int errsv = fixup_open_errno (errno); g_set_error_literal (&error, G_IO_ERROR, g_io_error_from_errno (errsv), g_strerror (errsv)); goto error; } } } else { /* Doesn't exist. Just write away */ g_free (backup_uri); backup_uri = NULL; } handle = g_new (SmbWriteHandle, 1); handle->file = file; handle->uri = uri; handle->tmp_uri = tmp_uri; handle->backup_uri = backup_uri; g_vfs_job_open_for_write_set_can_seek (job, TRUE); g_vfs_job_open_for_write_set_can_truncate (job, TRUE); g_vfs_job_open_for_write_set_handle (job, handle); g_vfs_job_succeeded (G_VFS_JOB (job)); return; error: g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_free (backup_uri); g_free (tmp_uri); g_free (uri); } static void do_write (GVfsBackend *backend, GVfsJobWrite *job, GVfsBackendHandle _handle, char *buffer, gsize buffer_size) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); SmbWriteHandle *handle = _handle; ssize_t res; smbc_write_fn smbc_write; smbc_write = smbc_getFunctionWrite (op_backend->smb_context); res = smbc_write (op_backend->smb_context, handle->file, buffer, buffer_size); if (res == -1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else { g_vfs_job_write_set_written_size (job, res); g_vfs_job_succeeded (G_VFS_JOB (job)); } } static void do_seek_on_write (GVfsBackend *backend, GVfsJobSeekWrite *job, GVfsBackendHandle _handle, goffset offset, GSeekType type) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); SmbWriteHandle *handle = _handle; int whence; off_t res; smbc_lseek_fn smbc_lseek; if ((whence = gvfs_seek_type_to_lseek (type)) == -1) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Unsupported seek type")); return; } smbc_lseek = smbc_getFunctionLseek (op_backend->smb_context); res = smbc_lseek (op_backend->smb_context, handle->file, offset, whence); if (res == (off_t)-1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else { g_vfs_job_seek_write_set_offset (job, res); g_vfs_job_succeeded (G_VFS_JOB (job)); } return; } static void do_truncate (GVfsBackend *backend, GVfsJobTruncate *job, GVfsBackendHandle _handle, goffset size) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); SmbWriteHandle *handle = _handle; smbc_ftruncate_fn smbc_ftruncate; smbc_ftruncate = smbc_getFunctionFtruncate (op_backend->smb_context); if (smbc_ftruncate (op_backend->smb_context, handle->file, size) == -1) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errno); else g_vfs_job_succeeded (G_VFS_JOB (job)); } static void do_query_info_on_write (GVfsBackend *backend, GVfsJobQueryInfoWrite *job, GVfsBackendHandle _handle, GFileInfo *info, GFileAttributeMatcher *matcher) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat st = {0}; SmbWriteHandle *handle = _handle; int res, saved_errno; smbc_fstat_fn smbc_fstat; smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context); res = smbc_fstat (op_backend->smb_context, handle->file, &st); saved_errno = errno; if (res == 0) { set_info_from_stat (op_backend, info, &st, NULL, matcher); g_vfs_job_succeeded (G_VFS_JOB (job)); } else g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno); } static void do_close_write (GVfsBackend *backend, GVfsJobCloseWrite *job, GVfsBackendHandle _handle) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); SmbWriteHandle *handle = _handle; struct stat stat_at_close; int stat_res, errsv; ssize_t res; smbc_fstat_fn smbc_fstat; smbc_close_fn smbc_close; smbc_unlink_fn smbc_unlink; smbc_rename_fn smbc_rename; smbc_fstat = smbc_getFunctionFstat (op_backend->smb_context); smbc_close = smbc_getFunctionClose (op_backend->smb_context); smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context); smbc_rename = smbc_getFunctionRename (op_backend->smb_context); stat_res = smbc_fstat (op_backend->smb_context, handle->file, &stat_at_close); res = smbc_close (op_backend->smb_context, handle->file); if (res == -1) { errsv = errno; if (handle->tmp_uri) smbc_unlink (op_backend->smb_context, handle->tmp_uri); g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); goto out; } if (handle->tmp_uri) { if (handle->backup_uri) { res = smbc_rename (op_backend->smb_context, handle->uri, op_backend->smb_context, handle->backup_uri); if (res == -1) { errsv = errno; smbc_unlink (op_backend->smb_context, handle->tmp_uri); g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP, _("Backup file creation failed: %s"), g_strerror (errsv)); goto out; } } else { res = smbc_unlink (op_backend->smb_context, handle->uri); if (res == -1) { errsv = errno; smbc_unlink (op_backend->smb_context, handle->tmp_uri); g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); goto out; } } res = smbc_rename (op_backend->smb_context, handle->tmp_uri, op_backend->smb_context, handle->uri); if (res == -1) { errsv = errno; smbc_unlink (op_backend->smb_context, handle->tmp_uri); g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); goto out; } } if (stat_res == 0) { char *etag; etag = create_etag (&stat_at_close); g_vfs_job_close_write_set_etag (job, etag); g_free (etag); } g_vfs_job_succeeded (G_VFS_JOB (job)); out: smb_write_handle_free (handle); } static void set_info_from_stat (GVfsBackendSmb *backend, GFileInfo *info, struct stat *statbuf, const char *basename, GFileAttributeMatcher *matcher) { GFileType file_type; char *display_name; if (basename) { g_file_info_set_name (info, basename); if (*basename == '.') g_file_info_set_is_hidden (info, TRUE); } if (basename != NULL && g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)) { if (strcmp (basename, "/") == 0) display_name = g_strdup_printf (_("%s on %s"), backend->share, backend->server); else display_name = g_filename_display_name (basename); if (strstr (display_name, "\357\277\275") != NULL) { char *p = display_name; display_name = g_strconcat (display_name, _(" (invalid encoding)"), NULL); g_free (p); } g_file_info_set_display_name (info, display_name); g_free (display_name); } if (basename != NULL && g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME)) { char *edit_name = g_filename_display_name (basename); g_file_info_set_edit_name (info, edit_name); g_free (edit_name); } file_type = G_FILE_TYPE_UNKNOWN; if (S_ISREG (statbuf->st_mode)) file_type = G_FILE_TYPE_REGULAR; else if (S_ISDIR (statbuf->st_mode)) file_type = G_FILE_TYPE_DIRECTORY; else if (S_ISCHR (statbuf->st_mode) || S_ISBLK (statbuf->st_mode) || S_ISFIFO (statbuf->st_mode) #ifdef S_ISSOCK || S_ISSOCK (statbuf->st_mode) #endif ) file_type = G_FILE_TYPE_SPECIAL; else if (S_ISLNK (statbuf->st_mode)) file_type = G_FILE_TYPE_SYMBOLIC_LINK; g_file_info_set_file_type (info, file_type); g_file_info_set_size (info, statbuf->st_size); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE, statbuf->st_blocks * G_GUINT64_CONSTANT (512)); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED, statbuf->st_mtime); #if defined (HAVE_STRUCT_STAT_ST_MTIMENSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, statbuf->st_mtimensec / 1000); #elif defined (HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, statbuf->st_mtim.tv_nsec / 1000); #else g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, 0); #endif if (g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE) || g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE) || g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_ICON) || g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON)) { GIcon *icon = NULL; GIcon *symbolic_icon = NULL; char *content_type = NULL; gboolean uncertain_content_type = FALSE; if (S_ISDIR(statbuf->st_mode)) { content_type = g_strdup ("inode/directory"); if (basename != NULL && strcmp (basename, "/") == 0) { icon = g_themed_icon_new ("folder-remote"); symbolic_icon = g_themed_icon_new ("folder-remote-symbolic"); } else { icon = g_content_type_get_icon (content_type); symbolic_icon = g_content_type_get_symbolic_icon (content_type); } } else if (basename != NULL) { content_type = g_content_type_guess (basename, NULL, 0, &uncertain_content_type); if (content_type) { icon = g_content_type_get_icon (content_type); symbolic_icon = g_content_type_get_symbolic_icon (content_type); } } if (content_type) { if (!uncertain_content_type) g_file_info_set_content_type (info, content_type); g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, content_type); g_free (content_type); } if (icon == NULL) icon = g_themed_icon_new ("text-x-generic"); if (symbolic_icon == NULL) symbolic_icon = g_themed_icon_new ("text-x-generic-symbolic"); g_file_info_set_icon (info, icon); g_object_unref (icon); g_file_info_set_symbolic_icon (info, symbolic_icon); g_object_unref (symbolic_icon); } /* Don't trust n_link, uid, gid, etc returned from libsmb, its just made up. These are ok though: */ g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_DEVICE, statbuf->st_dev); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_UNIX_INODE, statbuf->st_ino); /* If file is dos-readonly, libsmbclient doesn't set S_IWUSR, we use this to trigger ACCESS_WRITE = FALSE. Only set for regular files, see https://bugzilla.gnome.org/show_bug.cgi?id=598206 */ if (S_ISREG (statbuf->st_mode)) g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, statbuf->st_mode & S_IWUSR); g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS, statbuf->st_atime); #if defined (HAVE_STRUCT_STAT_ST_ATIMENSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_ACCESS_USEC, statbuf->st_atimensec / 1000); #elif defined (HAVE_STRUCT_STAT_ST_ATIM_TV_NSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_ACCESS_USEC, statbuf->st_atim.tv_nsec / 1000); #endif g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED, statbuf->st_ctime); #if defined (HAVE_STRUCT_STAT_ST_CTIMENSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, statbuf->st_ctimensec / 1000); #elif defined (HAVE_STRUCT_STAT_ST_CTIM_TV_NSEC) g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_CHANGED_USEC, statbuf->st_ctim.tv_nsec / 1000); #endif /* Libsmb sets the X bit on files to indicate some special things: */ if ((statbuf->st_mode & S_IFDIR) == 0) { if (statbuf->st_mode & S_IXOTH) g_file_info_set_is_hidden (info, TRUE); if (statbuf->st_mode & S_IXUSR) g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_DOS_IS_ARCHIVE, TRUE); if (statbuf->st_mode & S_IXGRP) g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_DOS_IS_SYSTEM, TRUE); } if (g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_ETAG_VALUE)) { char *etag = create_etag (statbuf); g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, etag); g_free (etag); } } static void do_query_info (GVfsBackend *backend, GVfsJobQueryInfo *job, const char *filename, GFileQueryInfoFlags flags, GFileInfo *info, GFileAttributeMatcher *matcher) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat st = {0}; char *uri; int res, saved_errno; char *basename; smbc_stat_fn smbc_stat; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); res = smbc_stat (op_backend->smb_context, uri, &st); saved_errno = errno; g_free (uri); if (res == 0) { basename = g_path_get_basename (filename); set_info_from_stat (op_backend, info, &st, basename, matcher); g_free (basename); g_vfs_job_succeeded (G_VFS_JOB (job)); } else g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno); } static void do_query_fs_info (GVfsBackend *backend, GVfsJobQueryFsInfo *job, const char *filename, GFileInfo *info, GFileAttributeMatcher *attribute_matcher) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); smbc_statvfs_fn smbc_statvfs; struct statvfs st = {0}; char *uri; int res, saved_errno; g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, "cifs"); g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_REMOTE, TRUE); g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW, G_FILESYSTEM_PREVIEW_TYPE_IF_ALWAYS); if (g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE) || g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_FREE) || g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_USED) || g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) { uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_statvfs = smbc_getFunctionStatVFS (op_backend->smb_context); res = smbc_statvfs (op_backend->smb_context, uri, &st); saved_errno = errno; g_free (uri); if (res == 0) { /* older samba versions ( < 3.0.28) return zero values in struct statvfs */ if (st.f_blocks > 0) { /* FIXME: inconsistent return values (libsmbclient-3.4.2) * - for linux samba hosts, f_frsize is zero and f_bsize is a real block size * - for some Windows hosts (XP), f_frsize and f_bsize should be multiplied to get real block size */ guint64 size, free_space; size = st.f_bsize * st.f_blocks * ((st.f_frsize == 0) ? 1 : st.f_frsize); free_space = st.f_bsize * st.f_bfree * ((st.f_frsize == 0) ? 1 : st.f_frsize); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE, size); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, free_space); g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USED, size - free_space); g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, st.f_flag & SMBC_VFS_FEATURE_RDONLY); } } else { g_vfs_job_failed_from_errno (G_VFS_JOB (job), saved_errno); return; } } g_vfs_job_succeeded (G_VFS_JOB (job)); } static gboolean try_query_settable_attributes (GVfsBackend *backend, GVfsJobQueryAttributes *job, const char *filename) { GFileAttributeInfoList *list; list = g_file_attribute_info_list_new (); /* TODO: Add all settable attributes here -- bug #559586 */ /* TODO: xattrs support? */ g_file_attribute_info_list_add (list, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_ATTRIBUTE_TYPE_UINT64, G_FILE_ATTRIBUTE_INFO_COPY_WITH_FILE | G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED); #if 0 /* FIXME: disabled; despite chmod is supported, it makes no sense on samba shares and libsmbclient lacks proper API to read unix file modes. The struct stat->st_mode member is used for special Windows attributes. */ 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); #endif 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) { GVfsBackendSmb *op_backend; char *uri; int res, errsv; struct timeval tbuf[2]; smbc_utimes_fn smbc_utimes; #if 0 smbc_chmod_fn smbc_chmod; #endif op_backend = G_VFS_BACKEND_SMB (backend); if (strcmp (attribute, G_FILE_ATTRIBUTE_TIME_MODIFIED) != 0 #if 0 && strcmp (attribute, G_FILE_ATTRIBUTE_UNIX_MODE) != 0 #endif ) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")); return; } uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); res = -1; if (strcmp (attribute, G_FILE_ATTRIBUTE_TIME_MODIFIED) == 0) { if (type == G_FILE_ATTRIBUTE_TYPE_UINT64) { smbc_utimes = smbc_getFunctionUtimes (op_backend->smb_context); tbuf[1].tv_sec = (*(guint64 *)value_p); /* mtime */ tbuf[1].tv_usec = 0; /* atime = mtime (atimes are usually disabled on desktop systems) */ tbuf[0].tv_sec = tbuf[1].tv_sec; tbuf[0].tv_usec = 0; res = smbc_utimes (op_backend->smb_context, uri, &tbuf[0]); } else { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "%s", _("Invalid attribute type (uint64 expected)")); } } #if 0 else if (strcmp (attribute, G_FILE_ATTRIBUTE_UNIX_MODE) == 0) { smbc_chmod = smbc_getFunctionChmod (op_backend->smb_context); res = smbc_chmod (op_backend->smb_context, uri, (*(guint32 *)value_p) & 0777); } #endif errsv = errno; g_free (uri); if (res != 0) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); else g_vfs_job_succeeded (G_VFS_JOB (job)); } static void do_enumerate (GVfsBackend *backend, GVfsJobEnumerate *job, const char *filename, GFileAttributeMatcher *matcher, GFileQueryInfoFlags flags) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat st = { 0 }; GError *error; SMBCFILE *dir; GFileInfo *info; GString *uri; smbc_opendir_fn smbc_opendir; smbc_closedir_fn smbc_closedir; #ifndef HAVE_SMBC_READDIRPLUS2 int res; char dirents[1024*4]; struct smbc_dirent *dirp; int uri_start_len; smbc_getdents_fn smbc_getdents; smbc_stat_fn smbc_stat; #else smbc_readdirplus2_fn smbc_readdirplus2; const struct libsmb_file_info *exstat; #endif uri = create_smb_uri_string (op_backend->server, op_backend->port, op_backend->share, filename); smbc_opendir = smbc_getFunctionOpendir (op_backend->smb_context); #ifndef HAVE_SMBC_READDIRPLUS2 smbc_getdents = smbc_getFunctionGetdents (op_backend->smb_context); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); #else smbc_readdirplus2 = smbc_getFunctionReaddirPlus2 (op_backend->smb_context); #endif smbc_closedir = smbc_getFunctionClosedir (op_backend->smb_context); dir = smbc_opendir (op_backend->smb_context, uri->str); if (dir == NULL) { int errsv = errno; error = NULL; g_set_error_literal (&error, G_IO_ERROR, g_io_error_from_errno (errsv), g_strerror (errsv)); goto error; } g_vfs_job_succeeded (G_VFS_JOB (job)); if (uri->str[uri->len - 1] != '/') g_string_append_c (uri, '/'); #ifndef HAVE_SMBC_READDIRPLUS2 uri_start_len = uri->len; while (TRUE) { res = smbc_getdents (op_backend->smb_context, dir, (struct smbc_dirent *)dirents, sizeof (dirents)); if (res <= 0) break; dirp = (struct smbc_dirent *)dirents; while (res > 0) { unsigned int dirlen; /* TODO: Only do stat if required for flags */ if ((dirp->smbc_type == SMBC_DIR || dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_LINK) && strcmp (dirp->name, ".") != 0 && strcmp (dirp->name, "..") != 0) { int stat_res; g_string_truncate (uri, uri_start_len); g_string_append_uri_escaped (uri, dirp->name, SUB_DELIM_CHARS ":@/", FALSE); if (matcher == NULL || g_file_attribute_matcher_matches_only (matcher, G_FILE_ATTRIBUTE_STANDARD_NAME)) { info = g_file_info_new (); g_file_info_set_name (info, dirp->name); g_vfs_job_enumerate_add_info (job, info); g_object_unref (info); } else { stat_res = smbc_stat (op_backend->smb_context, uri->str, &st); if (stat_res == 0) { info = g_file_info_new (); set_info_from_stat (op_backend, info, &st, dirp->name, matcher); g_vfs_job_enumerate_add_info (job, info); g_object_unref (info); } } } dirlen = dirp->dirlen; dirp = (struct smbc_dirent *) (((char *)dirp) + dirlen); res -= dirlen; } } #else while ((exstat = smbc_readdirplus2 (op_backend->smb_context, dir, &st)) != NULL) { if ((S_ISREG (st.st_mode) || S_ISDIR (st.st_mode) || S_ISLNK (st.st_mode)) && g_strcmp0 (exstat->name, ".") != 0 && g_strcmp0 (exstat->name, "..") != 0) { info = g_file_info_new (); set_info_from_stat (op_backend, info, &st, exstat->name, matcher); g_vfs_job_enumerate_add_info (job, info); g_object_unref (info); } memset (&st, 0, sizeof (struct stat)); } #endif smbc_closedir (op_backend->smb_context, dir); g_vfs_job_enumerate_done (job); g_string_free (uri, TRUE); return; error: g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_string_free (uri, TRUE); } static void do_set_display_name (GVfsBackend *backend, GVfsJobSetDisplayName *job, const char *filename, const char *display_name) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *from_uri, *to_uri; g_autofree char *basename = NULL; g_autofree char *old_name_case = NULL; g_autofree char *new_name_case = NULL; char *dirname, *new_path; int res, errsv; struct stat st; smbc_rename_fn smbc_rename; smbc_stat_fn smbc_stat; dirname = g_path_get_dirname (filename); basename = g_path_get_basename (filename); /* TODO: display name is in utf8, atm we assume libsmb uris are in utf8, but this might not be true if the user changed the smb.conf file. Can we check this and convert? */ new_path = g_build_filename (dirname, display_name, NULL); g_free (dirname); from_uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); to_uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, new_path); /* If we are simply changing the case of an existing file, we don't need to * worry about overwriting another file. */ old_name_case = g_utf8_casefold (basename, -1); new_name_case = g_utf8_casefold (display_name, -1); if (g_strcmp0 (old_name_case, new_name_case) != 0) { /* We can't rely on libsmbclient reporting EEXIST, let's always stat first. * https://bugzilla.gnome.org/show_bug.cgi?id=616645 */ smbc_stat = smbc_getFunctionStat (op_backend->smb_context); res = smbc_stat (op_backend->smb_context, to_uri, &st); if (res == 0) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Can’t rename file, filename already exists")); goto out; } } smbc_rename = smbc_getFunctionRename (op_backend->smb_context); res = smbc_rename (op_backend->smb_context, from_uri, op_backend->smb_context, to_uri); errsv = errno; if (res != 0) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); else { g_vfs_job_set_display_name_set_new_path (job, new_path); g_vfs_job_succeeded (G_VFS_JOB (job)); } out: g_free (from_uri); g_free (to_uri); g_free (new_path); } static void do_delete (GVfsBackend *backend, GVfsJobDelete *job, const char *filename) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); struct stat statbuf; char *uri; int errsv, res; smbc_stat_fn smbc_stat; smbc_rmdir_fn smbc_rmdir; smbc_unlink_fn smbc_unlink; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); smbc_rmdir = smbc_getFunctionRmdir (op_backend->smb_context); smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context); res = smbc_stat (op_backend->smb_context, uri, &statbuf); if (res == -1) { errsv = errno; g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, g_io_error_from_errno (errsv), _("Error deleting file: %s"), g_strerror (errsv)); g_free (uri); return; } if (S_ISDIR (statbuf.st_mode)) { res = smbc_rmdir (op_backend->smb_context, uri); /* We can't rely on libsmbclient reporting ENOTEMPTY, let's verify that * the dir has been really removed: * https://bugzilla.samba.org/show_bug.cgi?id=13204 */ if (res == 0 && smbc_stat (op_backend->smb_context, uri, &statbuf) == 0) { res = -1; errno = ENOTEMPTY; } } else res = smbc_unlink (op_backend->smb_context, uri); errsv = errno; g_free (uri); if (res != 0) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); else g_vfs_job_succeeded (G_VFS_JOB (job)); } static void do_make_directory (GVfsBackend *backend, GVfsJobMakeDirectory *job, const char *filename) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *uri; int errsv, res; smbc_mkdir_fn smbc_mkdir; uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, filename); smbc_mkdir = smbc_getFunctionMkdir (op_backend->smb_context); res = smbc_mkdir (op_backend->smb_context, uri, 0666); errsv = errno; g_free (uri); if (res != 0) g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); else g_vfs_job_succeeded (G_VFS_JOB (job)); } static void do_move (GVfsBackend *backend, GVfsJobMove *job, const char *source, const char *destination, GFileCopyFlags flags, GFileProgressCallback progress_callback, gpointer progress_callback_data) { GVfsBackendSmb *op_backend = G_VFS_BACKEND_SMB (backend); char *source_uri, *dest_uri, *backup_uri; gboolean destination_exist, source_is_dir; struct stat statbuf; int res, errsv; smbc_stat_fn smbc_stat; smbc_rename_fn smbc_rename; smbc_unlink_fn smbc_unlink; goffset size; source_uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, source); smbc_stat = smbc_getFunctionStat (op_backend->smb_context); smbc_rename = smbc_getFunctionRename (op_backend->smb_context); smbc_unlink = smbc_getFunctionUnlink (op_backend->smb_context); res = smbc_stat (op_backend->smb_context, source_uri, &statbuf); if (res == -1) { errsv = errno; g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, g_io_error_from_errno (errsv), _("Error moving file: %s"), g_strerror (errsv)); g_free (source_uri); return; } source_is_dir = S_ISDIR (statbuf.st_mode); size = statbuf.st_size; dest_uri = create_smb_uri (op_backend->server, op_backend->port, op_backend->share, destination); destination_exist = FALSE; res = smbc_stat (op_backend->smb_context, dest_uri, &statbuf); if (res == 0) { destination_exist = TRUE; /* Target file exists */ if (flags & G_FILE_COPY_OVERWRITE) { /* Always fail on dirs, even with overwrite */ if (S_ISDIR (statbuf.st_mode)) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_WOULD_MERGE, _("Can’t move directory over directory")); g_free (source_uri); g_free (dest_uri); return; } } else { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); g_free (source_uri); g_free (dest_uri); return; } } if (flags & G_FILE_COPY_BACKUP && destination_exist) { backup_uri = g_strconcat (dest_uri, "~", NULL); res = smbc_rename (op_backend->smb_context, dest_uri, op_backend->smb_context, backup_uri); if (res == -1) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP, _("Backup file creation failed")); g_free (source_uri); g_free (dest_uri); g_free (backup_uri); return; } g_free (backup_uri); destination_exist = FALSE; /* It did, but no more */ } if (source_is_dir && destination_exist && (flags & G_FILE_COPY_OVERWRITE)) { /* Source is a dir, destination exists (and is not a dir, because that would have failed earlier), and we're overwriting. Manually remove the target so we can do the rename. */ res = smbc_unlink (op_backend->smb_context, dest_uri); errsv = errno; if (res == -1) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, g_io_error_from_errno (errsv), _("Error removing target file: %s"), g_strerror (errsv)); g_free (source_uri); g_free (dest_uri); return; } } res = smbc_rename (op_backend->smb_context, source_uri, op_backend->smb_context, dest_uri); errsv = errno; g_free (source_uri); g_free (dest_uri); /* Catch moves across device boundaries */ if (res != 0) { if (errsv == EXDEV || /* Unfortunately libsmbclient doesn't correctly return EXDEV, but falls back to EINVAL, so we try to guess when this happens: */ (errsv == EINVAL && source_is_dir)) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_WOULD_RECURSE, _("Can’t recursively move directory")); else g_vfs_job_failed_from_errno (G_VFS_JOB (job), errsv); } else { g_vfs_job_progress_callback (size, size, job); g_vfs_job_succeeded (G_VFS_JOB (job)); } } static void g_vfs_backend_smb_class_init (GVfsBackendSmbClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass); gobject_class->finalize = g_vfs_backend_smb_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->read = do_read; backend_class->seek_on_read = do_seek_on_read; backend_class->query_info_on_read = do_query_info_on_read; backend_class->close_read = do_close_read; backend_class->create = do_create; backend_class->append_to = do_append_to; backend_class->replace = do_replace; backend_class->write = do_write; backend_class->seek_on_write = do_seek_on_write; backend_class->truncate = do_truncate; backend_class->query_info_on_write = do_query_info_on_write; backend_class->close_write = do_close_write; backend_class->query_info = do_query_info; backend_class->query_fs_info = do_query_fs_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; } void g_vfs_smb_daemon_init (void) { g_set_application_name (_("Windows Shares File System Service")); }