/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* nautilus-file-operations.c - Nautilus file operations. Copyright (C) 1999, 2000 Free Software Foundation Copyright (C) 2000, 2001 Eazel, Inc. Copyright (C) 2007 Red Hat, Inc. 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. Authors: Alexander Larsson Ettore Perazzoli Pavel Cisler */ #include #include #include #include #include #include #include #include #include #include "nautilus-file-operations.h" #include "nautilus-debug-log.h" #include "nautilus-file-changes-queue.h" #include "nautilus-lib-self-check-functions.h" #include "nautilus-progress-info.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "nautilus-file-changes-queue.h" #include "nautilus-file-private.h" #include "nautilus-desktop-icon-file.h" #include "nautilus-desktop-link-monitor.h" #include "nautilus-global-preferences.h" #include "nautilus-link.h" #include "nautilus-autorun.h" #include "nautilus-trash-monitor.h" #include "nautilus-file-utilities.h" static gboolean confirm_trash_auto_value; /* TODO: TESTING!!! */ typedef struct { GIOSchedulerJob *io_job; GTimer *time; GtkWindow *parent_window; int screen_num; int inhibit_cookie; NautilusProgressInfo *progress; GCancellable *cancellable; GHashTable *skip_files; GHashTable *skip_readdir_error; gboolean skip_all_error; gboolean skip_all_conflict; gboolean merge_all; gboolean replace_all; gboolean delete_all; } CommonJob; typedef struct { CommonJob common; gboolean is_move; GList *files; GFile *destination; GFile *desktop_location; GdkPoint *icon_positions; int n_icon_positions; GHashTable *debuting_files; NautilusCopyCallback done_callback; gpointer done_callback_data; } CopyMoveJob; typedef struct { CommonJob common; GList *files; gboolean try_trash; gboolean user_cancel; NautilusDeleteCallback done_callback; gpointer done_callback_data; } DeleteJob; typedef struct { CommonJob common; GFile *dest_dir; char *filename; gboolean make_dir; GFile *src; char *src_data; int length; GdkPoint position; gboolean has_position; GFile *created_file; NautilusCreateCallback done_callback; gpointer done_callback_data; } CreateJob; typedef struct { CommonJob common; GList *trash_dirs; gboolean should_confirm; NautilusOpCallback done_callback; gpointer done_callback_data; } EmptyTrashJob; typedef struct { CommonJob common; GFile *file; gboolean interactive; NautilusOpCallback done_callback; gpointer done_callback_data; } MarkTrustedJob; typedef struct { CommonJob common; GFile *file; NautilusOpCallback done_callback; gpointer done_callback_data; guint32 file_permissions; guint32 file_mask; guint32 dir_permissions; guint32 dir_mask; } SetPermissionsJob; typedef enum { OP_KIND_COPY, OP_KIND_MOVE, OP_KIND_DELETE, OP_KIND_TRASH } OpKind; typedef struct { int num_files; goffset num_bytes; int num_files_since_progress; OpKind op; } SourceInfo; typedef struct { int num_files; goffset num_bytes; OpKind op; guint64 last_report_time; int last_reported_files_left; } TransferInfo; #define SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE 15 #define NSEC_PER_SEC 1000000000 #define NSEC_PER_MSEC 1000000 #define MAXIMUM_DISPLAYED_FILE_NAME_LENGTH 50 #define IS_IO_ERROR(__error, KIND) (((__error)->domain == G_IO_ERROR && (__error)->code == G_IO_ERROR_ ## KIND)) #define SKIP _("_Skip") #define SKIP_ALL _("S_kip All") #define RETRY _("_Retry") #define DELETE_ALL _("Delete _All") #define REPLACE _("_Replace") #define REPLACE_ALL _("Replace _All") #define MERGE _("_Merge") #define MERGE_ALL _("Merge _All") #define COPY_FORCE _("Copy _Anyway") static void mark_desktop_file_trusted (CommonJob *common, GCancellable *cancellable, GFile *file, gboolean interactive); static gboolean is_all_button_text (const char *button_text) { g_assert (button_text != NULL); return !strcmp (button_text, SKIP_ALL) || !strcmp (button_text, REPLACE_ALL) || !strcmp (button_text, DELETE_ALL) || !strcmp (button_text, MERGE_ALL); } static void scan_sources (GList *files, SourceInfo *source_info, CommonJob *job, OpKind kind); static gboolean empty_trash_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data); static char * query_fs_type (GFile *file, GCancellable *cancellable); /* keep in time with format_time() * * This counts and outputs the number of “time units” * formatted and displayed by format_time(). * For instance, if format_time outputs “3 hours, 4 minutes” * it yields 7. */ static int seconds_count_format_time_units (int seconds) { int minutes; int hours; if (seconds < 0) { /* Just to make sure... */ seconds = 0; } if (seconds < 60) { /* seconds */ return seconds; } if (seconds < 60*60) { /* minutes */ minutes = seconds / 60; return minutes; } hours = seconds / (60*60); if (seconds < 60*60*4) { /* minutes + hours */ minutes = (seconds - hours * 60 * 60) / 60; return minutes + hours; } return hours; } static char * format_time (int seconds) { int minutes; int hours; char *res; if (seconds < 0) { /* Just to make sure... */ seconds = 0; } if (seconds < 60) { return g_strdup_printf (ngettext ("%'d second","%'d seconds", (int) seconds), (int) seconds); } if (seconds < 60*60) { minutes = seconds / 60; return g_strdup_printf (ngettext ("%'d minute", "%'d minutes", minutes), minutes); } hours = seconds / (60*60); if (seconds < 60*60*4) { char *h, *m; minutes = (seconds - hours * 60 * 60) / 60; h = g_strdup_printf (ngettext ("%'d hour", "%'d hours", hours), hours); m = g_strdup_printf (ngettext ("%'d minute", "%'d minutes", minutes), minutes); res = g_strconcat (h, ", ", m, NULL); g_free (h); g_free (m); return res; } return g_strdup_printf (ngettext ("approximately %'d hour", "approximately %'d hours", hours), hours); } static char * shorten_utf8_string (const char *base, int reduce_by_num_bytes) { int len; char *ret; const char *p; len = strlen (base); len -= reduce_by_num_bytes; if (len <= 0) { return NULL; } ret = g_new (char, len + 1); p = base; while (len) { char *next; next = g_utf8_next_char (p); if (next - p > len || *next == '\0') { break; } len -= next - p; p = next; } if (p - base == 0) { g_free (ret); return NULL; } else { memcpy (ret, base, p - base); ret[p - base] = '\0'; return ret; } } /* Note that we have these two separate functions with separate format * strings for ease of localization. */ static char * get_link_name (const char *name, int count, int max_length) { const char *format; char *result; int unshortened_length; gboolean use_count; g_assert (name != NULL); if (count < 0) { g_warning ("bad count in get_link_name"); count = 0; } if (count <= 2) { /* Handle special cases for low numbers. * Perhaps for some locales we will need to add more. */ switch (count) { default: g_assert_not_reached (); /* fall through */ case 0: /* duplicate original file name */ format = "%s"; break; case 1: /* appended to new link file */ format = _("Link to %s"); break; case 2: /* appended to new link file */ format = _("Another link to %s"); break; } use_count = FALSE; } else { /* Handle special cases for the first few numbers of each ten. * For locales where getting this exactly right is difficult, * these can just be made all the same as the general case below. */ switch (count % 10) { case 1: /* Localizers: Feel free to leave out the "st" suffix * if there's no way to do that nicely for a * particular language. */ format = _("%'dst link to %s"); break; case 2: /* appended to new link file */ format = _("%'dnd link to %s"); break; case 3: /* appended to new link file */ format = _("%'drd link to %s"); break; default: /* appended to new link file */ format = _("%'dth link to %s"); break; } use_count = TRUE; } if (use_count) result = g_strdup_printf (format, count, name); else result = g_strdup_printf (format, name); if (max_length > 0 && (unshortened_length = strlen (result)) > max_length) { char *new_name; new_name = shorten_utf8_string (name, unshortened_length - max_length); if (new_name) { g_free (result); if (use_count) result = g_strdup_printf (format, count, new_name); else result = g_strdup_printf (format, new_name); g_assert (strlen (result) <= max_length); g_free (new_name); } } return result; } /* Localizers: * Feel free to leave out the st, nd, rd and th suffix or * make some or all of them match. */ /* localizers: tag used to detect the first copy of a file */ static const char untranslated_copy_duplicate_tag[] = N_(" (copy)"); /* localizers: tag used to detect the second copy of a file */ static const char untranslated_another_copy_duplicate_tag[] = N_(" (another copy)"); /* localizers: tag used to detect the x11th copy of a file */ static const char untranslated_x11th_copy_duplicate_tag[] = N_("th copy)"); /* localizers: tag used to detect the x12th copy of a file */ static const char untranslated_x12th_copy_duplicate_tag[] = N_("th copy)"); /* localizers: tag used to detect the x13th copy of a file */ static const char untranslated_x13th_copy_duplicate_tag[] = N_("th copy)"); /* localizers: tag used to detect the x1st copy of a file */ static const char untranslated_st_copy_duplicate_tag[] = N_("st copy)"); /* localizers: tag used to detect the x2nd copy of a file */ static const char untranslated_nd_copy_duplicate_tag[] = N_("nd copy)"); /* localizers: tag used to detect the x3rd copy of a file */ static const char untranslated_rd_copy_duplicate_tag[] = N_("rd copy)"); /* localizers: tag used to detect the xxth copy of a file */ static const char untranslated_th_copy_duplicate_tag[] = N_("th copy)"); #define COPY_DUPLICATE_TAG _(untranslated_copy_duplicate_tag) #define ANOTHER_COPY_DUPLICATE_TAG _(untranslated_another_copy_duplicate_tag) #define X11TH_COPY_DUPLICATE_TAG _(untranslated_x11th_copy_duplicate_tag) #define X12TH_COPY_DUPLICATE_TAG _(untranslated_x12th_copy_duplicate_tag) #define X13TH_COPY_DUPLICATE_TAG _(untranslated_x13th_copy_duplicate_tag) #define ST_COPY_DUPLICATE_TAG _(untranslated_st_copy_duplicate_tag) #define ND_COPY_DUPLICATE_TAG _(untranslated_nd_copy_duplicate_tag) #define RD_COPY_DUPLICATE_TAG _(untranslated_rd_copy_duplicate_tag) #define TH_COPY_DUPLICATE_TAG _(untranslated_th_copy_duplicate_tag) /* localizers: appended to first file copy */ static const char untranslated_first_copy_duplicate_format[] = N_("%s (copy)%s"); /* localizers: appended to second file copy */ static const char untranslated_second_copy_duplicate_format[] = N_("%s (another copy)%s"); /* localizers: appended to x11th file copy */ static const char untranslated_x11th_copy_duplicate_format[] = N_("%s (%'dth copy)%s"); /* localizers: appended to x12th file copy */ static const char untranslated_x12th_copy_duplicate_format[] = N_("%s (%'dth copy)%s"); /* localizers: appended to x13th file copy */ static const char untranslated_x13th_copy_duplicate_format[] = N_("%s (%'dth copy)%s"); /* localizers: if in your language there's no difference between 1st, 2nd, 3rd and nth * plurals, you can leave the st, nd, rd suffixes out and just make all the translated * strings look like "%s (copy %'d)%s". */ /* localizers: appended to x1st file copy */ static const char untranslated_st_copy_duplicate_format[] = N_("%s (%'dst copy)%s"); /* localizers: appended to x2nd file copy */ static const char untranslated_nd_copy_duplicate_format[] = N_("%s (%'dnd copy)%s"); /* localizers: appended to x3rd file copy */ static const char untranslated_rd_copy_duplicate_format[] = N_("%s (%'drd copy)%s"); /* localizers: appended to xxth file copy */ static const char untranslated_th_copy_duplicate_format[] = N_("%s (%'dth copy)%s"); #define FIRST_COPY_DUPLICATE_FORMAT _(untranslated_first_copy_duplicate_format) #define SECOND_COPY_DUPLICATE_FORMAT _(untranslated_second_copy_duplicate_format) #define X11TH_COPY_DUPLICATE_FORMAT _(untranslated_x11th_copy_duplicate_format) #define X12TH_COPY_DUPLICATE_FORMAT _(untranslated_x12th_copy_duplicate_format) #define X13TH_COPY_DUPLICATE_FORMAT _(untranslated_x13th_copy_duplicate_format) #define ST_COPY_DUPLICATE_FORMAT _(untranslated_st_copy_duplicate_format) #define ND_COPY_DUPLICATE_FORMAT _(untranslated_nd_copy_duplicate_format) #define RD_COPY_DUPLICATE_FORMAT _(untranslated_rd_copy_duplicate_format) #define TH_COPY_DUPLICATE_FORMAT _(untranslated_th_copy_duplicate_format) static char * extract_string_until (const char *original, const char *until_substring) { char *result; g_assert ((int) strlen (original) >= until_substring - original); g_assert (until_substring - original >= 0); result = g_malloc (until_substring - original + 1); strncpy (result, original, until_substring - original); result[until_substring - original] = '\0'; return result; } /* Dismantle a file name, separating the base name, the file suffix and removing any * (xxxcopy), etc. string. Figure out the count that corresponds to the given * (xxxcopy) substring. */ static void parse_previous_duplicate_name (const char *name, char **name_base, const char **suffix, int *count) { const char *tag; g_assert (name[0] != '\0'); *suffix = strchr (name + 1, '.'); if (*suffix == NULL || (*suffix)[1] == '\0') { /* no suffix */ *suffix = ""; } tag = strstr (name, COPY_DUPLICATE_TAG); if (tag != NULL) { if (tag > *suffix) { /* handle case "foo. (copy)" */ *suffix = ""; } *name_base = extract_string_until (name, tag); *count = 1; return; } tag = strstr (name, ANOTHER_COPY_DUPLICATE_TAG); if (tag != NULL) { if (tag > *suffix) { /* handle case "foo. (another copy)" */ *suffix = ""; } *name_base = extract_string_until (name, tag); *count = 2; return; } /* Check to see if we got one of st, nd, rd, th. */ tag = strstr (name, X11TH_COPY_DUPLICATE_TAG); if (tag == NULL) { tag = strstr (name, X12TH_COPY_DUPLICATE_TAG); } if (tag == NULL) { tag = strstr (name, X13TH_COPY_DUPLICATE_TAG); } if (tag == NULL) { tag = strstr (name, ST_COPY_DUPLICATE_TAG); } if (tag == NULL) { tag = strstr (name, ND_COPY_DUPLICATE_TAG); } if (tag == NULL) { tag = strstr (name, RD_COPY_DUPLICATE_TAG); } if (tag == NULL) { tag = strstr (name, TH_COPY_DUPLICATE_TAG); } /* If we got one of st, nd, rd, th, fish out the duplicate number. */ if (tag != NULL) { /* localizers: opening parentheses to match the "th copy)" string */ tag = strstr (name, _(" (")); if (tag != NULL) { if (tag > *suffix) { /* handle case "foo. (22nd copy)" */ *suffix = ""; } *name_base = extract_string_until (name, tag); /* localizers: opening parentheses of the "th copy)" string */ if (sscanf (tag, _(" (%'d"), count) == 1) { if (*count < 1 || *count > 1000000) { /* keep the count within a reasonable range */ *count = 0; } return; } *count = 0; return; } } *count = 0; if (**suffix != '\0') { *name_base = extract_string_until (name, *suffix); } else { *name_base = g_strdup (name); } } static char * make_next_duplicate_name (const char *base, const char *suffix, int count, int max_length) { const char *format; char *result; int unshortened_length; gboolean use_count; if (count < 1) { g_warning ("bad count %d in get_duplicate_name", count); count = 1; } if (count <= 2) { /* Handle special cases for low numbers. * Perhaps for some locales we will need to add more. */ switch (count) { default: g_assert_not_reached (); /* fall through */ case 1: format = FIRST_COPY_DUPLICATE_FORMAT; break; case 2: format = SECOND_COPY_DUPLICATE_FORMAT; break; } use_count = FALSE; } else { /* Handle special cases for the first few numbers of each ten. * For locales where getting this exactly right is difficult, * these can just be made all the same as the general case below. */ /* Handle special cases for x11th - x20th. */ switch (count % 100) { case 11: format = X11TH_COPY_DUPLICATE_FORMAT; break; case 12: format = X12TH_COPY_DUPLICATE_FORMAT; break; case 13: format = X13TH_COPY_DUPLICATE_FORMAT; break; default: format = NULL; break; } if (format == NULL) { switch (count % 10) { case 1: format = ST_COPY_DUPLICATE_FORMAT; break; case 2: format = ND_COPY_DUPLICATE_FORMAT; break; case 3: format = RD_COPY_DUPLICATE_FORMAT; break; default: /* The general case. */ format = TH_COPY_DUPLICATE_FORMAT; break; } } use_count = TRUE; } if (use_count) result = g_strdup_printf (format, base, count, suffix); else result = g_strdup_printf (format, base, suffix); if (max_length > 0 && (unshortened_length = strlen (result)) > max_length) { char *new_base; new_base = shorten_utf8_string (base, unshortened_length - max_length); if (new_base) { g_free (result); if (use_count) result = g_strdup_printf (format, new_base, count, suffix); else result = g_strdup_printf (format, new_base, suffix); g_assert (strlen (result) <= max_length); g_free (new_base); } } return result; } static char * get_duplicate_name (const char *name, int count_increment, int max_length) { char *result; char *name_base; const char *suffix; int count; parse_previous_duplicate_name (name, &name_base, &suffix, &count); result = make_next_duplicate_name (name_base, suffix, count + count_increment, max_length); g_free (name_base); return result; } static gboolean has_invalid_xml_char (char *str) { gunichar c; while (*str != 0) { c = g_utf8_get_char (str); /* characters XML permits */ if (!(c == 0x9 || c == 0xA || c == 0xD || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF))) { return TRUE; } str = g_utf8_next_char (str); } return FALSE; } static char * custom_full_name_to_string (char *format, va_list va) { GFile *file; file = va_arg (va, GFile *); return g_file_get_parse_name (file); } static void custom_full_name_skip (va_list *va) { (void) va_arg (*va, GFile *); } static char * custom_basename_to_string (char *format, va_list va) { GFile *file; GFileInfo *info; char *name, *basename, *tmp; file = va_arg (va, GFile *); info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, 0, g_cancellable_get_current (), NULL); name = NULL; if (info) { name = g_strdup (g_file_info_get_display_name (info)); g_object_unref (info); } if (name == NULL) { basename = g_file_get_basename (file); if (g_utf8_validate (basename, -1, NULL)) { name = basename; } else { name = g_uri_escape_string (basename, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); g_free (basename); } } /* Some chars can't be put in the markup we use for the dialogs... */ if (has_invalid_xml_char (name)) { tmp = name; name = g_uri_escape_string (name, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, TRUE); g_free (tmp); } /* Finally, if the string is too long, truncate it. */ if (name != NULL) { tmp = name; name = eel_str_middle_truncate (tmp, MAXIMUM_DISPLAYED_FILE_NAME_LENGTH); g_free (tmp); } return name; } static void custom_basename_skip (va_list *va) { (void) va_arg (*va, GFile *); } static char * custom_size_to_string (char *format, va_list va) { goffset size; size = va_arg (va, goffset); return g_format_size_for_display (size); } static void custom_size_skip (va_list *va) { (void) va_arg (*va, goffset); } static char * custom_time_to_string (char *format, va_list va) { int secs; secs = va_arg (va, int); return format_time (secs); } static void custom_time_skip (va_list *va) { (void) va_arg (*va, int); } static char * custom_mount_to_string (char *format, va_list va) { GMount *mount; mount = va_arg (va, GMount *); return g_mount_get_name (mount); } static void custom_mount_skip (va_list *va) { (void) va_arg (*va, GMount *); } static EelPrintfHandler handlers[] = { { 'F', custom_full_name_to_string, custom_full_name_skip }, { 'B', custom_basename_to_string, custom_basename_skip }, { 'S', custom_size_to_string, custom_size_skip }, { 'T', custom_time_to_string, custom_time_skip }, { 'V', custom_mount_to_string, custom_mount_skip }, { 0 } }; static char * f (const char *format, ...) { va_list va; char *res; va_start (va, format); res = eel_strdup_vprintf_with_custom (handlers, format, va); va_end (va); return res; } #define op_job_new(__type, parent_window) ((__type *)(init_common (sizeof(__type), parent_window))) static gpointer init_common (gsize job_size, GtkWindow *parent_window) { CommonJob *common; GdkScreen *screen; common = g_malloc0 (job_size); if (parent_window) { common->parent_window = parent_window; eel_add_weak_pointer (&common->parent_window); } common->progress = nautilus_progress_info_new (); common->cancellable = nautilus_progress_info_get_cancellable (common->progress); common->time = g_timer_new (); common->inhibit_cookie = -1; common->screen_num = 0; if (parent_window) { screen = gtk_widget_get_screen (GTK_WIDGET (parent_window)); common->screen_num = gdk_screen_get_number (screen); } return common; } static void finalize_common (CommonJob *common) { nautilus_progress_info_finish (common->progress); if (common->inhibit_cookie != -1) { nautilus_uninhibit_power_manager (common->inhibit_cookie); } common->inhibit_cookie = -1; g_timer_destroy (common->time); eel_remove_weak_pointer (&common->parent_window); if (common->skip_files) { g_hash_table_destroy (common->skip_files); } if (common->skip_readdir_error) { g_hash_table_destroy (common->skip_readdir_error); } g_object_unref (common->progress); g_object_unref (common->cancellable); g_free (common); } static void skip_file (CommonJob *common, GFile *file) { if (common->skip_files == NULL) { common->skip_files = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); } g_hash_table_insert (common->skip_files, g_object_ref (file), file); } static void skip_readdir_error (CommonJob *common, GFile *dir) { if (common->skip_readdir_error == NULL) { common->skip_readdir_error = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); } g_hash_table_insert (common->skip_readdir_error, g_object_ref (dir), dir); } static gboolean should_skip_file (CommonJob *common, GFile *file) { if (common->skip_files != NULL) { return g_hash_table_lookup (common->skip_files, file) != NULL; } return FALSE; } static gboolean should_skip_readdir_error (CommonJob *common, GFile *dir) { if (common->skip_readdir_error != NULL) { return g_hash_table_lookup (common->skip_readdir_error, dir) != NULL; } return FALSE; } static void setup_autos (void) { static gboolean setup_autos = FALSE; if (!setup_autos) { setup_autos = TRUE; eel_preferences_add_auto_boolean (NAUTILUS_PREFERENCES_CONFIRM_TRASH, &confirm_trash_auto_value); } } static gboolean can_delete_without_confirm (GFile *file) { if (g_file_has_uri_scheme (file, "burn") || g_file_has_uri_scheme (file, "x-nautilus-desktop")) { return TRUE; } return FALSE; } static gboolean can_delete_files_without_confirm (GList *files) { g_assert (files != NULL); while (files != NULL) { if (!can_delete_without_confirm (files->data)) { return FALSE; } files = files->next; } return TRUE; } typedef struct { GtkWindow **parent_window; gboolean ignore_close_box; GtkMessageType message_type; const char *primary_text; const char *secondary_text; const char *details_text; const char **button_titles; gboolean show_all; int result; } RunSimpleDialogData; static gboolean do_run_simple_dialog (gpointer _data) { RunSimpleDialogData *data = _data; const char *button_title; GtkWidget *dialog; int result; int response_id; /* Create the dialog. */ dialog = eel_alert_dialog_new (*data->parent_window, 0, data->message_type, GTK_BUTTONS_NONE, data->primary_text, data->secondary_text); for (response_id = 0; data->button_titles[response_id] != NULL; response_id++) { button_title = data->button_titles[response_id]; if (!data->show_all && is_all_button_text (button_title)) { continue; } gtk_dialog_add_button (GTK_DIALOG (dialog), button_title, response_id); gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id); } if (data->details_text) { eel_alert_dialog_set_details_label (EEL_ALERT_DIALOG (dialog), data->details_text); } /* Run it. */ gtk_widget_show (dialog); result = gtk_dialog_run (GTK_DIALOG (dialog)); while ((result == GTK_RESPONSE_NONE || result == GTK_RESPONSE_DELETE_EVENT) && data->ignore_close_box) { gtk_widget_show (GTK_WIDGET (dialog)); result = gtk_dialog_run (GTK_DIALOG (dialog)); } gtk_object_destroy (GTK_OBJECT (dialog)); data->result = result; return FALSE; } /* NOTE: This frees the primary / secondary strings, in order to avoid doing that everywhere. So, make sure they are strduped */ static int run_simple_dialog_va (CommonJob *job, gboolean ignore_close_box, GtkMessageType message_type, char *primary_text, char *secondary_text, const char *details_text, gboolean show_all, va_list varargs) { RunSimpleDialogData *data; int res; const char *button_title; GPtrArray *ptr_array; g_timer_stop (job->time); data = g_new0 (RunSimpleDialogData, 1); data->parent_window = &job->parent_window; data->ignore_close_box = ignore_close_box; data->message_type = message_type; data->primary_text = primary_text; data->secondary_text = secondary_text; data->details_text = details_text; data->show_all = show_all; ptr_array = g_ptr_array_new (); while ((button_title = va_arg (varargs, const char *)) != NULL) { g_ptr_array_add (ptr_array, (char *)button_title); } g_ptr_array_add (ptr_array, NULL); data->button_titles = (const char **)g_ptr_array_free (ptr_array, FALSE); nautilus_progress_info_pause (job->progress); g_io_scheduler_job_send_to_mainloop (job->io_job, do_run_simple_dialog, data, NULL); nautilus_progress_info_resume (job->progress); res = data->result; g_free (data->button_titles); g_free (data); g_timer_continue (job->time); g_free (primary_text); g_free (secondary_text); return res; } #if 0 /* Not used at the moment */ static int run_simple_dialog (CommonJob *job, gboolean ignore_close_box, GtkMessageType message_type, char *primary_text, char *secondary_text, const char *details_text, ...) { va_list varargs; int res; va_start (varargs, details_text); res = run_simple_dialog_va (job, ignore_close_box, message_type, primary_text, secondary_text, details_text, varargs); va_end (varargs); return res; } #endif static int run_error (CommonJob *job, char *primary_text, char *secondary_text, const char *details_text, gboolean show_all, ...) { va_list varargs; int res; va_start (varargs, show_all); res = run_simple_dialog_va (job, FALSE, GTK_MESSAGE_ERROR, primary_text, secondary_text, details_text, show_all, varargs); va_end (varargs); return res; } static int run_warning (CommonJob *job, char *primary_text, char *secondary_text, const char *details_text, gboolean show_all, ...) { va_list varargs; int res; va_start (varargs, show_all); res = run_simple_dialog_va (job, FALSE, GTK_MESSAGE_WARNING, primary_text, secondary_text, details_text, show_all, varargs); va_end (varargs); return res; } static int run_question (CommonJob *job, char *primary_text, char *secondary_text, const char *details_text, gboolean show_all, ...) { va_list varargs; int res; va_start (varargs, show_all); res = run_simple_dialog_va (job, FALSE, GTK_MESSAGE_QUESTION, primary_text, secondary_text, details_text, show_all, varargs); va_end (varargs); return res; } static void inhibit_power_manager (CommonJob *job, const char *message) { job->inhibit_cookie = nautilus_inhibit_power_manager (message); } static void abort_job (CommonJob *job) { g_cancellable_cancel (job->cancellable); } static gboolean job_aborted (CommonJob *job) { return g_cancellable_is_cancelled (job->cancellable); } static gboolean confirm_delete_from_trash (CommonJob *job, GList *files) { char *prompt; int file_count; int response; /* Just Say Yes if the preference says not to confirm. */ if (!confirm_trash_auto_value) { return TRUE; } file_count = g_list_length (files); g_assert (file_count > 0); if (file_count == 1) { prompt = f (_("Are you sure you want to permanently delete \"%B\" " "from the trash?"), files->data); } else { prompt = f (ngettext("Are you sure you want to permanently delete " "the %'d selected item from the trash?", "Are you sure you want to permanently delete " "the %'d selected items from the trash?", file_count), file_count); } response = run_warning (job, prompt, f (_("If you delete an item, it will be permanently lost.")), NULL, FALSE, GTK_STOCK_CANCEL, GTK_STOCK_DELETE, NULL); return (response == 1); } static gboolean confirm_empty_trash (CommonJob *job) { char *prompt; int response; /* Just Say Yes if the preference says not to confirm. */ if (!confirm_trash_auto_value) { return TRUE; } prompt = f (_("Empty all of the items from the trash?")); response = run_warning (job, prompt, f(_("If you choose to empty the trash, all items " "in it will be permanently lost. Please note " "that you can also delete them separately.")), NULL, FALSE, GTK_STOCK_CANCEL, _("Empty _Trash"), NULL); return (response == 1); } static gboolean confirm_delete_directly (CommonJob *job, GList *files) { char *prompt; int file_count; int response; /* Just Say Yes if the preference says not to confirm. */ if (!confirm_trash_auto_value) { return TRUE; } file_count = g_list_length (files); g_assert (file_count > 0); if (can_delete_files_without_confirm (files)) { return TRUE; } if (file_count == 1) { prompt = f (_("Are you sure you want to permanently delete \"%B\"?"), files->data); } else { prompt = f (ngettext("Are you sure you want to permanently delete " "the %'d selected item?", "Are you sure you want to permanently delete " "the %'d selected items?", file_count), file_count); } response = run_warning (job, prompt, f (_("If you delete an item, it will be permanently lost.")), NULL, FALSE, GTK_STOCK_CANCEL, GTK_STOCK_DELETE, NULL); return response == 1; } static void report_delete_progress (CommonJob *job, SourceInfo *source_info, TransferInfo *transfer_info) { int files_left; double elapsed, transfer_rate; int remaining_time; guint64 now; char *files_left_s; now = g_thread_gettime (); if (transfer_info->last_report_time != 0 && ABS ((gint64)(transfer_info->last_report_time - now)) < 100 * NSEC_PER_MSEC) { return; } transfer_info->last_report_time = now; files_left = source_info->num_files - transfer_info->num_files; /* Races and whatnot could cause this to be negative... */ if (files_left < 0) { files_left = 1; } files_left_s = f (ngettext ("%'d file left to delete", "%'d files left to delete", files_left), files_left); nautilus_progress_info_take_status (job->progress, f (_("Deleting files"))); elapsed = g_timer_elapsed (job->time, NULL); if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE) { nautilus_progress_info_set_details (job->progress, files_left_s); } else { char *details, *time_left_s; transfer_rate = transfer_info->num_files / elapsed; remaining_time = files_left / transfer_rate; /* To translators: %T will expand to a time like "2 minutes". * The singular/plural form will be used depending on the remaining time (i.e. the %T argument). */ time_left_s = f (ngettext ("%T left", "%T left", seconds_count_format_time_units (remaining_time)), remaining_time); details = g_strconcat (files_left_s, "\xE2\x80\x94", time_left_s, NULL); nautilus_progress_info_take_details (job->progress, details); g_free (time_left_s); } g_free (files_left_s); if (source_info->num_files != 0) { nautilus_progress_info_set_progress (job->progress, transfer_info->num_files, source_info->num_files); } } static void delete_file (CommonJob *job, GFile *file, gboolean *skipped_file, SourceInfo *source_info, TransferInfo *transfer_info, gboolean toplevel); static void delete_dir (CommonJob *job, GFile *dir, gboolean *skipped_file, SourceInfo *source_info, TransferInfo *transfer_info, gboolean toplevel) { GFileInfo *info; GError *error; GFile *file; GFileEnumerator *enumerator; char *primary, *secondary, *details; int response; gboolean skip_error; gboolean local_skipped_file; local_skipped_file = FALSE; skip_error = should_skip_readdir_error (job, dir); retry: error = NULL; enumerator = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (enumerator) { error = NULL; while (!job_aborted (job) && (info = g_file_enumerator_next_file (enumerator, job->cancellable, skip_error?NULL:&error)) != NULL) { file = g_file_get_child (dir, g_file_info_get_name (info)); delete_file (job, file, &local_skipped_file, source_info, transfer_info, FALSE); g_object_unref (file); g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); if (error && IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else if (error) { primary = f (_("Error while deleting.")); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("Files in the folder \"%B\" cannot be deleted because you do " "not have permissions to see them."), dir); } else { secondary = f (_("There was an error getting information about the files in the folder \"%B\"."), dir); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, _("_Skip files"), NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* Skip: Do Nothing */ local_skipped_file = TRUE; } else { g_assert_not_reached (); } } } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { primary = f (_("Error while deleting.")); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("The folder \"%B\" cannot be deleted because you do not have " "permissions to read it."), dir); } else { secondary = f (_("There was an error reading the folder \"%B\"."), dir); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, SKIP, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* Skip: Do Nothing */ local_skipped_file = TRUE; } else if (response == 2) { goto retry; } else { g_assert_not_reached (); } } if (!job_aborted (job) && /* Don't delete dir if there was a skipped file */ !local_skipped_file) { if (!g_file_delete (dir, job->cancellable, &error)) { if (job->skip_all_error) { goto skip; } primary = f (_("Error while deleting.")); secondary = f (_("Could not remove the folder %B."), dir); details = error->message; response = run_warning (job, primary, secondary, details, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; local_skipped_file = TRUE; } else if (response == 2) { /* skip */ local_skipped_file = TRUE; } else { g_assert_not_reached (); } skip: g_error_free (error); } else { nautilus_file_changes_queue_file_removed (dir); transfer_info->num_files ++; report_delete_progress (job, source_info, transfer_info); return; } } if (local_skipped_file) { *skipped_file = TRUE; } } static void delete_file (CommonJob *job, GFile *file, gboolean *skipped_file, SourceInfo *source_info, TransferInfo *transfer_info, gboolean toplevel) { GError *error; char *primary, *secondary, *details; int response; if (should_skip_file (job, file)) { *skipped_file = TRUE; return; } error = NULL; if (g_file_delete (file, job->cancellable, &error)) { nautilus_file_changes_queue_file_removed (file); transfer_info->num_files ++; report_delete_progress (job, source_info, transfer_info); return; } if (IS_IO_ERROR (error, NOT_EMPTY)) { g_error_free (error); delete_dir (job, file, skipped_file, source_info, transfer_info, toplevel); return; } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { if (job->skip_all_error) { goto skip; } primary = f (_("Error while deleting.")); secondary = f (_("There was an error deleting %B."), file); details = error->message; response = run_warning (job, primary, secondary, details, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } skip: g_error_free (error); } *skipped_file = TRUE; } static void delete_files (CommonJob *job, GList *files, int *files_skipped) { GList *l; GFile *file; SourceInfo source_info; TransferInfo transfer_info; gboolean skipped_file; if (job_aborted (job)) { return; } scan_sources (files, &source_info, job, OP_KIND_DELETE); if (job_aborted (job)) { return; } g_timer_start (job->time); memset (&transfer_info, 0, sizeof (transfer_info)); report_delete_progress (job, &source_info, &transfer_info); for (l = files; l != NULL && !job_aborted (job); l = l->next) { file = l->data; skipped_file = FALSE; delete_file (job, file, &skipped_file, &source_info, &transfer_info, TRUE); if (skipped_file) { (*files_skipped)++; } } } static void report_trash_progress (CommonJob *job, int files_trashed, int total_files) { int files_left; char *s; files_left = total_files - files_trashed; nautilus_progress_info_take_status (job->progress, f (_("Moving files to trash"))); s = f (ngettext ("%'d file left to trash", "%'d files left to trash", files_left), files_left); nautilus_progress_info_take_details (job->progress, s); if (total_files != 0) { nautilus_progress_info_set_progress (job->progress, files_trashed, total_files); } } static void trash_files (CommonJob *job, GList *files, int *files_skipped) { GList *l; GFile *file; GList *to_delete; GError *error; int total_files, files_trashed; char *primary, *secondary, *details; int response; if (job_aborted (job)) { return; } total_files = g_list_length (files); files_trashed = 0; report_trash_progress (job, files_trashed, total_files); to_delete = NULL; for (l = files; l != NULL && !job_aborted (job); l = l->next) { file = l->data; error = NULL; if (!g_file_trash (file, job->cancellable, &error)) { if (job->skip_all_error) { (*files_skipped)++; goto skip; } if (job->delete_all) { to_delete = g_list_prepend (to_delete, file); goto skip; } primary = f (_("Cannot move file to trash, do you want to delete immediately?")); secondary = f (_("The file \"%B\" cannot be moved to the trash."), file); details = NULL; if (!IS_IO_ERROR (error, NOT_SUPPORTED)) { details = error->message; } response = run_question (job, primary, secondary, details, (total_files - files_trashed) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, DELETE_ALL, GTK_STOCK_DELETE, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { ((DeleteJob *) job)->user_cancel = TRUE; abort_job (job); } else if (response == 1) { /* skip all */ (*files_skipped)++; job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ (*files_skipped)++; } else if (response == 3) { /* delete all */ to_delete = g_list_prepend (to_delete, file); job->delete_all = TRUE; } else if (response == 4) { /* delete */ to_delete = g_list_prepend (to_delete, file); } skip: g_error_free (error); total_files--; } else { nautilus_file_changes_queue_file_removed (file); files_trashed++; report_trash_progress (job, files_trashed, total_files); } } if (to_delete) { to_delete = g_list_reverse (to_delete); delete_files (job, to_delete, files_skipped); g_list_free (to_delete); } } static gboolean delete_job_done (gpointer user_data) { DeleteJob *job; GHashTable *debuting_uris; job = user_data; eel_g_object_list_free (job->files); if (job->done_callback) { debuting_uris = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); job->done_callback (debuting_uris, job->user_cancel, job->done_callback_data); g_hash_table_unref (debuting_uris); } finalize_common ((CommonJob *)job); nautilus_file_changes_consume_changes (TRUE); return FALSE; } static gboolean delete_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { DeleteJob *job = user_data; GList *to_trash_files; GList *to_delete_files; GList *l; GFile *file; gboolean confirmed; CommonJob *common; gboolean must_confirm_delete_in_trash; gboolean must_confirm_delete; int files_skipped; common = (CommonJob *)job; common->io_job = io_job; nautilus_progress_info_start (job->common.progress); to_trash_files = NULL; to_delete_files = NULL; must_confirm_delete_in_trash = FALSE; must_confirm_delete = FALSE; files_skipped = 0; for (l = job->files; l != NULL; l = l->next) { file = l->data; if (job->try_trash && g_file_has_uri_scheme (file, "trash")) { must_confirm_delete_in_trash = TRUE; to_delete_files = g_list_prepend (to_delete_files, file); } else if (can_delete_without_confirm (file)) { to_delete_files = g_list_prepend (to_delete_files, file); } else { if (job->try_trash) { to_trash_files = g_list_prepend (to_trash_files, file); } else { must_confirm_delete = TRUE; to_delete_files = g_list_prepend (to_delete_files, file); } } } if (to_delete_files != NULL) { to_delete_files = g_list_reverse (to_delete_files); confirmed = TRUE; if (must_confirm_delete_in_trash) { confirmed = confirm_delete_from_trash (common, to_delete_files); } else if (must_confirm_delete) { confirmed = confirm_delete_directly (common, to_delete_files); } if (confirmed) { delete_files (common, to_delete_files, &files_skipped); } else { job->user_cancel = TRUE; } } if (to_trash_files != NULL) { to_trash_files = g_list_reverse (to_trash_files); trash_files (common, to_trash_files, &files_skipped); } g_list_free (to_trash_files); g_list_free (to_delete_files); if (files_skipped == g_list_length (job->files)) { /* User has skipped all files, report user cancel */ job->user_cancel = TRUE; } g_io_scheduler_job_send_to_mainloop_async (io_job, delete_job_done, job, NULL); return FALSE; } static void trash_or_delete_internal (GList *files, GtkWindow *parent_window, gboolean try_trash, NautilusDeleteCallback done_callback, gpointer done_callback_data) { DeleteJob *job; setup_autos (); /* TODO: special case desktop icon link files ... */ job = op_job_new (DeleteJob, parent_window); job->files = eel_g_object_list_copy (files); job->try_trash = try_trash; job->user_cancel = FALSE; job->done_callback = done_callback; job->done_callback_data = done_callback_data; if (try_trash) { inhibit_power_manager ((CommonJob *)job, _("Trashing Files")); } else { inhibit_power_manager ((CommonJob *)job, _("Deleting Files")); } g_io_scheduler_push_job (delete_job, job, NULL, 0, NULL); } void nautilus_file_operations_trash_or_delete (GList *files, GtkWindow *parent_window, NautilusDeleteCallback done_callback, gpointer done_callback_data) { trash_or_delete_internal (files, parent_window, TRUE, done_callback, done_callback_data); } void nautilus_file_operations_delete (GList *files, GtkWindow *parent_window, NautilusDeleteCallback done_callback, gpointer done_callback_data) { trash_or_delete_internal (files, parent_window, FALSE, done_callback, done_callback_data); } typedef struct { gboolean eject; GMount *mount; GtkWindow *parent_window; NautilusUnmountCallback callback; gpointer callback_data; } UnmountData; static void unmount_mount_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { UnmountData *data = user_data; GError *error; char *primary; gboolean unmounted; error = NULL; if (data->eject) { unmounted = g_mount_eject_with_operation_finish (G_MOUNT (source_object), res, &error); } else { unmounted = g_mount_unmount_with_operation_finish (G_MOUNT (source_object), res, &error); } if (! unmounted) { if (error->code != G_IO_ERROR_FAILED_HANDLED) { if (data->eject) { primary = f (_("Unable to eject %V"), source_object); } else { primary = f (_("Unable to unmount %V"), source_object); } eel_show_error_dialog (primary, error->message, data->parent_window); g_free (primary); } } if (data->callback) { data->callback (data->callback_data); } if (error != NULL) { g_error_free (error); } eel_remove_weak_pointer (&data->parent_window); g_object_unref (data->mount); g_free (data); } static void do_unmount (UnmountData *data) { GMountOperation *mount_op; mount_op = gtk_mount_operation_new (data->parent_window); if (data->eject) { g_mount_eject_with_operation (data->mount, 0, mount_op, NULL, unmount_mount_callback, data); } else { g_mount_unmount_with_operation (data->mount, 0, mount_op, NULL, unmount_mount_callback, data); } g_object_unref (mount_op); } static gboolean dir_has_files (GFile *dir) { GFileEnumerator *enumerator; gboolean res; GFileInfo *file_info; res = FALSE; enumerator = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, 0, NULL, NULL); if (enumerator) { file_info = g_file_enumerator_next_file (enumerator, NULL, NULL); if (file_info != NULL) { res = TRUE; g_object_unref (file_info); } g_file_enumerator_close (enumerator, NULL, NULL); g_object_unref (enumerator); } return res; } static GList * get_trash_dirs_for_mount (GMount *mount) { GFile *root; GFile *trash; char *relpath; GList *list; root = g_mount_get_root (mount); if (root == NULL) { return NULL; } list = NULL; if (g_file_is_native (root)) { relpath = g_strdup_printf (".Trash/%d", getuid ()); trash = g_file_resolve_relative_path (root, relpath); g_free (relpath); list = g_list_prepend (list, g_file_get_child (trash, "files")); list = g_list_prepend (list, g_file_get_child (trash, "info")); g_object_unref (trash); relpath = g_strdup_printf (".Trash-%d", getuid ()); trash = g_file_get_child (root, relpath); g_free (relpath); list = g_list_prepend (list, g_file_get_child (trash, "files")); list = g_list_prepend (list, g_file_get_child (trash, "info")); g_object_unref (trash); } g_object_unref (root); return list; } static gboolean has_trash_files (GMount *mount) { GList *dirs, *l; GFile *dir; gboolean res; dirs = get_trash_dirs_for_mount (mount); res = FALSE; for (l = dirs; l != NULL; l = l->next) { dir = l->data; if (dir_has_files (dir)) { res = TRUE; break; } } eel_g_object_list_free (dirs); return res; } static gint prompt_empty_trash (GtkWindow *parent_window) { gint result; GtkWidget *dialog; GdkScreen *screen; screen = NULL; if (parent_window != NULL) { screen = gtk_widget_get_screen (GTK_WIDGET (parent_window)); } /* Do we need to be modal ? */ dialog = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, _("Do you want to empty the trash before you unmount?")); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), _("In order to regain the " "free space on this volume " "the trash must be emptied. " "All trashed items on the volume " "will be permanently lost.")); gtk_dialog_add_buttons (GTK_DIALOG (dialog), _("Do _not Empty Trash"), GTK_RESPONSE_REJECT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, _("Empty _Trash"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); gtk_window_set_title (GTK_WINDOW (dialog), ""); /* as per HIG */ gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE); if (screen) { gtk_window_set_screen (GTK_WINDOW (dialog), screen); } atk_object_set_role (gtk_widget_get_accessible (dialog), ATK_ROLE_ALERT); gtk_window_set_wmclass (GTK_WINDOW (dialog), "empty_trash", "Nautilus"); /* Make transient for the window group */ gtk_widget_realize (dialog); if (screen != NULL) { gdk_window_set_transient_for (GTK_WIDGET (dialog)->window, gdk_screen_get_root_window (screen)); } result = gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); return result; } void nautilus_file_operations_unmount_mount_full (GtkWindow *parent_window, GMount *mount, gboolean eject, gboolean check_trash, NautilusUnmountCallback callback, gpointer callback_data) { UnmountData *data; int response; data = g_new0 (UnmountData, 1); data->callback = callback; data->callback_data = callback_data; if (parent_window) { data->parent_window = parent_window; eel_add_weak_pointer (&data->parent_window); } data->eject = eject; data->mount = g_object_ref (mount); if (check_trash && has_trash_files (mount)) { response = prompt_empty_trash (parent_window); if (response == GTK_RESPONSE_ACCEPT) { EmptyTrashJob *job; job = op_job_new (EmptyTrashJob, parent_window); job->should_confirm = FALSE; job->trash_dirs = get_trash_dirs_for_mount (mount); job->done_callback = (NautilusOpCallback)do_unmount; job->done_callback_data = data; g_io_scheduler_push_job (empty_trash_job, job, NULL, 0, NULL); return; } else if (response == GTK_RESPONSE_CANCEL) { if (callback) { callback (callback_data); } eel_remove_weak_pointer (&data->parent_window); g_object_unref (data->mount); g_free (data); return; } } do_unmount (data); } void nautilus_file_operations_unmount_mount (GtkWindow *parent_window, GMount *mount, gboolean eject, gboolean check_trash) { nautilus_file_operations_unmount_mount_full (parent_window, mount, eject, check_trash, NULL, NULL); } static void mount_callback_data_notify (gpointer data, GObject *object) { GMountOperation *mount_op; mount_op = G_MOUNT_OPERATION (data); g_object_set_data (G_OBJECT (mount_op), "mount-callback", NULL); g_object_set_data (G_OBJECT (mount_op), "mount-callback-data", NULL); } static void volume_mount_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { NautilusMountCallback mount_callback; GObject *mount_callback_data_object; GMountOperation *mount_op = user_data; GError *error; char *primary; char *name; error = NULL; nautilus_allow_autorun_for_volume_finish (G_VOLUME (source_object)); if (!g_volume_mount_finish (G_VOLUME (source_object), res, &error)) { if (error->code != G_IO_ERROR_FAILED_HANDLED) { name = g_volume_get_name (G_VOLUME (source_object)); primary = g_strdup_printf (_("Unable to mount %s"), name); g_free (name); eel_show_error_dialog (primary, error->message, NULL); g_free (primary); } g_error_free (error); } mount_callback = (NautilusMountCallback) g_object_get_data (G_OBJECT (mount_op), "mount-callback"); mount_callback_data_object = g_object_get_data (G_OBJECT (mount_op), "mount-callback-data"); if (mount_callback != NULL) { (* mount_callback) (G_VOLUME (source_object), mount_callback_data_object); if (mount_callback_data_object != NULL) { g_object_weak_unref (mount_callback_data_object, mount_callback_data_notify, mount_op); } } g_object_unref (mount_op); } void nautilus_file_operations_mount_volume (GtkWindow *parent_window, GVolume *volume, gboolean allow_autorun) { nautilus_file_operations_mount_volume_full (parent_window, volume, allow_autorun, NULL, NULL); } void nautilus_file_operations_mount_volume_full (GtkWindow *parent_window, GVolume *volume, gboolean allow_autorun, NautilusMountCallback mount_callback, GObject *mount_callback_data_object) { GMountOperation *mount_op; mount_op = gtk_mount_operation_new (parent_window); g_object_set_data (G_OBJECT (mount_op), "mount-callback", mount_callback); if (mount_callback != NULL && mount_callback_data_object != NULL) { g_object_weak_ref (mount_callback_data_object, mount_callback_data_notify, mount_op); } g_object_set_data (G_OBJECT (mount_op), "mount-callback-data", mount_callback_data_object); if (allow_autorun) nautilus_allow_autorun_for_volume (volume); g_volume_mount (volume, 0, mount_op, NULL, volume_mount_cb, mount_op); } static void report_count_progress (CommonJob *job, SourceInfo *source_info) { char *s; switch (source_info->op) { default: case OP_KIND_COPY: s = f (ngettext("Preparing to copy %'d file (%S)", "Preparing to copy %'d files (%S)", source_info->num_files), source_info->num_files, source_info->num_bytes); break; case OP_KIND_MOVE: s = f (ngettext("Preparing to move %'d file (%S)", "Preparing to move %'d files (%S)", source_info->num_files), source_info->num_files, source_info->num_bytes); break; case OP_KIND_DELETE: s = f (ngettext("Preparing to delete %'d file (%S)", "Preparing to delete %'d files (%S)", source_info->num_files), source_info->num_files, source_info->num_bytes); break; case OP_KIND_TRASH: s = f (ngettext("Preparing to trash %'d file", "Preparing to trash %'d files", source_info->num_files), source_info->num_files); break; } nautilus_progress_info_take_details (job->progress, s); nautilus_progress_info_pulse_progress (job->progress); } static void count_file (GFileInfo *info, CommonJob *job, SourceInfo *source_info) { source_info->num_files += 1; source_info->num_bytes += g_file_info_get_size (info); if (source_info->num_files_since_progress++ > 100) { report_count_progress (job, source_info); source_info->num_files_since_progress = 0; } } static char * get_scan_primary (OpKind kind) { switch (kind) { default: case OP_KIND_COPY: return f (_("Error while copying.")); case OP_KIND_MOVE: return f (_("Error while moving.")); case OP_KIND_DELETE: return f (_("Error while deleting.")); case OP_KIND_TRASH: return f (_("Error while moving files to trash.")); } } static void scan_dir (GFile *dir, SourceInfo *source_info, CommonJob *job, GQueue *dirs) { GFileInfo *info; GError *error; GFile *subdir; GFileEnumerator *enumerator; char *primary, *secondary, *details; int response; SourceInfo saved_info; saved_info = *source_info; retry: error = NULL; enumerator = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME"," G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (enumerator) { error = NULL; while ((info = g_file_enumerator_next_file (enumerator, job->cancellable, &error)) != NULL) { count_file (info, job, source_info); if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { subdir = g_file_get_child (dir, g_file_info_get_name (info)); /* Push to head, since we want depth-first */ g_queue_push_head (dirs, subdir); } g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); if (error && IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else if (error) { primary = get_scan_primary (source_info->op); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("Files in the folder \"%B\" cannot be handled because you do " "not have permissions to see them."), dir); } else { secondary = f (_("There was an error getting information about the files in the folder \"%B\"."), dir); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, RETRY, SKIP, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { *source_info = saved_info; goto retry; } else if (response == 2) { skip_readdir_error (job, dir); } else { g_assert_not_reached (); } } } else if (job->skip_all_error) { g_error_free (error); skip_file (job, dir); } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { primary = get_scan_primary (source_info->op); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("The folder \"%B\" cannot be handled because you do not have " "permissions to read it."), dir); } else { secondary = f (_("There was an error reading the folder \"%B\"."), dir); details = error->message; } /* set show_all to TRUE here, as we don't know how many * files we'll end up processing yet. */ response = run_warning (job, primary, secondary, details, TRUE, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1 || response == 2) { if (response == 1) { job->skip_all_error = TRUE; } skip_file (job, dir); } else if (response == 3) { goto retry; } else { g_assert_not_reached (); } } } static void scan_file (GFile *file, SourceInfo *source_info, CommonJob *job) { GFileInfo *info; GError *error; GQueue *dirs; GFile *dir; char *primary; char *secondary; char *details; int response; dirs = g_queue_new (); retry: error = NULL; info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (info) { count_file (info, job, source_info); if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { g_queue_push_head (dirs, g_object_ref (file)); } g_object_unref (info); } else if (job->skip_all_error) { g_error_free (error); skip_file (job, file); } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { primary = get_scan_primary (source_info->op); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("The file \"%B\" cannot be handled because you do not have " "permissions to read it."), file); } else { secondary = f (_("There was an error getting information about \"%B\"."), file); details = error->message; } /* set show_all to TRUE here, as we don't know how many * files we'll end up processing yet. */ response = run_warning (job, primary, secondary, details, TRUE, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1 || response == 2) { if (response == 1) { job->skip_all_error = TRUE; } skip_file (job, file); } else if (response == 3) { goto retry; } else { g_assert_not_reached (); } } while (!job_aborted (job) && (dir = g_queue_pop_head (dirs)) != NULL) { scan_dir (dir, source_info, job, dirs); g_object_unref (dir); } /* Free all from queue if we exited early */ g_queue_foreach (dirs, (GFunc)g_object_unref, NULL); g_queue_free (dirs); } static void scan_sources (GList *files, SourceInfo *source_info, CommonJob *job, OpKind kind) { GList *l; GFile *file; memset (source_info, 0, sizeof (SourceInfo)); source_info->op = kind; report_count_progress (job, source_info); for (l = files; l != NULL && !job_aborted (job); l = l->next) { file = l->data; scan_file (file, source_info, job); } /* Make sure we report the final count */ report_count_progress (job, source_info); } static void verify_destination (CommonJob *job, GFile *dest, char **dest_fs_id, goffset required_size) { GFileInfo *info, *fsinfo; GError *error; guint64 free_size; char *primary, *secondary, *details; int response; GFileType file_type; if (dest_fs_id) { *dest_fs_id = NULL; } retry: error = NULL; info = g_file_query_info (dest, G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_ID_FILESYSTEM, 0, job->cancellable, &error); if (info == NULL) { if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); return; } primary = f (_("Error while copying to \"%B\"."), dest); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("You do not have permissions to access the destination folder.")); } else { secondary = f (_("There was an error getting information about the destination.")); details = error->message; } response = run_error (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { goto retry; } else { g_assert_not_reached (); } return; } file_type = g_file_info_get_file_type (info); if (dest_fs_id) { *dest_fs_id = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_ID_FILESYSTEM)); } g_object_unref (info); if (file_type != G_FILE_TYPE_DIRECTORY) { primary = f (_("Error while copying to \"%B\"."), dest); secondary = f (_("The destination is not a folder.")); response = run_error (job, primary, secondary, NULL, FALSE, GTK_STOCK_CANCEL, NULL); abort_job (job); return; } fsinfo = g_file_query_filesystem_info (dest, G_FILE_ATTRIBUTE_FILESYSTEM_FREE"," G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, job->cancellable, NULL); if (fsinfo == NULL) { /* All sorts of things can go wrong getting the fs info (like not supported) * only check these things if the fs returns them */ return; } if (required_size > 0 && g_file_info_has_attribute (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_FREE)) { free_size = g_file_info_get_attribute_uint64 (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_FREE); if (free_size < required_size) { primary = f (_("Error while copying to \"%B\"."), dest); secondary = f(_("There is not enough space on the destination. Try to remove files to make space.")); details = f (_("There is %S available, but %S is required."), free_size, required_size); response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, COPY_FORCE, RETRY, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 2) { goto retry; } else if (response == 1) { /* We are forced to copy - just fall through ... */ } else { g_assert_not_reached (); } } } if (!job_aborted (job) && g_file_info_get_attribute_boolean (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) { primary = f (_("Error while copying to \"%B\"."), dest); secondary = f (_("The destination is read-only.")); response = run_error (job, primary, secondary, NULL, FALSE, GTK_STOCK_CANCEL, NULL); g_error_free (error); abort_job (job); } g_object_unref (fsinfo); } static void report_copy_progress (CopyMoveJob *copy_job, SourceInfo *source_info, TransferInfo *transfer_info) { int files_left; goffset total_size; double elapsed, transfer_rate; int remaining_time; guint64 now; CommonJob *job; gboolean is_move; job = (CommonJob *)copy_job; is_move = copy_job->is_move; now = g_thread_gettime (); if (transfer_info->last_report_time != 0 && ABS ((gint64)(transfer_info->last_report_time - now)) < 100 * NSEC_PER_MSEC) { return; } transfer_info->last_report_time = now; files_left = source_info->num_files - transfer_info->num_files; /* Races and whatnot could cause this to be negative... */ if (files_left < 0) { files_left = 1; } if (files_left != transfer_info->last_reported_files_left || transfer_info->last_reported_files_left == 0) { /* Avoid changing this unless files_left changed since last time */ transfer_info->last_reported_files_left = files_left; if (source_info->num_files == 1) { if (copy_job->destination != NULL) { nautilus_progress_info_take_status (job->progress, f (is_move ? _("Moving \"%B\" to \"%B\""): _("Copying \"%B\" to \"%B\""), (GFile *)copy_job->files->data, copy_job->destination)); } else { nautilus_progress_info_take_status (job->progress, f (_("Duplicating \"%B\""), (GFile *)copy_job->files->data)); } } else if (copy_job->files != NULL && copy_job->files->next == NULL) { if (copy_job->destination != NULL) { nautilus_progress_info_take_status (job->progress, f (is_move? ngettext ("Moving %'d file (in \"%B\") to \"%B\"", "Moving %'d files (in \"%B\") to \"%B\"", files_left) : ngettext ("Copying %'d file (in \"%B\") to \"%B\"", "Copying %'d files (in \"%B\") to \"%B\"", files_left), files_left, (GFile *)copy_job->files->data, copy_job->destination)); } else { nautilus_progress_info_take_status (job->progress, f (ngettext ("Duplicating %'d file (in \"%B\")", "Duplicating %'d files (in \"%B\")", files_left), files_left, (GFile *)copy_job->files->data)); } } else { if (copy_job->destination != NULL) { nautilus_progress_info_take_status (job->progress, f (is_move? ngettext ("Moving %'d file to \"%B\"", "Moving %'d files to \"%B\"", files_left) : ngettext ("Copying %'d file to \"%B\"", "Copying %'d files to \"%B\"", files_left), files_left, copy_job->destination)); } else { nautilus_progress_info_take_status (job->progress, f (ngettext ("Duplicating %'d file", "Duplicating %'d files", files_left), files_left)); } } } total_size = MAX (source_info->num_bytes, transfer_info->num_bytes); elapsed = g_timer_elapsed (job->time, NULL); transfer_rate = 0; if (elapsed > 0) { transfer_rate = transfer_info->num_bytes / elapsed; } if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE && transfer_rate > 0) { char *s; /* To translators: %S will expand to a size like "2 bytes" or "3 MB", so something like "4 kb of 4 MB" */ s = f (_("%S of %S"), transfer_info->num_bytes, total_size); nautilus_progress_info_take_details (job->progress, s); } else { char *s; remaining_time = (total_size - transfer_info->num_bytes) / transfer_rate; /* To translators: %S will expand to a size like "2 bytes" or "3 MB", %T to a time duration like * "2 minutes". So the whole thing will be something like "2 kb of 4 MB -- 2 hours left (4kb/sec)" * * The singular/plural form will be used depending on the remaining time (i.e. the %T argument). */ s = f (ngettext ("%S of %S \xE2\x80\x94 %T left (%S/sec)", "%S of %S \xE2\x80\x94 %T left (%S/sec)", seconds_count_format_time_units (remaining_time)), transfer_info->num_bytes, total_size, remaining_time, (goffset)transfer_rate); nautilus_progress_info_take_details (job->progress, s); } nautilus_progress_info_set_progress (job->progress, transfer_info->num_bytes, total_size); } static int get_max_name_length (GFile *file_dir) { int max_length; char *dir; long max_path; long max_name; max_length = -1; if (!g_file_has_uri_scheme (file_dir, "file")) return max_length; dir = g_file_get_path (file_dir); if (!dir) return max_length; max_path = pathconf (dir, _PC_PATH_MAX); max_name = pathconf (dir, _PC_NAME_MAX); if (max_name == -1 && max_path == -1) { max_length = -1; } else if (max_name == -1 && max_path != -1) { max_length = max_path - (strlen (dir) + 1); } else if (max_name != -1 && max_path == -1) { max_length = max_name; } else { int leftover; leftover = max_path - (strlen (dir) + 1); max_length = MIN (leftover, max_name); } g_free (dir); return max_length; } #define FAT_FORBIDDEN_CHARACTERS "/:;*?\"<>" static gboolean str_replace (char *str, const char *chars_to_replace, char replacement) { gboolean success; int i; success = FALSE; for (i = 0; str[i] != '\0'; i++) { if (strchr (chars_to_replace, str[i])) { success = TRUE; str[i] = replacement; } } return success; } static gboolean make_file_name_valid_for_dest_fs (char *filename, const char *dest_fs_type) { if (dest_fs_type != NULL && filename != NULL) { if (!strcmp (dest_fs_type, "fat") || !strcmp (dest_fs_type, "vfat") || !strcmp (dest_fs_type, "msdos") || !strcmp (dest_fs_type, "msdosfs")) { gboolean ret; int i, old_len; ret = str_replace (filename, FAT_FORBIDDEN_CHARACTERS, '_'); old_len = strlen (filename); for (i = 0; i < old_len; i++) { if (filename[i] != ' ') { g_strchomp (filename); ret |= (old_len != strlen (filename)); break; } } return ret; } } return FALSE; } static GFile * get_unique_target_file (GFile *src, GFile *dest_dir, gboolean same_fs, const char *dest_fs_type, int count) { const char *editname, *end; char *basename, *new_name; GFileInfo *info; GFile *dest; int max_length; max_length = get_max_name_length (dest_dir); dest = NULL; info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME, 0, NULL, NULL); if (info != NULL) { editname = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME); if (editname != NULL) { new_name = get_duplicate_name (editname, count, max_length); make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child_for_display_name (dest_dir, new_name, NULL); g_free (new_name); } g_object_unref (info); } if (dest == NULL) { basename = g_file_get_basename (src); if (g_utf8_validate (basename, -1, NULL)) { new_name = get_duplicate_name (basename, count, max_length); make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child_for_display_name (dest_dir, new_name, NULL); g_free (new_name); } if (dest == NULL) { end = strrchr (basename, '.'); if (end != NULL) { count += atoi (end + 1); } new_name = g_strdup_printf ("%s.%d", basename, count); make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child (dest_dir, new_name); g_free (new_name); } g_free (basename); } return dest; } static GFile * get_target_file_for_link (GFile *src, GFile *dest_dir, const char *dest_fs_type, int count) { const char *editname; char *basename, *new_name; GFileInfo *info; GFile *dest; int max_length; max_length = get_max_name_length (dest_dir); dest = NULL; info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME, 0, NULL, NULL); if (info != NULL) { editname = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME); if (editname != NULL) { new_name = get_link_name (editname, count, max_length); make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child_for_display_name (dest_dir, new_name, NULL); g_free (new_name); } g_object_unref (info); } if (dest == NULL) { basename = g_file_get_basename (src); make_file_name_valid_for_dest_fs (basename, dest_fs_type); if (g_utf8_validate (basename, -1, NULL)) { new_name = get_link_name (basename, count, max_length); make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child_for_display_name (dest_dir, new_name, NULL); g_free (new_name); } if (dest == NULL) { if (count == 1) { new_name = g_strdup_printf ("%s.lnk", basename); } else { new_name = g_strdup_printf ("%s.lnk%d", basename, count); } make_file_name_valid_for_dest_fs (new_name, dest_fs_type); dest = g_file_get_child (dest_dir, new_name); g_free (new_name); } g_free (basename); } return dest; } static GFile * get_target_file (GFile *src, GFile *dest_dir, const char *dest_fs_type, gboolean same_fs) { char *basename; GFile *dest; GFileInfo *info; char *copyname; dest = NULL; if (!same_fs) { info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME, 0, NULL, NULL); if (info) { copyname = g_strdup (g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME)); if (copyname) { make_file_name_valid_for_dest_fs (copyname, dest_fs_type); dest = g_file_get_child_for_display_name (dest_dir, copyname, NULL); g_free (copyname); } g_object_unref (info); } } if (dest == NULL) { basename = g_file_get_basename (src); make_file_name_valid_for_dest_fs (basename, dest_fs_type); dest = g_file_get_child (dest_dir, basename); g_free (basename); } return dest; } static gboolean has_fs_id (GFile *file, const char *fs_id) { const char *id; GFileInfo *info; gboolean res; res = FALSE; info = g_file_query_info (file, G_FILE_ATTRIBUTE_ID_FILESYSTEM, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); if (info) { id = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_ID_FILESYSTEM); if (id && strcmp (id, fs_id) == 0) { res = TRUE; } g_object_unref (info); } return res; } static gboolean is_dir (GFile *file) { GFileInfo *info; gboolean res; res = FALSE; info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); if (info) { res = g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY; g_object_unref (info); } return res; } static void copy_move_file (CopyMoveJob *job, GFile *src, GFile *dest_dir, gboolean same_fs, gboolean unique_names, char **dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info, GHashTable *debuting_files, GdkPoint *point, gboolean overwrite, gboolean *skipped_file, gboolean readonly_source_fs); typedef enum { CREATE_DEST_DIR_RETRY, CREATE_DEST_DIR_FAILED, CREATE_DEST_DIR_SUCCESS } CreateDestDirResult; static CreateDestDirResult create_dest_dir (CommonJob *job, GFile *src, GFile **dest, gboolean same_fs, char **dest_fs_type) { GError *error; GFile *new_dest, *dest_dir; char *primary, *secondary, *details; int response; gboolean handled_invalid_filename; handled_invalid_filename = *dest_fs_type != NULL; retry: /* First create the directory, then copy stuff to it before copying the attributes, because we need to be sure we can write to it */ error = NULL; if (!g_file_make_directory (*dest, job->cancellable, &error)) { if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); return CREATE_DEST_DIR_FAILED; } else if (IS_IO_ERROR (error, INVALID_FILENAME) && !handled_invalid_filename) { handled_invalid_filename = TRUE; g_assert (*dest_fs_type == NULL); dest_dir = g_file_get_parent (*dest); if (dest_dir != NULL) { *dest_fs_type = query_fs_type (dest_dir, job->cancellable); new_dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); g_object_unref (dest_dir); if (!g_file_equal (*dest, new_dest)) { g_object_unref (*dest); *dest = new_dest; g_error_free (error); return CREATE_DEST_DIR_RETRY; } else { g_object_unref (new_dest); } } } primary = f (_("Error while copying.")); details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("The folder \"%B\" cannot be copied because you do not have " "permissions to create it in the destination."), src); } else { secondary = f (_("There was an error creating the folder \"%B\"."), src); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, SKIP, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* Skip: Do Nothing */ } else if (response == 2) { goto retry; } else { g_assert_not_reached (); } return CREATE_DEST_DIR_FAILED; } nautilus_file_changes_queue_file_added (*dest); return CREATE_DEST_DIR_SUCCESS; } /* a return value of FALSE means retry, i.e. * the destination has changed and the source * is expected to re-try the preceeding * g_file_move() or g_file_copy() call with * the new destination. */ static gboolean copy_move_directory (CopyMoveJob *copy_job, GFile *src, GFile **dest, gboolean same_fs, gboolean create_dest, char **parent_dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info, GHashTable *debuting_files, gboolean *skipped_file, gboolean readonly_source_fs) { GFileInfo *info; GError *error; GFile *src_file; GFileEnumerator *enumerator; char *primary, *secondary, *details; char *dest_fs_type; int response; gboolean skip_error; gboolean local_skipped_file; CommonJob *job; GFileCopyFlags flags; job = (CommonJob *)copy_job; if (create_dest) { switch (create_dest_dir (job, src, dest, same_fs, parent_dest_fs_type)) { case CREATE_DEST_DIR_RETRY: /* next time copy_move_directory() is called, * create_dest will be FALSE if a directory already * exists under the new name (i.e. WOULD_RECURSE) */ return FALSE; case CREATE_DEST_DIR_FAILED: *skipped_file = TRUE; return TRUE; case CREATE_DEST_DIR_SUCCESS: default: break; } if (debuting_files) { g_hash_table_replace (debuting_files, g_object_ref (*dest), GINT_TO_POINTER (TRUE)); } } local_skipped_file = FALSE; dest_fs_type = NULL; skip_error = should_skip_readdir_error (job, src); retry: error = NULL; enumerator = g_file_enumerate_children (src, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (enumerator) { error = NULL; while (!job_aborted (job) && (info = g_file_enumerator_next_file (enumerator, job->cancellable, skip_error?NULL:&error)) != NULL) { src_file = g_file_get_child (src, g_file_info_get_name (info)); copy_move_file (copy_job, src_file, *dest, same_fs, FALSE, &dest_fs_type, source_info, transfer_info, NULL, NULL, FALSE, &local_skipped_file, readonly_source_fs); g_object_unref (src_file); g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); if (error && IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else if (error) { if (copy_job->is_move) { primary = f (_("Error while moving.")); } else { primary = f (_("Error while copying.")); } details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("Files in the folder \"%B\" cannot be copied because you do " "not have permissions to see them."), src); } else { secondary = f (_("There was an error getting information about the files in the folder \"%B\"."), src); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, _("_Skip files"), NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* Skip: Do Nothing */ local_skipped_file = TRUE; } else { g_assert_not_reached (); } } /* Count the copied directory as a file */ transfer_info->num_files ++; report_copy_progress (copy_job, source_info, transfer_info); if (debuting_files) { g_hash_table_replace (debuting_files, g_object_ref (*dest), GINT_TO_POINTER (create_dest)); } } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { if (copy_job->is_move) { primary = f (_("Error while moving.")); } else { primary = f (_("Error while copying.")); } details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = f (_("The folder \"%B\" cannot be copied because you do not have " "permissions to read it."), src); } else { secondary = f (_("There was an error reading the folder \"%B\"."), src); details = error->message; } response = run_warning (job, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, SKIP, RETRY, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* Skip: Do Nothing */ local_skipped_file = TRUE; } else if (response == 2) { goto retry; } else { g_assert_not_reached (); } } if (create_dest) { flags = (readonly_source_fs) ? G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_TARGET_DEFAULT_PERMS : G_FILE_COPY_NOFOLLOW_SYMLINKS; /* Ignore errors here. Failure to copy metadata is not a hard error */ g_file_copy_attributes (src, *dest, flags, job->cancellable, NULL); } if (!job_aborted (job) && copy_job->is_move && /* Don't delete source if there was a skipped file */ !local_skipped_file) { if (!g_file_delete (src, job->cancellable, &error)) { if (job->skip_all_error) { goto skip; } primary = f (_("Error while moving \"%B\"."), src); secondary = f (_("Could not remove the source folder.")); details = error->message; response = run_warning (job, primary, secondary, details, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; local_skipped_file = TRUE; } else if (response == 2) { /* skip */ local_skipped_file = TRUE; } else { g_assert_not_reached (); } skip: g_error_free (error); } } if (local_skipped_file) { *skipped_file = TRUE; } g_free (dest_fs_type); return TRUE; } static gboolean remove_target_recursively (CommonJob *job, GFile *src, GFile *toplevel_dest, GFile *file) { GFileEnumerator *enumerator; GError *error; GFile *child; gboolean stop; char *primary, *secondary, *details; int response; GFileInfo *info; stop = FALSE; error = NULL; enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (enumerator) { error = NULL; while (!job_aborted (job) && (info = g_file_enumerator_next_file (enumerator, job->cancellable, &error)) != NULL) { child = g_file_get_child (file, g_file_info_get_name (info)); if (!remove_target_recursively (job, src, toplevel_dest, child)) { stop = TRUE; break; } g_object_unref (child); g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); } else if (IS_IO_ERROR (error, NOT_DIRECTORY)) { /* Not a dir, continue */ g_error_free (error); } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } else { if (job->skip_all_error) { goto skip1; } primary = f (_("Error while copying \"%B\"."), src); secondary = f (_("Could not remove files from the already existing folder %F."), file); details = error->message; /* set show_all to TRUE here, as we don't know how many * files we'll end up processing yet. */ response = run_warning (job, primary, secondary, details, TRUE, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } skip1: g_error_free (error); stop = TRUE; } if (stop) { return FALSE; } error = NULL; if (!g_file_delete (file, job->cancellable, &error)) { if (job->skip_all_error || IS_IO_ERROR (error, CANCELLED)) { goto skip2; } primary = f (_("Error while copying \"%B\"."), src); secondary = f (_("Could not remove the already existing file %F."), file); details = error->message; /* set show_all to TRUE here, as we don't know how many * files we'll end up processing yet. */ response = run_warning (job, primary, secondary, details, TRUE, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } skip2: g_error_free (error); return FALSE; } nautilus_file_changes_queue_file_removed (file); return TRUE; } typedef struct { CopyMoveJob *job; goffset last_size; SourceInfo *source_info; TransferInfo *transfer_info; } ProgressData; static void copy_file_progress_callback (goffset current_num_bytes, goffset total_num_bytes, gpointer user_data) { ProgressData *pdata; goffset new_size; pdata = user_data; new_size = current_num_bytes - pdata->last_size; if (new_size > 0) { pdata->transfer_info->num_bytes += new_size; pdata->last_size = current_num_bytes; report_copy_progress (pdata->job, pdata->source_info, pdata->transfer_info); } } static gboolean test_dir_is_parent (GFile *child, GFile *root) { GFile *f; f = g_file_dup (child); while (f) { if (g_file_equal (f, root)) { g_object_unref (f); return TRUE; } f = g_file_get_parent (f); } if (f) { g_object_unref (f); } return FALSE; } static char * query_fs_type (GFile *file, GCancellable *cancellable) { GFileInfo *fsinfo; char *ret; ret = NULL; fsinfo = g_file_query_filesystem_info (file, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, cancellable, NULL); if (fsinfo != NULL) { ret = g_strdup (g_file_info_get_attribute_string (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE)); g_object_unref (fsinfo); } if (ret == NULL) { /* ensure that we don't attempt to query * the FS type for each file in a given * directory, if it can't be queried. */ ret = g_strdup (""); } return ret; } static gboolean is_trusted_desktop_file (GFile *file, GCancellable *cancellable) { char *basename; gboolean res; GFileInfo *info; /* Don't trust non-local files */ if (!g_file_is_native (file)) { return FALSE; } basename = g_file_get_basename (file); if (!g_str_has_suffix (basename, ".desktop")) { g_free (basename); return FALSE; } g_free (basename); info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, NULL); if (info == NULL) { return FALSE; } res = FALSE; /* Weird file => not trusted, Already executable => no need to mark trusted */ if (g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR && !g_file_info_get_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE) && nautilus_is_in_system_dir (file)) { res = TRUE; } g_object_unref (info); return res; } /* Debuting files is non-NULL only for toplevel items */ static void copy_move_file (CopyMoveJob *copy_job, GFile *src, GFile *dest_dir, gboolean same_fs, gboolean unique_names, char **dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info, GHashTable *debuting_files, GdkPoint *position, gboolean overwrite, gboolean *skipped_file, gboolean readonly_source_fs) { GFile *dest, *new_dest; GError *error; GFileCopyFlags flags; char *primary, *secondary, *details; int response; ProgressData pdata; gboolean would_recurse, is_merge; CommonJob *job; gboolean res; int unique_name_nr; gboolean handled_invalid_filename; job = (CommonJob *)copy_job; if (should_skip_file (job, src)) { *skipped_file = TRUE; return; } unique_name_nr = 1; /* another file in the same directory might have handled the invalid * filename condition for us */ handled_invalid_filename = *dest_fs_type != NULL; if (unique_names) { dest = get_unique_target_file (src, dest_dir, same_fs, *dest_fs_type, unique_name_nr++); } else { dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); } /* Don't allow recursive move/copy into itself. * (We would get a file system error if we proceeded but it is nicer to * detect and report it at this level) */ if (test_dir_is_parent (dest_dir, src)) { if (job->skip_all_error) { goto out; } /* the run_warning() frees all strings passed in automatically */ primary = copy_job->is_move ? g_strdup (_("You cannot move a folder into itself.")) : g_strdup (_("You cannot copy a folder into itself.")); secondary = g_strdup (_("The destination folder is inside the source folder.")); response = run_warning (job, primary, secondary, NULL, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } goto out; } /* Don't allow copying over the source or one of the parents of the source. */ if (test_dir_is_parent (src, dest)) { if (job->skip_all_error) { goto out; } /* the run_warning() frees all strings passed in automatically */ primary = copy_job->is_move ? g_strdup (_("You cannot move a file over itself.")) : g_strdup (_("You cannot copy a file over itself.")); secondary = g_strdup (_("The source file would be overwritten by the destination.")); response = run_warning (job, primary, secondary, NULL, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } goto out; } retry: error = NULL; flags = G_FILE_COPY_NOFOLLOW_SYMLINKS; if (overwrite) { flags |= G_FILE_COPY_OVERWRITE; } if (readonly_source_fs) { flags |= G_FILE_COPY_TARGET_DEFAULT_PERMS; } pdata.job = copy_job; pdata.last_size = 0; pdata.source_info = source_info; pdata.transfer_info = transfer_info; if (copy_job->is_move) { res = g_file_move (src, dest, flags, job->cancellable, copy_file_progress_callback, &pdata, &error); } else { res = g_file_copy (src, dest, flags, job->cancellable, copy_file_progress_callback, &pdata, &error); } if (res) { transfer_info->num_files ++; report_copy_progress (copy_job, source_info, transfer_info); if (debuting_files) { if (position) { nautilus_file_changes_queue_schedule_position_set (dest, *position, job->screen_num); } else { nautilus_file_changes_queue_schedule_position_remove (dest); } g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); } if (copy_job->is_move) { nautilus_file_changes_queue_file_moved (src, dest); } else { nautilus_file_changes_queue_file_added (dest); } /* If copying a trusted desktop file to the desktop, mark it as trusted. */ if (copy_job->desktop_location != NULL && g_file_equal (copy_job->desktop_location, dest_dir) && is_trusted_desktop_file (src, job->cancellable)) { mark_desktop_file_trusted (job, job->cancellable, dest, FALSE); } g_object_unref (dest); return; } if (!handled_invalid_filename && IS_IO_ERROR (error, INVALID_FILENAME)) { handled_invalid_filename = TRUE; g_assert (*dest_fs_type == NULL); *dest_fs_type = query_fs_type (dest_dir, job->cancellable); if (unique_names) { new_dest = get_unique_target_file (src, dest_dir, same_fs, *dest_fs_type, unique_name_nr); } else { new_dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); } if (!g_file_equal (dest, new_dest)) { g_object_unref (dest); dest = new_dest; g_error_free (error); goto retry; } else { g_object_unref (new_dest); } } /* Conflict */ if (!overwrite && IS_IO_ERROR (error, EXISTS)) { gboolean is_merge; if (unique_names) { g_object_unref (dest); dest = get_unique_target_file (src, dest_dir, same_fs, *dest_fs_type, unique_name_nr++); g_error_free (error); goto retry; } is_merge = FALSE; if (is_dir (dest)) { if (is_dir (src)) { is_merge = TRUE; primary = f (_("A folder named \"%B\" already exists. Do you want to merge the source folder?"), dest); secondary = f (_("The source folder already exists in \"%B\". " "Merging will ask for confirmation before replacing any files in the folder that conflict with the files being copied."), dest_dir); } else { primary = f (_("A folder named \"%B\" already exists. Do you want to replace it?"), dest); secondary = f (_("The folder already exists in \"%F\". " "Replacing it will remove all files in the folder."), dest_dir); } } else { primary = f (_("A file named \"%B\" already exists. Do you want to replace it?"), dest); secondary = f (_("The file already exists in \"%F\". " "Replacing it will overwrite its content."), dest_dir); } if ((is_merge && job->merge_all) || (!is_merge && job->replace_all)) { g_free (primary); g_free (secondary); g_error_free (error); overwrite = TRUE; goto retry; } if (job->skip_all_conflict) { g_free (primary); g_free (secondary); g_error_free (error); goto out; } response = run_warning (job, primary, secondary, NULL, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, is_merge?MERGE_ALL:REPLACE_ALL, SKIP, is_merge?MERGE:REPLACE, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1 || response == 3) { /* skip all / skip */ if (response == 1) { job->skip_all_conflict = TRUE; } } else if (response == 2 || response == 4) { /* merge/replace all / merge/replace*/ if (response == 2) { if (is_merge) { job->merge_all = TRUE; } else { job->replace_all = TRUE; } } overwrite = TRUE; goto retry; } else { g_assert_not_reached (); } } else if (overwrite && IS_IO_ERROR (error, IS_DIRECTORY)) { g_error_free (error); if (remove_target_recursively (job, src, dest, dest)) { goto retry; } } /* Needs to recurse */ else if (IS_IO_ERROR (error, WOULD_RECURSE) || IS_IO_ERROR (error, WOULD_MERGE)) { is_merge = error->code == G_IO_ERROR_WOULD_MERGE; would_recurse = error->code == G_IO_ERROR_WOULD_RECURSE; g_error_free (error); if (overwrite && would_recurse) { error = NULL; /* Copying a dir onto file, first remove the file */ if (!g_file_delete (dest, job->cancellable, &error) && !IS_IO_ERROR (error, NOT_FOUND)) { if (job->skip_all_error) { g_error_free (error); goto out; } if (copy_job->is_move) { primary = f (_("Error while moving \"%B\"."), src); } else { primary = f (_("Error while copying \"%B\"."), src); } secondary = f (_("Could not remove the already existing file with the same name in %F."), dest_dir); details = error->message; /* setting TRUE on show_all here, as we could have * another error on the same file later. */ response = run_warning (job, primary, secondary, details, TRUE, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } goto out; } if (error) { g_error_free (error); error = NULL; } nautilus_file_changes_queue_file_removed (dest); } if (is_merge) { /* On merge we now write in the target directory, which may not be in the same directory as the source, even if the parent is (if the merged directory is a mountpoint). This could cause problems as we then don't transcode filenames. We just set same_fs to FALSE which is safe but a bit slower. */ same_fs = FALSE; } if (!copy_move_directory (copy_job, src, &dest, same_fs, would_recurse, dest_fs_type, source_info, transfer_info, debuting_files, skipped_file, readonly_source_fs)) { /* destination changed, since it was an invalid file name */ g_assert (*dest_fs_type != NULL); handled_invalid_filename = TRUE; goto retry; } g_object_unref (dest); return; } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } /* Other error */ else { if (job->skip_all_error) { g_error_free (error); goto out; } primary = f (_("Error while copying \"%B\"."), src); secondary = f (_("There was an error copying the file into %F."), dest_dir); details = error->message; response = run_warning (job, primary, secondary, details, (source_info->num_files - transfer_info->num_files) > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } } out: *skipped_file = TRUE; /* Or aborted, but same-same */ g_object_unref (dest); } static void copy_files (CopyMoveJob *job, const char *dest_fs_id, SourceInfo *source_info, TransferInfo *transfer_info) { CommonJob *common; GList *l; GFile *src; gboolean same_fs; int i; GdkPoint *point; gboolean skipped_file; gboolean unique_names; GFile *dest; GFile *source_dir; char *dest_fs_type; GFileInfo *inf; gboolean readonly_source_fs; dest_fs_type = NULL; readonly_source_fs = FALSE; common = &job->common; report_copy_progress (job, source_info, transfer_info); /* Query the source dir, not the file because if its a symlink we'll follow it */ source_dir = g_file_get_parent ((GFile *) job->files->data); if (source_dir) { inf = g_file_query_filesystem_info (source_dir, "filesystem::readonly", NULL, NULL); if (inf != NULL) { readonly_source_fs = g_file_info_get_attribute_boolean (inf, "filesystem::readonly"); g_object_unref (inf); } g_object_unref (source_dir); } unique_names = (job->destination == NULL); i = 0; for (l = job->files; l != NULL && !job_aborted (common); l = l->next) { src = l->data; if (i < job->n_icon_positions) { point = &job->icon_positions[i]; } else { point = NULL; } same_fs = FALSE; if (dest_fs_id) { same_fs = has_fs_id (src, dest_fs_id); } if (job->destination) { dest = g_object_ref (job->destination); } else { dest = g_file_get_parent (src); } if (dest) { skipped_file = FALSE; copy_move_file (job, src, dest, same_fs, unique_names, &dest_fs_type, source_info, transfer_info, job->debuting_files, point, FALSE, &skipped_file, readonly_source_fs); g_object_unref (dest); } i++; } g_free (dest_fs_type); } static gboolean copy_job_done (gpointer user_data) { CopyMoveJob *job; job = user_data; if (job->done_callback) { job->done_callback (job->debuting_files, job->done_callback_data); } eel_g_object_list_free (job->files); if (job->destination) { g_object_unref (job->destination); } if (job->desktop_location) { g_object_unref (job->desktop_location); } g_hash_table_unref (job->debuting_files); g_free (job->icon_positions); finalize_common ((CommonJob *)job); nautilus_file_changes_consume_changes (TRUE); return FALSE; } static gboolean copy_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { CopyMoveJob *job; CommonJob *common; SourceInfo source_info; TransferInfo transfer_info; char *dest_fs_id; GFile *dest; job = user_data; common = &job->common; common->io_job = io_job; dest_fs_id = NULL; nautilus_progress_info_start (job->common.progress); scan_sources (job->files, &source_info, common, OP_KIND_COPY); if (job_aborted (common)) { goto aborted; } if (job->destination) { dest = g_object_ref (job->destination); } else { /* Duplication, no dest, * use source for free size, etc */ dest = g_file_get_parent (job->files->data); } verify_destination (&job->common, dest, &dest_fs_id, source_info.num_bytes); g_object_unref (dest); if (job_aborted (common)) { goto aborted; } g_timer_start (job->common.time); memset (&transfer_info, 0, sizeof (transfer_info)); copy_files (job, dest_fs_id, &source_info, &transfer_info); aborted: g_free (dest_fs_id); g_io_scheduler_job_send_to_mainloop_async (io_job, copy_job_done, job, NULL); return FALSE; } void nautilus_file_operations_copy (GList *files, GArray *relative_item_points, GFile *target_dir, GtkWindow *parent_window, NautilusCopyCallback done_callback, gpointer done_callback_data) { CopyMoveJob *job; job = op_job_new (CopyMoveJob, parent_window); job->desktop_location = nautilus_get_desktop_location (); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->files = eel_g_object_list_copy (files); job->destination = g_object_ref (target_dir); if (relative_item_points != NULL && relative_item_points->len > 0) { job->icon_positions = g_memdup (relative_item_points->data, sizeof (GdkPoint) * relative_item_points->len); job->n_icon_positions = relative_item_points->len; } job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); inhibit_power_manager ((CommonJob *)job, _("Copying Files")); g_io_scheduler_push_job (copy_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } static void report_move_progress (CopyMoveJob *move_job, int total, int left) { CommonJob *job; job = (CommonJob *)move_job; nautilus_progress_info_take_status (job->progress, f (_("Preparing to Move to \"%B\""), move_job->destination)); nautilus_progress_info_take_details (job->progress, f (ngettext ("Preparing to move %'d file", "Preparing to move %'d files", left), left)); nautilus_progress_info_pulse_progress (job->progress); } typedef struct { GFile *file; gboolean overwrite; gboolean has_position; GdkPoint position; } MoveFileCopyFallback; static MoveFileCopyFallback * move_copy_file_callback_new (GFile *file, gboolean overwrite, GdkPoint *position) { MoveFileCopyFallback *fallback; fallback = g_new (MoveFileCopyFallback, 1); fallback->file = file; fallback->overwrite = overwrite; if (position) { fallback->has_position = TRUE; fallback->position = *position; } else { fallback->has_position = FALSE; } return fallback; } static GList * get_files_from_fallbacks (GList *fallbacks) { MoveFileCopyFallback *fallback; GList *res, *l; res = NULL; for (l = fallbacks; l != NULL; l = l->next) { fallback = l->data; res = g_list_prepend (res, fallback->file); } return g_list_reverse (res); } static void move_file_prepare (CopyMoveJob *move_job, GFile *src, GFile *dest_dir, gboolean same_fs, char **dest_fs_type, GHashTable *debuting_files, GdkPoint *position, GList **fallback_files, int files_left) { GFile *dest, *new_dest; GError *error; CommonJob *job; gboolean overwrite; char *primary, *secondary, *details; int response; GFileCopyFlags flags; MoveFileCopyFallback *fallback; gboolean handled_invalid_filename; overwrite = FALSE; handled_invalid_filename = *dest_fs_type != NULL; job = (CommonJob *)move_job; dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); /* Don't allow recursive move/copy into itself. * (We would get a file system error if we proceeded but it is nicer to * detect and report it at this level) */ if (test_dir_is_parent (dest_dir, src)) { if (job->skip_all_error) { goto out; } /* the run_warning() frees all strings passed in automatically */ primary = move_job->is_move ? g_strdup (_("You cannot move a folder into itself.")) : g_strdup (_("You cannot copy a folder into itself.")); secondary = g_strdup (_("The destination folder is inside the source folder.")); response = run_warning (job, primary, secondary, NULL, files_left > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } goto out; } retry: flags = G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_NO_FALLBACK_FOR_MOVE; if (overwrite) { flags |= G_FILE_COPY_OVERWRITE; } error = NULL; if (g_file_move (src, dest, flags, job->cancellable, NULL, NULL, &error)) { if (debuting_files) { g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); } nautilus_file_changes_queue_file_moved (src, dest); if (position) { nautilus_file_changes_queue_schedule_position_set (dest, *position, job->screen_num); } else { nautilus_file_changes_queue_schedule_position_remove (dest); } return; } if (IS_IO_ERROR (error, INVALID_FILENAME) && !handled_invalid_filename) { handled_invalid_filename = TRUE; g_assert (*dest_fs_type == NULL); *dest_fs_type = query_fs_type (dest_dir, job->cancellable); new_dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); if (!g_file_equal (dest, new_dest)) { g_object_unref (dest); dest = new_dest; goto retry; } else { g_object_unref (new_dest); } } /* Conflict */ else if (!overwrite && IS_IO_ERROR (error, EXISTS)) { gboolean is_merge; g_error_free (error); is_merge = FALSE; if (is_dir (dest)) { if (is_dir (src)) { is_merge = TRUE; primary = f (_("A folder named \"%B\" already exists. Do you want to merge the source folder?"), dest); secondary = f (_("The source folder already exists in \"%B\". " "Merging will ask for confirmation before replacing any files in the folder that conflict with the files being moved."), dest_dir); } else { primary = f (_("A folder named \"%B\" already exists. Do you want to replace it?"), dest); secondary = f (_("The folder already exists in \"%F\". " "Replacing it will remove all files in the folder."), dest_dir); } } else { primary = f (_("A file named \"%B\" already exists. Do you want to replace it?"), dest); secondary = f (_("The file already exists in \"%F\". " "Replacing it will overwrite its content."), dest_dir); } if ((is_merge && job->merge_all) || (!is_merge && job->replace_all)) { g_free (primary); g_free (secondary); overwrite = TRUE; goto retry; } if (job->skip_all_conflict) { g_free (primary); g_free (secondary); goto out; } response = run_warning (job, primary, secondary, NULL, files_left > 1, GTK_STOCK_CANCEL, SKIP_ALL, is_merge?MERGE_ALL:REPLACE_ALL, SKIP, is_merge?MERGE:REPLACE, NULL); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1 || response == 3) { /* skip all / skip */ if (response == 1) { job->skip_all_conflict = TRUE; } } else if (response == 2 || response == 4) { /* merge/replace all / merge/replace*/ if (response == 2) { if (is_merge) { job->merge_all = TRUE; } else { job->replace_all = TRUE; } } overwrite = TRUE; goto retry; } else { g_assert_not_reached (); } } else if (IS_IO_ERROR (error, WOULD_RECURSE) || IS_IO_ERROR (error, WOULD_MERGE) || IS_IO_ERROR (error, NOT_SUPPORTED) || (overwrite && IS_IO_ERROR (error, IS_DIRECTORY))) { g_error_free (error); fallback = move_copy_file_callback_new (src, overwrite, position); *fallback_files = g_list_prepend (*fallback_files, fallback); } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } /* Other error */ else { if (job->skip_all_error) { goto out; } primary = f (_("Error while moving \"%B\"."), src); secondary = f (_("There was an error moving the file into %F."), dest_dir); details = error->message; response = run_warning (job, primary, secondary, details, files_left > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (job); } else if (response == 1) { /* skip all */ job->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } } out: g_object_unref (dest); } static void move_files_prepare (CopyMoveJob *job, const char *dest_fs_id, char **dest_fs_type, GList **fallbacks) { CommonJob *common; GList *l; GFile *src; gboolean same_fs; int i; GdkPoint *point; int total, left; common = &job->common; total = left = g_list_length (job->files); report_move_progress (job, total, left); i = 0; for (l = job->files; l != NULL && !job_aborted (common); l = l->next) { src = l->data; if (i < job->n_icon_positions) { point = &job->icon_positions[i]; } else { point = NULL; } same_fs = FALSE; if (dest_fs_id) { same_fs = has_fs_id (src, dest_fs_id); } move_file_prepare (job, src, job->destination, same_fs, dest_fs_type, job->debuting_files, point, fallbacks, left); report_move_progress (job, total, --left); i++; } *fallbacks = g_list_reverse (*fallbacks); } static void move_files (CopyMoveJob *job, GList *fallbacks, const char *dest_fs_id, char **dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info) { CommonJob *common; GList *l; GFile *src; gboolean same_fs; int i; GdkPoint *point; gboolean skipped_file; MoveFileCopyFallback *fallback; common = &job->common; report_copy_progress (job, source_info, transfer_info); i = 0; for (l = fallbacks; l != NULL && !job_aborted (common); l = l->next) { fallback = l->data; src = fallback->file; if (fallback->has_position) { point = &fallback->position; } else { point = NULL; } same_fs = FALSE; if (dest_fs_id) { same_fs = has_fs_id (src, dest_fs_id); } /* Set overwrite to true, as the user has selected overwrite on all toplevel items */ skipped_file = FALSE; copy_move_file (job, src, job->destination, same_fs, FALSE, dest_fs_type, source_info, transfer_info, job->debuting_files, point, fallback->overwrite, &skipped_file, FALSE); i++; } } static gboolean move_job_done (gpointer user_data) { CopyMoveJob *job; job = user_data; if (job->done_callback) { job->done_callback (job->debuting_files, job->done_callback_data); } eel_g_object_list_free (job->files); g_object_unref (job->destination); g_hash_table_unref (job->debuting_files); g_free (job->icon_positions); finalize_common ((CommonJob *)job); nautilus_file_changes_consume_changes (TRUE); return FALSE; } static gboolean move_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { CopyMoveJob *job; CommonJob *common; GList *fallbacks; SourceInfo source_info; TransferInfo transfer_info; char *dest_fs_id; char *dest_fs_type; GList *fallback_files; job = user_data; common = &job->common; common->io_job = io_job; dest_fs_id = NULL; dest_fs_type = NULL; fallbacks = NULL; nautilus_progress_info_start (job->common.progress); verify_destination (&job->common, job->destination, &dest_fs_id, -1); if (job_aborted (common)) { goto aborted; } /* This moves all files that we can do without copy + delete */ move_files_prepare (job, dest_fs_id, &dest_fs_type, &fallbacks); if (job_aborted (common)) { goto aborted; } /* The rest we need to do deep copy + delete behind on, so scan for size */ fallback_files = get_files_from_fallbacks (fallbacks); scan_sources (fallback_files, &source_info, common, OP_KIND_MOVE); g_list_free (fallback_files); if (job_aborted (common)) { goto aborted; } verify_destination (&job->common, job->destination, NULL, source_info.num_bytes); if (job_aborted (common)) { goto aborted; } memset (&transfer_info, 0, sizeof (transfer_info)); move_files (job, fallbacks, dest_fs_id, &dest_fs_type, &source_info, &transfer_info); aborted: eel_g_list_free_deep (fallbacks); g_free (dest_fs_id); g_free (dest_fs_type); g_io_scheduler_job_send_to_mainloop (io_job, move_job_done, job, NULL); return FALSE; } void nautilus_file_operations_move (GList *files, GArray *relative_item_points, GFile *target_dir, GtkWindow *parent_window, NautilusCopyCallback done_callback, gpointer done_callback_data) { CopyMoveJob *job; job = op_job_new (CopyMoveJob, parent_window); job->is_move = TRUE; job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->files = eel_g_object_list_copy (files); job->destination = g_object_ref (target_dir); if (relative_item_points != NULL && relative_item_points->len > 0) { job->icon_positions = g_memdup (relative_item_points->data, sizeof (GdkPoint) * relative_item_points->len); job->n_icon_positions = relative_item_points->len; } job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); inhibit_power_manager ((CommonJob *)job, _("Moving Files")); g_io_scheduler_push_job (move_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } static void report_link_progress (CopyMoveJob *link_job, int total, int left) { CommonJob *job; job = (CommonJob *)link_job; nautilus_progress_info_take_status (job->progress, f (_("Creating links in \"%B\""), link_job->destination)); nautilus_progress_info_take_details (job->progress, f (ngettext ("Making link to %'d file", "Making links to %'d files", left), left)); nautilus_progress_info_set_progress (job->progress, left, total); } static char * get_abs_path_for_symlink (GFile *file) { GFile *root, *parent; char *relative, *abs; if (g_file_is_native (file)) { return g_file_get_path (file); } root = g_object_ref (file); while ((parent = g_file_get_parent (root)) != NULL) { g_object_unref (root); root = parent; } relative = g_file_get_relative_path (root, file); g_object_unref (root); abs = g_strconcat ("/", relative, NULL); g_free (relative); return abs; } static void link_file (CopyMoveJob *job, GFile *src, GFile *dest_dir, char **dest_fs_type, GHashTable *debuting_files, GdkPoint *position, int files_left) { GFile *src_dir, *dest, *new_dest; int count; char *path; gboolean not_local; GError *error; CommonJob *common; char *primary, *secondary, *details; int response; gboolean handled_invalid_filename; common = (CommonJob *)job; count = 0; src_dir = g_file_get_parent (src); if (g_file_equal (src_dir, dest_dir)) { count = 1; } g_object_unref (src_dir); handled_invalid_filename = *dest_fs_type != NULL; dest = get_target_file_for_link (src, dest_dir, *dest_fs_type, count); retry: error = NULL; not_local = FALSE; path = get_abs_path_for_symlink (src); if (path == NULL) { not_local = TRUE; } else if (g_file_make_symbolic_link (dest, path, common->cancellable, &error)) { g_free (path); if (debuting_files) { g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); } nautilus_file_changes_queue_file_added (dest); if (position) { nautilus_file_changes_queue_schedule_position_set (dest, *position, common->screen_num); } else { nautilus_file_changes_queue_schedule_position_remove (dest); } g_object_unref (dest); return; } g_free (path); if (error != NULL && IS_IO_ERROR (error, INVALID_FILENAME) && !handled_invalid_filename) { handled_invalid_filename = TRUE; g_assert (*dest_fs_type == NULL); *dest_fs_type = query_fs_type (dest_dir, common->cancellable); new_dest = get_target_file_for_link (src, dest_dir, *dest_fs_type, count); if (!g_file_equal (dest, new_dest)) { g_object_unref (dest); dest = new_dest; g_error_free (error); goto retry; } else { g_object_unref (new_dest); } } /* Conflict */ if (error != NULL && IS_IO_ERROR (error, EXISTS)) { g_object_unref (dest); dest = get_target_file_for_link (src, dest_dir, *dest_fs_type, count++); g_error_free (error); goto retry; } else if (error != NULL && IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } /* Other error */ else { if (common->skip_all_error) { goto out; } primary = f (_("Error while creating link to %B."), src); if (not_local) { secondary = f (_("Symbolic links only supported for local files")); details = NULL; } else if (IS_IO_ERROR (error, NOT_SUPPORTED)) { secondary = f (_("The target doesn't support symbolic links.")); details = NULL; } else { secondary = f (_("There was an error creating the symlink in %F."), dest_dir); details = error->message; } response = run_warning (common, primary, secondary, details, files_left > 1, GTK_STOCK_CANCEL, SKIP_ALL, SKIP, NULL); if (error) { g_error_free (error); } if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { /* skip all */ common->skip_all_error = TRUE; } else if (response == 2) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } } out: g_object_unref (dest); } static gboolean link_job_done (gpointer user_data) { CopyMoveJob *job; job = user_data; if (job->done_callback) { job->done_callback (job->debuting_files, job->done_callback_data); } eel_g_object_list_free (job->files); g_object_unref (job->destination); g_hash_table_unref (job->debuting_files); g_free (job->icon_positions); finalize_common ((CommonJob *)job); nautilus_file_changes_consume_changes (TRUE); return FALSE; } static gboolean link_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { CopyMoveJob *job; CommonJob *common; GList *copy_files; GArray *copy_positions; GFile *src; GdkPoint *point; char *dest_fs_type; int total, left; int i; GList *l; job = user_data; common = &job->common; common->io_job = io_job; copy_files = NULL; copy_positions = NULL; dest_fs_type = NULL; nautilus_progress_info_start (job->common.progress); verify_destination (&job->common, job->destination, NULL, -1); if (job_aborted (common)) { goto aborted; } total = left = g_list_length (job->files); report_link_progress (job, total, left); i = 0; for (l = job->files; l != NULL && !job_aborted (common); l = l->next) { src = l->data; if (i < job->n_icon_positions) { point = &job->icon_positions[i]; } else { point = NULL; } link_file (job, src, job->destination, &dest_fs_type, job->debuting_files, point, left); report_link_progress (job, total, --left); i++; } aborted: g_free (dest_fs_type); g_io_scheduler_job_send_to_mainloop (io_job, link_job_done, job, NULL); return FALSE; } void nautilus_file_operations_link (GList *files, GArray *relative_item_points, GFile *target_dir, GtkWindow *parent_window, NautilusCopyCallback done_callback, gpointer done_callback_data) { CopyMoveJob *job; job = op_job_new (CopyMoveJob, parent_window); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->files = eel_g_object_list_copy (files); job->destination = g_object_ref (target_dir); if (relative_item_points != NULL && relative_item_points->len > 0) { job->icon_positions = g_memdup (relative_item_points->data, sizeof (GdkPoint) * relative_item_points->len); job->n_icon_positions = relative_item_points->len; } job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); g_io_scheduler_push_job (link_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } void nautilus_file_operations_duplicate (GList *files, GArray *relative_item_points, GtkWindow *parent_window, NautilusCopyCallback done_callback, gpointer done_callback_data) { CopyMoveJob *job; job = op_job_new (CopyMoveJob, parent_window); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->files = eel_g_object_list_copy (files); job->destination = NULL; if (relative_item_points != NULL && relative_item_points->len > 0) { job->icon_positions = g_memdup (relative_item_points->data, sizeof (GdkPoint) * relative_item_points->len); job->n_icon_positions = relative_item_points->len; } job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc)g_file_equal, g_object_unref, NULL); g_io_scheduler_push_job (copy_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } static gboolean set_permissions_job_done (gpointer user_data) { SetPermissionsJob *job; job = user_data; g_object_unref (job->file); if (job->done_callback) { job->done_callback (job->done_callback_data); } finalize_common ((CommonJob *)job); return FALSE; } static void set_permissions_file (SetPermissionsJob *job, GFile *file, GFileInfo *info) { CommonJob *common; GFileInfo *child_info; gboolean free_info; guint32 current; guint32 value; guint32 mask; GFileEnumerator *enumerator; GFile *child; common = (CommonJob *)job; nautilus_progress_info_pulse_progress (common->progress); free_info = FALSE; if (info == NULL) { free_info = TRUE; info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, common->cancellable, NULL); /* Ignore errors */ if (info == NULL) { return; } } if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { value = job->dir_permissions; mask = job->dir_mask; } else { value = job->file_permissions; mask = job->file_mask; } if (!job_aborted (common) && g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_UNIX_MODE)) { current = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE); current = (current & ~mask) | value; g_file_set_attribute_uint32 (file, G_FILE_ATTRIBUTE_UNIX_MODE, current, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, common->cancellable, NULL); } if (!job_aborted (common) && g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) { enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME"," G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, common->cancellable, NULL); if (enumerator) { while (!job_aborted (common) && (child_info = g_file_enumerator_next_file (enumerator, common->cancellable, NULL)) != NULL) { child = g_file_get_child (file, g_file_info_get_name (child_info)); set_permissions_file (job, child, child_info); g_object_unref (child); g_object_unref (child_info); } g_file_enumerator_close (enumerator, common->cancellable, NULL); g_object_unref (enumerator); } } if (free_info) { g_object_unref (info); } } static gboolean set_permissions_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { SetPermissionsJob *job = user_data; CommonJob *common; common = (CommonJob *)job; common->io_job = io_job; nautilus_progress_info_set_status (common->progress, _("Setting permissions")); nautilus_progress_info_start (job->common.progress); set_permissions_file (job, job->file, NULL); g_io_scheduler_job_send_to_mainloop_async (io_job, set_permissions_job_done, job, NULL); return FALSE; } void nautilus_file_set_permissions_recursive (const char *directory, guint32 file_permissions, guint32 file_mask, guint32 dir_permissions, guint32 dir_mask, NautilusOpCallback callback, gpointer callback_data) { SetPermissionsJob *job; job = op_job_new (SetPermissionsJob, NULL); job->file = g_file_new_for_uri (directory); job->file_permissions = file_permissions; job->file_mask = file_mask; job->dir_permissions = dir_permissions; job->dir_mask = dir_mask; job->done_callback = callback; job->done_callback_data = callback_data; g_io_scheduler_push_job (set_permissions_job, job, NULL, 0, NULL); } static GList * location_list_from_uri_list (const GList *uris) { const GList *l; GList *files; GFile *f; files = NULL; for (l = uris; l != NULL; l = l->next) { f = g_file_new_for_uri (l->data); files = g_list_prepend (files, f); } return g_list_reverse (files); } typedef struct { NautilusCopyCallback real_callback; gpointer real_data; } MoveTrashCBData; static void callback_for_move_to_trash (GHashTable *debuting_uris, gboolean user_cancelled, MoveTrashCBData *data) { if (data->real_callback) data->real_callback (debuting_uris, data->real_data); g_slice_free (MoveTrashCBData, data); } void nautilus_file_operations_copy_move (const GList *item_uris, GArray *relative_item_points, const char *target_dir, GdkDragAction copy_action, GtkWidget *parent_view, NautilusCopyCallback done_callback, gpointer done_callback_data) { GList *locations; GList *p; GFile *dest, *src_dir; GtkWindow *parent_window; gboolean target_is_mapping; gboolean have_nonmapping_source; dest = NULL; target_is_mapping = FALSE; have_nonmapping_source = FALSE; if (target_dir) { dest = g_file_new_for_uri (target_dir); if (g_file_has_uri_scheme (dest, "burn")) { target_is_mapping = TRUE; } } locations = location_list_from_uri_list (item_uris); for (p = location_list_from_uri_list (item_uris); p != NULL; p = p->next) { if (!g_file_has_uri_scheme ((GFile* )p->data, "burn")) { have_nonmapping_source = TRUE; } } if (target_is_mapping && have_nonmapping_source && copy_action == GDK_ACTION_MOVE) { /* never move to "burn:///", but fall back to copy. * This is a workaround, because otherwise the source files would be removed. */ copy_action = GDK_ACTION_COPY; } parent_window = NULL; if (parent_view) { parent_window = (GtkWindow *)gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } if (copy_action == GDK_ACTION_COPY) { src_dir = g_file_get_parent (locations->data); if (target_dir == NULL || (src_dir != NULL && g_file_equal (src_dir, dest))) { nautilus_file_operations_duplicate (locations, relative_item_points, parent_window, done_callback, done_callback_data); } else { nautilus_file_operations_copy (locations, relative_item_points, dest, parent_window, done_callback, done_callback_data); } if (src_dir) { g_object_unref (src_dir); } } else if (copy_action == GDK_ACTION_MOVE) { if (g_file_has_uri_scheme (dest, "trash")) { MoveTrashCBData *cb_data; cb_data = g_slice_new0 (MoveTrashCBData); cb_data->real_callback = done_callback; cb_data->real_data = done_callback_data; nautilus_file_operations_trash_or_delete (locations, parent_window, (NautilusDeleteCallback) callback_for_move_to_trash, cb_data); } else { nautilus_file_operations_move (locations, relative_item_points, dest, parent_window, done_callback, done_callback_data); } } else { nautilus_file_operations_link (locations, relative_item_points, dest, parent_window, done_callback, done_callback_data); } eel_g_object_list_free (locations); if (dest) { g_object_unref (dest); } } static gboolean create_job_done (gpointer user_data) { CreateJob *job; job = user_data; if (job->done_callback) { job->done_callback (job->created_file, job->done_callback_data); } g_object_unref (job->dest_dir); if (job->src) { g_object_unref (job->src); } g_free (job->src_data); g_free (job->filename); if (job->created_file) { g_object_unref (job->created_file); } finalize_common ((CommonJob *)job); nautilus_file_changes_consume_changes (TRUE); return FALSE; } static gboolean create_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { CreateJob *job; CommonJob *common; int count; GFile *dest; char *filename, *filename2, *new_filename; char *dest_fs_type; GError *error; gboolean res; gboolean filename_is_utf8; char *primary, *secondary, *details; int response; char *data; int length; GFileOutputStream *out; gboolean handled_invalid_filename; int max_length; job = user_data; common = &job->common; common->io_job = io_job; nautilus_progress_info_start (job->common.progress); handled_invalid_filename = FALSE; dest_fs_type = NULL; filename = NULL; dest = NULL; max_length = get_max_name_length (job->dest_dir); verify_destination (common, job->dest_dir, NULL, -1); if (job_aborted (common)) { goto aborted; } filename = g_strdup (job->filename); filename_is_utf8 = FALSE; if (filename) { filename_is_utf8 = g_utf8_validate (filename, -1, NULL); } if (filename == NULL) { if (job->make_dir) { /* localizers: the initial name of a new folder */ filename = g_strdup (_("untitled folder")); filename_is_utf8 = TRUE; /* Pass in utf8 */ } else { if (job->src != NULL) { filename = g_file_get_basename (job->src); } if (filename == NULL) { /* localizers: the initial name of a new empty file */ filename = g_strdup (_("new file")); filename_is_utf8 = TRUE; /* Pass in utf8 */ } } } make_file_name_valid_for_dest_fs (filename, dest_fs_type); if (filename_is_utf8) { dest = g_file_get_child_for_display_name (job->dest_dir, filename, NULL); } if (dest == NULL) { dest = g_file_get_child (job->dest_dir, filename); } count = 1; retry: error = NULL; if (job->make_dir) { res = g_file_make_directory (dest, common->cancellable, &error); } else { if (job->src) { res = g_file_copy (job->src, dest, G_FILE_COPY_NONE, common->cancellable, NULL, NULL, &error); } else { data = ""; length = 0; if (job->src_data) { data = job->src_data; length = job->length; } out = g_file_create (dest, G_FILE_CREATE_NONE, common->cancellable, &error); if (out) { res = g_output_stream_write_all (G_OUTPUT_STREAM (out), data, length, NULL, common->cancellable, &error); if (res) { res = g_output_stream_close (G_OUTPUT_STREAM (out), common->cancellable, &error); } /* This will close if the write failed and we didn't close */ g_object_unref (out); } else { res = FALSE; } } } if (res) { job->created_file = g_object_ref (dest); nautilus_file_changes_queue_file_added (dest); if (job->has_position) { nautilus_file_changes_queue_schedule_position_set (dest, job->position, common->screen_num); } else { nautilus_file_changes_queue_schedule_position_remove (dest); } } else { g_assert (error != NULL); if (IS_IO_ERROR (error, INVALID_FILENAME) && !handled_invalid_filename) { handled_invalid_filename = TRUE; g_assert (dest_fs_type == NULL); dest_fs_type = query_fs_type (job->dest_dir, common->cancellable); g_object_unref (dest); if (count == 1) { new_filename = g_strdup (filename); } else if (job->make_dir) { filename2 = g_strdup_printf ("%s %d", filename, count); new_filename = NULL; if (max_length > 0 && strlen (filename2) > max_length) { new_filename = shorten_utf8_string (filename2, strlen (filename2) - max_length); } if (new_filename == NULL) { new_filename = g_strdup (filename2); } g_free (filename2); } else { new_filename = get_duplicate_name (filename, count, max_length); } if (make_file_name_valid_for_dest_fs (new_filename, dest_fs_type)) { g_object_unref (dest); if (filename_is_utf8) { dest = g_file_get_child_for_display_name (job->dest_dir, new_filename, NULL); } if (dest == NULL) { dest = g_file_get_child (job->dest_dir, new_filename); } g_free (new_filename); g_error_free (error); goto retry; } g_free (new_filename); } else if (IS_IO_ERROR (error, EXISTS)) { g_object_unref (dest); dest = NULL; if (job->make_dir) { filename2 = g_strdup_printf ("%s %d", filename, ++count); if (max_length > 0 && strlen (filename2) > max_length) { new_filename = shorten_utf8_string (filename2, strlen (filename2) - max_length); if (new_filename != NULL) { g_free (filename2); filename2 = new_filename; } } } else { filename2 = get_duplicate_name (filename, count++, max_length); } make_file_name_valid_for_dest_fs (filename2, dest_fs_type); if (filename_is_utf8) { dest = g_file_get_child_for_display_name (job->dest_dir, filename2, NULL); } if (dest == NULL) { dest = g_file_get_child (job->dest_dir, filename2); } g_free (filename2); g_error_free (error); goto retry; } else if (IS_IO_ERROR (error, CANCELLED)) { g_error_free (error); } /* Other error */ else { if (job->make_dir) { primary = f (_("Error while creating directory %B."), dest); } else { primary = f (_("Error while creating file %B."), dest); } secondary = f (_("There was an error creating the directory in %F."), job->dest_dir); details = error->message; response = run_warning (common, primary, secondary, details, FALSE, GTK_STOCK_CANCEL, SKIP, NULL); g_error_free (error); if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { /* skip */ /* do nothing */ } else { g_assert_not_reached (); } } } aborted: if (dest) { g_object_unref (dest); } g_free (filename); g_free (dest_fs_type); g_io_scheduler_job_send_to_mainloop_async (io_job, create_job_done, job, NULL); return FALSE; } void nautilus_file_operations_new_folder (GtkWidget *parent_view, GdkPoint *target_point, const char *parent_dir, NautilusCreateCallback done_callback, gpointer done_callback_data) { CreateJob *job; GtkWindow *parent_window; parent_window = NULL; if (parent_view) { parent_window = (GtkWindow *)gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } job = op_job_new (CreateJob, parent_window); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->dest_dir = g_file_new_for_uri (parent_dir); job->make_dir = TRUE; if (target_point != NULL) { job->position = *target_point; job->has_position = TRUE; } g_io_scheduler_push_job (create_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } void nautilus_file_operations_new_file_from_template (GtkWidget *parent_view, GdkPoint *target_point, const char *parent_dir, const char *target_filename, const char *template_uri, NautilusCreateCallback done_callback, gpointer done_callback_data) { CreateJob *job; GtkWindow *parent_window; parent_window = NULL; if (parent_view) { parent_window = (GtkWindow *)gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } job = op_job_new (CreateJob, parent_window); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->dest_dir = g_file_new_for_uri (parent_dir); if (target_point != NULL) { job->position = *target_point; job->has_position = TRUE; } job->filename = g_strdup (target_filename); if (template_uri) { job->src = g_file_new_for_uri (template_uri); } g_io_scheduler_push_job (create_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } void nautilus_file_operations_new_file (GtkWidget *parent_view, GdkPoint *target_point, const char *parent_dir, const char *target_filename, const char *initial_contents, int length, NautilusCreateCallback done_callback, gpointer done_callback_data) { CreateJob *job; GtkWindow *parent_window; parent_window = NULL; if (parent_view) { parent_window = (GtkWindow *)gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } job = op_job_new (CreateJob, parent_window); job->done_callback = done_callback; job->done_callback_data = done_callback_data; job->dest_dir = g_file_new_for_uri (parent_dir); if (target_point != NULL) { job->position = *target_point; job->has_position = TRUE; } job->src_data = g_memdup (initial_contents, length); job->length = length; job->filename = g_strdup (target_filename); g_io_scheduler_push_job (create_job, job, NULL, /* destroy notify */ 0, job->common.cancellable); } static void delete_trash_file (CommonJob *job, GFile *file, gboolean del_file, gboolean del_children) { GFileInfo *info; GFile *child; GFileEnumerator *enumerator; if (job_aborted (job)) { return; } if (del_children) { enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, NULL); if (enumerator) { while (!job_aborted (job) && (info = g_file_enumerator_next_file (enumerator, job->cancellable, NULL)) != NULL) { child = g_file_get_child (file, g_file_info_get_name (info)); delete_trash_file (job, child, TRUE, g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY); g_object_unref (child); g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); } } if (!job_aborted (job) && del_file) { g_file_delete (file, job->cancellable, NULL); } } static gboolean empty_trash_job_done (gpointer user_data) { EmptyTrashJob *job; job = user_data; eel_g_object_list_free (job->trash_dirs); if (job->done_callback) { job->done_callback (job->done_callback_data); } finalize_common ((CommonJob *)job); return FALSE; } static gboolean empty_trash_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { EmptyTrashJob *job = user_data; CommonJob *common; GList *l; gboolean confirmed; common = (CommonJob *)job; common->io_job = io_job; nautilus_progress_info_start (job->common.progress); if (job->should_confirm) { confirmed = confirm_empty_trash (common); } else { confirmed = TRUE; } if (confirmed) { for (l = job->trash_dirs; l != NULL && !job_aborted (common); l = l->next) { delete_trash_file (common, l->data, FALSE, TRUE); } } g_io_scheduler_job_send_to_mainloop_async (io_job, empty_trash_job_done, job, NULL); return FALSE; } void nautilus_file_operations_empty_trash (GtkWidget *parent_view) { EmptyTrashJob *job; GtkWindow *parent_window; parent_window = NULL; if (parent_view) { parent_window = (GtkWindow *)gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } setup_autos (); job = op_job_new (EmptyTrashJob, parent_window); job->trash_dirs = g_list_prepend (job->trash_dirs, g_file_new_for_uri ("trash:")); job->should_confirm = TRUE; inhibit_power_manager ((CommonJob *)job, _("Emptying Trash")); g_io_scheduler_push_job (empty_trash_job, job, NULL, 0, NULL); } static gboolean mark_trusted_job_done (gpointer user_data) { MarkTrustedJob *job = user_data; g_object_unref (job->file); if (job->done_callback) { job->done_callback (job->done_callback_data); } finalize_common ((CommonJob *)job); return FALSE; } #define TRUSTED_SHEBANG "#!/usr/bin/env xdg-open\n" static void mark_desktop_file_trusted (CommonJob *common, GCancellable *cancellable, GFile *file, gboolean interactive) { char *contents, *new_contents; gsize length, new_length; GError *error; guint32 current_perms, new_perms; int response; GFileInfo *info; retry: error = NULL; if (!g_file_load_contents (file, cancellable, &contents, &length, NULL, &error)) { if (interactive) { response = run_error (common, g_strdup (_("Unable to mark launcher trusted (executable)")), error->message, NULL, FALSE, GTK_STOCK_CANCEL, RETRY, NULL); } else { response = 0; } if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { goto retry; } else { g_assert_not_reached (); } goto out; } if (!g_str_has_prefix (contents, "#!")) { new_length = length + strlen (TRUSTED_SHEBANG); new_contents = g_malloc (new_length); strcpy (new_contents, TRUSTED_SHEBANG); memcpy (new_contents + strlen (TRUSTED_SHEBANG), contents, length); if (!g_file_replace_contents (file, new_contents, new_length, NULL, FALSE, 0, NULL, cancellable, &error)) { g_free (contents); g_free (new_contents); if (interactive) { response = run_error (common, g_strdup (_("Unable to mark launcher trusted (executable)")), error->message, NULL, FALSE, GTK_STOCK_CANCEL, RETRY, NULL); } else { response = 0; } if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { goto retry; } else { g_assert_not_reached (); } goto out; } g_free (new_contents); } g_free (contents); info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE"," G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, common->cancellable, &error); if (info == NULL) { if (interactive) { response = run_error (common, g_strdup (_("Unable to mark launcher trusted (executable)")), error->message, NULL, FALSE, GTK_STOCK_CANCEL, RETRY, NULL); } else { response = 0; } if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { goto retry; } else { g_assert_not_reached (); } goto out; } if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_UNIX_MODE)) { current_perms = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE); new_perms = current_perms | S_IXGRP | S_IXUSR | S_IXOTH; if ((current_perms != new_perms) && !g_file_set_attribute_uint32 (file, G_FILE_ATTRIBUTE_UNIX_MODE, new_perms, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, common->cancellable, &error)) { g_object_unref (info); if (interactive) { response = run_error (common, g_strdup (_("Unable to mark launcher trusted (executable)")), error->message, NULL, FALSE, GTK_STOCK_CANCEL, RETRY, NULL); } else { response = 0; } if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) { abort_job (common); } else if (response == 1) { goto retry; } else { g_assert_not_reached (); } goto out; } } g_object_unref (info); out: ; } static gboolean mark_trusted_job (GIOSchedulerJob *io_job, GCancellable *cancellable, gpointer user_data) { MarkTrustedJob *job = user_data; CommonJob *common; common = (CommonJob *)job; common->io_job = io_job; nautilus_progress_info_start (job->common.progress); mark_desktop_file_trusted (common, cancellable, job->file, job->interactive); g_io_scheduler_job_send_to_mainloop_async (io_job, mark_trusted_job_done, job, NULL); return FALSE; } void nautilus_file_mark_desktop_file_trusted (GFile *file, GtkWindow *parent_window, gboolean interactive, NautilusOpCallback done_callback, gpointer done_callback_data) { MarkTrustedJob *job; job = op_job_new (MarkTrustedJob, parent_window); job->file = g_object_ref (file); job->interactive = interactive; job->done_callback = done_callback; job->done_callback_data = done_callback_data; g_io_scheduler_push_job (mark_trusted_job, job, NULL, 0, NULL); } #if !defined (NAUTILUS_OMIT_SELF_CHECK) void nautilus_self_check_file_operations (void) { setlocale (LC_MESSAGES, "C"); /* test the next duplicate name generator */ EEL_CHECK_STRING_RESULT (get_duplicate_name (" (copy)", 1, -1), " (another copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo", 1, -1), "foo (copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name (".bashrc", 1, -1), ".bashrc (copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name (".foo.txt", 1, -1), ".foo (copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo", 1, -1), "foo foo (copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo.txt", 1, -1), "foo (copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo.txt", 1, -1), "foo foo (copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo.txt txt", 1, -1), "foo foo (copy).txt txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo...txt", 1, -1), "foo (copy)...txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo...", 1, -1), "foo (copy)..."); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo. (copy)", 1, -1), "foo. (another copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (copy)", 1, -1), "foo (another copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (copy).txt", 1, -1), "foo (another copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (another copy)", 1, -1), "foo (3rd copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (another copy).txt", 1, -1), "foo (3rd copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (another copy).txt", 1, -1), "foo foo (3rd copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (13th copy)", 1, -1), "foo (14th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (13th copy).txt", 1, -1), "foo (14th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (21st copy)", 1, -1), "foo (22nd copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (21st copy).txt", 1, -1), "foo (22nd copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (22nd copy)", 1, -1), "foo (23rd copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (22nd copy).txt", 1, -1), "foo (23rd copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (23rd copy)", 1, -1), "foo (24th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (23rd copy).txt", 1, -1), "foo (24th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (24th copy)", 1, -1), "foo (25th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (24th copy).txt", 1, -1), "foo (25th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (24th copy)", 1, -1), "foo foo (25th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (24th copy).txt", 1, -1), "foo foo (25th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (100000000000000th copy).txt", 1, -1), "foo foo (copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (10th copy)", 1, -1), "foo (11th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (10th copy).txt", 1, -1), "foo (11th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (11th copy)", 1, -1), "foo (12th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (11th copy).txt", 1, -1), "foo (12th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (12th copy)", 1, -1), "foo (13th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (12th copy).txt", 1, -1), "foo (13th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (110th copy)", 1, -1), "foo (111th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (110th copy).txt", 1, -1), "foo (111th copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (122nd copy)", 1, -1), "foo (123rd copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (122nd copy).txt", 1, -1), "foo (123rd copy).txt"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (123rd copy)", 1, -1), "foo (124th copy)"); EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (123rd copy).txt", 1, -1), "foo (124th copy).txt"); setlocale (LC_MESSAGES, ""); } #endif