diff options
Diffstat (limited to 'libcaja-private/caja-file-operations.c')
-rw-r--r-- | libcaja-private/caja-file-operations.c | 6469 |
1 files changed, 6469 insertions, 0 deletions
diff --git a/libcaja-private/caja-file-operations.c b/libcaja-private/caja-file-operations.c new file mode 100644 index 00000000..90205b83 --- /dev/null +++ b/libcaja-private/caja-file-operations.c @@ -0,0 +1,6469 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ + +/* caja-file-operations.c - Caja 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 <[email protected]> + Ettore Perazzoli <[email protected]> + Pavel Cisler <[email protected]> + */ + +#include <config.h> +#include <string.h> +#include <stdio.h> +#include <stdarg.h> +#include <locale.h> +#include <math.h> +#include <unistd.h> +#include <sys/types.h> +#include <stdlib.h> + +#include "caja-file-operations.h" + +#include "caja-debug-log.h" +#include "caja-file-changes-queue.h" +#include "caja-lib-self-check-functions.h" + +#include "caja-progress-info.h" + +#include <eel/eel-alert-dialog.h> +#include <eel/eel-glib-extensions.h> +#include <eel/eel-pango-extensions.h> +#include <eel/eel-gtk-extensions.h> +#include <eel/eel-stock-dialogs.h> +#include <eel/eel-vfs-extensions.h> + +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gdk/gdk.h> +#include <gtk/gtk.h> +#include <gio/gio.h> +#include <glib.h> + +#include "caja-file-changes-queue.h" +#include "caja-file-private.h" +#include "caja-desktop-icon-file.h" +#include "caja-desktop-link-monitor.h" +#include "caja-global-preferences.h" +#include "caja-link.h" +#include "caja-autorun.h" +#include "caja-trash-monitor.h" +#include "caja-file-utilities.h" +#include "caja-file-conflict-dialog.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; + CajaProgressInfo *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; + CajaCopyCallback done_callback; + gpointer done_callback_data; +} CopyMoveJob; + +typedef struct { + CommonJob common; + GList *files; + gboolean try_trash; + gboolean user_cancel; + CajaDeleteCallback 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; + CajaCreateCallback done_callback; + gpointer done_callback_data; +} CreateJob; + + +typedef struct { + CommonJob common; + GList *trash_dirs; + gboolean should_confirm; + CajaOpCallback done_callback; + gpointer done_callback_data; +} EmptyTrashJob; + +typedef struct { + CommonJob common; + GFile *file; + gboolean interactive; + CajaOpCallback done_callback; + gpointer done_callback_data; +} MarkTrustedJob; + +typedef struct { + CommonJob common; + GFile *file; + CajaOpCallback 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); + + #if GLIB_CHECK_VERSION(2, 30, 0) + return g_format_size(size); + #else + return g_format_size_for_display(size); + #endif +} + +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 = caja_progress_info_new (); + common->cancellable = caja_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) +{ + caja_progress_info_finish (common->progress); + + if (common->inhibit_cookie != -1) { + caja_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 (CAJA_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-caja-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); + + caja_progress_info_pause (job->progress); + g_io_scheduler_job_send_to_mainloop (job->io_job, + do_run_simple_dialog, + data, + NULL); + caja_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 = caja_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 items from Trash?")); + + response = run_warning (job, + prompt, + f(_("All items in the Trash will be permanently deleted.")), + 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); + + caja_progress_info_take_status (job->progress, + f (_("Deleting files"))); + + elapsed = g_timer_elapsed (job->time, NULL); + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE) { + + caja_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); + caja_progress_info_take_details (job->progress, details); + + g_free (time_left_s); + } + + g_free (files_left_s); + + if (source_info->num_files != 0) { + caja_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 { + caja_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)) { + caja_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; + + caja_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); + caja_progress_info_take_details (job->progress, s); + + if (total_files != 0) { + caja_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 { + caja_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); + + caja_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; + + caja_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, + CajaDeleteCallback 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 +caja_file_operations_trash_or_delete (GList *files, + GtkWindow *parent_window, + CajaDeleteCallback done_callback, + gpointer done_callback_data) +{ + trash_or_delete_internal (files, parent_window, + TRUE, + done_callback, done_callback_data); +} + +void +caja_file_operations_delete (GList *files, + GtkWindow *parent_window, + CajaDeleteCallback 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; + CajaUnmountCallback 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", + "Caja"); + + /* Make transient for the window group */ + gtk_widget_realize (dialog); + if (screen != NULL) { + gdk_window_set_transient_for (gtk_widget_get_window (GTK_WIDGET (dialog)), + gdk_screen_get_root_window (screen)); + } + + result = gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + return result; +} + +void +caja_file_operations_unmount_mount_full (GtkWindow *parent_window, + GMount *mount, + gboolean eject, + gboolean check_trash, + CajaUnmountCallback 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 = (CajaOpCallback)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 +caja_file_operations_unmount_mount (GtkWindow *parent_window, + GMount *mount, + gboolean eject, + gboolean check_trash) +{ + caja_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) +{ + CajaMountCallback mount_callback; + GObject *mount_callback_data_object; + GMountOperation *mount_op = user_data; + GError *error; + char *primary; + char *name; + + error = NULL; + caja_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 = (CajaMountCallback) + 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 +caja_file_operations_mount_volume (GtkWindow *parent_window, + GVolume *volume, + gboolean allow_autorun) +{ + caja_file_operations_mount_volume_full (parent_window, volume, + allow_autorun, NULL, NULL); +} + +void +caja_file_operations_mount_volume_full (GtkWindow *parent_window, + GVolume *volume, + gboolean allow_autorun, + CajaMountCallback mount_callback, + GObject *mount_callback_data_object) +{ + GMountOperation *mount_op; + + mount_op = gtk_mount_operation_new (parent_window); + g_mount_operation_set_password_save (mount_op, G_PASSWORD_SAVE_FOR_SESSION); + 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) + caja_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; + } + + caja_progress_info_take_details (job->progress, s); + caja_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) { + caja_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 { + caja_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) { + caja_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 { + caja_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) { + caja_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 { + caja_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); + caja_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); + caja_progress_info_take_details (job->progress, s); + } + + caja_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; + } + caja_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; + } + caja_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) && + caja_is_in_system_dir (file)) { + res = TRUE; + } + g_object_unref (info); + + return res; +} + +typedef struct { + int id; + char *new_name; + gboolean apply_to_all; +} ConflictResponseData; + +typedef struct { + GFile *src; + GFile *dest; + GFile *dest_dir; + GtkWindow *parent; + ConflictResponseData *resp_data; +} ConflictDialogData; + +static gboolean +do_run_conflict_dialog (gpointer _data) +{ + ConflictDialogData *data = _data; + GtkWidget *dialog; + int response; + + dialog = caja_file_conflict_dialog_new (data->parent, + data->src, + data->dest, + data->dest_dir); + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == CONFLICT_RESPONSE_RENAME) { + data->resp_data->new_name = + caja_file_conflict_dialog_get_new_name (CAJA_FILE_CONFLICT_DIALOG (dialog)); + } else if (response != GTK_RESPONSE_CANCEL || + response != GTK_RESPONSE_NONE) { + data->resp_data->apply_to_all = + caja_file_conflict_dialog_get_apply_to_all + (CAJA_FILE_CONFLICT_DIALOG (dialog)); + } + + data->resp_data->id = response; + + gtk_widget_destroy (dialog); + + return FALSE; +} + +static ConflictResponseData * +run_conflict_dialog (CommonJob *job, + GFile *src, + GFile *dest, + GFile *dest_dir) +{ + ConflictDialogData *data; + ConflictResponseData *resp_data; + + g_timer_stop (job->time); + + data = g_slice_new0 (ConflictDialogData); + data->parent = job->parent_window; + data->src = src; + data->dest = dest; + data->dest_dir = dest_dir; + + resp_data = g_slice_new0 (ConflictResponseData); + resp_data->new_name = NULL; + data->resp_data = resp_data; + + caja_progress_info_pause (job->progress); + g_io_scheduler_job_send_to_mainloop (job->io_job, + do_run_conflict_dialog, + data, + NULL); + caja_progress_info_resume (job->progress); + + g_slice_free (ConflictDialogData, data); + + g_timer_continue (job->time); + + return resp_data; +} + +static void +conflict_response_data_free (ConflictResponseData *data) +{ + g_free (data->new_name); + g_slice_free (ConflictResponseData, data); +} + +static GFile * +get_target_file_for_display_name (GFile *dir, + char *name) +{ + GFile *dest; + + dest = NULL; + dest = g_file_get_child_for_display_name (dir, name, NULL); + + if (dest == NULL) { + dest = g_file_get_child (dir, name); + } + + return dest; +} + +/* 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) { + caja_file_changes_queue_schedule_position_set (dest, *position, job->screen_num); + } else { + caja_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) { + caja_file_changes_queue_file_moved (src, dest); + } else { + caja_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; + ConflictResponseData *response; + + g_error_free (error); + + if (unique_names) { + g_object_unref (dest); + dest = get_unique_target_file (src, dest_dir, same_fs, *dest_fs_type, unique_name_nr++); + goto retry; + } + + is_merge = FALSE; + + if (is_dir (dest) && is_dir (src)) { + is_merge = TRUE; + } + + if ((is_merge && job->merge_all) || + (!is_merge && job->replace_all)) { + overwrite = TRUE; + goto retry; + } + + if (job->skip_all_conflict) { + goto out; + } + + response = run_conflict_dialog (job, src, dest, dest_dir); + + if (response->id == GTK_RESPONSE_CANCEL || + response->id == GTK_RESPONSE_DELETE_EVENT) { + conflict_response_data_free (response); + abort_job (job); + } else if (response->id == CONFLICT_RESPONSE_SKIP) { + if (response->apply_to_all) { + job->skip_all_conflict = TRUE; + } + conflict_response_data_free (response); + } else if (response->id == CONFLICT_RESPONSE_REPLACE) { /* merge/replace */ + if (response->apply_to_all) { + if (is_merge) { + job->merge_all = TRUE; + } else { + job->replace_all = TRUE; + } + } + overwrite = TRUE; + conflict_response_data_free (response); + goto retry; + } else if (response->id == CONFLICT_RESPONSE_RENAME) { + g_object_unref (dest); + dest = get_target_file_for_display_name (dest_dir, + response->new_name); + conflict_response_data_free (response); + 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; + } + caja_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); + + caja_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; + + caja_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 +caja_file_operations_copy (GList *files, + GArray *relative_item_points, + GFile *target_dir, + GtkWindow *parent_window, + CajaCopyCallback done_callback, + gpointer done_callback_data) +{ + CopyMoveJob *job; + + job = op_job_new (CopyMoveJob, parent_window); + job->desktop_location = caja_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; + + caja_progress_info_take_status (job->progress, + f (_("Preparing to Move to \"%B\""), + move_job->destination)); + + caja_progress_info_take_details (job->progress, + f (ngettext ("Preparing to move %'d file", + "Preparing to move %'d files", + left), left)); + + caja_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, renamed; + char *primary, *secondary, *details; + int response; + GFileCopyFlags flags; + MoveFileCopyFallback *fallback; + gboolean handled_invalid_filename; + + overwrite = FALSE; + renamed = 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)); + } + + caja_file_changes_queue_file_moved (src, dest); + + if (position) { + caja_file_changes_queue_schedule_position_set (dest, *position, job->screen_num); + } else { + caja_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; + ConflictResponseData *response; + + g_error_free (error); + + is_merge = FALSE; + if (is_dir (dest) && is_dir (src)) { + is_merge = TRUE; + } + + if ((is_merge && job->merge_all) || + (!is_merge && job->replace_all)) { + overwrite = TRUE; + goto retry; + } + + if (job->skip_all_conflict) { + goto out; + } + + response = run_conflict_dialog (job, src, dest, dest_dir); + + if (response->id == GTK_RESPONSE_CANCEL || + response->id == GTK_RESPONSE_DELETE_EVENT) { + conflict_response_data_free (response); + abort_job (job); + } else if (response->id == CONFLICT_RESPONSE_SKIP) { + if (response->apply_to_all) { + job->skip_all_conflict = TRUE; + } + conflict_response_data_free (response); + } else if (response->id == CONFLICT_RESPONSE_REPLACE) { /* merge/replace */ + if (response->apply_to_all) { + if (is_merge) { + job->merge_all = TRUE; + } else { + job->replace_all = TRUE; + } + } + overwrite = TRUE; + conflict_response_data_free (response); + goto retry; + } else if (response->id == CONFLICT_RESPONSE_RENAME) { + g_object_unref (dest); + dest = get_target_file_for_display_name (dest_dir, + response->new_name); + conflict_response_data_free (response); + goto retry; + } else if (response->id == CONFLICT_RESPONSE_RENAME) { + g_object_unref (dest); + dest = get_target_file_for_display_name (dest_dir, + response->new_name); + renamed = TRUE; + conflict_response_data_free (response); + 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); + + caja_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; + + caja_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 +caja_file_operations_move (GList *files, + GArray *relative_item_points, + GFile *target_dir, + GtkWindow *parent_window, + CajaCopyCallback 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; + + caja_progress_info_take_status (job->progress, + f (_("Creating links in \"%B\""), + link_job->destination)); + + caja_progress_info_take_details (job->progress, + f (ngettext ("Making link to %'d file", + "Making links to %'d files", + left), left)); + + caja_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)); + } + + caja_file_changes_queue_file_added (dest); + if (position) { + caja_file_changes_queue_schedule_position_set (dest, *position, common->screen_num); + } else { + caja_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); + + caja_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; + + caja_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 +caja_file_operations_link (GList *files, + GArray *relative_item_points, + GFile *target_dir, + GtkWindow *parent_window, + CajaCopyCallback 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 +caja_file_operations_duplicate (GList *files, + GArray *relative_item_points, + GtkWindow *parent_window, + CajaCopyCallback 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; + + caja_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; + + caja_progress_info_set_status (common->progress, + _("Setting permissions")); + + caja_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 +caja_file_set_permissions_recursive (const char *directory, + guint32 file_permissions, + guint32 file_mask, + guint32 dir_permissions, + guint32 dir_mask, + CajaOpCallback 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 { + CajaCopyCallback 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 +caja_file_operations_copy_move (const GList *item_uris, + GArray *relative_item_points, + const char *target_dir, + GdkDragAction copy_action, + GtkWidget *parent_view, + CajaCopyCallback 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))) { + caja_file_operations_duplicate (locations, + relative_item_points, + parent_window, + done_callback, done_callback_data); + } else { + caja_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; + caja_file_operations_trash_or_delete (locations, + parent_window, + (CajaDeleteCallback) callback_for_move_to_trash, + cb_data); + } else { + caja_file_operations_move (locations, + relative_item_points, + dest, + parent_window, + done_callback, done_callback_data); + } + } else { + caja_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); + + caja_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; + + caja_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); + caja_file_changes_queue_file_added (dest); + if (job->has_position) { + caja_file_changes_queue_schedule_position_set (dest, job->position, common->screen_num); + } else { + caja_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 +caja_file_operations_new_folder (GtkWidget *parent_view, + GdkPoint *target_point, + const char *parent_dir, + CajaCreateCallback 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 +caja_file_operations_new_file_from_template (GtkWidget *parent_view, + GdkPoint *target_point, + const char *parent_dir, + const char *target_filename, + const char *template_uri, + CajaCreateCallback 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 +caja_file_operations_new_file (GtkWidget *parent_view, + GdkPoint *target_point, + const char *parent_dir, + const char *target_filename, + const char *initial_contents, + int length, + CajaCreateCallback 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; + + caja_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 +caja_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; + + caja_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 +caja_file_mark_desktop_file_trusted (GFile *file, + GtkWindow *parent_window, + gboolean interactive, + CajaOpCallback 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 (CAJA_OMIT_SELF_CHECK) + +void +caja_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 |