/* vi:set et ai sw=2 sts=2 ts=2: */ /*- * Copyright (c) 2009-2010 Jannis Pohlmann * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifdef __linux__ #define _GNU_SOURCE #endif #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_STDLIB_H #include #endif #ifdef HAVE_STRING_H #include #endif #include #ifdef HAVE_GIO_UNIX #ifdef __linux__ #define HAVE_DIRECT_IO 1 #include #include #if HAVE_STATX #include #ifdef STATX_DIOALIGN #define HAVE_STATX_DIOALIGN 1 #endif /* STATX_DIOALIGN */ #endif /* HAVE_STATX */ #endif /* __linux__ */ #include #include #endif /* HAVE_GIO_UNIX */ #include "thunar/thunar-file.h" #include "thunar/thunar-gio-extensions.h" #include "thunar/thunar-preferences.h" #include "thunar/thunar-private.h" #include "thunar/thunar-util.h" #include #define CMP_BUF_MIN_ALIGN (16) #define CMP_BUF_SIZE (1024 * 512) #ifndef O_BINARY #define O_BINARY (0) #endif /* See : https://freedesktop.org/wiki/Specifications/icon-naming-spec/ */ static struct { const gchar *icon_name; const gchar *type; } /* clang-format off */ device_icon_name [] = { /* Implementation specific */ { "multimedia-player-apple-ipod-touch" , N_("iPod touch") }, { "computer-apple-ipad" , N_("iPad") }, { "phone-apple-iphone" , N_("iPhone") }, { "drive-harddisk-solidstate" , N_("Solid State Drive") }, { "drive-harddisk-system" , N_("System Drive") }, { "drive-harddisk-usb" , N_("USB Drive") }, { "drive-removable-media-usb" , N_("USB Drive") }, /* Freedesktop icon-naming-spec */ { "camera-photo*" , N_("Camera") }, { "drive-harddisk*" , N_("Harddisk") }, { "drive-optical*" , N_("Optical Drive") }, { "drive-removable-media*" , N_("Removable Drive") }, { "media-flash*" , N_("Flash Drive") }, { "media-floppy*" , N_("Floppy") }, { "media-optical*" , N_("Optical Media") }, { "media-tape*" , N_("Tape") }, { "multimedia-player*" , N_("Multimedia Player") }, { "pda*" , N_("PDA") }, { "phone*" , N_("Phone") }, { NULL , NULL } }; /* clang-format on */ static const gchar * guess_device_type_from_icon_name (const gchar *icon_name); static GFileInfo * thunar_g_file_get_content_type_querry_info (GFile *gfile, GError *err); static void thunar_g_file_info_set_attribute (GFileInfo *info, ThunarGType type, const gchar *setting_name, const gchar *setting_value); GFile * thunar_g_file_new_for_home (void) { return g_file_new_for_path (xfce_get_homedir ()); } GFile * thunar_g_file_new_for_root (void) { return g_file_new_for_uri ("file:///"); } GFile * thunar_g_file_new_for_recent (void) { return g_file_new_for_uri ("recent:///"); } GFile * thunar_g_file_new_for_trash (void) { return g_file_new_for_uri ("trash:///"); } GFile * thunar_g_file_new_for_computer (void) { return g_file_new_for_uri ("computer://"); } GFile * thunar_g_file_new_for_network (void) { return g_file_new_for_uri ("network://"); } GFile * thunar_g_file_new_for_desktop (void) { return g_file_new_for_path (g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP)); } GFile * thunar_g_file_new_for_bookmarks (void) { gchar *filename; GFile *bookmarks; filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL); bookmarks = g_file_new_for_path (filename); g_free (filename); return bookmarks; } /* internal version, for recursive execution. Uses a lookup table to detect loops */ static GFile * _thunar_g_file_resolve_symlink_internal (GFile *file, GHashTable *lookup_table) { g_autoptr (GHashTable) lookup_history = NULL; g_autoptr (GFileInfo) info = NULL; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); if (lookup_table == NULL) lookup_history = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); else lookup_history = g_hash_table_ref (lookup_table); /* Resolve the link target recursively, until the target we found is not a symlink */ char *file_uri = g_file_get_uri (file); if (file_uri == NULL) return NULL; /* Detect loops: if the file already was seen, return NULL */ if (g_hash_table_contains (lookup_history, file_uri)) { g_info ("Symlink loop detected - resolve symlink aborted"); g_free (file_uri); return NULL; } g_hash_table_add (lookup_history, file_uri); info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET "," G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (info == NULL) return NULL; /* Not a symlink: return that file */ if (!g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK)) return g_object_ref (file); /* It's a symlink, get the target */ const char *target_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET); if (target_path == NULL) { g_info ("Failed to read target path from symlink: %s", g_file_get_path (file)); return NULL; } /* The target is as well a symlink ... so lets use recursion */ if (g_path_is_absolute (target_path)) { g_autoptr (GFile) target = g_file_new_for_path (target_path); return _thunar_g_file_resolve_symlink_internal (target, lookup_history); } else { /* Resolve a possible relative path */ g_autoptr (GFile) target = NULL; g_autoptr (GFile) parent = NULL; g_autoptr (GFile) parent_resolved = NULL; parent = g_file_get_parent (file); if (parent == NULL) return NULL; /* Resolve parent, if required */ parent_resolved = _thunar_g_file_resolve_symlink_internal (parent, lookup_history); if (parent_resolved == NULL) { g_info ("Failed to resolve parent of symlink"); return NULL; } target = g_file_resolve_relative_path (parent_resolved, target_path); /* Sicne as well that file might be a symlink, we need to use recursion */ return _thunar_g_file_resolve_symlink_internal (target, lookup_history); } } /** * thunar_g_file_resolve_symlink: * @file : a #GFile * * Returns the resolved symlink target of @file as a new #GFile. * If @file is not a symlink, @file will just be returned * * Return value: (nullable) (transfer full): A #GFile on success and %NULL on failure. **/ GFile * thunar_g_file_resolve_symlink (GFile *file) { return _thunar_g_file_resolve_symlink_internal (file, NULL); } gboolean thunar_g_file_is_root (GFile *file) { GFile *parent; gboolean is_root = TRUE; parent = g_file_get_parent (file); if (G_UNLIKELY (parent != NULL)) { is_root = FALSE; g_object_unref (parent); } return is_root; } gboolean thunar_g_file_is_trashed (GFile *file) { _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); return g_file_has_uri_scheme (file, "trash"); } gboolean thunar_g_file_is_in_recent (GFile *file) { _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); return g_file_has_uri_scheme (file, "recent"); } gboolean thunar_g_file_is_home (GFile *file) { GFile *home; gboolean is_home = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); home = thunar_g_file_new_for_home (); is_home = g_file_equal (home, file); g_object_unref (home); return is_home; } gboolean thunar_g_file_is_trash (GFile *file) { char *uri; gboolean is_trash = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); uri = g_file_get_uri (file); is_trash = g_strcmp0 (uri, "trash:///") == 0; g_free (uri); return is_trash; } gboolean thunar_g_file_is_recent (GFile *file) { char *uri; gboolean is_recent = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); uri = g_file_get_uri (file); is_recent = g_strcmp0 (uri, "recent:///") == 0; g_free (uri); return is_recent; } gboolean thunar_g_file_is_computer (GFile *file) { char *uri; gboolean is_computer = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); uri = g_file_get_uri (file); is_computer = g_strcmp0 (uri, "computer:///") == 0; g_free (uri); return is_computer; } gboolean thunar_g_file_is_network (GFile *file) { char *uri; gboolean is_network = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); uri = g_file_get_uri (file); is_network = g_strcmp0 (uri, "network:///") == 0; g_free (uri); return is_network; } GKeyFile * thunar_g_file_query_key_file (GFile *file, GCancellable *cancellable, GError **error) { GKeyFile *key_file; gchar *contents = NULL; gsize length; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); _thunar_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); _thunar_return_val_if_fail (error == NULL || *error == NULL, NULL); /* try to load the entire file into memory */ if (!g_file_load_contents (file, cancellable, &contents, &length, NULL, error)) return NULL; /* allocate a new key file */ key_file = g_key_file_new (); /* try to parse the key file from the contents of the file */ if (G_LIKELY (length == 0 || g_key_file_load_from_data (key_file, contents, length, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, error))) { g_free (contents); return key_file; } else { g_free (contents); g_key_file_free (key_file); return NULL; } } gboolean thunar_g_file_write_key_file (GFile *file, GKeyFile *key_file, GCancellable *cancellable, GError **error) { gchar *contents; gsize length; gboolean result = TRUE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); _thunar_return_val_if_fail (key_file != NULL, FALSE); _thunar_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* write the key file into the contents buffer */ contents = g_key_file_to_data (key_file, &length, NULL); /* try to replace the file contents with the key file data */ if (contents != NULL) { result = g_file_replace_contents (file, contents, length, NULL, FALSE, G_FILE_CREATE_NONE, NULL, cancellable, error); /* cleanup */ g_free (contents); } return result; } gchar * thunar_g_file_get_location (GFile *file) { gchar *location; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); location = g_file_get_path (file); if (location == NULL) location = g_file_get_uri (file); return location; } static const gchar * guess_device_type_from_icon_name (const gchar *icon_name) { for (int i = 0; device_icon_name[i].type != NULL; i++) { if (g_pattern_match_simple (device_icon_name[i].icon_name, icon_name)) return g_dgettext (NULL, device_icon_name[i].type); } return NULL; } /** * thunar_g_file_guess_device_type: * @file : a #GFile instance. * * Returns : (transfer none) (nullable): the string of the device type. */ const gchar * thunar_g_file_guess_device_type (GFile *file) { GFileInfo *fileinfo = NULL; GIcon *icon = NULL; const gchar *icon_name = NULL; const gchar *device_type = NULL; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); fileinfo = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_ICON, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (fileinfo == NULL) return NULL; icon = G_ICON (g_file_info_get_attribute_object (fileinfo, G_FILE_ATTRIBUTE_STANDARD_ICON)); if (!G_IS_THEMED_ICON (icon)) { g_object_unref (fileinfo); return NULL; } icon_name = g_themed_icon_get_names (G_THEMED_ICON (icon))[0]; device_type = guess_device_type_from_icon_name (icon_name); g_object_unref (fileinfo); return device_type; } gchar * thunar_g_file_get_display_name (GFile *file) { gchar *base_name; gchar *display_name; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); base_name = g_file_get_basename (file); if (G_LIKELY (base_name != NULL)) { if (strcmp (base_name, "/") == 0) display_name = g_strdup (_("File System")); else if (thunar_g_file_is_trash (file)) display_name = g_strdup (_("Trash")); else if (g_utf8_validate (base_name, -1, NULL)) display_name = g_strdup (base_name); else display_name = g_uri_escape_string (base_name, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); g_free (base_name); } else { display_name = g_strdup ("?"); } return display_name; } gchar * thunar_g_file_get_display_name_remote (GFile *mount_point) { gchar *scheme; gchar *parse_name; const gchar *p; const gchar *path; gchar *unescaped; gchar *hostname; gchar *display_name = NULL; const gchar *skip; const gchar *firstdot; const gchar skip_chars[] = ":@"; guint n; _thunar_return_val_if_fail (G_IS_FILE (mount_point), NULL); /* not intended for local mounts */ if (!g_file_is_native (mount_point)) { scheme = g_file_get_uri_scheme (mount_point); parse_name = g_file_get_parse_name (mount_point); if (scheme != NULL && g_str_has_prefix (parse_name, scheme)) { /* extract the hostname */ p = parse_name + strlen (scheme); while (*p == ':' || *p == '/') ++p; /* goto path part */ path = strchr (p, '/'); firstdot = strchr (p, '.'); if (firstdot != NULL) { /* skip password or login names in the hostname */ for (n = 0; n < G_N_ELEMENTS (skip_chars) - 1; n++) { skip = strchr (p, skip_chars[n]); if (skip != NULL && (path == NULL || skip < path) && (skip < firstdot)) p = skip + 1; } } /* extract the path and hostname from the string */ if (G_LIKELY (path != NULL)) { hostname = g_strndup (p, path - p); } else { hostname = g_strdup (p); path = "/"; } /* unescape the path so that spaces and other characters are shown correctly */ unescaped = g_uri_unescape_string (path, NULL); /* TRANSLATORS: this will result in " on " */ display_name = g_strdup_printf (_("%s on %s"), unescaped, hostname); g_free (unescaped); g_free (hostname); } if (scheme != NULL) g_free (scheme); g_free (parse_name); } /* never return null */ if (display_name == NULL) display_name = thunar_g_file_get_display_name (mount_point); return display_name; } gboolean thunar_g_vfs_is_uri_scheme_supported (const gchar *scheme) { const gchar *const *supported_schemes; gboolean supported = FALSE; guint n; GVfs *gvfs; _thunar_return_val_if_fail (scheme != NULL && *scheme != '\0', FALSE); gvfs = g_vfs_get_default (); supported_schemes = g_vfs_get_supported_uri_schemes (gvfs); if (supported_schemes == NULL) return FALSE; for (n = 0; !supported && supported_schemes[n] != NULL; ++n) if (g_strcmp0 (supported_schemes[n], scheme) == 0) supported = TRUE; return supported; } /** * thunar_g_file_get_free_space: * @file : a #GFile instance. * @fs_free_return : return location for the amount of * free space or %NULL. * @fs_size_return : return location for the total volume size. * * Determines the amount of free space of the volume on * which @file resides. Returns %TRUE if the amount of * free space was determined successfully and placed into * @free_space_return, else %FALSE will be returned. * * Return value: %TRUE if successfull, else %FALSE. **/ gboolean thunar_g_file_get_free_space (GFile *file, guint64 *fs_free_return, guint64 *fs_size_return) { GFileInfo *filesystem_info; gboolean success = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); filesystem_info = g_file_query_filesystem_info (file, THUNARX_FILESYSTEM_INFO_NAMESPACE, NULL, NULL); if (filesystem_info != NULL) { if (fs_free_return != NULL) { *fs_free_return = g_file_info_get_attribute_uint64 (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE); success = g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE); } if (fs_size_return != NULL) { *fs_size_return = g_file_info_get_attribute_uint64 (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE); success = g_file_info_has_attribute (filesystem_info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE); } g_object_unref (filesystem_info); } return success; } gchar * thunar_g_file_get_free_space_string (GFile *file, gboolean file_size_binary) { gchar *fs_size_free_str; gchar *fs_size_used_str; guint64 fs_size_free; guint64 fs_size_total; gchar *free_space_string = NULL; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); if (thunar_g_file_get_free_space (file, &fs_size_free, &fs_size_total) && fs_size_total > 0) { fs_size_free_str = g_format_size_full (fs_size_free, file_size_binary ? G_FORMAT_SIZE_IEC_UNITS : G_FORMAT_SIZE_DEFAULT); fs_size_used_str = g_format_size_full (fs_size_total - fs_size_free, file_size_binary ? G_FORMAT_SIZE_IEC_UNITS : G_FORMAT_SIZE_DEFAULT); free_space_string = g_strdup_printf (_("%s used (%.0f%%) | %s free (%.0f%%)"), fs_size_used_str, ((fs_size_total - fs_size_free) * 100.0 / fs_size_total), fs_size_free_str, (fs_size_free * 100.0 / fs_size_total)); g_free (fs_size_free_str); g_free (fs_size_used_str); } return free_space_string; } GType thunar_g_file_list_get_type (void) { static GType type = G_TYPE_INVALID; if (G_UNLIKELY (type == G_TYPE_INVALID)) { type = g_boxed_type_register_static (I_ ("ThunarGFileList"), (GBoxedCopyFunc) thunar_g_list_copy_deep, (GBoxedFreeFunc) thunar_g_list_free_full); } return type; } static GHashTable * thunar_g_hastable_copy_deep (GHashTable *table) { GHashTable *copy = g_hash_table_new_full (g_direct_hash, NULL, g_object_unref, NULL); GHashTableIter iter; gpointer key; g_hash_table_iter_init (&iter, table); while (g_hash_table_iter_next (&iter, &key, NULL)) g_hash_table_add (copy, g_object_ref (key)); return copy; } GType thunar_g_file_hastable_get_type (void) { static GType type = G_TYPE_INVALID; if (G_UNLIKELY (type == G_TYPE_INVALID)) { type = g_boxed_type_register_static (I_ ("ThunarGHashTableList"), (GBoxedCopyFunc) thunar_g_hastable_copy_deep, (GBoxedFreeFunc) g_hash_table_destroy); } return type; } /** * thunar_g_file_copy: * @source : input #GFile * @destination : destination #GFile * @flags : set of #GFileCopyFlags * @use_partial : option to use *.partial~ * @cancellable : (nullable): optional #GCancellable object * @progress_callback : (nullable) (scope call): function to callback with progress information * @progress_callback_data : (clousure): user data to pass to @progress_callback * @error : (nullable): #GError to set on error * * Calls g_file_copy() if @use_partial is not enabled. * If enabled, copies files to *.partial~ first and then * renames *.partial~ into its original name. * * Return value: %TRUE on success, %FALSE otherwise. **/ gboolean thunar_g_file_copy (GFile *source, GFile *destination, GFileCopyFlags flags, gboolean use_partial, GCancellable *cancellable, GFileProgressCallback progress_callback, gpointer progress_callback_data, GError **error) { gboolean success; GFileQueryInfoFlags query_flags; GFileInfo *info = NULL; GFile *parent; GFile *partial; gchar *partial_name; gchar *base_name; _thunar_return_val_if_fail (g_file_has_parent (destination, NULL), FALSE); if (use_partial) { query_flags = (flags & G_FILE_COPY_NOFOLLOW_SYMLINKS) ? G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS : G_FILE_QUERY_INFO_NONE; info = g_file_query_info (source, G_FILE_ATTRIBUTE_STANDARD_TYPE, query_flags, cancellable, NULL); } /* directory does not need .partial */ if (info == NULL) { use_partial = FALSE; } else { use_partial = g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR; g_clear_object (&info); } if (!use_partial) { success = g_file_copy (source, destination, flags, cancellable, progress_callback, progress_callback_data, error); return success; } /* check destination */ if (g_file_query_exists (destination, NULL)) { if (flags & G_FILE_COPY_OVERWRITE) { /* We want to overwrite. Just delete the old file */ if (error != NULL) g_clear_error (error); error = NULL; g_file_delete (destination, NULL, error); if (error != NULL) return FALSE; } else { /* Try to mimic g_file_copy() error */ if (error != NULL) *error = g_error_new (G_IO_ERROR, G_IO_ERROR_EXISTS, "Error opening file \"%s\": File exists", g_file_peek_path (destination)); return FALSE; } } /* generate partial file name */ base_name = g_file_get_basename (destination); if (base_name == NULL) { base_name = g_strdup ("UNNAMED"); } /* limit filename length */ partial_name = g_strdup_printf ("%.100s.partial~", base_name); parent = g_file_get_parent (destination); /* parent can't be NULL since destination must be a file */ partial = g_file_get_child (parent, partial_name); g_clear_object (&parent); g_free (partial_name); /* check if partial file exists */ if (g_file_query_exists (partial, NULL)) g_file_delete (partial, NULL, error); /* copy file to .partial */ success = g_file_copy (source, partial, flags, cancellable, progress_callback, progress_callback_data, error); if (success) { /* rename .partial if done without problem */ GFile *renamed_file = g_file_set_display_name (partial, base_name, NULL, error); success = (renamed_file != NULL); if (success) g_object_unref (renamed_file); } if (!success) { /* try to remove incomplete file. */ /* failure is expected so error is ignored */ /* it must be triggered if cancelled */ /* thus cancellable is also ignored */ g_file_delete (partial, NULL, NULL); } g_clear_object (&partial); g_free (base_name); return success; } #ifdef HAVE_DIRECT_IO static GInputStream * open_file_and_buffer_for_direct_io (GFile *file, GCancellable *cancellable, void **buf_out, unsigned int *buf_align_out, size_t *buf_size_out) { int fd = -1; size_t buf_align = 0; size_t offset_align = 0; size_t buf_size; void *buf = NULL; #ifdef HAVE_STATX_DIOALIGN struct statx statx_buf; // First we use statx () to ask the filesystem the required buffer alignment // for direct I/O (stx_dio_mem_align), as well as what the file offset // alignment (stx_dio_offset_align), which tells us what the buffer size is // required to be a multiple of. DBG ("Trying statx ()"); if (statx (-1, g_file_peek_path (file), AT_STATX_SYNC_AS_STAT, STATX_DIOALIGN, &statx_buf) == 0 && statx_buf.stx_dio_mem_align > 0 && statx_buf.stx_dio_offset_align > 0) { DBG ("statx () succeeded, direct I/O align=%u, offset_align=%u", statx_buf.stx_dio_mem_align, statx_buf.stx_dio_offset_align); // Ensure alignment is at least 16, which is required for most SIMD // operations. If somehow the required filesystem alignment/offset // alignment is lower than 16, it's always safe to go higher. buf_align = MAX (CMP_BUF_MIN_ALIGN, statx_buf.stx_dio_mem_align); offset_align = MAX (CMP_BUF_MIN_ALIGN, statx_buf.stx_dio_offset_align); } #endif if (buf_align == 0 || offset_align == 0) { DBG ("statx () not supported or failed; using alignment/offset alignment fallback of 512"); // Generally this fallback value should work on linux 2.6 & above. buf_align = 512; offset_align = 512; } // Since we're using blocking stdio operations and not cancellable GIO // operations, we should check the cancellable periodically. if (cancellable != NULL && g_cancellable_is_cancelled (cancellable)) { goto out_err; } // Ensure the buffer size is a multiple of offset_align. buf_size = (CMP_BUF_SIZE / offset_align) * offset_align; DBG ("buf_size will be %" G_GSIZE_FORMAT " based on offset_align", buf_size); if (buf_size == 0) { // This shouldn't happen, but if somehow we get a truly bizarre // offst_align value that is larger than CMP_BUF_SIZE, we'll just bail. goto out_err; } // Open using the same flags g_file_read () uses, plus O_DIRECT. fd = open (g_file_peek_path (file), O_RDONLY | O_BINARY | O_CLOEXEC | O_DIRECT, 0); if (fd == -1 || (cancellable != NULL && g_cancellable_is_cancelled (cancellable))) { goto out_err; } DBG ("Successfully opened file with O_DIRECT"); // Create a buffer to read data into, with the required alignment and size. if (posix_memalign (&buf, buf_align, buf_size) != 0) { goto out_err; } DBG ("Successfully allocated memory for direct I/O buffers"); // Even if we've successfully opened the file for direct I/O and have // allocated our buffer, it's still possible that there's something wrong // with our buffer size or alignment. So do a test read to see if it // succeeds, and if so, seek back to the beginning of the file. if (read (fd, buf, buf_size) == -1 || lseek (fd, 0, SEEK_SET) == (off_t) -1 || (cancellable != NULL && g_cancellable_is_cancelled (cancellable))) { goto out_err; } DBG ("Direct I/O is available and seems to be working"); *buf_out = buf; *buf_align_out = buf_align; *buf_size_out = buf_size; return g_unix_input_stream_new (fd, TRUE); out_err: if (fd >= 0) { close (fd); } free (buf); return NULL; } #endif static GInputStream * open_file_and_buffer_fallback (GFile *file, GCancellable *cancellable, void **buf_out, unsigned int *buf_align_out, size_t *buf_size_out, GError **error) { GFileInputStream *finp; void *buf = NULL; finp = g_file_read (file, cancellable, error); if (finp != NULL) { /* Use 16-byte aligned allocations (rather than allocating on the stack), as that's usually a requirement for any SIMD optimizations the compiler might be able to do. */ if (posix_memalign (&buf, CMP_BUF_MIN_ALIGN, CMP_BUF_SIZE) != 0) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Failed to allocate aligned memory: %s", strerror (errno)); } } if (finp != NULL && buf != NULL) { *buf_out = buf; *buf_align_out = CMP_BUF_MIN_ALIGN; *buf_size_out = CMP_BUF_SIZE; return G_INPUT_STREAM (finp); } else { if (finp != NULL) { g_object_unref (finp); } free (buf); return FALSE; } } /** * thunar_g_file_compare_contents: * @file_a : a #GFile * @file_b : a #GFile * @cancellable : (nullalble): optional #GCancellable object * @error : (nullalble): optional #GError * * Compares the contents of @file_a and @file_b. * * Return value: %TRUE if the files match, %FALSE if not. **/ gboolean thunar_g_file_compare_contents (GFile *file_a, GFile *file_b, GCancellable *cancellable, GError **error) { GFileInputStream *inp_a = NULL; GInputStream *inp_b = NULL; void *buf_a = NULL; void *buf_b = NULL; unsigned int buf_align = 0; size_t buf_size = 0; gboolean is_equal = FALSE; g_return_val_if_fail (G_IS_FILE (file_a), FALSE); g_return_val_if_fail (G_IS_FILE (file_b), FALSE); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); // We assume that file_b is the destination file, and that's the one that // really needs to be opened using O_DIRECT, as it's just been written, and // some (or even all) of it will still be in the kernel's buffer cache. // file_a can be opened normally, and will hopefully/likely be read from // buffer cache, which is both fine and faster. inp_a = g_file_read (file_a, cancellable, error); if (inp_a != NULL) { #ifdef HAVE_DIRECT_IO if (g_file_has_uri_scheme (file_b, "file")) { DBG ("Attempting direct I/O for destination file"); inp_b = open_file_and_buffer_for_direct_io (file_b, cancellable, &buf_b, &buf_align, &buf_size); } #endif if (inp_b == NULL) { inp_b = open_file_and_buffer_fallback (file_b, cancellable, &buf_b, &buf_align, &buf_size, error); } } if (inp_b != NULL) { // Now that we know the alignment and size needed for file_b, we can also // allocate the buffer for file_a. if (posix_memalign (&buf_a, buf_align, buf_size) != 0) { g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno), "Failed to allocate aligned memory: %s", strerror (errno)); } } if (inp_a != NULL && inp_b != NULL && buf_a != NULL) { for (;;) { gsize bytes_read_a; gsize bytes_read_b; if (!g_input_stream_read_all (G_INPUT_STREAM (inp_a), buf_a, buf_size, &bytes_read_a, cancellable, error)) { break; } if (!g_input_stream_read_all (G_INPUT_STREAM (inp_b), buf_b, buf_size, &bytes_read_b, cancellable, error)) { break; } if (bytes_read_a == 0 && bytes_read_b == 0) { is_equal = TRUE; break; } else if (bytes_read_a != bytes_read_b) { break; } else if (memcmp (buf_a, buf_b, bytes_read_a) != 0) { break; } } } if (inp_a != NULL) { g_object_unref (inp_a); } if (inp_b != NULL) { g_object_unref (inp_b); } free (buf_a); free (buf_b); return is_equal; } /** * thunar_g_file_list_new_from_string: * @string : a string representation of an URI list. * * Splits an URI list conforming to the text/uri-list * mime type defined in RFC 2483 into individual URIs, * discarding any comments and whitespace. The resulting * list will hold one #GFile for each URI. * * If @string contains no URIs, this function * will return %NULL. * * Return value: the list of #GFiles or %NULL. **/ GList * thunar_g_file_list_new_from_string (const gchar *string) { GList *list = NULL; gchar **uris; gsize n; uris = g_uri_list_extract_uris (string); for (n = 0; uris != NULL && uris[n] != NULL; ++n) list = g_list_append (list, g_file_new_for_uri (uris[n])); g_strfreev (uris); return list; } /** * thunar_g_file_list_to_stringv: * @list : a list of #GFiles. * * Free the returned value using g_strfreev() when you * are done with it. Useful for gtk_selection_data_set_uris. * * Return value: and array of uris. **/ gchar ** thunar_g_file_list_to_stringv (GList *list) { gchar **uris; guint n; GList *lp; /* allocate initial string */ uris = g_new0 (gchar *, g_list_length (list) + 1); for (lp = list, n = 0; lp != NULL; lp = lp->next) { /* Prefer native paths for interoperability. */ gchar *path = g_file_get_path (G_FILE (lp->data)); if (path == NULL) { uris[n++] = g_file_get_uri (G_FILE (lp->data)); } else { uris[n++] = g_filename_to_uri (path, NULL, NULL); g_free (path); } } return uris; } /** * thunar_g_file_list_get_parents: * @list : a list of #GFiles. * * Collects all parent folders of the passed files * If multiple files share the same parent, the parent will only be added once to the returned list. * Each list element of the returned list needs to be freed with g_object_unref() after use. * * Return value: A list of #GFiles of all parent folders. Free the returned list with calling g_object_unref() on each element **/ GList * thunar_g_file_list_get_parents (GList *file_list) { GList *lp_file_list; GList *lp_parent_folder_list; GFile *parent_folder; GList *parent_folder_list = NULL; gboolean folder_already_added; for (lp_file_list = file_list; lp_file_list != NULL; lp_file_list = lp_file_list->next) { if (!G_IS_FILE (lp_file_list->data)) continue; parent_folder = g_file_get_parent (lp_file_list->data); if (parent_folder == NULL) continue; folder_already_added = FALSE; /* Check if the folder already is in our list */ for (lp_parent_folder_list = parent_folder_list; lp_parent_folder_list != NULL; lp_parent_folder_list = lp_parent_folder_list->next) { if (g_file_equal (lp_parent_folder_list->data, parent_folder)) { folder_already_added = TRUE; break; } } /* Keep the reference for each folder added to parent_folder_list */ if (folder_already_added) g_object_unref (parent_folder); else parent_folder_list = g_list_append (parent_folder_list, parent_folder); } return parent_folder_list; } /** * thunar_g_file_is_descendant: * @descendant : a #GFile that is a potential descendant of @ancestor * @ancestor : a #GFile * * Check if @descendant is a subdirectory of @ancestor. * @descendant == @ancestor also counts. * * Returns: %TRUE if @descendant is a subdirectory of @ancestor. **/ gboolean thunar_g_file_is_descendant (GFile *descendant, GFile *ancestor) { _thunar_return_val_if_fail (descendant != NULL && G_IS_FILE (descendant), FALSE); _thunar_return_val_if_fail (ancestor != NULL && G_IS_FILE (ancestor), FALSE); for (GFile *parent = g_object_ref (descendant), *temp; parent != NULL; temp = g_file_get_parent (parent), g_object_unref (parent), parent = temp) { if (g_file_equal (parent, ancestor)) { g_object_unref (parent); return TRUE; } } return FALSE; } gboolean thunar_g_app_info_launch (GAppInfo *info, GFile *working_directory, GList *path_list, GAppLaunchContext *context, GError **error) { ThunarFile *file; GAppInfo *default_app_info; GList *recommended_app_infos; GList *lp; const gchar *content_type; gboolean result = FALSE; gchar *new_path = NULL; gchar *old_path = NULL; gboolean skip_app_info_update; _thunar_return_val_if_fail (G_IS_APP_INFO (info), FALSE); _thunar_return_val_if_fail (working_directory == NULL || G_IS_FILE (working_directory), FALSE); _thunar_return_val_if_fail (path_list != NULL, FALSE); _thunar_return_val_if_fail (G_IS_APP_LAUNCH_CONTEXT (context), FALSE); _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE); skip_app_info_update = (g_object_get_data (G_OBJECT (info), "skip-app-info-update") != NULL); /* check if we want to set the working directory of the spawned app */ if (working_directory != NULL) { /* determine the working directory path */ new_path = g_file_get_path (working_directory); if (new_path != NULL) { /* switch to the desired working directory, remember that of Thunar itself */ old_path = thunar_util_change_working_directory (new_path); /* forget about the new working directory path */ g_free (new_path); } } /* launch the paths with the specified app info */ result = g_app_info_launch (info, path_list, context, error); /* if successful, remember the application as last used for the file types */ if (result == TRUE) { for (lp = path_list; lp != NULL; lp = lp->next) { gboolean update_app_info = !skip_app_info_update; file = thunar_file_get (lp->data, NULL); if (file == NULL) continue; content_type = thunar_file_get_content_type (file); /* determine default application */ default_app_info = thunar_file_get_default_handler (file); if (default_app_info != NULL) { /* check if the application is the default one */ if (g_app_info_equal (info, default_app_info)) update_app_info = FALSE; g_object_unref (default_app_info); } if (update_app_info) { /* obtain list of last used applications */ recommended_app_infos = g_app_info_get_recommended_for_type (content_type); if (recommended_app_infos != NULL) { /* check if the application is already the last used one * by comparing it with the first entry in the list */ if (g_app_info_equal (info, recommended_app_infos->data)) update_app_info = FALSE; g_list_free_full (recommended_app_infos, g_object_unref); } } /* emit "changed" on the file if we successfully changed the last used application */ if (update_app_info && g_app_info_set_as_last_used_for_type (info, content_type, NULL)) thunar_file_changed (file); g_object_unref (file); } } /* check if we need to reset the working directory to the one Thunar was * opened from */ if (old_path != NULL) { /* switch to Thunar's original working directory */ new_path = thunar_util_change_working_directory (old_path); /* clean up */ g_free (new_path); g_free (old_path); } return result; } gboolean thunar_g_app_info_should_show (GAppInfo *info) { #ifdef HAVE_GIO_UNIX _thunar_return_val_if_fail (G_IS_APP_INFO (info), FALSE); if (G_IS_DESKTOP_APP_INFO (info)) { /* NoDisplay=true files should be visible in the interface, * because this key is intent to hide mime-helpers from the * application menu. Hidden=true is never returned by GIO. */ return g_desktop_app_info_get_show_in (G_DESKTOP_APP_INFO (info), NULL); } return TRUE; #else /* we cannot exclude custom actions, so show everything */ return TRUE; #endif } gboolean thunar_g_vfs_metadata_is_supported (void) { GFile *root; GFileAttributeInfoList *attr_info_list; gint n; gboolean metadata_is_supported = FALSE; /* get a GFile for the root directory, and obtain the list of writable namespaces for it */ root = thunar_g_file_new_for_root (); attr_info_list = g_file_query_writable_namespaces (root, NULL, NULL); g_object_unref (root); /* loop through the returned namespace names and check if "metadata" is included */ for (n = 0; n < attr_info_list->n_infos; n++) { if (g_strcmp0 (attr_info_list->infos[n].name, "metadata") == 0) { metadata_is_supported = TRUE; break; } } /* release the attribute info list */ g_file_attribute_info_list_unref (attr_info_list); return metadata_is_supported; } /** * thunar_g_file_is_on_local_device: * @file : the source or target #GFile to test. * * Tries to find if the @file is on a local device or not. * Local device if (all conditions should match): * - the file has a 'file' uri scheme. * - the file is located on devices not handled by the #GVolumeMonitor (GVFS). * - the device is handled by #GVolumeMonitor (GVFS) and cannot be unmounted * (USB key/disk, fuse mounts, Samba shares, PTP devices). * * The target @file may not exist yet when this function is used, so recurse * the parent directory, possibly reaching the root mountpoint. * * This should be enough to determine if a @file is on a local device or not. * * Return value: %TRUE if #GFile @file is on a so-called local device. **/ gboolean thunar_g_file_is_on_local_device (GFile *file) { gboolean is_local = FALSE; GFile *target_file; GFile *target_parent; GMount *file_mount; _thunar_return_val_if_fail (file != NULL, TRUE); _thunar_return_val_if_fail (G_IS_FILE (file), TRUE); if (g_file_has_uri_scheme (file, "file") == FALSE) return FALSE; for (target_file = g_object_ref (file); target_file != NULL; target_file = target_parent) { if (g_file_query_exists (target_file, NULL)) break; /* file or parent directory does not exist (yet) * query the parent directory */ target_parent = g_file_get_parent (target_file); g_object_unref (target_file); } if (target_file == NULL) return FALSE; /* file_mount will be NULL for local files on local partitions/devices */ file_mount = g_file_find_enclosing_mount (target_file, NULL, NULL); g_object_unref (target_file); if (file_mount == NULL) is_local = TRUE; else { /* mountpoints which cannot be unmounted are local devices. * attached devices like USB key/disk, fuse mounts, Samba shares, * PTP devices can always be unmounted and are considered remote/slow. */ is_local = !g_mount_can_unmount (file_mount); g_object_unref (file_mount); } return is_local; } /** * thunar_g_file_set_executable_flags: * @file : the #GFile for which execute flags should be set * * Tries to set +x flag of the file for user, group and others * * Return value: %TRUE on sucess, %FALSE on error **/ gboolean thunar_g_file_set_executable_flags (GFile *file, GError **error) { ThunarFileMode old_mode; ThunarFileMode new_mode; GFileInfo *info; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); _thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* try to query information about the file */ info = g_file_query_info (file, G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error); if (G_LIKELY (info != NULL)) { if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_UNIX_MODE)) { /* determine the current mode */ old_mode = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE); /* generate the new mode */ new_mode = old_mode | THUNAR_FILE_MODE_USR_EXEC | THUNAR_FILE_MODE_GRP_EXEC | THUNAR_FILE_MODE_OTH_EXEC; if (old_mode != new_mode) { g_file_set_attribute_uint32 (file, G_FILE_ATTRIBUTE_UNIX_MODE, new_mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, error); } } else { g_warning ("No %s attribute found", G_FILE_ATTRIBUTE_UNIX_MODE); } g_object_unref (info); } return (error == NULL); } /** * thunar_g_file_is_in_xdg_data_dir: * @file : a #GFile. * * Returns %TRUE if @file is located below one of the directories given in XDG_DATA_DIRS * * Return value: %TRUE if @file is located inside a XDG_DATA_DIR **/ gboolean thunar_g_file_is_in_xdg_data_dir (GFile *file) { const gchar *const *data_dirs; guint i; gchar *path; gboolean found = FALSE; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); if (g_file_is_native (file)) { data_dirs = g_get_system_data_dirs (); if (G_LIKELY (data_dirs != NULL)) { path = g_file_get_path (file); for (i = 0; data_dirs[i] != NULL; i++) { if (g_str_has_prefix (path, data_dirs[i])) { found = TRUE; break; } } g_free (path); } } return found; } /** * thunar_g_file_is_desktop_file: * @file : a #GFile. * * Returns %TRUE if @file is a .desktop file. * * Return value: %TRUE if @file is a .desktop file. **/ gboolean thunar_g_file_is_desktop_file (GFile *file) { gchar *basename; gboolean is_desktop_file = FALSE; GFileInfo *info; _thunar_return_val_if_fail (G_IS_FILE (file), FALSE); basename = g_file_get_basename (file); /* only allow regular files with a .desktop extension */ if (g_str_has_suffix (basename, ".desktop")) { info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); if (G_LIKELY (info != NULL && g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR)) is_desktop_file = TRUE; g_object_unref (info); } g_free (basename); return is_desktop_file; } /** * thunar_g_file_get_link_path_for_symlink: * @file_to_link : the #GFile to which @symlink will have to point * @symlink : a #GFile representing the symlink * * Method to build the link target path in order to link from @symlink to @file_to_link. * The caller is responsible for freeing the string using g_free() when done. * * Return value: The link path, or NULL on failure **/ char * thunar_g_file_get_link_path_for_symlink (GFile *file_to_link, GFile *symlink) { GFile *root; GFile *parent; char *relative_path; char *link_path; _thunar_return_val_if_fail (G_IS_FILE (file_to_link), NULL); _thunar_return_val_if_fail (G_IS_FILE (symlink), NULL); /* */ if (g_file_is_native (file_to_link) || g_file_is_native (symlink)) { return g_file_get_path (file_to_link); } /* Search for the filesystem root */ root = g_object_ref (file_to_link); while ((parent = g_file_get_parent (root)) != NULL) { g_object_unref (root); root = parent; } /* Build a absolute path, using the relative path up to the filesystem root */ relative_path = g_file_get_relative_path (root, file_to_link); g_object_unref (root); link_path = g_strconcat ("/", relative_path, NULL); g_free (relative_path); return link_path; } static GFileInfo * thunar_g_file_get_content_type_querry_info (GFile *gfile, GError *err) { GFileInfo *info = NULL; info = g_file_query_info (gfile, G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, NULL, &err); return info; } /** * thunar_g_file_get_content_type: * @file : #GFile for which the content type will be returned * * Gets the content type of the passed #GFile. Will always return a valid string. * * Return value: (transfer full): The content type as #gchar * The returned string should be freed with g_free() when no longer needed. **/ char * thunar_g_file_get_content_type (GFile *gfile) { gboolean is_symlink = FALSE; GFileInfo *info = NULL; GError *err = NULL; gchar *content_type = NULL; info = thunar_g_file_get_content_type_querry_info (gfile, err); /* follow symlink, if found */ if (G_LIKELY (info != NULL)) { is_symlink = g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK); if (is_symlink) { GFile *link_target = thunar_g_file_resolve_symlink (gfile); g_object_unref (info); info = NULL; if (link_target != NULL) { info = thunar_g_file_get_content_type_querry_info (link_target, err); g_object_unref (link_target); } } } if (G_LIKELY (info != NULL)) { if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { /* this we known for sure */ content_type = g_strdup ("inode/directory"); } else { content_type = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE)); if (G_UNLIKELY (content_type == NULL)) content_type = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE)); } g_object_unref (info); } else { /* If gfile retrieved above is NULL, then g_file_query_info won't be called, thus keeping info NULL. * In this case, err will also be NULL. So it will fallback to "unknown" mime-type */ if (G_LIKELY (err != NULL)) { /* The mime-type 'inode/symlink' is only used for broken links. * When the link is functional, the mime-type of the link target will be used */ if (G_LIKELY (is_symlink && err->code == G_IO_ERROR_NOT_FOUND)) content_type = g_strdup ("inode/symlink"); else g_warning ("Content type loading failed for %s: %s", g_file_get_uri (gfile), err->message); g_error_free (err); } } /* fallback */ if (content_type == NULL) content_type = g_strdup (DEFAULT_CONTENT_TYPE); return content_type; } static void thunar_g_file_set_metadata_setting_finish (GObject *source_object, GAsyncResult *result, gpointer user_data) { GFile *file = NULL; GError *error = NULL; ThunarFile *thunar_file = NULL; file = G_FILE (source_object); if (file == NULL) return; if (!g_file_set_attributes_finish (file, result, NULL, &error)) { g_warning ("Failed to set metadata: %s", error->message); g_error_free (error); } thunar_file = thunar_file_get (file, NULL); if (thunar_file == NULL) return; thunar_file_changed (thunar_file); g_object_unref (thunar_file); } static void thunar_g_file_info_set_attribute (GFileInfo *info, ThunarGType type, const gchar *setting_name, const gchar *setting_value) { switch (type) { case THUNAR_GTYPE_STRING: g_file_info_set_attribute_string (info, setting_name, setting_value); break; case THUNAR_GTYPE_STRINGV: { gchar **setting_values; setting_values = g_strsplit (setting_value, THUNAR_METADATA_STRING_DELIMETER, 100); g_file_info_set_attribute_stringv (info, setting_name, setting_values); g_strfreev (setting_values); break; } default: g_warning ("ThunarGType not supported, skipping"); break; } } static gchar * thunar_g_file_info_get_attribute (GFileInfo *info, ThunarGType type, const gchar *setting_name) { switch (type) { case THUNAR_GTYPE_STRING: return g_strdup (g_file_info_get_attribute_string (info, setting_name)); case THUNAR_GTYPE_STRINGV: { gchar **stringv = g_file_info_get_attribute_stringv (info, setting_name); GList *string_list = NULL; gchar *joined_string = NULL; if (stringv == NULL) return NULL; for (; *stringv != NULL; ++stringv) string_list = g_list_append (string_list, *stringv); joined_string = thunar_util_strjoin_list (string_list, THUNAR_METADATA_STRING_DELIMETER); g_list_free (string_list); return joined_string; } default: g_warning ("ThunarGType not supported, skipping"); return NULL; } } /** * thunar_g_file_set_metadata_setting: * @file : a #GFile instance. * @info : Additional #GFileInfo instance to update * @type : #ThunarGType of the metadata * @setting_name : the name of the setting to set * @setting_value : the value to set * @async : whether g_file_set_attributes_async or g_file_set_attributes_from_info should be used * * Sets the setting @setting_name of @file to @setting_value and stores it in * the @files metadata. **/ void thunar_g_file_set_metadata_setting (GFile *file, GFileInfo *info, ThunarGType type, const gchar *setting_name, const gchar *setting_value, gboolean async) { GFileInfo *info_new; gchar *attr_name; _thunar_return_if_fail (G_IS_FILE (file)); _thunar_return_if_fail (G_IS_FILE_INFO (info)); /* convert the setting name to an attribute name */ attr_name = g_strdup_printf ("metadata::%s", setting_name); /* set the value in the current info. this call is needed to update the in-memory * GFileInfo structure to ensure that the new attribute value is available immediately */ if (setting_value) thunar_g_file_info_set_attribute (info, type, attr_name, setting_value); /* send meta data to the daemon. this call is needed to store the new value of * the attribute in the file system */ info_new = g_file_info_new (); thunar_g_file_info_set_attribute (info_new, type, attr_name, setting_value); if (async) { g_file_set_attributes_async (file, info_new, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL, thunar_g_file_set_metadata_setting_finish, NULL); } else { g_file_set_attributes_from_info (file, info_new, G_FILE_QUERY_INFO_NONE, NULL, NULL); } g_free (attr_name); g_object_unref (G_OBJECT (info_new)); } /** * thunar_file_clear_metadata_setting: * @file : a #GFile instance. * @setting_name : the name of the setting to clear * * Clear the metadata setting @setting_name of @file **/ void thunar_g_file_clear_metadata_setting (GFile *file, GFileInfo *info, const gchar *setting_name) { gchar *attr_name; _thunar_return_if_fail (G_IS_FILE (file)); if (info == NULL) return; /* convert the setting name to an attribute name */ attr_name = g_strdup_printf ("metadata::%s", setting_name); if (!g_file_info_has_attribute (info, attr_name)) { g_free (attr_name); return; } g_file_info_remove_attribute (info, attr_name); g_file_set_attribute (file, attr_name, G_FILE_ATTRIBUTE_TYPE_INVALID, NULL, G_FILE_QUERY_INFO_NONE, NULL, NULL); g_free (attr_name); } /** * thunar_file_get_metadata_setting: * @file : a #GFile instance. * @info : #GFileInfo instance to extract the setting * @type : #ThunarGType of the metadata * @setting_name : the name of the setting to get * * Gets the stored value of the metadata setting @setting_name for @file. Returns %NULL * if there is no stored setting. After usage the returned string needs to be released with 'g_free' * * Return value: (transfer full): the stored value of the setting for @file, or %NULL **/ gchar * thunar_g_file_get_metadata_setting (GFile *file, GFileInfo *info, ThunarGType type, const gchar *setting_name) { gchar *attr_name; gchar *attr_value; _thunar_return_val_if_fail (G_IS_FILE (file), NULL); if (info == NULL) return NULL; /* convert the setting name to an attribute name */ attr_name = g_strdup_printf ("metadata::%s", setting_name); if (!g_file_info_has_attribute (info, attr_name)) { g_free (attr_name); return NULL; } attr_value = thunar_g_file_info_get_attribute (info, type, attr_name); g_free (attr_name); return attr_value; }