/* GTK - The GIMP Toolkit * gtkfilesystem.c: Filesystem abstraction functions. * Copyright (C) 2003, Red Hat, Inc. * Copyright (C) 2007-2008 Carlos Garnacho * * 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, see . * * Authors: Carlos Garnacho */ #include "config.h" #include #include #include "gtkfilechooser.h" #include "gtkfilesystem.h" #include "gtkicontheme.h" #include "gtkprivate.h" #include "gtkintl.h" /* #define DEBUG_MODE */ #ifdef DEBUG_MODE #define DEBUG(x) g_debug (x); #else #define DEBUG(x) #endif #define FILES_PER_QUERY 100 /* The pointers we return for a GtkFileSystemVolume are opaque tokens; they are * really pointers to GDrive, GVolume or GMount objects. We need an extra * token for the fake “File System” volume. So, we’ll return a pointer to * this particular string. */ static const gchar *root_volume_token = N_("File System"); #define IS_ROOT_VOLUME(volume) ((gpointer) (volume) == (gpointer) root_volume_token) enum { PROP_0, PROP_FILE, PROP_ENUMERATOR, PROP_ATTRIBUTES }; enum { VOLUMES_CHANGED, FS_LAST_SIGNAL }; enum { FILES_ADDED, FILES_REMOVED, FILES_CHANGED, FINISHED_LOADING, DELETED, FOLDER_LAST_SIGNAL }; static guint fs_signals [FS_LAST_SIGNAL] = { 0, }; typedef struct AsyncFuncData AsyncFuncData; struct GtkFileSystemPrivate { GVolumeMonitor *volume_monitor; /* This list contains elements that can be * of type GDrive, GVolume and GMount */ GSList *volumes; }; struct AsyncFuncData { GtkFileSystem *file_system; GFile *file; GCancellable *cancellable; gpointer callback; gpointer data; }; G_DEFINE_TYPE_WITH_PRIVATE (GtkFileSystem, _gtk_file_system, G_TYPE_OBJECT) /* GtkFileSystem methods */ static void volumes_changed (GVolumeMonitor *volume_monitor, gpointer volume, gpointer user_data) { GtkFileSystem *file_system; gdk_threads_enter (); file_system = GTK_FILE_SYSTEM (user_data); g_signal_emit (file_system, fs_signals[VOLUMES_CHANGED], 0, volume); gdk_threads_leave (); } static void gtk_file_system_dispose (GObject *object) { GtkFileSystem *file_system = GTK_FILE_SYSTEM (object); GtkFileSystemPrivate *priv = file_system->priv; DEBUG ("dispose"); if (priv->volumes) { g_slist_free_full (priv->volumes, g_object_unref); priv->volumes = NULL; } if (priv->volume_monitor) { g_signal_handlers_disconnect_by_func (priv->volume_monitor, volumes_changed, object); g_object_unref (priv->volume_monitor); priv->volume_monitor = NULL; } G_OBJECT_CLASS (_gtk_file_system_parent_class)->dispose (object); } static void _gtk_file_system_class_init (GtkFileSystemClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); object_class->dispose = gtk_file_system_dispose; fs_signals[VOLUMES_CHANGED] = g_signal_new (I_("volumes-changed"), G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkFileSystemClass, volumes_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static gboolean mount_referenced_by_volume_activation_root (GList *volumes, GMount *mount) { GList *l; GFile *mount_root; gboolean ret; ret = FALSE; mount_root = g_mount_get_root (mount); for (l = volumes; l != NULL; l = l->next) { GVolume *volume = G_VOLUME (l->data); GFile *volume_activation_root; volume_activation_root = g_volume_get_activation_root (volume); if (volume_activation_root != NULL) { if (g_file_has_prefix (volume_activation_root, mount_root)) { ret = TRUE; g_object_unref (volume_activation_root); break; } g_object_unref (volume_activation_root); } } g_object_unref (mount_root); return ret; } static void get_volumes_list (GtkFileSystem *file_system) { GtkFileSystemPrivate *priv = file_system->priv; GList *l, *ll; GList *drives; GList *volumes; GList *mounts; GDrive *drive; GVolume *volume; GMount *mount; if (priv->volumes) { g_slist_free_full (priv->volumes, g_object_unref); priv->volumes = NULL; } /* first go through all connected drives */ drives = g_volume_monitor_get_connected_drives (priv->volume_monitor); for (l = drives; l != NULL; l = l->next) { drive = l->data; volumes = g_drive_get_volumes (drive); if (volumes) { for (ll = volumes; ll != NULL; ll = ll->next) { volume = ll->data; mount = g_volume_get_mount (volume); if (mount) { /* Show mounted volume */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (mount)); g_object_unref (mount); } else { /* Do show the unmounted volumes in the sidebar; * this is so the user can mount it (in case automounting * is off). * * Also, even if automounting is enabled, this gives a visual * cue that the user should remember to yank out the media if * he just unmounted it. */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (volume)); } g_object_unref (volume); } g_list_free (volumes); } else if (g_drive_is_media_removable (drive) && !g_drive_is_media_check_automatic (drive)) { /* If the drive has no mountable volumes and we cannot detect media change.. we * display the drive in the sidebar so the user can manually poll the drive by * right clicking and selecting "Rescan..." * * This is mainly for drives like floppies where media detection doesn't * work.. but it's also for human beings who like to turn off media detection * in the OS to save battery juice. */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (drive)); } g_object_unref (drive); } g_list_free (drives); /* add all volumes that is not associated with a drive */ volumes = g_volume_monitor_get_volumes (priv->volume_monitor); for (l = volumes; l != NULL; l = l->next) { volume = l->data; drive = g_volume_get_drive (volume); if (drive) { g_object_unref (drive); continue; } mount = g_volume_get_mount (volume); if (mount) { /* show this mount */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (mount)); g_object_unref (mount); } else { /* see comment above in why we add an icon for a volume */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (volume)); } g_object_unref (volume); } /* add mounts that has no volume (/etc/mtab mounts, ftp, sftp,...) */ mounts = g_volume_monitor_get_mounts (priv->volume_monitor); for (l = mounts; l != NULL; l = l->next) { mount = l->data; volume = g_mount_get_volume (mount); if (volume) { g_object_unref (volume); continue; } /* if there's exists one or more volumes with an activation root inside the mount, * don't display the mount */ if (mount_referenced_by_volume_activation_root (volumes, mount)) { g_object_unref (mount); continue; } /* show this mount */ priv->volumes = g_slist_prepend (priv->volumes, g_object_ref (mount)); g_object_unref (mount); } g_list_free (volumes); g_list_free (mounts); } static void _gtk_file_system_init (GtkFileSystem *file_system) { GtkFileSystemPrivate *priv; DEBUG ("init"); file_system->priv = priv = _gtk_file_system_get_instance_private (file_system); /* Volumes */ priv->volume_monitor = g_volume_monitor_get (); g_signal_connect (priv->volume_monitor, "mount-added", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "mount-removed", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "mount-changed", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "volume-added", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "volume-removed", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "volume-changed", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "drive-connected", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "drive-disconnected", G_CALLBACK (volumes_changed), file_system); g_signal_connect (priv->volume_monitor, "drive-changed", G_CALLBACK (volumes_changed), file_system); } /* GtkFileSystem public methods */ GtkFileSystem * _gtk_file_system_new (void) { return g_object_new (GTK_TYPE_FILE_SYSTEM, NULL); } GSList * _gtk_file_system_list_volumes (GtkFileSystem *file_system) { GtkFileSystemPrivate *priv = file_system->priv; GSList *list; DEBUG ("list_volumes"); get_volumes_list (file_system); list = g_slist_copy (priv->volumes); #ifndef G_OS_WIN32 /* Prepend root volume */ list = g_slist_prepend (list, (gpointer) root_volume_token); #endif return list; } static void free_async_data (AsyncFuncData *async_data) { g_object_unref (async_data->file_system); g_object_unref (async_data->file); g_object_unref (async_data->cancellable); g_free (async_data); } static void query_info_callback (GObject *source_object, GAsyncResult *result, gpointer user_data) { AsyncFuncData *async_data; GError *error = NULL; GFileInfo *file_info; GFile *file; DEBUG ("query_info_callback"); file = G_FILE (source_object); async_data = (AsyncFuncData *) user_data; file_info = g_file_query_info_finish (file, result, &error); if (async_data->callback) { gdk_threads_enter (); ((GtkFileSystemGetInfoCallback) async_data->callback) (async_data->cancellable, file_info, error, async_data->data); gdk_threads_leave (); } if (file_info) g_object_unref (file_info); if (error) g_error_free (error); free_async_data (async_data); } GCancellable * _gtk_file_system_get_info (GtkFileSystem *file_system, GFile *file, const gchar *attributes, GtkFileSystemGetInfoCallback callback, gpointer data) { GCancellable *cancellable; AsyncFuncData *async_data; g_return_val_if_fail (GTK_IS_FILE_SYSTEM (file_system), NULL); g_return_val_if_fail (G_IS_FILE (file), NULL); cancellable = g_cancellable_new (); async_data = g_new0 (AsyncFuncData, 1); async_data->file_system = g_object_ref (file_system); async_data->file = g_object_ref (file); async_data->cancellable = g_object_ref (cancellable); async_data->callback = callback; async_data->data = data; g_file_query_info_async (file, attributes, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable, query_info_callback, async_data); return cancellable; } static void drive_poll_for_media_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { AsyncFuncData *async_data; GError *error = NULL; g_drive_poll_for_media_finish (G_DRIVE (source_object), result, &error); async_data = (AsyncFuncData *) user_data; gdk_threads_enter (); ((GtkFileSystemVolumeMountCallback) async_data->callback) (async_data->cancellable, (GtkFileSystemVolume *) source_object, error, async_data->data); gdk_threads_leave (); if (error) g_error_free (error); } static void volume_mount_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { AsyncFuncData *async_data; GError *error = NULL; g_volume_mount_finish (G_VOLUME (source_object), result, &error); async_data = (AsyncFuncData *) user_data; gdk_threads_enter (); ((GtkFileSystemVolumeMountCallback) async_data->callback) (async_data->cancellable, (GtkFileSystemVolume *) source_object, error, async_data->data); gdk_threads_leave (); if (error) g_error_free (error); } GCancellable * _gtk_file_system_mount_volume (GtkFileSystem *file_system, GtkFileSystemVolume *volume, GMountOperation *mount_operation, GtkFileSystemVolumeMountCallback callback, gpointer data) { GCancellable *cancellable; AsyncFuncData *async_data; gboolean handled = FALSE; DEBUG ("volume_mount"); cancellable = g_cancellable_new (); async_data = g_new0 (AsyncFuncData, 1); async_data->file_system = g_object_ref (file_system); async_data->cancellable = g_object_ref (cancellable); async_data->callback = callback; async_data->data = data; if (G_IS_DRIVE (volume)) { /* this path happens for drives that are not polled by the OS and where the last media * check indicated that no media was available. So the thing to do here is to * invoke poll_for_media() on the drive */ g_drive_poll_for_media (G_DRIVE (volume), cancellable, drive_poll_for_media_cb, async_data); handled = TRUE; } else if (G_IS_VOLUME (volume)) { g_volume_mount (G_VOLUME (volume), G_MOUNT_MOUNT_NONE, mount_operation, cancellable, volume_mount_cb, async_data); handled = TRUE; } if (!handled) free_async_data (async_data); return cancellable; } static void enclosing_volume_mount_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GtkFileSystemVolume *volume; AsyncFuncData *async_data; GError *error = NULL; async_data = (AsyncFuncData *) user_data; g_file_mount_enclosing_volume_finish (G_FILE (source_object), result, &error); volume = _gtk_file_system_get_volume_for_file (async_data->file_system, G_FILE (source_object)); /* Silently drop G_IO_ERROR_ALREADY_MOUNTED error for gvfs backends without visible mounts. */ /* Better than doing query_info with additional I/O every time. */ if (error && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_ALREADY_MOUNTED)) g_clear_error (&error); gdk_threads_enter (); ((GtkFileSystemVolumeMountCallback) async_data->callback) (async_data->cancellable, volume, error, async_data->data); gdk_threads_leave (); if (error) g_error_free (error); _gtk_file_system_volume_unref (volume); } GCancellable * _gtk_file_system_mount_enclosing_volume (GtkFileSystem *file_system, GFile *file, GMountOperation *mount_operation, GtkFileSystemVolumeMountCallback callback, gpointer data) { GCancellable *cancellable; AsyncFuncData *async_data; g_return_val_if_fail (GTK_IS_FILE_SYSTEM (file_system), NULL); g_return_val_if_fail (G_IS_FILE (file), NULL); DEBUG ("mount_enclosing_volume"); cancellable = g_cancellable_new (); async_data = g_new0 (AsyncFuncData, 1); async_data->file_system = g_object_ref (file_system); async_data->file = g_object_ref (file); async_data->cancellable = g_object_ref (cancellable); async_data->callback = callback; async_data->data = data; g_file_mount_enclosing_volume (file, G_MOUNT_MOUNT_NONE, mount_operation, cancellable, enclosing_volume_mount_cb, async_data); return cancellable; } GtkFileSystemVolume * _gtk_file_system_get_volume_for_file (GtkFileSystem *file_system, GFile *file) { GMount *mount; DEBUG ("get_volume_for_file"); mount = g_file_find_enclosing_mount (file, NULL, NULL); if (!mount && g_file_is_native (file)) return (GtkFileSystemVolume *) root_volume_token; return (GtkFileSystemVolume *) mount; } /* GtkFileSystemVolume public methods */ gchar * _gtk_file_system_volume_get_display_name (GtkFileSystemVolume *volume) { DEBUG ("volume_get_display_name"); if (IS_ROOT_VOLUME (volume)) return g_strdup (_(root_volume_token)); if (G_IS_DRIVE (volume)) return g_drive_get_name (G_DRIVE (volume)); else if (G_IS_MOUNT (volume)) return g_mount_get_name (G_MOUNT (volume)); else if (G_IS_VOLUME (volume)) return g_volume_get_name (G_VOLUME (volume)); return NULL; } gboolean _gtk_file_system_volume_is_mounted (GtkFileSystemVolume *volume) { gboolean mounted; DEBUG ("volume_is_mounted"); if (IS_ROOT_VOLUME (volume)) return TRUE; mounted = FALSE; if (G_IS_MOUNT (volume)) mounted = TRUE; else if (G_IS_VOLUME (volume)) { GMount *mount; mount = g_volume_get_mount (G_VOLUME (volume)); if (mount) { mounted = TRUE; g_object_unref (mount); } } return mounted; } GFile * _gtk_file_system_volume_get_root (GtkFileSystemVolume *volume) { GFile *file = NULL; DEBUG ("volume_get_base"); if (IS_ROOT_VOLUME (volume)) return g_file_new_for_uri ("file:///"); if (G_IS_MOUNT (volume)) file = g_mount_get_root (G_MOUNT (volume)); else if (G_IS_VOLUME (volume)) { GMount *mount; mount = g_volume_get_mount (G_VOLUME (volume)); if (mount) { file = g_mount_get_root (mount); g_object_unref (mount); } } return file; } static cairo_surface_t * get_surface_from_gicon (GIcon *icon, GtkWidget *widget, gint icon_size, GError **error) { GdkScreen *screen; GtkIconTheme *icon_theme; GtkIconInfo *icon_info; GdkPixbuf *pixbuf; cairo_surface_t *surface; screen = gtk_widget_get_screen (GTK_WIDGET (widget)); icon_theme = gtk_icon_theme_get_for_screen (screen); icon_info = gtk_icon_theme_lookup_by_gicon_for_scale (icon_theme, icon, icon_size, gtk_widget_get_scale_factor (widget), GTK_ICON_LOOKUP_USE_BUILTIN); if (!icon_info) return NULL; pixbuf = gtk_icon_info_load_symbolic_for_context (icon_info, gtk_widget_get_style_context (widget), NULL, error); g_object_unref (icon_info); if (pixbuf == NULL) return NULL; surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, gtk_widget_get_scale_factor (widget), gtk_widget_get_window (widget)); g_object_unref (pixbuf); return surface; } cairo_surface_t * _gtk_file_system_volume_render_icon (GtkFileSystemVolume *volume, GtkWidget *widget, gint icon_size, GError **error) { GIcon *icon = NULL; cairo_surface_t *surface; if (IS_ROOT_VOLUME (volume)) icon = g_themed_icon_new ("drive-harddisk"); else if (G_IS_DRIVE (volume)) icon = g_drive_get_icon (G_DRIVE (volume)); else if (G_IS_VOLUME (volume)) icon = g_volume_get_icon (G_VOLUME (volume)); else if (G_IS_MOUNT (volume)) icon = g_mount_get_icon (G_MOUNT (volume)); if (!icon) return NULL; surface = get_surface_from_gicon (icon, widget, icon_size, error); g_object_unref (icon); return surface; } GIcon * _gtk_file_system_volume_get_symbolic_icon (GtkFileSystemVolume *volume) { if (IS_ROOT_VOLUME (volume)) return g_themed_icon_new ("drive-harddisk-symbolic"); else if (G_IS_DRIVE (volume)) return g_drive_get_symbolic_icon (G_DRIVE (volume)); else if (G_IS_VOLUME (volume)) return g_volume_get_symbolic_icon (G_VOLUME (volume)); else if (G_IS_MOUNT (volume)) return g_mount_get_symbolic_icon (G_MOUNT (volume)); else return NULL; } cairo_surface_t * _gtk_file_system_volume_render_symbolic_icon (GtkFileSystemVolume *volume, GtkWidget *widget, gint icon_size, GError **error) { GIcon *icon; cairo_surface_t *surface; icon = _gtk_file_system_volume_get_symbolic_icon (volume); surface = get_surface_from_gicon (icon, widget, icon_size, error); g_object_unref (icon); return surface; } GtkFileSystemVolume * _gtk_file_system_volume_ref (GtkFileSystemVolume *volume) { if (IS_ROOT_VOLUME (volume)) return volume; if (G_IS_MOUNT (volume) || G_IS_VOLUME (volume) || G_IS_DRIVE (volume)) g_object_ref (volume); return volume; } void _gtk_file_system_volume_unref (GtkFileSystemVolume *volume) { /* Root volume doesn't need to be freed */ if (IS_ROOT_VOLUME (volume)) return; if (G_IS_MOUNT (volume) || G_IS_VOLUME (volume) || G_IS_DRIVE (volume)) g_object_unref (volume); } /* GFileInfo helper functions */ static cairo_surface_t * _gtk_file_info_render_icon_internal (GFileInfo *info, GtkWidget *widget, gint icon_size, gboolean symbolic) { GIcon *icon; GdkPixbuf *pixbuf; const gchar *thumbnail_path; cairo_surface_t *surface = NULL; int scale; thumbnail_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH); if (thumbnail_path) { scale = gtk_widget_get_scale_factor (widget); pixbuf = gdk_pixbuf_new_from_file_at_size (thumbnail_path, icon_size*scale, icon_size*scale, NULL); if (pixbuf != NULL) { surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, gtk_widget_get_window (widget)); g_object_unref (pixbuf); } } if (!surface) { if (symbolic) icon = g_file_info_get_symbolic_icon (info); else icon = g_file_info_get_icon (info); if (icon) surface = get_surface_from_gicon (icon, widget, icon_size, NULL); if (!surface) { /* Use general fallback for all files without icon */ if (symbolic) icon = g_themed_icon_new ("text-x-generic-symbolic"); else icon = g_themed_icon_new ("text-x-generic"); surface = get_surface_from_gicon (icon, widget, icon_size, NULL); g_object_unref (icon); } } return surface; } cairo_surface_t * _gtk_file_info_render_icon (GFileInfo *info, GtkWidget *widget, gint icon_size) { return _gtk_file_info_render_icon_internal (info, widget, icon_size, FALSE); } cairo_surface_t * _gtk_file_info_render_symbolic_icon (GFileInfo *info, GtkWidget *widget, gint icon_size) { return _gtk_file_info_render_icon_internal (info, widget, icon_size, TRUE); } gboolean _gtk_file_info_consider_as_directory (GFileInfo *info) { GFileType type = g_file_info_get_file_type (info); return (type == G_FILE_TYPE_DIRECTORY || type == G_FILE_TYPE_MOUNTABLE || type == G_FILE_TYPE_SHORTCUT); } gboolean _gtk_file_has_native_path (GFile *file) { char *local_file_path; gboolean has_native_path; /* Don't use g_file_is_native(), as we want to support FUSE paths if available */ local_file_path = g_file_get_path (file); has_native_path = (local_file_path != NULL); g_free (local_file_path); return has_native_path; } static const gchar * const remote_types[] = { "afp", "google-drive", "sftp", "webdav", "ftp", "nfs", "cifs", NULL }; gboolean _gtk_file_consider_as_remote (GFile *file) { GFileInfo *info; gboolean is_remote; info = g_file_query_filesystem_info (file, "filesystem::type", NULL, NULL); if (info) { const gchar *type; type = g_file_info_get_attribute_string (info, "filesystem::type"); is_remote = g_strv_contains (remote_types, type); g_object_unref (info); } else is_remote = FALSE; return is_remote; }