diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 15:59:36 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 15:59:36 +0000 |
commit | ec52555862913a23417735f9f7f5402f5230da13 (patch) | |
tree | 5e43a30d289a3daa69dddfbb060216ff6332f197 /src/nautilus-file-operations.c | |
parent | Initial commit. (diff) | |
download | nautilus-upstream/3.38.2.tar.xz nautilus-upstream/3.38.2.zip |
Adding upstream version 3.38.2.upstream/3.38.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/nautilus-file-operations.c')
-rw-r--r-- | src/nautilus-file-operations.c | 8894 |
1 files changed, 8894 insertions, 0 deletions
diff --git a/src/nautilus-file-operations.c b/src/nautilus-file-operations.c new file mode 100644 index 0000000..3adf3b5 --- /dev/null +++ b/src/nautilus-file-operations.c @@ -0,0 +1,8894 @@ +/* nautilus-file-operations.c - Nautilus file operations. + * + * Copyright (C) 1999, 2000 Free Software Foundation + * Copyright (C) 2000, 2001 Eazel, Inc. + * Copyright (C) 2007 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Alexander Larsson <alexl@redhat.com> + * Ettore Perazzoli <ettore@gnu.org> + * Pavel Cisler <pavel@eazel.com> + */ + +#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 "nautilus-file-operations.h" + +#include "nautilus-file-changes-queue.h" +#include "nautilus-lib-self-check-functions.h" + +#include "nautilus-progress-info.h" + +#include <eel/eel-glib-extensions.h> +#include <eel/eel-vfs-extensions.h> +#include <eel/eel-string.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 "nautilus-error-reporting.h" +#include "nautilus-operations-ui-manager.h" +#include "nautilus-file-changes-queue.h" +#include "nautilus-file-private.h" +#include "nautilus-global-preferences.h" +#include "nautilus-trash-monitor.h" +#include "nautilus-file-utilities.h" +#include "nautilus-file-undo-operations.h" +#include "nautilus-file-undo-manager.h" +#include "nautilus-ui-utilities.h" + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif + +/* TODO: TESTING!!! */ + +typedef struct +{ + GTimer *time; + GtkWindow *parent_window; + NautilusFileOperationsDBusData *dbus_data; + guint inhibit_cookie; + NautilusProgressInfo *progress; + GCancellable *cancellable; + GHashTable *skip_files; + GHashTable *skip_readdir_error; + NautilusFileUndoInfo *undo_info; + 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 *fake_display_source; + GHashTable *debuting_files; + gchar *target_name; + NautilusCopyCallback done_callback; + gpointer done_callback_data; +} CopyMoveJob; + +typedef struct +{ + CommonJob common; + GList *files; + gboolean try_trash; + gboolean user_cancel; + NautilusDeleteCallback done_callback; + gpointer done_callback_data; +} DeleteJob; + +typedef struct +{ + CommonJob common; + GFile *dest_dir; + char *filename; + gboolean make_dir; + GFile *src; + char *src_data; + int length; + GFile *created_file; + NautilusCreateCallback done_callback; + gpointer done_callback_data; +} CreateJob; + + +typedef struct +{ + CommonJob common; + GList *trash_dirs; + gboolean should_confirm; + NautilusOpCallback done_callback; + gpointer done_callback_data; +} EmptyTrashJob; + +typedef struct +{ + CommonJob common; + GFile *file; + gboolean interactive; + NautilusOpCallback done_callback; + gpointer done_callback_data; +} MarkTrustedJob; + +typedef struct +{ + CommonJob common; + GFile *file; + NautilusOpCallback done_callback; + gpointer done_callback_data; + guint32 file_permissions; + guint32 file_mask; + guint32 dir_permissions; + guint32 dir_mask; +} SetPermissionsJob; + +typedef enum +{ + OP_KIND_COPY, + OP_KIND_MOVE, + OP_KIND_DELETE, + OP_KIND_TRASH, + OP_KIND_COMPRESS +} 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; + +typedef struct +{ + CommonJob common; + GList *source_files; + GFile *destination_directory; + GList *output_files; + + gdouble base_progress; + + guint64 archive_compressed_size; + guint64 total_compressed_size; + + NautilusExtractCallback done_callback; + gpointer done_callback_data; +} ExtractJob; + +typedef struct +{ + CommonJob common; + GList *source_files; + GFile *output_file; + + AutoarFormat format; + AutoarFilter filter; + + guint64 total_size; + guint total_files; + + gboolean success; + + NautilusCreateCallback done_callback; + gpointer done_callback_data; +} CompressJob; + +#define SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE 8 +#define NSEC_PER_MICROSEC 1000 +#define PROGRESS_NOTIFY_INTERVAL 100 * NSEC_PER_MICROSEC + +#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 CANCEL _("_Cancel") +#define SKIP _("_Skip") +#define SKIP_ALL _("S_kip All") +#define RETRY _("_Retry") +#define DELETE _("_Delete") +#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") +#define EMPTY_TRASH _("Empty _Trash") + +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 void empty_trash_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void empty_trash_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data); + +static char *query_fs_type (GFile *file, + GCancellable *cancellable); + +static void nautilus_file_operations_copy (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +static void nautilus_file_operations_move (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable); + +/* keep in time with get_formatted_time () + * + * This counts and outputs the number of “time units” + * formatted and displayed by get_formatted_time (). + * For instance, if get_formatted_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 gchar * +get_formatted_time (int seconds) +{ + int minutes; + int hours; + gchar *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) + { + gchar *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 ("%'d hour", + "%'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; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + 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); + } + } +#pragma GCC diagnostic pop + 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, + gboolean ignore_extension) +{ + const char *tag; + + g_assert (name[0] != '\0'); + + *suffix = eel_filename_get_extension_offset (name); + + 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; + /* ignore_extension was not used before to let above code handle case "dir (copy).dir" for directories */ + if (**suffix != '\0' && !ignore_extension) + { + *name_base = extract_string_until (name, *suffix); + } + else + { + /* making sure extension is ignored in directories */ + *suffix = ""; + *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; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + 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); + } + } +#pragma GCC diagnostic pop + + return result; +} + +static char * +get_duplicate_name (const char *name, + int count_increment, + int max_length, + gboolean ignore_extension) +{ + char *result; + char *name_base; + const char *suffix; + int count; + + parse_previous_duplicate_name (name, &name_base, &suffix, &count, ignore_extension); + 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 gchar * +get_basename (GFile *file) +{ + GFileInfo *info; + gchar *name, *basename, *tmp; + GMount *mount; + + if ((mount = nautilus_get_mounted_mount_for_root (file)) != NULL) + { + name = g_mount_get_name (mount); + g_object_unref (mount); + } + else + { + 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 gchar * +get_truncated_parse_name (GFile *file) +{ + g_autofree gchar *parse_name = NULL; + + g_assert (G_IS_FILE (file)); + + parse_name = g_file_get_parse_name (file); + + return eel_str_middle_truncate (parse_name, MAXIMUM_DISPLAYED_FILE_NAME_LENGTH); +} + +#define op_job_new(__type, parent_window, dbus_data) ((__type *) (init_common (sizeof (__type), parent_window, dbus_data))) + +static gpointer +init_common (gsize job_size, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data) +{ + CommonJob *common; + + common = g_malloc0 (job_size); + + if (parent_window) + { + common->parent_window = parent_window; + g_object_add_weak_pointer (G_OBJECT (common->parent_window), + (gpointer *) &common->parent_window); + } + + if (dbus_data) + { + common->dbus_data = nautilus_file_operations_dbus_data_ref (dbus_data); + } + + common->progress = nautilus_progress_info_new (); + common->cancellable = nautilus_progress_info_get_cancellable (common->progress); + common->time = g_timer_new (); + common->inhibit_cookie = 0; + + return common; +} + +static void +finalize_common (CommonJob *common) +{ + nautilus_progress_info_finish (common->progress); + + if (common->inhibit_cookie != 0) + { + gtk_application_uninhibit (GTK_APPLICATION (g_application_get_default ()), + common->inhibit_cookie); + } + + common->inhibit_cookie = 0; + g_timer_destroy (common->time); + + if (common->parent_window) + { + g_object_remove_weak_pointer (G_OBJECT (common->parent_window), + (gpointer *) &common->parent_window); + } + + if (common->dbus_data) + { + nautilus_file_operations_dbus_data_unref (common->dbus_data); + } + + if (common->skip_files) + { + g_hash_table_destroy (common->skip_files); + } + if (common->skip_readdir_error) + { + g_hash_table_destroy (common->skip_readdir_error); + } + + if (common->undo_info != NULL) + { + nautilus_file_undo_manager_set_action (common->undo_info); + g_object_unref (common->undo_info); + } + + 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 gboolean +can_delete_without_confirm (GFile *file) +{ + /* In the case of testing, we want to be able to delete + * without asking for confirmation from the user. + */ + if (g_file_has_uri_scheme (file, "burn") || + g_file_has_uri_scheme (file, "recent") || + !g_strcmp0 (g_getenv ("RUNNING_TESTS"), "TRUE")) + { + 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; + NautilusFileOperationsDBusData *dbus_data; + 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; + /* Dialogs are ran from operation threads, which need to be blocked until + * the user gives a valid response + */ + gboolean completed; + GMutex mutex; + GCond cond; +} RunSimpleDialogData; + +static void +set_transient_for (GdkWindow *child_window, + const char *parent_handle) +{ + GdkDisplay *display; + const char *prefix; + + display = gdk_window_get_display (child_window); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (display)) + { + prefix = "x11:"; + + if (g_str_has_prefix (parent_handle, prefix)) + { + const char *handle; + GdkWindow *window; + + handle = parent_handle + strlen (prefix); + window = gdk_x11_window_foreign_new_for_display (display, strtol (handle, NULL, 16)); + + if (window != NULL) + { + gdk_window_set_transient_for (child_window, window); + g_object_unref (window); + } + } + } +#endif + +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (display)) + { + prefix = "wayland:"; + + if (g_str_has_prefix (parent_handle, prefix)) + { + const char *handle; + + handle = parent_handle + strlen (prefix); + + gdk_wayland_window_set_transient_for_exported (child_window, (char *) handle); + } + } +#endif +} + +static void +dialog_realize_cb (GtkWidget *widget, + gpointer user_data) +{ + NautilusFileOperationsDBusData *dbus_data = user_data; + const char *parent_handle; + + parent_handle = nautilus_file_operations_dbus_data_get_parent_handle (dbus_data); + set_transient_for (gtk_widget_get_window (widget), parent_handle); +} + +static gboolean +do_run_simple_dialog (gpointer _data) +{ + RunSimpleDialogData *data = _data; + const char *button_title; + GtkWidget *dialog; + GtkWidget *button; + int result; + int response_id; + + g_mutex_lock (&data->mutex); + + /* Create the dialog. */ + dialog = gtk_message_dialog_new (*data->parent_window, + 0, + data->message_type, + GTK_BUTTONS_NONE, + NULL); + + g_object_set (dialog, + "text", data->primary_text, + "secondary-text", data->secondary_text, + NULL); + + 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; + } + + button = gtk_dialog_add_button (GTK_DIALOG (dialog), button_title, response_id); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id); + + if (g_strcmp0 (button_title, DELETE) == 0 || + g_strcmp0 (button_title, EMPTY_TRASH) == 0 || + g_strcmp0 (button_title, DELETE_ALL) == 0) + { + gtk_style_context_add_class (gtk_widget_get_style_context (button), + "destructive-action"); + } + } + + if (data->details_text) + { + GtkWidget *content_area, *label; + content_area = gtk_message_dialog_get_message_area (GTK_MESSAGE_DIALOG (dialog)); + + label = gtk_label_new (data->details_text); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_selectable (GTK_LABEL (label), TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0); + /* Ideally, we shouldn’t do this. + * + * Refer to https://gitlab.gnome.org/GNOME/nautilus/merge_requests/94 + * and https://gitlab.gnome.org/GNOME/nautilus/issues/270. + */ + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_MIDDLE); + gtk_label_set_max_width_chars (GTK_LABEL (label), + MAXIMUM_DISPLAYED_ERROR_MESSAGE_LENGTH); + + gtk_container_add (GTK_CONTAINER (content_area), label); + + gtk_widget_show (label); + } + + if (data->dbus_data != NULL) + { + guint32 timestamp; + + timestamp = nautilus_file_operations_dbus_data_get_timestamp (data->dbus_data); + + /* Assuming this is used for desktop implementations, we want the + * dialog to be centered on the screen rather than the parent window, + * which could extend to all monitors. This is the case for + * gnome-flashback. + */ + gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER); + + if (nautilus_file_operations_dbus_data_get_parent_handle (data->dbus_data) != NULL) + { + g_signal_connect (dialog, "realize", G_CALLBACK (dialog_realize_cb), data->dbus_data); + } + + if (timestamp != 0) + { + gtk_window_present_with_time (GTK_WINDOW (dialog), timestamp); + } + } + + /* Run it. */ + result = gtk_dialog_run (GTK_DIALOG (dialog)); + + while ((result == GTK_RESPONSE_NONE || result == GTK_RESPONSE_DELETE_EVENT) && data->ignore_close_box) + { + result = gtk_dialog_run (GTK_DIALOG (dialog)); + } + + gtk_widget_destroy (dialog); + + data->result = result; + data->completed = TRUE; + + g_cond_signal (&data->cond); + g_mutex_unlock (&data->mutex); + + 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->dbus_data = job->dbus_data; + 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; + data->completed = FALSE; + g_mutex_init (&data->mutex); + g_cond_init (&data->cond); + + ptr_array = g_ptr_array_new (); + while ((button_title = va_arg (varargs, const char *)) != NULL) + { + g_ptr_array_add (ptr_array, (char *) button_title); + } + g_ptr_array_add (ptr_array, NULL); + data->button_titles = (const char **) g_ptr_array_free (ptr_array, FALSE); + + nautilus_progress_info_pause (job->progress); + + g_mutex_lock (&data->mutex); + + g_main_context_invoke (NULL, + do_run_simple_dialog, + data); + + while (!data->completed) + { + g_cond_wait (&data->cond, &data->mutex); + } + + nautilus_progress_info_resume (job->progress); + res = data->result; + + g_mutex_unlock (&data->mutex); + g_mutex_clear (&data->mutex); + g_cond_clear (&data->cond); + + 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 int +run_cancel_or_skip_warning (CommonJob *job, + char *primary_text, + char *secondary_text, + const char *details_text, + int total_operations, + int operations_remaining) +{ + int response; + + if (total_operations == 1) + { + response = run_warning (job, + primary_text, + secondary_text, + details_text, + FALSE, + CANCEL, + NULL); + } + else + { + response = run_warning (job, + primary_text, + secondary_text, + details_text, + operations_remaining > 1, + CANCEL, SKIP_ALL, SKIP, + NULL); + } + + return response; +} + +static void +inhibit_power_manager (CommonJob *job, + const char *message) +{ + job->inhibit_cookie = gtk_application_inhibit (GTK_APPLICATION (g_application_get_default ()), + GTK_WINDOW (job->parent_window), + GTK_APPLICATION_INHIBIT_LOGOUT | + GTK_APPLICATION_INHIBIT_SUSPEND, + message); +} + +static void +abort_job (CommonJob *job) +{ + /* destroy the undo action data too */ + g_clear_object (&job->undo_info); + + g_cancellable_cancel (job->cancellable); +} + +static gboolean +job_aborted (CommonJob *job) +{ + return g_cancellable_is_cancelled (job->cancellable); +} + +/* Since this happens on a thread we can't use the global prefs object */ +static gboolean +should_confirm_trash (void) +{ + GSettings *prefs; + gboolean confirm_trash; + + prefs = g_settings_new ("org.gnome.nautilus.preferences"); + confirm_trash = g_settings_get_boolean (prefs, NAUTILUS_PREFERENCES_CONFIRM_TRASH); + g_object_unref (prefs); + return confirm_trash; +} + +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 (!should_confirm_trash ()) + { + return TRUE; + } + + file_count = g_list_length (files); + g_assert (file_count > 0); + + if (file_count == 1) + { + g_autofree gchar *basename = NULL; + + basename = get_basename (files->data); + prompt = g_strdup_printf (_("Are you sure you want to permanently delete “%s” " + "from the trash?"), basename); + } + else + { + prompt = g_strdup_printf (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, + g_strdup (_("If you delete an item, it will be permanently lost.")), + NULL, + FALSE, + CANCEL, 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 (!should_confirm_trash ()) + { + return TRUE; + } + + prompt = g_strdup (_("Empty all items from Trash?")); + + response = run_warning (job, + prompt, + g_strdup (_("All items in the Trash will be permanently deleted.")), + NULL, + FALSE, + 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 (!should_confirm_trash ()) + { + 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) + { + g_autofree gchar *basename = NULL; + + basename = get_basename (files->data); + prompt = g_strdup_printf (_("Are you sure you want to permanently delete “%s”?"), + basename); + } + else + { + prompt = g_strdup_printf (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, + g_strdup (_("If you delete an item, it will be permanently lost.")), + NULL, + FALSE, + CANCEL, DELETE, + NULL); + + return response == 1; +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +static void +report_delete_progress (CommonJob *job, + SourceInfo *source_info, + TransferInfo *transfer_info) +{ + int files_left; + double elapsed, transfer_rate; + int remaining_time; + gint64 now; + char *details; + char *status; + DeleteJob *delete_job; + + delete_job = (DeleteJob *) job; + now = g_get_monotonic_time (); + 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 = 0; + } + + /* If the number of files left is 0, we want to update the status without + * considering this time, since we want to change the status to completed + * and probably we won't get more calls to this function */ + if (transfer_info->last_report_time != 0 && + ABS ((gint64) (transfer_info->last_report_time - now)) < 100 * NSEC_PER_MICROSEC && + files_left > 0) + { + return; + } + + transfer_info->last_report_time = now; + + if (source_info->num_files == 1) + { + g_autofree gchar *basename = NULL; + + if (files_left == 0) + { + status = _("Deleted “%s”"); + } + else + { + status = _("Deleting “%s”"); + } + + basename = get_basename (G_FILE (delete_job->files->data)); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, basename)); + } + else + { + if (files_left == 0) + { + status = ngettext ("Deleted %'d file", + "Deleted %'d files", + source_info->num_files); + } + else + { + status = ngettext ("Deleting %'d file", + "Deleting %'d files", + source_info->num_files); + } + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, + source_info->num_files)); + } + + elapsed = g_timer_elapsed (job->time, NULL); + transfer_rate = 0; + remaining_time = INT_MAX; + if (elapsed > 0) + { + transfer_rate = transfer_info->num_files / elapsed; + if (transfer_rate > 0) + { + remaining_time = (source_info->num_files - transfer_info->num_files) / transfer_rate; + } + } + + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE) + { + if (files_left > 0) + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files + 1, + source_info->num_files); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + else + { + if (files_left > 0) + { + gchar *time_left_message; + gchar *files_per_second_message; + gchar *concat_detail; + g_autofree gchar *formatted_time = NULL; + + /* To translators: %s will expand to a time duration like "2 minutes". + * So the whole thing will be something like "1 / 5 -- 2 hours left (4 files/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + time_left_message = ngettext ("%'d / %'d \xE2\x80\x94 %s left", + "%'d / %'d \xE2\x80\x94 %s left", + seconds_count_format_time_units (remaining_time)); + transfer_rate += 0.5; + files_per_second_message = ngettext ("(%d file/sec)", + "(%d files/sec)", + (int) transfer_rate); + concat_detail = g_strconcat (time_left_message, " ", files_per_second_message, NULL); + + formatted_time = get_formatted_time (remaining_time); + details = g_strdup_printf (concat_detail, + transfer_info->num_files + 1, source_info->num_files, + formatted_time, + (int) transfer_rate); + + g_free (concat_detail); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + nautilus_progress_info_take_details (job->progress, details); + + if (elapsed > SECONDS_NEEDED_FOR_APROXIMATE_TRANSFER_RATE) + { + nautilus_progress_info_set_remaining_time (job->progress, + remaining_time); + nautilus_progress_info_set_elapsed_time (job->progress, + elapsed); + } + + if (source_info->num_files != 0) + { + nautilus_progress_info_set_progress (job->progress, transfer_info->num_files, source_info->num_files); + } +} +#pragma GCC diagnostic pop + +typedef void (*DeleteCallback) (GFile *file, + GError *error, + gpointer callback_data); + +static gboolean +delete_file_recursively (GFile *file, + GCancellable *cancellable, + DeleteCallback callback, + gpointer callback_data) +{ + gboolean success; + g_autoptr (GError) error = NULL; + + do + { + g_autoptr (GFileEnumerator) enumerator = NULL; + + success = g_file_delete (file, cancellable, &error); + if (success || + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_EMPTY)) + { + break; + } + + g_clear_error (&error); + + enumerator = g_file_enumerate_children (file, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, + cancellable, &error); + + if (enumerator) + { + GFileInfo *info; + + success = TRUE; + + info = g_file_enumerator_next_file (enumerator, + cancellable, + &error); + + while (info != NULL) + { + g_autoptr (GFile) child = NULL; + + child = g_file_enumerator_get_child (enumerator, info); + + success = success && delete_file_recursively (child, + cancellable, + callback, + callback_data); + + g_object_unref (info); + + info = g_file_enumerator_next_file (enumerator, + cancellable, + &error); + } + } + + if (error != NULL) + { + success = FALSE; + } + } + while (success); + + if (callback) + { + callback (file, error, callback_data); + } + + return success; +} + +typedef struct +{ + CommonJob *job; + SourceInfo *source_info; + TransferInfo *transfer_info; +} DeleteData; + +static void +file_deleted_callback (GFile *file, + GError *error, + gpointer callback_data) +{ + DeleteData *data = callback_data; + CommonJob *job; + SourceInfo *source_info; + TransferInfo *transfer_info; + GFileType file_type; + char *primary; + char *secondary; + char *details = NULL; + int response; + g_autofree gchar *basename = NULL; + + job = data->job; + source_info = data->source_info; + transfer_info = data->transfer_info; + + data->transfer_info->num_files++; + + if (error == NULL) + { + nautilus_file_changes_queue_file_removed (file); + report_delete_progress (data->job, data->source_info, data->transfer_info); + + return; + } + + if (job_aborted (job) || + job->skip_all_error || + should_skip_file (job, file) || + should_skip_readdir_error (job, file)) + { + return; + } + + primary = g_strdup (_("Error while deleting.")); + + file_type = g_file_query_file_type (file, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + job->cancellable); + + basename = get_basename (file); + + if (file_type == G_FILE_TYPE_DIRECTORY) + { + secondary = IS_IO_ERROR (error, PERMISSION_DENIED) ? + g_strdup_printf (_("There was an error deleting the " + "folder “%s”."), + basename) : + g_strdup_printf (_("You do not have sufficient permissions " + "to delete the folder “%s”."), + basename); + } + else + { + secondary = IS_IO_ERROR (error, PERMISSION_DENIED) ? + g_strdup_printf (_("There was an error deleting the " + "file “%s”."), + basename) : + g_strdup_printf (_("You do not have sufficient permissions " + "to delete the file “%s”."), + basename); + } + + details = error->message; + + response = run_cancel_or_skip_warning (job, + primary, + secondary, + details, + source_info->num_files, + source_info->num_files - transfer_info->num_files); + + if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) + { + abort_job (job); + } + else if (response == 1) + { + /* skip all */ + job->skip_all_error = TRUE; + } +} + +static void +delete_files (CommonJob *job, + GList *files, + int *files_skipped) +{ + GList *l; + GFile *file; + SourceInfo source_info; + TransferInfo transfer_info; + DeleteData data; + + 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); + + data.job = job; + data.source_info = &source_info; + data.transfer_info = &transfer_info; + + for (l = files; + l != NULL && !job_aborted (job); + l = l->next) + { + gboolean success; + + file = l->data; + + if (should_skip_file (job, file)) + { + (*files_skipped)++; + continue; + } + + success = delete_file_recursively (file, job->cancellable, + file_deleted_callback, + &data); + + if (!success) + { + (*files_skipped)++; + } + } +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +static void +report_trash_progress (CommonJob *job, + SourceInfo *source_info, + TransferInfo *transfer_info) +{ + int files_left; + double elapsed, transfer_rate; + int remaining_time; + gint64 now; + char *details; + char *status; + DeleteJob *delete_job; + + delete_job = (DeleteJob *) job; + now = g_get_monotonic_time (); + 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 = 0; + } + + /* If the number of files left is 0, we want to update the status without + * considering this time, since we want to change the status to completed + * and probably we won't get more calls to this function */ + if (transfer_info->last_report_time != 0 && + ABS ((gint64) (transfer_info->last_report_time - now)) < 100 * NSEC_PER_MICROSEC && + files_left > 0) + { + return; + } + + transfer_info->last_report_time = now; + + if (source_info->num_files == 1) + { + g_autofree gchar *basename = NULL; + + if (files_left > 0) + { + status = _("Trashing “%s”"); + } + else + { + status = _("Trashed “%s”"); + } + + basename = get_basename (G_FILE (delete_job->files->data)); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, basename)); + } + else + { + if (files_left > 0) + { + status = ngettext ("Trashing %'d file", + "Trashing %'d files", + source_info->num_files); + } + else + { + status = ngettext ("Trashed %'d file", + "Trashed %'d files", + source_info->num_files); + } + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, + source_info->num_files)); + } + + + elapsed = g_timer_elapsed (job->time, NULL); + transfer_rate = 0; + remaining_time = INT_MAX; + if (elapsed > 0) + { + transfer_rate = transfer_info->num_files / elapsed; + if (transfer_rate > 0) + { + remaining_time = (source_info->num_files - transfer_info->num_files) / transfer_rate; + } + } + + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE) + { + if (files_left > 0) + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files + 1, + source_info->num_files); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + else + { + if (files_left > 0) + { + gchar *time_left_message; + gchar *files_per_second_message; + gchar *concat_detail; + g_autofree gchar *formatted_time = NULL; + + /* To translators: %s will expand to a time duration like "2 minutes". + * So the whole thing will be something like "1 / 5 -- 2 hours left (4 files/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + time_left_message = ngettext ("%'d / %'d \xE2\x80\x94 %s left", + "%'d / %'d \xE2\x80\x94 %s left", + seconds_count_format_time_units (remaining_time)); + files_per_second_message = ngettext ("(%d file/sec)", + "(%d files/sec)", + (int) (transfer_rate + 0.5)); + concat_detail = g_strconcat (time_left_message, " ", files_per_second_message, NULL); + + formatted_time = get_formatted_time (remaining_time); + details = g_strdup_printf (concat_detail, + transfer_info->num_files + 1, + source_info->num_files, + formatted_time, + (int) transfer_rate + 0.5); + + g_free (concat_detail); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + nautilus_progress_info_set_details (job->progress, details); + + if (elapsed > SECONDS_NEEDED_FOR_APROXIMATE_TRANSFER_RATE) + { + nautilus_progress_info_set_remaining_time (job->progress, + remaining_time); + nautilus_progress_info_set_elapsed_time (job->progress, + elapsed); + } + + if (source_info->num_files != 0) + { + nautilus_progress_info_set_progress (job->progress, transfer_info->num_files, source_info->num_files); + } +} +#pragma GCC diagnostic pop + +static void +trash_file (CommonJob *job, + GFile *file, + gboolean *skipped_file, + SourceInfo *source_info, + TransferInfo *transfer_info, + gboolean toplevel, + GList **to_delete) +{ + GError *error; + char *primary, *secondary, *details; + int response; + g_autofree gchar *basename = NULL; + + if (should_skip_file (job, file)) + { + *skipped_file = TRUE; + return; + } + + error = NULL; + + if (g_file_trash (file, job->cancellable, &error)) + { + transfer_info->num_files++; + nautilus_file_changes_queue_file_removed (file); + + if (job->undo_info != NULL) + { + nautilus_file_undo_info_trash_add_file (NAUTILUS_FILE_UNDO_INFO_TRASH (job->undo_info), file); + } + + report_trash_progress (job, source_info, transfer_info); + return; + } + + if (job->skip_all_error) + { + *skipped_file = TRUE; + goto skip; + } + + if (job->delete_all) + { + *to_delete = g_list_prepend (*to_delete, file); + goto skip; + } + + basename = get_basename (file); + /* Translators: %s is a file name */ + primary = g_strdup_printf (_("“%s” can’t be put in the trash. Do you want " + "to delete it immediately?"), + basename); + + details = NULL; + secondary = NULL; + if (!IS_IO_ERROR (error, NOT_SUPPORTED)) + { + details = error->message; + } + else if (!g_file_is_native (file)) + { + secondary = g_strdup (_("This remote location does not support sending items to the trash.")); + } + + response = run_question (job, + primary, + secondary, + details, + (source_info->num_files - transfer_info->num_files) > 1, + CANCEL, SKIP_ALL, SKIP, DELETE_ALL, DELETE, + NULL); + + if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) + { + ((DeleteJob *) job)->user_cancel = TRUE; + abort_job (job); + } + else if (response == 1) /* skip all */ + { + *skipped_file = TRUE; + job->skip_all_error = TRUE; + } + else if (response == 2) /* skip */ + { + *skipped_file = TRUE; + job->skip_all_error = TRUE; + } + 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); +} + +static void +source_info_remove_file_from_count (GFile *file, + CommonJob *job, + SourceInfo *source_info) +{ + g_autoptr (GFileInfo) file_info = NULL; + + if (g_cancellable_is_cancelled (job->cancellable)) + { + return; + } + + file_info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + job->cancellable, + NULL); + + source_info->num_files--; + if (file_info != NULL) + { + source_info->num_bytes -= g_file_info_get_size (file_info); + } +} + +static void +trash_files (CommonJob *job, + GList *files, + int *files_skipped) +{ + GList *l; + GFile *file; + GList *to_delete; + SourceInfo source_info; + TransferInfo transfer_info; + gboolean skipped_file; + + if (job_aborted (job)) + { + return; + } + + scan_sources (files, + &source_info, + job, + OP_KIND_TRASH); + if (job_aborted (job)) + { + return; + } + + g_timer_start (job->time); + + memset (&transfer_info, 0, sizeof (transfer_info)); + report_trash_progress (job, &source_info, &transfer_info); + + to_delete = NULL; + for (l = files; + l != NULL && !job_aborted (job); + l = l->next) + { + file = l->data; + + skipped_file = FALSE; + trash_file (job, file, + &skipped_file, + &source_info, &transfer_info, + TRUE, &to_delete); + if (skipped_file) + { + (*files_skipped)++; + source_info_remove_file_from_count (file, job, &source_info); + report_trash_progress (job, &source_info, &transfer_info); + } + } + + if (to_delete) + { + to_delete = g_list_reverse (to_delete); + delete_files (job, to_delete, files_skipped); + g_list_free (to_delete); + } +} + +static void +delete_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + DeleteJob *job; + GHashTable *debuting_uris; + + job = user_data; + + g_list_free_full (job->files, g_object_unref); + + if (job->done_callback) + { + debuting_uris = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); + job->done_callback (debuting_uris, job->user_cancel, job->done_callback_data); + g_hash_table_unref (debuting_uris); + } + + finalize_common ((CommonJob *) job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static void +trash_or_delete_internal (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + DeleteJob *job = task_data; + g_autoptr (GList) to_trash_files = NULL; + g_autoptr (GList) to_delete_files = NULL; + GList *l; + GFile *file; + gboolean confirmed; + CommonJob *common; + gboolean must_confirm_delete_in_trash; + gboolean must_confirm_delete; + int files_skipped; + + common = (CommonJob *) job; + + nautilus_progress_info_start (job->common.progress); + + 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); + } + + if (files_skipped == g_list_length (job->files)) + { + /* User has skipped all files, report user cancel */ + job->user_cancel = TRUE; + } +} + +static DeleteJob * +setup_delete_job (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + gboolean try_trash, + NautilusDeleteCallback done_callback, + gpointer done_callback_data) +{ + DeleteJob *job; + + /* TODO: special case desktop icon link files ... */ + job = op_job_new (DeleteJob, parent_window, dbus_data); + job->files = g_list_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + job->try_trash = try_trash; + job->user_cancel = FALSE; + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + + if (g_strcmp0 (g_getenv ("RUNNING_TESTS"), "TRUE")) + { + if (try_trash) + { + inhibit_power_manager ((CommonJob *) job, _("Trashing Files")); + } + else + { + inhibit_power_manager ((CommonJob *) job, _("Deleting Files")); + } + } + + if (!nautilus_file_undo_manager_is_operating () && try_trash) + { + job->common.undo_info = nautilus_file_undo_info_trash_new (g_list_length (files)); + } + + return job; +} + +static void +trash_or_delete_internal_sync (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + gboolean try_trash) +{ + GTask *task; + DeleteJob *job; + + job = setup_delete_job (files, + parent_window, + dbus_data, + try_trash, + NULL, + NULL); + + task = g_task_new (NULL, NULL, NULL, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread_sync (task, trash_or_delete_internal); + g_object_unref (task); + /* Since g_task_run_in_thread_sync doesn't work with callbacks (in this case not reaching + * delete_task_done) we need to set up the undo information ourselves. + */ + delete_task_done (NULL, NULL, job); +} + +static void +trash_or_delete_internal_async (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + gboolean try_trash, + NautilusDeleteCallback done_callback, + gpointer done_callback_data) +{ + GTask *task; + DeleteJob *job; + + job = setup_delete_job (files, + parent_window, + dbus_data, + try_trash, + done_callback, + done_callback_data); + + task = g_task_new (NULL, NULL, delete_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, trash_or_delete_internal); + g_object_unref (task); +} + +void +nautilus_file_operations_trash_or_delete_sync (GList *files) +{ + trash_or_delete_internal_sync (files, NULL, NULL, TRUE); +} + +void +nautilus_file_operations_delete_sync (GList *files) +{ + trash_or_delete_internal_sync (files, NULL, NULL, FALSE); +} + +void +nautilus_file_operations_trash_or_delete_async (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusDeleteCallback done_callback, + gpointer done_callback_data) +{ + trash_or_delete_internal_async (files, parent_window, + dbus_data, + TRUE, + done_callback, done_callback_data); +} + +void +nautilus_file_operations_delete_async (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusDeleteCallback done_callback, + gpointer done_callback_data) +{ + trash_or_delete_internal_async (files, parent_window, + dbus_data, + FALSE, + done_callback, done_callback_data); +} + + + +typedef struct +{ + gboolean eject; + GMount *mount; + GMountOperation *mount_operation; + GtkWindow *parent_window; + NautilusUnmountCallback callback; + gpointer callback_data; +} UnmountData; + +static void +unmount_data_free (UnmountData *data) +{ + if (data->parent_window) + { + g_object_remove_weak_pointer (G_OBJECT (data->parent_window), + (gpointer *) &data->parent_window); + } + + g_clear_object (&data->mount_operation); + g_object_unref (data->mount); + g_free (data); +} + +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) + { + g_autofree gchar *mount_name = NULL; + + mount_name = g_mount_get_name (G_MOUNT (source_object)); + if (data->eject) + { + primary = g_strdup_printf (_("Unable to eject %s"), + mount_name); + } + else + { + primary = g_strdup_printf (_("Unable to unmount %s"), + mount_name); + } + show_dialog (primary, + error->message, + data->parent_window, + GTK_MESSAGE_ERROR); + g_free (primary); + } + } + + if (data->callback) + { + data->callback (data->callback_data); + } + + if (error != NULL) + { + g_error_free (error); + } + + unmount_data_free (data); +} + +static void +do_unmount (UnmountData *data) +{ + GMountOperation *mount_op; + + if (data->mount_operation) + { + mount_op = g_object_ref (data->mount_operation); + } + else + { + mount_op = gtk_mount_operation_new (data->parent_window); + } + + g_signal_connect (mount_op, "show-unmount-progress", + G_CALLBACK (show_unmount_progress_cb), NULL); + g_signal_connect (mount_op, "aborted", + G_CALLBACK (show_unmount_progress_aborted_cb), NULL); + + 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; + } + } + + g_list_free_full (dirs, g_object_unref); + + 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, + 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); + + /* 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; +} + +static void +empty_trash_for_unmount_done (gboolean success, + gpointer user_data) +{ + UnmountData *data = user_data; + do_unmount (data); +} + +void +nautilus_file_operations_unmount_mount_full (GtkWindow *parent_window, + GMount *mount, + GMountOperation *mount_operation, + gboolean eject, + gboolean check_trash, + NautilusUnmountCallback callback, + gpointer callback_data) +{ + UnmountData *data; + int response; + + data = g_new0 (UnmountData, 1); + data->callback = callback; + data->callback_data = callback_data; + if (parent_window) + { + data->parent_window = parent_window; + g_object_add_weak_pointer (G_OBJECT (data->parent_window), + (gpointer *) &data->parent_window); + } + if (mount_operation) + { + data->mount_operation = g_object_ref (mount_operation); + } + 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) + { + GTask *task; + EmptyTrashJob *job; + + job = op_job_new (EmptyTrashJob, parent_window, NULL); + job->should_confirm = FALSE; + job->trash_dirs = get_trash_dirs_for_mount (mount); + job->done_callback = empty_trash_for_unmount_done; + job->done_callback_data = data; + + task = g_task_new (NULL, NULL, empty_trash_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, empty_trash_thread_func); + g_object_unref (task); + return; + } + else if (response == GTK_RESPONSE_CANCEL) + { + if (callback) + { + callback (callback_data); + } + + unmount_data_free (data); + return; + } + } + + do_unmount (data); +} + +void +nautilus_file_operations_unmount_mount (GtkWindow *parent_window, + GMount *mount, + gboolean eject, + gboolean check_trash) +{ + nautilus_file_operations_unmount_mount_full (parent_window, mount, NULL, eject, + check_trash, NULL, NULL); +} + +static void +mount_callback_data_notify (gpointer data, + GObject *object) +{ + GMountOperation *mount_op; + + mount_op = G_MOUNT_OPERATION (data); + g_object_set_data (G_OBJECT (mount_op), "mount-callback", NULL); + g_object_set_data (G_OBJECT (mount_op), "mount-callback-data", NULL); +} + +static void +volume_mount_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusMountCallback mount_callback; + GObject *mount_callback_data_object; + GMountOperation *mount_op = user_data; + GError *error; + char *primary; + char *name; + gboolean success; + + success = TRUE; + error = NULL; + if (!g_volume_mount_finish (G_VOLUME (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED && + error->code != G_IO_ERROR_ALREADY_MOUNTED) + { + GtkWindow *parent; + + parent = gtk_mount_operation_get_parent (GTK_MOUNT_OPERATION (mount_op)); + name = g_volume_get_name (G_VOLUME (source_object)); + primary = g_strdup_printf (_("Unable to access “%s”"), name); + g_free (name); + success = FALSE; + show_dialog (primary, + error->message, + parent, + GTK_MESSAGE_ERROR); + g_free (primary); + } + g_error_free (error); + } + + mount_callback = (NautilusMountCallback) + g_object_get_data (G_OBJECT (mount_op), "mount-callback"); + mount_callback_data_object = + g_object_get_data (G_OBJECT (mount_op), "mount-callback-data"); + + if (mount_callback != NULL) + { + (*mount_callback)(G_VOLUME (source_object), + success, + mount_callback_data_object); + + if (mount_callback_data_object != NULL) + { + g_object_weak_unref (mount_callback_data_object, + mount_callback_data_notify, + mount_op); + } + } + + g_object_unref (mount_op); +} + + +void +nautilus_file_operations_mount_volume (GtkWindow *parent_window, + GVolume *volume) +{ + nautilus_file_operations_mount_volume_full (parent_window, volume, + NULL, NULL); +} + +void +nautilus_file_operations_mount_volume_full (GtkWindow *parent_window, + GVolume *volume, + NautilusMountCallback 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); + + g_volume_mount (volume, 0, mount_op, NULL, volume_mount_cb, mount_op); +} + +static void +report_preparing_count_progress (CommonJob *job, + SourceInfo *source_info) +{ + char *s; + + switch (source_info->op) + { + default: + case OP_KIND_COPY: + { + g_autofree gchar *formatted_size = NULL; + + formatted_size = g_format_size (source_info->num_bytes); + s = g_strdup_printf (ngettext ("Preparing to copy %'d file (%s)", + "Preparing to copy %'d files (%s)", + source_info->num_files), + source_info->num_files, + formatted_size); + } + break; + + case OP_KIND_MOVE: + { + g_autofree gchar *formatted_size = NULL; + + formatted_size = g_format_size (source_info->num_bytes); + s = g_strdup_printf (ngettext ("Preparing to move %'d file (%s)", + "Preparing to move %'d files (%s)", + source_info->num_files), + source_info->num_files, + formatted_size); + } + break; + + case OP_KIND_DELETE: + { + g_autofree gchar *formatted_size = NULL; + + formatted_size = g_format_size (source_info->num_bytes); + s = g_strdup_printf (ngettext ("Preparing to delete %'d file (%s)", + "Preparing to delete %'d files (%s)", + source_info->num_files), + source_info->num_files, + formatted_size); + } + break; + + case OP_KIND_TRASH: + { + s = g_strdup_printf (ngettext ("Preparing to trash %'d file", + "Preparing to trash %'d files", + source_info->num_files), + source_info->num_files); + } + break; + + case OP_KIND_COMPRESS: + s = g_strdup_printf (ngettext ("Preparing to compress %'d file", + "Preparing to compress %'d files", + source_info->num_files), + source_info->num_files); + } + + nautilus_progress_info_take_details (job->progress, s); + nautilus_progress_info_pulse_progress (job->progress); +} + +static void +count_file (GFileInfo *info, + CommonJob *job, + SourceInfo *source_info) +{ + source_info->num_files += 1; + source_info->num_bytes += g_file_info_get_size (info); + + if (source_info->num_files_since_progress++ > 100) + { + report_preparing_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 g_strdup (_("Error while copying.")); + } + + case OP_KIND_MOVE: + { + return g_strdup (_("Error while moving.")); + } + + case OP_KIND_DELETE: + { + return g_strdup (_("Error while deleting.")); + } + + case OP_KIND_TRASH: + { + return g_strdup (_("Error while moving files to trash.")); + } + + case OP_KIND_COMPRESS: + return g_strdup (_("Error while compressing files.")); + } +} + +static void +scan_dir (GFile *dir, + SourceInfo *source_info, + CommonJob *job, + GQueue *dirs, + GHashTable *scanned) +{ + 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) + { + g_autoptr (GFile) file = NULL; + g_autofree char *file_uri = NULL; + + file = g_file_enumerator_get_child (enumerator, info); + file_uri = g_file_get_uri (file); + + if (!g_hash_table_contains (scanned, file_uri)) + { + g_hash_table_add (scanned, g_strdup (file_uri)); + + 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) + { + g_autofree gchar *basename = NULL; + + primary = get_scan_primary (source_info->op); + details = NULL; + basename = get_basename (dir); + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("Files in the folder “%s” cannot be handled " + "because you do not have permissions to see them."), + basename); + } + else + { + secondary = g_strdup_printf (_("There was an error getting information about the " + "files in the folder “%s”."), basename); + details = error->message; + } + + response = run_warning (job, + primary, + secondary, + details, + FALSE, + 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 + { + g_autofree gchar *basename = NULL; + + primary = get_scan_primary (source_info->op); + details = NULL; + basename = get_basename (dir); + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("The folder “%s” cannot be handled because you " + "do not have permissions to read it."), + basename); + } + else + { + secondary = g_strdup_printf (_("There was an error reading the folder “%s”."), + basename); + 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, + 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, + GHashTable *scanned) +{ + 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) + { + g_autofree char *file_uri = NULL; + + file_uri = g_file_get_uri (file); + if (!g_hash_table_contains (scanned, file_uri)) + { + g_hash_table_add (scanned, g_strdup (file_uri)); + + count_file (info, job, source_info); + + /* trashing operation doesn't recurse */ + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY && + source_info->op != OP_KIND_TRASH) + { + 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 + { + g_autofree gchar *basename = NULL; + + primary = get_scan_primary (source_info->op); + details = NULL; + basename = get_basename (file); + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("The file “%s” cannot be handled because you do not have " + "permissions to read it."), basename); + } + else + { + secondary = g_strdup_printf (_("There was an error getting information about “%s”."), + basename); + 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, + 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, scanned); + 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; + g_autoptr (GHashTable) scanned = NULL; + + memset (source_info, 0, sizeof (SourceInfo)); + source_info->op = kind; + + scanned = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + + report_preparing_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, + scanned); + } + + /* Make sure we report the final count */ + report_preparing_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; + guint64 size_difference; + char *primary, *secondary, *details; + int response; + GFileType file_type; + gboolean dest_is_symlink = FALSE; + + 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, + dest_is_symlink ? G_FILE_QUERY_INFO_NONE : G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + job->cancellable, + &error); + + if (info == NULL) + { + g_autofree gchar *basename = NULL; + + if (IS_IO_ERROR (error, CANCELLED)) + { + g_error_free (error); + return; + } + + basename = get_basename (dest); + primary = g_strdup_printf (_("Error while copying to “%s”."), basename); + details = NULL; + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup (_("You do not have permissions to access the destination folder.")); + } + else + { + secondary = g_strdup (_("There was an error getting information about the destination.")); + details = error->message; + } + + response = run_error (job, + primary, + secondary, + details, + FALSE, + 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_is_symlink && file_type == G_FILE_TYPE_SYMBOLIC_LINK) + { + /* Record that destination is a symlink and do real stat() once again */ + dest_is_symlink = TRUE; + g_object_unref (info); + goto retry; + } + + 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) + { + g_autofree gchar *basename = NULL; + + basename = get_basename (dest); + primary = g_strdup_printf (_("Error while copying to “%s”."), basename); + secondary = g_strdup (_("The destination is not a folder.")); + + run_error (job, + primary, + secondary, + NULL, + FALSE, + CANCEL, + NULL); + + abort_job (job); + return; + } + + if (dest_is_symlink) + { + /* We can't reliably statfs() destination if it's a symlink, thus not doing any further checks. */ + 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) + { + g_autofree gchar *basename = NULL; + g_autofree gchar *formatted_size = NULL; + + basename = get_basename (dest); + size_difference = required_size - free_size; + primary = g_strdup_printf (_("Error while copying to “%s”."), basename); + secondary = g_strdup (_("There is not enough space on the destination." + " Try to remove files to make space.")); + + formatted_size = g_format_size (size_difference); + details = g_strdup_printf (_("%s more space is required to copy to the destination."), + formatted_size); + + response = run_warning (job, + primary, + secondary, + details, + FALSE, + 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)) + { + g_autofree gchar *basename = NULL; + + basename = get_basename (dest); + primary = g_strdup_printf (_("Error while copying to “%s”."), basename); + secondary = g_strdup (_("The destination is read-only.")); + + run_error (job, + primary, + secondary, + NULL, + FALSE, + CANCEL, + NULL); + + g_error_free (error); + + abort_job (job); + } + + g_object_unref (fsinfo); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +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; + gchar *status; + char *details; + gchar *tmp; + + job = (CommonJob *) copy_job; + + is_move = copy_job->is_move; + + now = g_get_monotonic_time (); + + 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 = 0; + } + + /* If the number of files left is 0, we want to update the status without + * considering this time, since we want to change the status to completed + * and probably we won't get more calls to this function */ + if (transfer_info->last_report_time != 0 && + ABS ((gint64) (transfer_info->last_report_time - now)) < 100 * NSEC_PER_MICROSEC && + files_left > 0) + { + return; + } + transfer_info->last_report_time = now; + + 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) + { + g_autofree gchar *basename_dest = NULL; + + if (copy_job->destination != NULL) + { + if (is_move) + { + if (files_left > 0) + { + status = _("Moving “%s” to “%s”"); + } + else + { + status = _("Moved “%s” to “%s”"); + } + } + else + { + if (files_left > 0) + { + status = _("Copying “%s” to “%s”"); + } + else + { + status = _("Copied “%s” to “%s”"); + } + } + + basename_dest = get_basename (G_FILE (copy_job->destination)); + + if (copy_job->fake_display_source != NULL) + { + g_autofree gchar *basename_fake_display_source = NULL; + + basename_fake_display_source = get_basename (copy_job->fake_display_source); + tmp = g_strdup_printf (status, + basename_fake_display_source, + basename_dest); + } + else + { + g_autofree gchar *basename_data = NULL; + + basename_data = get_basename (G_FILE (copy_job->files->data)); + tmp = g_strdup_printf (status, + basename_data, + basename_dest); + } + + nautilus_progress_info_take_status (job->progress, + tmp); + } + else + { + g_autofree gchar *basename = NULL; + + if (files_left > 0) + { + status = _("Duplicating “%s”"); + } + else + { + status = _("Duplicated “%s”"); + } + + basename = get_basename (G_FILE (copy_job->files->data)); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, + basename)); + } + } + else if (copy_job->files != NULL) + { + if (copy_job->destination != NULL) + { + if (files_left > 0) + { + g_autofree gchar *basename = NULL; + + if (is_move) + { + status = ngettext ("Moving %'d file to “%s”", + "Moving %'d files to “%s”", + source_info->num_files); + } + else + { + status = ngettext ("Copying %'d file to “%s”", + "Copying %'d files to “%s”", + source_info->num_files); + } + + basename = get_basename (G_FILE (copy_job->destination)); + tmp = g_strdup_printf (status, + source_info->num_files, + basename); + + nautilus_progress_info_take_status (job->progress, + tmp); + } + else + { + g_autofree gchar *basename = NULL; + + if (is_move) + { + status = ngettext ("Moved %'d file to “%s”", + "Moved %'d files to “%s”", + source_info->num_files); + } + else + { + status = ngettext ("Copied %'d file to “%s”", + "Copied %'d files to “%s”", + source_info->num_files); + } + + basename = get_basename (G_FILE (copy_job->destination)); + tmp = g_strdup_printf (status, + source_info->num_files, + basename); + + nautilus_progress_info_take_status (job->progress, + tmp); + } + } + else + { + GFile *parent; + g_autofree gchar *basename = NULL; + + parent = g_file_get_parent (copy_job->files->data); + basename = get_basename (parent); + if (files_left > 0) + { + status = ngettext ("Duplicating %'d file in “%s”", + "Duplicating %'d files in “%s”", + source_info->num_files); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, + source_info->num_files, + basename)); + } + else + { + status = ngettext ("Duplicated %'d file in “%s”", + "Duplicated %'d files in “%s”", + source_info->num_files); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (status, + source_info->num_files, + basename)); + } + g_object_unref (parent); + } + } + } + + total_size = MAX (source_info->num_bytes, transfer_info->num_bytes); + + elapsed = g_timer_elapsed (job->time, NULL); + transfer_rate = 0; + remaining_time = INT_MAX; + if (elapsed > 0) + { + transfer_rate = transfer_info->num_bytes / elapsed; + if (transfer_rate > 0) + { + remaining_time = (total_size - transfer_info->num_bytes) / transfer_rate; + } + } + + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE && + transfer_rate > 0) + { + if (source_info->num_files == 1) + { + g_autofree gchar *formatted_size_num_bytes = NULL; + g_autofree gchar *formatted_size_total_size = NULL; + + formatted_size_num_bytes = g_format_size (transfer_info->num_bytes); + formatted_size_total_size = g_format_size (total_size); + /* To translators: %s will expand to a size like "2 bytes" or "3 MB", so something like "4 kb / 4 MB" */ + details = g_strdup_printf (_("%s / %s"), + formatted_size_num_bytes, + formatted_size_total_size); + } + else + { + if (files_left > 0) + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files + 1, + source_info->num_files); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + } + else + { + if (source_info->num_files == 1) + { + if (files_left > 0) + { + g_autofree gchar *formatted_time = NULL; + g_autofree gchar *formatted_size_num_bytes = NULL; + g_autofree gchar *formatted_size_total_size = NULL; + g_autofree gchar *formatted_size_transfer_rate = NULL; + + formatted_time = get_formatted_time (remaining_time); + formatted_size_num_bytes = g_format_size (transfer_info->num_bytes); + formatted_size_total_size = g_format_size (total_size); + formatted_size_transfer_rate = g_format_size ((goffset) transfer_rate); + /* To translators: %s will expand to a size like "2 bytes" or "3 MB", %s to a time duration like + * "2 minutes". So the whole thing will be something like "2 kb / 4 MB -- 2 hours left (4kb/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + details = g_strdup_printf (ngettext ("%s / %s \xE2\x80\x94 %s left (%s/sec)", + "%s / %s \xE2\x80\x94 %s left (%s/sec)", + seconds_count_format_time_units (remaining_time)), + formatted_size_num_bytes, + formatted_size_total_size, + formatted_time, + formatted_size_transfer_rate); + } + else + { + g_autofree gchar *formatted_size_num_bytes = NULL; + g_autofree gchar *formatted_size_total_size = NULL; + + formatted_size_num_bytes = g_format_size (transfer_info->num_bytes); + formatted_size_total_size = g_format_size (total_size); + /* To translators: %s will expand to a size like "2 bytes" or "3 MB". */ + details = g_strdup_printf (_("%s / %s"), + formatted_size_num_bytes, + formatted_size_total_size); + } + } + else + { + if (files_left > 0) + { + g_autofree gchar *formatted_time = NULL; + g_autofree gchar *formatted_size = NULL; + formatted_time = get_formatted_time (remaining_time); + formatted_size = g_format_size ((goffset) transfer_rate); + /* To translators: %s will expand to a time duration like "2 minutes". + * So the whole thing will be something like "1 / 5 -- 2 hours left (4kb/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + details = g_strdup_printf (ngettext ("%'d / %'d \xE2\x80\x94 %s left (%s/sec)", + "%'d / %'d \xE2\x80\x94 %s left (%s/sec)", + seconds_count_format_time_units (remaining_time)), + transfer_info->num_files + 1, source_info->num_files, + formatted_time, + formatted_size); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + transfer_info->num_files, + source_info->num_files); + } + } + } + nautilus_progress_info_take_details (job->progress, details); + + if (elapsed > SECONDS_NEEDED_FOR_APROXIMATE_TRANSFER_RATE) + { + nautilus_progress_info_set_remaining_time (job->progress, + remaining_time); + nautilus_progress_info_set_elapsed_time (job->progress, + elapsed); + } + + nautilus_progress_info_set_progress (job->progress, transfer_info->num_bytes, total_size); +} +#pragma GCC diagnostic pop + +#define FAT_FORBIDDEN_CHARACTERS "/:*?\"<>\\|" + +static gboolean +fat_str_replace (char *str, + char replacement) +{ + gboolean success; + int i; + + success = FALSE; + for (i = 0; str[i] != '\0'; i++) + { + if (strchr (FAT_FORBIDDEN_CHARACTERS, str[i]) || + str[i] < 32) + { + 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") || + /* The fuseblk filesystem type could be of any type + * in theory, but in practice is usually NTFS or exFAT. + * This assumption is a pragmatic way to solve + * https://gitlab.gnome.org/GNOME/nautilus/-/issues/1343 */ + !strcmp (dest_fs_type, "fuse") || + !strcmp (dest_fs_type, "ntfs") || + !strcmp (dest_fs_type, "msdos") || + !strcmp (dest_fs_type, "msdosfs")) + { + gboolean ret; + int i, old_len; + + ret = fat_str_replace (filename, '_'); + + 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; + NautilusFile *file; + gboolean ignore_extension; + + max_length = nautilus_get_max_child_name_length_for_location (dest_dir); + + file = nautilus_file_get (src); + ignore_extension = nautilus_file_is_directory (file); + nautilus_file_unref (file); + + 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, ignore_extension); + 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, ignore_extension); + 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 = nautilus_get_max_child_name_length_for_location (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_with_custom_name (GFile *src, + GFile *dest_dir, + const char *dest_fs_type, + gboolean same_fs, + const gchar *custom_name) +{ + char *basename; + GFile *dest; + GFileInfo *info; + char *copyname; + + dest = NULL; + + if (custom_name != NULL) + { + copyname = g_strdup (custom_name); + 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); + } + + if (dest == NULL && !same_fs) + { + info = g_file_query_info (src, + G_FILE_ATTRIBUTE_STANDARD_COPY_NAME "," + G_FILE_ATTRIBUTE_TRASH_ORIG_PATH, + 0, NULL, NULL); + + if (info) + { + copyname = NULL; + + /* if file is being restored from trash make sure it uses its original name */ + if (g_file_has_uri_scheme (src, "trash")) + { + copyname = g_path_get_basename (g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_TRASH_ORIG_PATH)); + } + + if (copyname == NULL) + { + 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 GFile * +get_target_file (GFile *src, + GFile *dest_dir, + const char *dest_fs_type, + gboolean same_fs) +{ + return get_target_file_with_custom_name (src, dest_dir, dest_fs_type, same_fs, NULL); +} + +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) +{ + GFileType file_type; + + file_type = g_file_query_file_type (file, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL); + + return file_type == G_FILE_TYPE_DIRECTORY; +} + +static GFile * +map_possibly_volatile_file_to_real (GFile *volatile_file, + GCancellable *cancellable, + GError **error) +{ + GFile *real_file = NULL; + GFileInfo *info = NULL; + + info = g_file_query_info (volatile_file, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," + G_FILE_ATTRIBUTE_STANDARD_IS_VOLATILE "," + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable, + error); + if (info == NULL) + { + return NULL; + } + else + { + gboolean is_volatile; + + is_volatile = g_file_info_get_attribute_boolean (info, + G_FILE_ATTRIBUTE_STANDARD_IS_VOLATILE); + if (is_volatile) + { + const gchar *target; + + target = g_file_info_get_symlink_target (info); + real_file = g_file_resolve_relative_path (volatile_file, target); + } + } + + g_object_unref (info); + + if (real_file == NULL) + { + real_file = g_object_ref (volatile_file); + } + + return real_file; +} + +static GFile * +map_possibly_volatile_file_to_real_on_write (GFile *volatile_file, + GFileOutputStream *stream, + GCancellable *cancellable, + GError **error) +{ + GFile *real_file = NULL; + GFileInfo *info = NULL; + + info = g_file_output_stream_query_info (stream, + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," + G_FILE_ATTRIBUTE_STANDARD_IS_VOLATILE "," + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + cancellable, + error); + if (info == NULL) + { + return NULL; + } + else + { + gboolean is_volatile; + + is_volatile = g_file_info_get_attribute_boolean (info, + G_FILE_ATTRIBUTE_STANDARD_IS_VOLATILE); + if (is_volatile) + { + const gchar *target; + + target = g_file_info_get_symlink_target (info); + real_file = g_file_resolve_relative_path (volatile_file, target); + } + } + + g_object_unref (info); + + if (real_file == NULL) + { + real_file = g_object_ref (volatile_file); + } + + return real_file; +} + +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, + 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; + gboolean res; + + 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; + res = g_file_make_directory (*dest, job->cancellable, &error); + + if (res) + { + GFile *real; + + real = map_possibly_volatile_file_to_real (*dest, job->cancellable, &error); + if (real == NULL) + { + res = FALSE; + } + else + { + g_object_unref (*dest); + *dest = real; + } + } + + if (!res) + { + g_autofree gchar *basename = NULL; + + 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 = g_strdup (_("Error while copying.")); + details = NULL; + basename = get_basename (src); + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("The folder “%s” cannot be copied because you do not " + "have permissions to create it in the destination."), + basename); + } + else + { + secondary = g_strdup_printf (_("There was an error creating the folder “%s”."), + basename); + details = error->message; + } + + response = run_warning (job, + primary, + secondary, + details, + FALSE, + CANCEL, SKIP, RETRY, + NULL); + + g_error_free (error); + + if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) + { + abort_job (job); + } + else if (response == 1) + { + /* Skip: Do Nothing */ + } + else if (response == 2) + { + goto retry; + } + else + { + g_assert_not_reached (); + } + return CREATE_DEST_DIR_FAILED; + } + nautilus_file_changes_queue_file_added (*dest); + + if (job->undo_info != NULL) + { + nautilus_file_undo_info_ext_add_origin_target_pair (NAUTILUS_FILE_UNDO_INFO_EXT (job->undo_info), + src, *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 preceding + * 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, FALSE, &local_skipped_file, + readonly_source_fs); + + if (local_skipped_file) + { + source_info_remove_file_from_count (src_file, job, source_info); + report_copy_progress (copy_job, source_info, transfer_info); + } + + 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) + { + g_autofree gchar *basename = NULL; + + if (copy_job->is_move) + { + primary = g_strdup (_("Error while moving.")); + } + else + { + primary = g_strdup (_("Error while copying.")); + } + details = NULL; + basename = get_basename (src); + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("Files in the folder “%s” cannot be copied because you do " + "not have permissions to see them."), basename); + } + else + { + secondary = g_strdup_printf (_("There was an error getting information about " + "the files in the folder “%s”."), + basename); + details = error->message; + } + + response = run_warning (job, + primary, + secondary, + details, + FALSE, + 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 + { + g_autofree gchar *basename = NULL; + + if (copy_job->is_move) + { + primary = g_strdup (_("Error while moving.")); + } + else + { + primary = g_strdup (_("Error while copying.")); + } + details = NULL; + basename = get_basename (src); + + if (IS_IO_ERROR (error, PERMISSION_DENIED)) + { + secondary = g_strdup_printf (_("The folder “%s” cannot be copied because you do not have " + "permissions to read it."), basename); + } + else + { + secondary = g_strdup_printf (_("There was an error reading the folder “%s”."), + basename); + + details = error->message; + } + + response = run_warning (job, + primary, + secondary, + details, + FALSE, + 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)) + { + g_autofree gchar *basename = NULL; + + if (job->skip_all_error) + { + goto skip; + } + basename = get_basename (src); + primary = g_strdup_printf (_("Error while moving “%s”."), basename); + secondary = g_strdup (_("Could not remove the source folder.")); + details = error->message; + + response = run_cancel_or_skip_warning (job, + primary, + secondary, + details, + source_info->num_files, + source_info->num_files - transfer_info->num_files); + + 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; +} + + +typedef struct +{ + CommonJob *job; + GFile *source; +} DeleteExistingFileData; + +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, *tmp; + + f = g_file_dup (child); + while (f) + { + if (g_file_equal (f, root)) + { + g_object_unref (f); + return TRUE; + } + tmp = f; + f = g_file_get_parent (f); + g_object_unref (tmp); + } + 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 FileConflictResponse * +handle_copy_move_conflict (CommonJob *job, + GFile *src, + GFile *dest, + GFile *dest_dir) +{ + FileConflictResponse *response; + + g_timer_stop (job->time); + nautilus_progress_info_pause (job->progress); + + response = copy_move_conflict_ask_user_action (job->parent_window, + src, + dest, + dest_dir); + + nautilus_progress_info_resume (job->progress); + g_timer_continue (job->time); + + return response; +} + +static GFile * +get_target_file_for_display_name (GFile *dir, + const gchar *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; +} + +/* This is a workaround to resolve broken conflict dialog for google-drive + * locations. This is needed to provide POSIX-like behavior for google-drive + * filesystem, where each file has an unique identificator that is not tied to + * its display_name. See the following MR for more details: + * https://gitlab.gnome.org/GNOME/nautilus/merge_requests/514. + */ +static GFile * +get_target_file_from_source_display_name (CopyMoveJob *copy_job, + GFile *src, + GFile *dir) +{ + CommonJob *job; + g_autoptr (GError) error = NULL; + g_autoptr (GFileInfo) info = NULL; + gchar *primary, *secondary; + GFile *dest = NULL; + + job = (CommonJob *) copy_job; + + info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, 0, NULL, &error); + if (info == NULL) + { + if (copy_job->is_move) + { + primary = g_strdup (_("Error while moving.")); + } + else + { + primary = g_strdup (_("Error while copying.")); + } + secondary = g_strdup (_("There was an error getting information about the source.")); + + run_error (job, + primary, + secondary, + error->message, + FALSE, + CANCEL, + NULL); + + abort_job (job); + } + else + { + dest = get_target_file_for_display_name (dir, g_file_info_get_display_name (info)); + } + + 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, + gboolean overwrite, + gboolean *skipped_file, + gboolean readonly_source_fs) +{ + GFile *dest, *new_dest; + g_autofree gchar *dest_uri = NULL; + GError *error; + GFileCopyFlags flags; + char *primary, *secondary, *details; + ProgressData pdata; + gboolean would_recurse; + CommonJob *job; + gboolean res; + int unique_name_nr; + gboolean handled_invalid_filename; + + job = (CommonJob *) copy_job; + + *skipped_file = FALSE; + + 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 if (copy_job->target_name != NULL) + { + dest = get_target_file_with_custom_name (src, dest_dir, *dest_fs_type, same_fs, + copy_job->target_name); + } + else if (g_file_has_uri_scheme (src, "google-drive") && + g_file_has_uri_scheme (dest_dir, "google-drive")) + { + dest = get_target_file_from_source_display_name (copy_job, src, dest_dir); + if (dest == NULL) + { + return; + } + } + 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)) + { + int response; + + 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_cancel_or_skip_warning (job, + primary, + secondary, + NULL, + source_info->num_files, + source_info->num_files - transfer_info->num_files); + + 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)) + { + int response; + + 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_cancel_or_skip_warning (job, + primary, + secondary, + NULL, + source_info->num_files, + source_info->num_files - transfer_info->num_files); + + 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) + { + GFile *real; + + real = map_possibly_volatile_file_to_real (dest, job->cancellable, &error); + if (real == NULL) + { + res = FALSE; + } + else + { + g_object_unref (dest); + dest = real; + } + } + + if (res) + { + transfer_info->num_files++; + report_copy_progress (copy_job, source_info, transfer_info); + + if (debuting_files) + { + dest_uri = g_file_get_uri (dest); + + g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); + } + if (copy_job->is_move) + { + nautilus_file_changes_queue_file_moved (src, dest); + } + else + { + nautilus_file_changes_queue_file_added (dest); + } + + if (job->undo_info != NULL) + { + nautilus_file_undo_info_ext_add_origin_target_pair (NAUTILUS_FILE_UNDO_INFO_EXT (job->undo_info), + src, dest); + } + + 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 source_is_directory; + gboolean destination_is_directory; + gboolean is_merge; + FileConflictResponse *response; + + source_is_directory = is_dir (src); + destination_is_directory = is_dir (dest); + + 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 (source_is_directory && destination_is_directory) + { + is_merge = TRUE; + } + else if (!source_is_directory && destination_is_directory) + { + /* Any sane backend will fail with G_IO_ERROR_IS_DIRECTORY. */ + overwrite = TRUE; + goto retry; + } + + if ((is_merge && job->merge_all) || + (!is_merge && job->replace_all)) + { + overwrite = TRUE; + goto retry; + } + + if (job->skip_all_conflict) + { + goto out; + } + + response = handle_copy_move_conflict (job, src, dest, dest_dir); + + if (response->id == GTK_RESPONSE_CANCEL || + response->id == GTK_RESPONSE_DELETE_EVENT) + { + file_conflict_response_free (response); + abort_job (job); + } + else if (response->id == CONFLICT_RESPONSE_SKIP) + { + if (response->apply_to_all) + { + job->skip_all_conflict = TRUE; + } + file_conflict_response_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; + file_conflict_response_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); + file_conflict_response_free (response); + goto retry; + } + else + { + g_assert_not_reached (); + } + } + /* Needs to recurse */ + else if (IS_IO_ERROR (error, WOULD_RECURSE) || + IS_IO_ERROR (error, WOULD_MERGE)) + { + gboolean is_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)) + { + g_autofree gchar *basename = NULL; + g_autofree gchar *filename = NULL; + int response; + + if (job->skip_all_error) + { + g_error_free (error); + goto out; + } + + basename = get_basename (src); + if (copy_job->is_move) + { + primary = g_strdup_printf (_("Error while moving “%s”."), basename); + } + else + { + primary = g_strdup_printf (_("Error while copying “%s”."), basename); + } + filename = get_truncated_parse_name (dest_dir); + secondary = g_strdup_printf (_("Could not remove the already existing file " + "with the same name in %s."), + filename); + 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, + CANCEL, SKIP_ALL, SKIP, + NULL); + + g_error_free (error); + + if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) + { + abort_job (job); + } + else if (response == 1) /* skip all */ + { + job->skip_all_error = TRUE; + } + else if (response == 2) /* skip */ + { /* do nothing */ + } + else + { + g_assert_not_reached (); + } + goto out; + } + if (error) + { + g_error_free (error); + error = NULL; + } + nautilus_file_changes_queue_file_removed (dest); + } + + if (is_merge) + { + /* On merge we now write in the target directory, which may not + * be in the same directory as the source, even if the parent is + * (if the merged directory is a mountpoint). This could cause + * problems as we then don't transcode filenames. + * We just set same_fs to FALSE which is safe but a bit slower. */ + same_fs = FALSE; + } + + if (!copy_move_directory (copy_job, src, &dest, same_fs, + would_recurse, dest_fs_type, + source_info, transfer_info, + debuting_files, skipped_file, + readonly_source_fs)) + { + /* destination changed, since it was an invalid file name */ + g_assert (*dest_fs_type != NULL); + handled_invalid_filename = TRUE; + goto retry; + } + + g_object_unref (dest); + return; + } + else if (IS_IO_ERROR (error, CANCELLED)) + { + g_error_free (error); + } + /* Other error */ + else + { + g_autofree gchar *basename = NULL; + g_autofree gchar *filename = NULL; + int response; + + if (job->skip_all_error) + { + g_error_free (error); + goto out; + } + basename = get_basename (src); + primary = g_strdup_printf (_("Error while copying “%s”."), basename); + filename = get_truncated_parse_name (dest_dir); + secondary = g_strdup_printf (_("There was an error copying the file into %s."), + filename); + details = error->message; + + response = run_cancel_or_skip_warning (job, + primary, + secondary, + details, + source_info->num_files, + source_info->num_files - transfer_info->num_files); + + 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; + 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 it's 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; + + 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, + FALSE, &skipped_file, + readonly_source_fs); + g_object_unref (dest); + + if (skipped_file) + { + source_info_remove_file_from_count (src, common, source_info); + report_copy_progress (job, source_info, transfer_info); + } + } + i++; + } + + g_free (dest_fs_type); +} + +static void +copy_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CopyMoveJob *job; + + job = user_data; + if (job->done_callback) + { + job->done_callback (job->debuting_files, + !job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + g_list_free_full (job->files, g_object_unref); + if (job->destination) + { + g_object_unref (job->destination); + } + g_hash_table_unref (job->debuting_files); + g_free (job->target_name); + + g_clear_object (&job->fake_display_source); + + finalize_common ((CommonJob *) job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static CopyMoveJob * +copy_job_setup (GList *files, + GFile *target_dir, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + CopyMoveJob *job; + + job = op_job_new (CopyMoveJob, parent_window, dbus_data); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->files = g_list_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + job->destination = g_object_ref (target_dir); + /* Need to indicate the destination for the operation notification open + * button. */ + nautilus_progress_info_set_destination (((CommonJob *) job)->progress, target_dir); + job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); + + return job; +} + +static void +nautilus_file_operations_copy (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CopyMoveJob *job; + CommonJob *common; + SourceInfo source_info; + TransferInfo transfer_info; + g_autofree char *dest_fs_id = NULL; + GFile *dest; + + job = task_data; + common = &job->common; + + if (g_strcmp0 (g_getenv ("RUNNING_TESTS"), "TRUE")) + { + inhibit_power_manager ((CommonJob *) job, _("Copying Files")); + } + + if (!nautilus_file_undo_manager_is_operating ()) + { + g_autoptr (GFile) src_dir = NULL; + + src_dir = g_file_get_parent (job->files->data); + /* In the case of duplicate, the undo_info is already set, so we don't want to + * overwrite it wrongfully. + */ + if (job->common.undo_info == NULL) + { + job->common.undo_info = nautilus_file_undo_info_ext_new (NAUTILUS_FILE_UNDO_OP_COPY, + g_list_length (job->files), + src_dir, job->destination); + } + } + + nautilus_progress_info_start (job->common.progress); + + scan_sources (job->files, + &source_info, + common, + OP_KIND_COPY); + if (job_aborted (common)) + { + return; + } + + 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)) + { + return; + } + + g_timer_start (job->common.time); + + memset (&transfer_info, 0, sizeof (transfer_info)); + copy_files (job, + dest_fs_id, + &source_info, &transfer_info); +} + +void +nautilus_file_operations_copy_sync (GList *files, + GFile *target_dir) +{ + GTask *task; + CopyMoveJob *job; + + job = copy_job_setup (files, + target_dir, + NULL, + NULL, + NULL, + NULL); + + task = g_task_new (NULL, job->common.cancellable, NULL, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread_sync (task, nautilus_file_operations_copy); + g_object_unref (task); + /* Since g_task_run_in_thread_sync doesn't work with callbacks (in this case not reaching + * copy_task_done) we need to set up the undo information ourselves. + */ + copy_task_done (NULL, NULL, job); +} + +void +nautilus_file_operations_copy_async (GList *files, + GFile *target_dir, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + GTask *task; + CopyMoveJob *job; + + job = copy_job_setup (files, + target_dir, + parent_window, + dbus_data, + done_callback, + done_callback_data); + + task = g_task_new (NULL, job->common.cancellable, copy_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, nautilus_file_operations_copy); + g_object_unref (task); +} + +static void +report_preparing_move_progress (CopyMoveJob *move_job, + int total, + int left) +{ + CommonJob *job; + g_autofree gchar *basename = NULL; + + job = (CommonJob *) move_job; + basename = get_basename (move_job->destination); + + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (_("Preparing to move to “%s”"), + basename)); + + nautilus_progress_info_take_details (job->progress, + g_strdup_printf (ngettext ("Preparing to move %'d file", + "Preparing to move %'d files", + left), + left)); + + nautilus_progress_info_pulse_progress (job->progress); +} + +typedef struct +{ + GFile *file; + gboolean overwrite; +} MoveFileCopyFallback; + +static MoveFileCopyFallback * +move_copy_file_callback_new (GFile *file, + gboolean overwrite) +{ + MoveFileCopyFallback *fallback; + + fallback = g_new (MoveFileCopyFallback, 1); + fallback->file = file; + fallback->overwrite = overwrite; + + 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, + GList **fallback_files, + int files_left) +{ + GFile *dest, *new_dest; + g_autofree gchar *dest_uri = NULL; + GError *error; + CommonJob *job; + gboolean overwrite; + char *primary, *secondary, *details; + GFileCopyFlags flags; + MoveFileCopyFallback *fallback; + gboolean handled_invalid_filename; + + overwrite = FALSE; + handled_invalid_filename = *dest_fs_type != NULL; + + job = (CommonJob *) move_job; + + if (g_file_has_uri_scheme (src, "google-drive") && + g_file_has_uri_scheme (dest_dir, "google-drive")) + { + dest = get_target_file_from_source_display_name (move_job, src, dest_dir); + if (dest == NULL) + { + return; + } + } + 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)) + { + int response; + + 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, + CANCEL, SKIP_ALL, SKIP, + NULL); + + if (response == 0 || response == GTK_RESPONSE_DELETE_EVENT) + { + abort_job (job); + } + else if (response == 1) /* skip all */ + { + job->skip_all_error = TRUE; + } + else if (response == 2) /* skip */ + { /* do nothing */ + } + else + { + g_assert_not_reached (); + } + + goto out; + } + +retry: + + flags = G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_NO_FALLBACK_FOR_MOVE; + if (overwrite) + { + flags |= G_FILE_COPY_OVERWRITE; + } + + error = NULL; + if (g_file_move (src, dest, + flags, + job->cancellable, + NULL, + NULL, + &error)) + { + if (debuting_files) + { + g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); + } + + nautilus_file_changes_queue_file_moved (src, dest); + + dest_uri = g_file_get_uri (dest); + + if (job->undo_info != NULL) + { + nautilus_file_undo_info_ext_add_origin_target_pair (NAUTILUS_FILE_UNDO_INFO_EXT (job->undo_info), + src, dest); + } + + return; + } + + if (IS_IO_ERROR (error, INVALID_FILENAME) && + !handled_invalid_filename) + { + g_error_free (error); + + 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 source_is_directory; + gboolean destination_is_directory; + gboolean is_merge; + FileConflictResponse *response; + + source_is_directory = is_dir (src); + destination_is_directory = is_dir (dest); + + g_error_free (error); + + is_merge = FALSE; + if (source_is_directory && destination_is_directory) + { + is_merge = TRUE; + } + else if (!source_is_directory && destination_is_directory) + { + /* Any sane backend will fail with G_IO_ERROR_IS_DIRECTORY. */ + overwrite = TRUE; + goto retry; + } + + if ((is_merge && job->merge_all) || + (!is_merge && job->replace_all)) + { + overwrite = TRUE; + goto retry; + } + + if (job->skip_all_conflict) + { + goto out; + } + + response = handle_copy_move_conflict (job, src, dest, dest_dir); + + if (response->id == GTK_RESPONSE_CANCEL || + response->id == GTK_RESPONSE_DELETE_EVENT) + { + file_conflict_response_free (response); + abort_job (job); + } + else if (response->id == CONFLICT_RESPONSE_SKIP) + { + if (response->apply_to_all) + { + job->skip_all_conflict = TRUE; + } + file_conflict_response_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; + file_conflict_response_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); + file_conflict_response_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)) + { + g_error_free (error); + + fallback = move_copy_file_callback_new (src, + overwrite); + *fallback_files = g_list_prepend (*fallback_files, fallback); + } + else if (IS_IO_ERROR (error, CANCELLED)) + { + g_error_free (error); + } + /* Other error */ + else + { + g_autofree gchar *basename = NULL; + g_autofree gchar *filename = NULL; + int response; + + if (job->skip_all_error) + { + g_error_free (error); + goto out; + } + basename = get_basename (src); + primary = g_strdup_printf (_("Error while moving “%s”."), basename); + filename = get_truncated_parse_name (dest_dir); + secondary = g_strdup_printf (_("There was an error moving the file into %s."), + filename); + + details = error->message; + + response = run_warning (job, + primary, + secondary, + details, + files_left > 1, + 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; + int total, left; + + common = &job->common; + + total = left = g_list_length (job->files); + + report_preparing_move_progress (job, total, left); + + i = 0; + for (l = job->files; + l != NULL && !job_aborted (common); + l = l->next) + { + src = l->data; + + 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, + fallbacks, + left); + report_preparing_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; + 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; + + 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, + fallback->overwrite, &skipped_file, FALSE); + i++; + + if (skipped_file) + { + source_info_remove_file_from_count (src, common, source_info); + report_copy_progress (job, source_info, transfer_info); + } + } +} + + +static void +move_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CopyMoveJob *job; + + job = user_data; + if (job->done_callback) + { + job->done_callback (job->debuting_files, + !job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + g_list_free_full (job->files, g_object_unref); + g_object_unref (job->destination); + g_hash_table_unref (job->debuting_files); + + finalize_common ((CommonJob *) job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static CopyMoveJob * +move_job_setup (GList *files, + GFile *target_dir, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + CopyMoveJob *job; + + job = op_job_new (CopyMoveJob, parent_window, dbus_data); + job->is_move = TRUE; + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->files = g_list_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + job->destination = g_object_ref (target_dir); + + /* Need to indicate the destination for the operation notification open + * button. */ + nautilus_progress_info_set_destination (((CommonJob *) job)->progress, job->destination); + job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); + + return job; +} + +void +nautilus_file_operations_move_sync (GList *files, + GFile *target_dir) +{ + GTask *task; + CopyMoveJob *job; + + job = move_job_setup (files, target_dir, NULL, NULL, NULL, NULL); + task = g_task_new (NULL, job->common.cancellable, NULL, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread_sync (task, nautilus_file_operations_move); + g_object_unref (task); + /* Since g_task_run_in_thread_sync doesn't work with callbacks (in this case not reaching + * move_task_done) we need to set up the undo information ourselves. + */ + move_task_done (NULL, NULL, job); +} + +void +nautilus_file_operations_move_async (GList *files, + GFile *target_dir, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + GTask *task; + CopyMoveJob *job; + + job = move_job_setup (files, + target_dir, + parent_window, + dbus_data, + done_callback, + done_callback_data); + + task = g_task_new (NULL, job->common.cancellable, move_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, nautilus_file_operations_move); + g_object_unref (task); +} + +static void +nautilus_file_operations_move (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CopyMoveJob *job; + CommonJob *common; + GList *fallbacks; + SourceInfo source_info; + TransferInfo transfer_info; + g_autofree char *dest_fs_id = NULL; + g_autofree char *dest_fs_type = NULL; + GList *fallback_files; + + job = task_data; + + /* Since we never initiate the app (in the case of + * testing), we can't inhibit its power manager. + * This would terminate the testing. So we avoid + * doing this by checking if the RUNNING_TESTS + * environment variable is set to "TRUE". + */ + if (g_strcmp0 (g_getenv ("RUNNING_TESTS"), "TRUE")) + { + inhibit_power_manager ((CommonJob *) job, _("Moving Files")); + } + + if (!nautilus_file_undo_manager_is_operating ()) + { + g_autoptr (GFile) src_dir = NULL; + + src_dir = g_file_get_parent ((job->files)->data); + + if (g_file_has_uri_scheme (g_list_first (job->files)->data, "trash")) + { + job->common.undo_info = nautilus_file_undo_info_ext_new (NAUTILUS_FILE_UNDO_OP_RESTORE_FROM_TRASH, + g_list_length (job->files), + src_dir, job->destination); + } + else + { + job->common.undo_info = nautilus_file_undo_info_ext_new (NAUTILUS_FILE_UNDO_OP_MOVE, + g_list_length (job->files), + src_dir, job->destination); + } + } + + common = &job->common; + + nautilus_progress_info_start (job->common.progress); + + fallbacks = NULL; + + 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: + g_list_free_full (fallbacks, g_free); +} + +static void +report_preparing_link_progress (CopyMoveJob *link_job, + int total, + int left) +{ + CommonJob *job; + g_autofree gchar *basename = NULL; + + job = (CommonJob *) link_job; + basename = get_basename (link_job->destination); + nautilus_progress_info_take_status (job->progress, + g_strdup_printf (_("Creating links in “%s”"), + basename)); + + nautilus_progress_info_take_details (job->progress, + g_strdup_printf (ngettext ("Making link to %'d file", + "Making links to %'d files", + left), + left)); + + nautilus_progress_info_set_progress (job->progress, left, total); +} + +static char * +get_abs_path_for_symlink (GFile *file, + GFile *destination) +{ + GFile *root, *parent; + char *relative, *abs; + + if (g_file_is_native (file) || g_file_is_native (destination)) + { + 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, + int files_left) +{ + GFile *src_dir; + GFile *new_dest; + g_autoptr (GFile) dest = NULL; + g_autofree gchar *dest_uri = NULL; + 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, dest); + if (path == NULL) + { + not_local = TRUE; + } + else if (g_file_make_symbolic_link (dest, + path, + common->cancellable, + &error)) + { + if (common->undo_info != NULL) + { + nautilus_file_undo_info_ext_add_origin_target_pair (NAUTILUS_FILE_UNDO_INFO_EXT (common->undo_info), + src, dest); + } + + g_free (path); + if (debuting_files) + { + g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (TRUE)); + } + + nautilus_file_changes_queue_file_added (dest); + dest_uri = g_file_get_uri (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 (error != NULL) + { + g_autofree gchar *basename = NULL; + + if (common->skip_all_error) + { + return; + } + basename = get_basename (src); + primary = g_strdup_printf (_("Error while creating link to %s."), + basename); + if (not_local) + { + secondary = g_strdup (_("Symbolic links only supported for local files")); + details = NULL; + } + else if (IS_IO_ERROR (error, NOT_SUPPORTED)) + { + secondary = g_strdup (_("The target doesn’t support symbolic links.")); + details = NULL; + } + else + { + g_autofree gchar *filename = NULL; + + filename = get_truncated_parse_name (dest_dir); + secondary = g_strdup_printf (_("There was an error creating the symlink in %s."), + filename); + details = error->message; + } + + response = run_warning (common, + primary, + secondary, + details, + files_left > 1, + 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 (); + } + } +} + +static void +link_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CopyMoveJob *job; + + job = user_data; + if (job->done_callback) + { + job->done_callback (job->debuting_files, + !job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + g_list_free_full (job->files, g_object_unref); + g_object_unref (job->destination); + g_hash_table_unref (job->debuting_files); + + finalize_common ((CommonJob *) job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static void +link_task_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CopyMoveJob *job; + CommonJob *common; + GFile *src; + g_autofree char *dest_fs_type = NULL; + int total, left; + int i; + GList *l; + + job = task_data; + common = &job->common; + + nautilus_progress_info_start (job->common.progress); + + verify_destination (&job->common, + job->destination, + NULL, + -1); + if (job_aborted (common)) + { + return; + } + + total = left = g_list_length (job->files); + + report_preparing_link_progress (job, total, left); + + i = 0; + for (l = job->files; + l != NULL && !job_aborted (common); + l = l->next) + { + src = l->data; + + link_file (job, src, job->destination, + &dest_fs_type, job->debuting_files, + left); + report_preparing_link_progress (job, total, --left); + i++; + } +} + +void +nautilus_file_operations_link (GList *files, + GFile *target_dir, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + CopyMoveJob *job; + + job = op_job_new (CopyMoveJob, parent_window, dbus_data); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->files = g_list_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + job->destination = g_object_ref (target_dir); + /* Need to indicate the destination for the operation notification open + * button. */ + nautilus_progress_info_set_destination (((CommonJob *) job)->progress, target_dir); + job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); + + if (!nautilus_file_undo_manager_is_operating ()) + { + g_autoptr (GFile) src_dir = NULL; + + src_dir = g_file_get_parent (files->data); + job->common.undo_info = nautilus_file_undo_info_ext_new (NAUTILUS_FILE_UNDO_OP_CREATE_LINK, + g_list_length (files), + src_dir, target_dir); + } + + task = g_task_new (NULL, job->common.cancellable, link_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, link_task_thread_func); +} + + +void +nautilus_file_operations_duplicate (GList *files, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + CopyMoveJob *job; + g_autoptr (GFile) parent = NULL; + + job = op_job_new (CopyMoveJob, parent_window, dbus_data); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->files = g_list_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + job->destination = NULL; + /* Duplicate files doesn't have a destination, since is the same as source. + * For that set as destination the source parent folder */ + parent = g_file_get_parent (files->data); + /* Need to indicate the destination for the operation notification open + * button. */ + nautilus_progress_info_set_destination (((CommonJob *) job)->progress, parent); + job->debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); + + if (!nautilus_file_undo_manager_is_operating ()) + { + g_autoptr (GFile) src_dir = NULL; + + src_dir = g_file_get_parent (files->data); + job->common.undo_info = + nautilus_file_undo_info_ext_new (NAUTILUS_FILE_UNDO_OP_DUPLICATE, + g_list_length (files), + src_dir, src_dir); + } + + task = g_task_new (NULL, job->common.cancellable, copy_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, nautilus_file_operations_copy); +} + +static void +set_permissions_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SetPermissionsJob *job; + + job = user_data; + + g_object_unref (job->file); + + if (job->done_callback) + { + job->done_callback (!job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + finalize_common ((CommonJob *) job); +} + +static void +set_permissions_file (SetPermissionsJob *job, + GFile *file, + GFileInfo *info); + +static void +set_permissions_contained_files (SetPermissionsJob *job, + GFile *file) +{ + CommonJob *common; + GFileEnumerator *enumerator; + + common = (CommonJob *) job; + + 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) + { + GFileInfo *child_info; + + while (!job_aborted (common) && + (child_info = g_file_enumerator_next_file (enumerator, common->cancellable, NULL)) != NULL) + { + GFile *child; + + 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); + } +} + +static void +set_permissions_file (SetPermissionsJob *job, + GFile *file, + GFileInfo *info) +{ + CommonJob *common; + gboolean free_info; + guint32 current; + guint32 value; + guint32 mask; + + common = (CommonJob *) job; + + nautilus_progress_info_pulse_progress (common->progress); + + free_info = FALSE; + if (info == NULL) + { + free_info = TRUE; + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_TYPE "," + G_FILE_ATTRIBUTE_UNIX_MODE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + common->cancellable, + NULL); + /* Ignore errors */ + if (info == NULL) + { + return; + } + } + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + { + value = job->dir_permissions; + mask = job->dir_mask; + } + else + { + value = job->file_permissions; + mask = job->file_mask; + } + + + if (!job_aborted (common) && + g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_UNIX_MODE)) + { + current = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_MODE); + + if (common->undo_info != NULL) + { + nautilus_file_undo_info_rec_permissions_add_file (NAUTILUS_FILE_UNDO_INFO_REC_PERMISSIONS (common->undo_info), + file, current); + } + + 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) + { + set_permissions_contained_files (job, file); + } + if (free_info) + { + g_object_unref (info); + } +} + +static void +set_permissions_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + SetPermissionsJob *job = task_data; + CommonJob *common; + + common = (CommonJob *) job; + + nautilus_progress_info_set_status (common->progress, + _("Setting permissions")); + + nautilus_progress_info_start (job->common.progress); + set_permissions_contained_files (job, job->file); +} + +void +nautilus_file_set_permissions_recursive (const char *directory, + guint32 file_permissions, + guint32 file_mask, + guint32 dir_permissions, + guint32 dir_mask, + NautilusOpCallback callback, + gpointer callback_data) +{ + g_autoptr (GTask) task = NULL; + SetPermissionsJob *job; + + job = op_job_new (SetPermissionsJob, NULL, 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; + + if (!nautilus_file_undo_manager_is_operating ()) + { + job->common.undo_info = + nautilus_file_undo_info_rec_permissions_new (job->file, + file_permissions, file_mask, + dir_permissions, dir_mask); + } + + task = g_task_new (NULL, NULL, set_permissions_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, set_permissions_thread_func); +} + +static GList * +location_list_from_uri_list (const GList *uris) +{ + const GList *l; + GList *files; + GFile *f; + + files = NULL; + for (l = uris; l != NULL; l = l->next) + { + f = g_file_new_for_uri (l->data); + files = g_list_prepend (files, f); + } + + return g_list_reverse (files); +} + +typedef struct +{ + NautilusCopyCallback real_callback; + gpointer real_data; +} MoveTrashCBData; + +static void +callback_for_move_to_trash (GHashTable *debuting_uris, + gboolean user_cancelled, + MoveTrashCBData *data) +{ + if (data->real_callback) + { + data->real_callback (debuting_uris, !user_cancelled, data->real_data); + } + g_slice_free (MoveTrashCBData, data); +} + +void +nautilus_file_operations_copy_move (const GList *item_uris, + const char *target_dir, + GdkDragAction copy_action, + GtkWidget *parent_view, + NautilusFileOperationsDBusData *dbus_data, + NautilusCopyCallback done_callback, + gpointer done_callback_data) +{ + GList *locations; + GList *p; + GFile *dest, *src_dir; + GtkWindow *parent_window; + gboolean target_is_mapping; + gboolean have_nonmapping_source; + + dest = NULL; + target_is_mapping = FALSE; + have_nonmapping_source = FALSE; + + if (target_dir) + { + dest = g_file_new_for_uri (target_dir); + if (g_file_has_uri_scheme (dest, "burn")) + { + target_is_mapping = TRUE; + } + } + + locations = location_list_from_uri_list (item_uris); + + for (p = locations; p != NULL; p = p->next) + { + if (!g_file_has_uri_scheme ((GFile * ) p->data, "burn")) + { + have_nonmapping_source = TRUE; + } + } + + if (target_is_mapping && have_nonmapping_source && copy_action == GDK_ACTION_MOVE) + { + /* never move to "burn:///", but fall back to copy. + * This is a workaround, because otherwise the source files would be removed. + */ + copy_action = GDK_ACTION_COPY; + } + + parent_window = NULL; + if (parent_view) + { + parent_window = (GtkWindow *) gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); + } + + if (copy_action == GDK_ACTION_COPY) + { + src_dir = g_file_get_parent (locations->data); + if (target_dir == NULL || + (src_dir != NULL && + g_file_equal (src_dir, dest))) + { + nautilus_file_operations_duplicate (locations, + parent_window, + dbus_data, + done_callback, done_callback_data); + } + else + { + nautilus_file_operations_copy_async (locations, + dest, + parent_window, + dbus_data, + done_callback, done_callback_data); + } + if (src_dir) + { + g_object_unref (src_dir); + } + } + else if (copy_action == GDK_ACTION_MOVE) + { + if (g_file_has_uri_scheme (dest, "trash")) + { + MoveTrashCBData *cb_data; + + cb_data = g_slice_new0 (MoveTrashCBData); + cb_data->real_callback = done_callback; + cb_data->real_data = done_callback_data; + + nautilus_file_operations_trash_or_delete_async (locations, + parent_window, + dbus_data, + (NautilusDeleteCallback) callback_for_move_to_trash, + cb_data); + } + else + { + nautilus_file_operations_move_async (locations, + dest, + parent_window, + dbus_data, + done_callback, done_callback_data); + } + } + else + { + nautilus_file_operations_link (locations, + dest, + parent_window, + dbus_data, + done_callback, done_callback_data); + } + + g_list_free_full (locations, g_object_unref); + if (dest) + { + g_object_unref (dest); + } +} + +static void +create_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CreateJob *job; + + job = user_data; + if (job->done_callback) + { + job->done_callback (job->created_file, + !job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + g_object_unref (job->dest_dir); + if (job->src) + { + g_object_unref (job->src); + } + g_free (job->src_data); + g_free (job->filename); + if (job->created_file) + { + g_object_unref (job->created_file); + } + + finalize_common ((CommonJob *) job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static void +create_task_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CreateJob *job; + CommonJob *common; + int count; + g_autoptr (GFile) dest = NULL; + g_autofree gchar *dest_uri = NULL; + g_autofree char *filename = NULL; + char *filename_base; + g_autofree char *dest_fs_type = NULL; + 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, offset; + + job = task_data; + common = &job->common; + + nautilus_progress_info_start (job->common.progress); + + handled_invalid_filename = FALSE; + + max_length = nautilus_get_max_child_name_length_for_location (job->dest_dir); + + verify_destination (common, + job->dest_dir, + NULL, -1); + if (job_aborted (common)) + { + return; + } + + 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) + { + g_autofree char *basename = NULL; + + basename = g_file_get_basename (job->src); + filename = g_strdup_printf ("%s", basename); + } + if (filename == NULL) + { + /* localizers: the initial name of a new empty document */ + filename = g_strdup (_("Untitled Document")); + 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); + + if (res) + { + GFile *real; + + real = map_possibly_volatile_file_to_real (dest, common->cancellable, &error); + if (real == NULL) + { + res = FALSE; + } + else + { + g_object_unref (dest); + dest = real; + } + } + + if (res && common->undo_info != NULL) + { + nautilus_file_undo_info_create_set_data (NAUTILUS_FILE_UNDO_INFO_CREATE (common->undo_info), + dest, NULL, 0); + } + } + else + { + if (job->src) + { + res = g_file_copy (job->src, + dest, + G_FILE_COPY_TARGET_DEFAULT_PERMS, + common->cancellable, + NULL, NULL, + &error); + + if (res) + { + GFile *real; + + real = map_possibly_volatile_file_to_real (dest, common->cancellable, &error); + if (real == NULL) + { + res = FALSE; + } + else + { + g_object_unref (dest); + dest = real; + } + } + + if (res && common->undo_info != NULL) + { + g_autofree gchar *uri = NULL; + + uri = g_file_get_uri (job->src); + nautilus_file_undo_info_create_set_data (NAUTILUS_FILE_UNDO_INFO_CREATE (common->undo_info), + dest, uri, 0); + } + } + 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) + { + GFile *real; + + real = map_possibly_volatile_file_to_real_on_write (dest, + out, + common->cancellable, + &error); + if (real == NULL) + { + res = FALSE; + g_object_unref (out); + } + else + { + g_object_unref (dest); + dest = real; + + 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); + + if (res && common->undo_info != NULL) + { + nautilus_file_undo_info_create_set_data (NAUTILUS_FILE_UNDO_INFO_CREATE (common->undo_info), + dest, data, length); + } + } + + /* This will close if the write failed and we didn't close */ + g_object_unref (out); + } + } + else + { + res = FALSE; + } + } + } + + if (res) + { + job->created_file = g_object_ref (dest); + nautilus_file_changes_queue_file_added (dest); + dest_uri = g_file_get_uri (dest); + gtk_recent_manager_add_item (gtk_recent_manager_get_default (), dest_uri); + } + else + { + g_assert (error != NULL); + + if (IS_IO_ERROR (error, INVALID_FILENAME) && + !handled_invalid_filename) + { + g_autofree gchar *new_filename = NULL; + + handled_invalid_filename = TRUE; + + g_assert (dest_fs_type == NULL); + dest_fs_type = query_fs_type (job->dest_dir, common->cancellable); + + if (count == 1) + { + new_filename = g_strdup (filename); + } + else + { + g_autofree char *filename2 = NULL; + g_autofree char *suffix = NULL; + + filename_base = filename; + if (job->src != NULL) + { + g_autoptr (NautilusFile) file = NULL; + file = nautilus_file_get (job->src); + if (!nautilus_file_is_directory (file)) + { + filename_base = eel_filename_strip_extension (filename); + } + } + + offset = strlen (filename_base); + suffix = g_strdup (filename + offset); + + filename2 = g_strdup_printf ("%s %d%s", filename_base, count, suffix); + + 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); + } + } + + 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_error_free (error); + goto retry; + } + } + + if (IS_IO_ERROR (error, EXISTS)) + { + g_autofree char *suffix = NULL; + g_autofree gchar *filename2 = NULL; + + g_clear_object (&dest); + + filename_base = filename; + if (job->src != NULL) + { + g_autoptr (NautilusFile) file = NULL; + + file = nautilus_file_get (job->src); + if (!nautilus_file_is_directory (file)) + { + filename_base = eel_filename_strip_extension (filename); + } + } + + + offset = strlen (filename_base); + suffix = g_strdup (filename + offset); + + filename2 = g_strdup_printf ("%s %d%s", filename_base, ++count, suffix); + + if (max_length > 0 && strlen (filename2) > max_length) + { + g_autofree char *new_filename = NULL; + + new_filename = shorten_utf8_string (filename2, strlen (filename2) - max_length); + if (new_filename != NULL) + { + g_free (filename2); + filename2 = new_filename; + } + } + + 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_error_free (error); + goto retry; + } + else if (IS_IO_ERROR (error, CANCELLED)) + { + g_error_free (error); + } + /* Other error */ + else + { + g_autofree gchar *basename = NULL; + g_autofree gchar *parse_name = NULL; + + basename = get_basename (dest); + if (job->make_dir) + { + primary = g_strdup_printf (_("Error while creating directory %s."), + basename); + } + else + { + primary = g_strdup_printf (_("Error while creating file %s."), + basename); + } + parse_name = get_truncated_parse_name (job->dest_dir); + secondary = g_strdup_printf (_("There was an error creating the directory in %s."), + parse_name); + + details = error->message; + + response = run_warning (common, + primary, + secondary, + details, + FALSE, + 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 (); + } + } + } +} + +void +nautilus_file_operations_new_folder (GtkWidget *parent_view, + NautilusFileOperationsDBusData *dbus_data, + const char *parent_dir, + const char *folder_name, + NautilusCreateCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + 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, dbus_data); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->dest_dir = g_file_new_for_uri (parent_dir); + job->filename = g_strdup (folder_name); + job->make_dir = TRUE; + + if (!nautilus_file_undo_manager_is_operating ()) + { + job->common.undo_info = nautilus_file_undo_info_create_new (NAUTILUS_FILE_UNDO_OP_CREATE_FOLDER); + } + + task = g_task_new (NULL, job->common.cancellable, create_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, create_task_thread_func); +} + +void +nautilus_file_operations_new_file_from_template (GtkWidget *parent_view, + const char *parent_dir, + const char *target_filename, + const char *template_uri, + NautilusCreateCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + 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, NULL); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->dest_dir = g_file_new_for_uri (parent_dir); + job->filename = g_strdup (target_filename); + + if (template_uri) + { + job->src = g_file_new_for_uri (template_uri); + } + + if (!nautilus_file_undo_manager_is_operating ()) + { + job->common.undo_info = nautilus_file_undo_info_create_new (NAUTILUS_FILE_UNDO_OP_CREATE_FILE_FROM_TEMPLATE); + } + + task = g_task_new (NULL, job->common.cancellable, create_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, create_task_thread_func); +} + +void +nautilus_file_operations_new_file (GtkWidget *parent_view, + const char *parent_dir, + const char *target_filename, + const char *initial_contents, + int length, + NautilusCreateCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + 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, NULL); + job->done_callback = done_callback; + job->done_callback_data = done_callback_data; + job->dest_dir = g_file_new_for_uri (parent_dir); + job->src_data = g_memdup (initial_contents, length); + job->length = length; + job->filename = g_strdup (target_filename); + + if (!nautilus_file_undo_manager_is_operating ()) + { + job->common.undo_info = nautilus_file_undo_info_create_new (NAUTILUS_FILE_UNDO_OP_CREATE_EMPTY_FILE); + } + + task = g_task_new (NULL, job->common.cancellable, create_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, create_task_thread_func); +} + + + +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) + { + gboolean should_recurse; + + /* The g_file_delete operation works differently for locations provided + * by the trash backend as it prevents modifications of trashed items + * For that reason, it is enough to call g_file_delete on top-level + * items only. + */ + should_recurse = !g_file_has_uri_scheme (file, "trash"); + + 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) + { + gboolean is_dir; + + child = g_file_get_child (file, + g_file_info_get_name (info)); + is_dir = (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY); + + delete_trash_file (job, child, TRUE, should_recurse && is_dir); + 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 void +empty_trash_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + EmptyTrashJob *job; + + job = user_data; + + g_list_free_full (job->trash_dirs, g_object_unref); + + if (job->done_callback) + { + job->done_callback (!job_aborted ((CommonJob *) job), + job->done_callback_data); + } + + finalize_common ((CommonJob *) job); +} + +static void +empty_trash_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + EmptyTrashJob *job = task_data; + CommonJob *common; + GList *l; + gboolean confirmed; + + common = (CommonJob *) job; + + nautilus_progress_info_start (job->common.progress); + + if (job->should_confirm) + { + confirmed = confirm_empty_trash (common); + } + else + { + confirmed = TRUE; + } + if (confirmed) + { + for (l = job->trash_dirs; + l != NULL && !job_aborted (common); + l = l->next) + { + delete_trash_file (common, l->data, FALSE, TRUE); + } + } +} + +void +nautilus_file_operations_empty_trash (GtkWidget *parent_view, + gboolean ask_confirmation, + NautilusFileOperationsDBusData *dbus_data) +{ + g_autoptr (GTask) task = NULL; + EmptyTrashJob *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 (EmptyTrashJob, parent_window, dbus_data); + job->trash_dirs = g_list_prepend (job->trash_dirs, + g_file_new_for_uri ("trash:")); + job->should_confirm = ask_confirmation; + + inhibit_power_manager ((CommonJob *) job, _("Emptying Trash")); + + task = g_task_new (NULL, NULL, empty_trash_task_done, job); + g_task_set_task_data (task, job, NULL); + g_task_run_in_thread (task, empty_trash_thread_func); +} + +static void +extract_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + ExtractJob *extract_job; + + extract_job = user_data; + + if (extract_job->done_callback) + { + extract_job->done_callback (extract_job->output_files, + extract_job->done_callback_data); + } + + g_list_free_full (extract_job->source_files, g_object_unref); + g_list_free_full (extract_job->output_files, g_object_unref); + g_object_unref (extract_job->destination_directory); + + finalize_common ((CommonJob *) extract_job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static GFile * +extract_job_on_decide_destination (AutoarExtractor *extractor, + GFile *destination, + GList *files, + gpointer user_data) +{ + ExtractJob *extract_job = user_data; + GFile *decided_destination; + g_autofree char *basename = NULL; + + nautilus_progress_info_set_details (extract_job->common.progress, + _("Verifying destination")); + + basename = g_file_get_basename (destination); + decided_destination = nautilus_generate_unique_file_in_directory (extract_job->destination_directory, + basename); + + if (job_aborted ((CommonJob *) extract_job)) + { + g_object_unref (decided_destination); + return NULL; + } + + extract_job->output_files = g_list_prepend (extract_job->output_files, + decided_destination); + + return g_object_ref (decided_destination); +} + +static void +extract_job_on_progress (AutoarExtractor *extractor, + guint64 archive_current_decompressed_size, + guint archive_current_decompressed_files, + gpointer user_data) +{ + ExtractJob *extract_job = user_data; + CommonJob *common = user_data; + GFile *source_file; + char *details; + double elapsed; + double transfer_rate; + int remaining_time; + guint64 archive_total_decompressed_size; + gdouble archive_weight; + gdouble archive_decompress_progress; + guint64 job_completed_size; + gdouble job_progress; + g_autofree gchar *basename = NULL; + g_autofree gchar *formatted_size_job_completed_size = NULL; + g_autofree gchar *formatted_size_total_compressed_size = NULL; + + source_file = autoar_extractor_get_source_file (extractor); + + basename = get_basename (source_file); + nautilus_progress_info_take_status (common->progress, + g_strdup_printf (_("Extracting “%s”"), + basename)); + + archive_total_decompressed_size = autoar_extractor_get_total_size (extractor); + + archive_decompress_progress = (gdouble) archive_current_decompressed_size / + (gdouble) archive_total_decompressed_size; + + archive_weight = 0; + if (extract_job->total_compressed_size) + { + archive_weight = (gdouble) extract_job->archive_compressed_size / + (gdouble) extract_job->total_compressed_size; + } + + job_progress = archive_decompress_progress * archive_weight + extract_job->base_progress; + + elapsed = g_timer_elapsed (common->time, NULL); + + transfer_rate = 0; + remaining_time = -1; + + job_completed_size = job_progress * extract_job->total_compressed_size; + + if (elapsed > 0) + { + transfer_rate = job_completed_size / elapsed; + } + if (transfer_rate > 0) + { + remaining_time = (extract_job->total_compressed_size - job_completed_size) / + transfer_rate; + } + + formatted_size_job_completed_size = g_format_size (job_completed_size); + formatted_size_total_compressed_size = g_format_size (extract_job->total_compressed_size); + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE || + transfer_rate == 0) + { + /* To translators: %s will expand to a size like "2 bytes" or + * "3 MB", so something like "4 kb / 4 MB" + */ + details = g_strdup_printf (_("%s / %s"), formatted_size_job_completed_size, + formatted_size_total_compressed_size); + } + else + { + g_autofree gchar *formatted_time = NULL; + g_autofree gchar *formatted_size_transfer_rate = NULL; + + formatted_time = get_formatted_time (remaining_time); + formatted_size_transfer_rate = g_format_size ((goffset) transfer_rate); + /* To translators: %s will expand to a size like "2 bytes" or + * "3 MB", %s to a time duration like "2 minutes". So the whole + * thing will be something like + * "2 kb / 4 MB -- 2 hours left (4kb/sec)" + * + * The singular/plural form will be used depending on the + * remaining time (i.e. the %s argument). + */ + details = g_strdup_printf (ngettext ("%s / %s \xE2\x80\x94 %s left (%s/sec)", + "%s / %s \xE2\x80\x94 %s left (%s/sec)", + seconds_count_format_time_units (remaining_time)), + formatted_size_job_completed_size, + formatted_size_total_compressed_size, + formatted_time, + formatted_size_transfer_rate); + } + + nautilus_progress_info_take_details (common->progress, details); + + if (elapsed > SECONDS_NEEDED_FOR_APROXIMATE_TRANSFER_RATE) + { + nautilus_progress_info_set_remaining_time (common->progress, + remaining_time); + nautilus_progress_info_set_elapsed_time (common->progress, + elapsed); + } + + nautilus_progress_info_set_progress (common->progress, job_progress, 1); +} + +static void +extract_job_on_error (AutoarExtractor *extractor, + GError *error, + gpointer user_data) +{ + ExtractJob *extract_job = user_data; + GFile *source_file; + gint response_id; + g_autofree gchar *basename = NULL; + + source_file = autoar_extractor_get_source_file (extractor); + + if (IS_IO_ERROR (error, NOT_SUPPORTED)) + { + handle_unsupported_compressed_file (extract_job->common.parent_window, + source_file); + + return; + } + + basename = get_basename (source_file); + nautilus_progress_info_take_status (extract_job->common.progress, + g_strdup_printf (_("Error extracting “%s”"), + basename)); + + response_id = run_warning ((CommonJob *) extract_job, + g_strdup_printf (_("There was an error while extracting “%s”."), + basename), + g_strdup (error->message), + NULL, + FALSE, + CANCEL, + SKIP, + NULL); + + if (response_id == 0 || response_id == GTK_RESPONSE_DELETE_EVENT) + { + abort_job ((CommonJob *) extract_job); + } +} + +static void +extract_job_on_completed (AutoarExtractor *extractor, + gpointer user_data) +{ + ExtractJob *extract_job = user_data; + GFile *output_file; + + output_file = G_FILE (extract_job->output_files->data); + + nautilus_file_changes_queue_file_added (output_file); +} + +static void +extract_job_on_scanned (AutoarExtractor *extractor, + guint total_files, + gpointer user_data) +{ + guint64 total_size; + ExtractJob *extract_job; + GFile *source_file; + g_autofree gchar *basename = NULL; + GFileInfo *fsinfo; + guint64 free_size; + + extract_job = user_data; + total_size = autoar_extractor_get_total_size (extractor); + source_file = autoar_extractor_get_source_file (extractor); + basename = get_basename (source_file); + + fsinfo = g_file_query_filesystem_info (source_file, + G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," + G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, + extract_job->common.cancellable, + NULL); + free_size = g_file_info_get_attribute_uint64 (fsinfo, + G_FILE_ATTRIBUTE_FILESYSTEM_FREE); + + /* FIXME: G_MAXUINT64 is the value used by autoar when the file size cannot + * be determined. Ideally an API should be used instead. + */ + if (total_size != G_MAXUINT64 && total_size > free_size) + { + nautilus_progress_info_take_status (extract_job->common.progress, + g_strdup_printf (_("Error extracting “%s”"), + basename)); + run_error (&extract_job->common, + g_strdup_printf (_("Not enough free space to extract %s"), basename), + NULL, + NULL, + FALSE, + CANCEL, + NULL); + + abort_job ((CommonJob *) extract_job); + } +} + +static void +report_extract_final_progress (ExtractJob *extract_job, + gint total_files) +{ + char *status; + g_autofree gchar *basename_dest = NULL; + g_autofree gchar *formatted_size = NULL; + + nautilus_progress_info_set_destination (extract_job->common.progress, + extract_job->destination_directory); + basename_dest = get_basename (extract_job->destination_directory); + + if (total_files == 1) + { + GFile *source_file; + g_autofree gchar *basename = NULL; + + source_file = G_FILE (extract_job->source_files->data); + basename = get_basename (source_file); + status = g_strdup_printf (_("Extracted “%s” to “%s”"), + basename, + basename_dest); + } + else + { + status = g_strdup_printf (ngettext ("Extracted %'d file to “%s”", + "Extracted %'d files to “%s”", + total_files), + total_files, + basename_dest); + } + + nautilus_progress_info_take_status (extract_job->common.progress, + status); + formatted_size = g_format_size (extract_job->total_compressed_size); + nautilus_progress_info_take_details (extract_job->common.progress, + g_strdup_printf (_("%s / %s"), + formatted_size, + formatted_size)); +} + +static void +extract_task_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + ExtractJob *extract_job = task_data; + GList *l; + GList *existing_output_files = NULL; + gint total_files; + g_autofree guint64 *archive_compressed_sizes = NULL; + gint i; + + g_timer_start (extract_job->common.time); + + nautilus_progress_info_start (extract_job->common.progress); + + nautilus_progress_info_set_details (extract_job->common.progress, + _("Preparing to extract")); + + total_files = g_list_length (extract_job->source_files); + + archive_compressed_sizes = g_malloc0_n (total_files, sizeof (guint64)); + extract_job->total_compressed_size = 0; + + for (l = extract_job->source_files, i = 0; + l != NULL && !job_aborted ((CommonJob *) extract_job); + l = l->next, i++) + { + GFile *source_file; + g_autoptr (GFileInfo) info = NULL; + + source_file = G_FILE (l->data); + info = g_file_query_info (source_file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + extract_job->common.cancellable, + NULL); + + if (info) + { + archive_compressed_sizes[i] = g_file_info_get_size (info); + extract_job->total_compressed_size += archive_compressed_sizes[i]; + } + } + + extract_job->base_progress = 0; + + for (l = extract_job->source_files, i = 0; + l != NULL && !job_aborted ((CommonJob *) extract_job); + l = l->next, i++) + { + g_autoptr (AutoarExtractor) extractor = NULL; + + extractor = autoar_extractor_new (G_FILE (l->data), + extract_job->destination_directory); + + autoar_extractor_set_notify_interval (extractor, + PROGRESS_NOTIFY_INTERVAL); + g_signal_connect (extractor, "scanned", + G_CALLBACK (extract_job_on_scanned), + extract_job); + g_signal_connect (extractor, "error", + G_CALLBACK (extract_job_on_error), + extract_job); + g_signal_connect (extractor, "decide-destination", + G_CALLBACK (extract_job_on_decide_destination), + extract_job); + g_signal_connect (extractor, "progress", + G_CALLBACK (extract_job_on_progress), + extract_job); + g_signal_connect (extractor, "completed", + G_CALLBACK (extract_job_on_completed), + extract_job); + + extract_job->archive_compressed_size = archive_compressed_sizes[i]; + + autoar_extractor_start (extractor, + extract_job->common.cancellable); + + g_signal_handlers_disconnect_by_data (extractor, + extract_job); + + extract_job->base_progress += (gdouble) extract_job->archive_compressed_size / + (gdouble) extract_job->total_compressed_size; + } + + if (!job_aborted ((CommonJob *) extract_job)) + { + report_extract_final_progress (extract_job, total_files); + } + + for (l = extract_job->output_files; l != NULL; l = l->next) + { + GFile *output_file; + + output_file = G_FILE (l->data); + + if (g_file_query_exists (output_file, NULL)) + { + existing_output_files = g_list_prepend (existing_output_files, + g_object_ref (output_file)); + } + } + + g_list_free_full (extract_job->output_files, g_object_unref); + + extract_job->output_files = existing_output_files; + + if (extract_job->common.undo_info) + { + if (extract_job->output_files) + { + NautilusFileUndoInfoExtract *undo_info; + + undo_info = NAUTILUS_FILE_UNDO_INFO_EXTRACT (extract_job->common.undo_info); + + nautilus_file_undo_info_extract_set_outputs (undo_info, + extract_job->output_files); + } + else + { + /* There is nothing to undo if there is no output */ + g_clear_object (&extract_job->common.undo_info); + } + } +} + +void +nautilus_file_operations_extract_files (GList *files, + GFile *destination_directory, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusExtractCallback done_callback, + gpointer done_callback_data) +{ + ExtractJob *extract_job; + g_autoptr (GTask) task = NULL; + + extract_job = op_job_new (ExtractJob, parent_window, dbus_data); + extract_job->source_files = g_list_copy_deep (files, + (GCopyFunc) g_object_ref, + NULL); + extract_job->destination_directory = g_object_ref (destination_directory); + extract_job->done_callback = done_callback; + extract_job->done_callback_data = done_callback_data; + + inhibit_power_manager ((CommonJob *) extract_job, _("Extracting Files")); + + if (!nautilus_file_undo_manager_is_operating ()) + { + extract_job->common.undo_info = nautilus_file_undo_info_extract_new (files, + destination_directory); + } + + task = g_task_new (NULL, extract_job->common.cancellable, + extract_task_done, extract_job); + g_task_set_task_data (task, extract_job, NULL); + g_task_run_in_thread (task, extract_task_thread_func); +} + +static void +compress_task_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + CompressJob *compress_job = user_data; + + if (compress_job->done_callback) + { + compress_job->done_callback (compress_job->output_file, + compress_job->success, + compress_job->done_callback_data); + } + + g_object_unref (compress_job->output_file); + g_list_free_full (compress_job->source_files, g_object_unref); + + finalize_common ((CommonJob *) compress_job); + + nautilus_file_changes_consume_changes (TRUE); +} + +static void +compress_job_on_progress (AutoarCompressor *compressor, + guint64 completed_size, + guint completed_files, + gpointer user_data) +{ + CompressJob *compress_job = user_data; + CommonJob *common = user_data; + char *status; + char *details; + int files_left; + double elapsed; + double transfer_rate; + int remaining_time; + g_autofree gchar *basename_output_file = NULL; + + files_left = compress_job->total_files - completed_files; + basename_output_file = get_basename (compress_job->output_file); + if (compress_job->total_files == 1) + { + g_autofree gchar *basename_data = NULL; + + basename_data = get_basename (G_FILE (compress_job->source_files->data)); + status = g_strdup_printf (_("Compressing “%s” into “%s”"), + basename_data, + basename_output_file); + } + else + { + status = g_strdup_printf (ngettext ("Compressing %'d file into “%s”", + "Compressing %'d files into “%s”", + compress_job->total_files), + compress_job->total_files, + basename_output_file); + } + nautilus_progress_info_take_status (common->progress, status); + + elapsed = g_timer_elapsed (common->time, NULL); + + transfer_rate = 0; + remaining_time = -1; + + if (elapsed > 0) + { + if (completed_size > 0) + { + transfer_rate = completed_size / elapsed; + remaining_time = (compress_job->total_size - completed_size) / transfer_rate; + } + else if (completed_files > 0) + { + transfer_rate = completed_files / elapsed; + remaining_time = (compress_job->total_files - completed_files) / transfer_rate; + } + } + + if (elapsed < SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE || + transfer_rate == 0) + { + if (compress_job->total_files == 1) + { + g_autofree gchar *formatted_size_completed_size = NULL; + g_autofree gchar *formatted_size_total_size = NULL; + + formatted_size_completed_size = g_format_size (completed_size); + formatted_size_total_size = g_format_size (compress_job->total_size); + /* To translators: %s will expand to a size like "2 bytes" or "3 MB", so something like "4 kb / 4 MB" */ + details = g_strdup_printf (_("%s / %s"), formatted_size_completed_size, + formatted_size_total_size); + } + else + { + details = g_strdup_printf (_("%'d / %'d"), + files_left > 0 ? completed_files + 1 : completed_files, + compress_job->total_files); + } + } + else + { + if (compress_job->total_files == 1) + { + g_autofree gchar *formatted_size_completed_size = NULL; + g_autofree gchar *formatted_size_total_size = NULL; + + formatted_size_completed_size = g_format_size (completed_size); + formatted_size_total_size = g_format_size (compress_job->total_size); + + if (files_left > 0) + { + g_autofree gchar *formatted_time = NULL; + g_autofree gchar *formatted_size_transfer_rate = NULL; + + formatted_time = get_formatted_time (remaining_time); + formatted_size_transfer_rate = g_format_size ((goffset) transfer_rate); + /* To translators: %s will expand to a size like "2 bytes" or "3 MB", %s to a time duration like + * "2 minutes". So the whole thing will be something like "2 kb / 4 MB -- 2 hours left (4kb/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + details = g_strdup_printf (ngettext ("%s / %s \xE2\x80\x94 %s left (%s/sec)", + "%s / %s \xE2\x80\x94 %s left (%s/sec)", + seconds_count_format_time_units (remaining_time)), + formatted_size_completed_size, + formatted_size_total_size, + formatted_time, + formatted_size_transfer_rate); + } + else + { + /* To translators: %s will expand to a size like "2 bytes" or "3 MB". */ + details = g_strdup_printf (_("%s / %s"), + formatted_size_completed_size, + formatted_size_total_size); + } + } + else + { + if (files_left > 0) + { + g_autofree gchar *formatted_time = NULL; + g_autofree gchar *formatted_size = NULL; + + formatted_time = get_formatted_time (remaining_time); + formatted_size = g_format_size ((goffset) transfer_rate); + /* To translators: %s will expand to a time duration like "2 minutes". + * So the whole thing will be something like "1 / 5 -- 2 hours left (4kb/sec)" + * + * The singular/plural form will be used depending on the remaining time (i.e. the %s argument). + */ + details = g_strdup_printf (ngettext ("%'d / %'d \xE2\x80\x94 %s left (%s/sec)", + "%'d / %'d \xE2\x80\x94 %s left (%s/sec)", + seconds_count_format_time_units (remaining_time)), + completed_files + 1, compress_job->total_files, + formatted_time, + formatted_size); + } + else + { + /* To translators: %'d is the number of files completed for the operation, + * so it will be something like 2/14. */ + details = g_strdup_printf (_("%'d / %'d"), + completed_files, + compress_job->total_files); + } + } + } + + nautilus_progress_info_take_details (common->progress, details); + + if (elapsed > SECONDS_NEEDED_FOR_APROXIMATE_TRANSFER_RATE) + { + nautilus_progress_info_set_remaining_time (common->progress, + remaining_time); + nautilus_progress_info_set_elapsed_time (common->progress, + elapsed); + } + + nautilus_progress_info_set_progress (common->progress, + completed_size, + compress_job->total_size); +} + +static void +compress_job_on_error (AutoarCompressor *compressor, + GError *error, + gpointer user_data) +{ + CompressJob *compress_job = user_data; + char *status; + g_autofree gchar *basename_output_file = NULL; + + basename_output_file = get_basename (compress_job->output_file); + if (compress_job->total_files == 1) + { + g_autofree gchar *basename_data = NULL; + + basename_data = get_basename (G_FILE (compress_job->source_files->data)); + status = g_strdup_printf (_("Error compressing “%s” into “%s”"), + basename_data, + basename_output_file); + } + else + { + status = g_strdup_printf (ngettext ("Error compressing %'d file into “%s”", + "Error compressing %'d files into “%s”", + compress_job->total_files), + compress_job->total_files, + basename_output_file); + } + nautilus_progress_info_take_status (compress_job->common.progress, + status); + + run_error ((CommonJob *) compress_job, + g_strdup (_("There was an error while compressing files.")), + g_strdup (error->message), + NULL, + FALSE, + CANCEL, + NULL); + + abort_job ((CommonJob *) compress_job); +} + +static void +compress_job_on_completed (AutoarCompressor *compressor, + gpointer user_data) +{ + CompressJob *compress_job = user_data; + g_autoptr (GFile) destination_directory = NULL; + char *status; + g_autofree gchar *basename_output_file = NULL; + + basename_output_file = get_basename (compress_job->output_file); + if (compress_job->total_files == 1) + { + g_autofree gchar *basename_data = NULL; + + basename_data = get_basename (G_FILE (compress_job->source_files->data)); + status = g_strdup_printf (_("Compressed “%s” into “%s”"), + basename_data, + basename_output_file); + } + else + { + status = g_strdup_printf (ngettext ("Compressed %'d file into “%s”", + "Compressed %'d files into “%s”", + compress_job->total_files), + compress_job->total_files, + basename_output_file); + } + + nautilus_progress_info_take_status (compress_job->common.progress, + status); + + nautilus_file_changes_queue_file_added (compress_job->output_file); + + destination_directory = g_file_get_parent (compress_job->output_file); + nautilus_progress_info_set_destination (compress_job->common.progress, + destination_directory); +} + +static void +compress_task_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CompressJob *compress_job = task_data; + SourceInfo source_info; + g_autoptr (AutoarCompressor) compressor = NULL; + + g_timer_start (compress_job->common.time); + + nautilus_progress_info_start (compress_job->common.progress); + + scan_sources (compress_job->source_files, + &source_info, + (CommonJob *) compress_job, + OP_KIND_COMPRESS); + + compress_job->total_files = source_info.num_files; + compress_job->total_size = source_info.num_bytes; + + compressor = autoar_compressor_new (compress_job->source_files, + compress_job->output_file, + compress_job->format, + compress_job->filter, + FALSE); + + autoar_compressor_set_output_is_dest (compressor, TRUE); + + autoar_compressor_set_notify_interval (compressor, + PROGRESS_NOTIFY_INTERVAL); + + g_signal_connect (compressor, "progress", + G_CALLBACK (compress_job_on_progress), compress_job); + g_signal_connect (compressor, "error", + G_CALLBACK (compress_job_on_error), compress_job); + g_signal_connect (compressor, "completed", + G_CALLBACK (compress_job_on_completed), compress_job); + autoar_compressor_start (compressor, + compress_job->common.cancellable); + + compress_job->success = g_file_query_exists (compress_job->output_file, + NULL); + + /* There is nothing to undo if the output was not created */ + if (compress_job->common.undo_info != NULL && !compress_job->success) + { + g_clear_object (&compress_job->common.undo_info); + } +} + +void +nautilus_file_operations_compress (GList *files, + GFile *output, + AutoarFormat format, + AutoarFilter filter, + GtkWindow *parent_window, + NautilusFileOperationsDBusData *dbus_data, + NautilusCreateCallback done_callback, + gpointer done_callback_data) +{ + g_autoptr (GTask) task = NULL; + CompressJob *compress_job; + + compress_job = op_job_new (CompressJob, parent_window, dbus_data); + compress_job->source_files = g_list_copy_deep (files, + (GCopyFunc) g_object_ref, + NULL); + compress_job->output_file = g_object_ref (output); + compress_job->format = format; + compress_job->filter = filter; + compress_job->done_callback = done_callback; + compress_job->done_callback_data = done_callback_data; + + inhibit_power_manager ((CommonJob *) compress_job, _("Compressing Files")); + + if (!nautilus_file_undo_manager_is_operating ()) + { + compress_job->common.undo_info = nautilus_file_undo_info_compress_new (files, + output, + format, + filter); + } + + task = g_task_new (NULL, compress_job->common.cancellable, + compress_task_done, compress_job); + g_task_set_task_data (task, compress_job, NULL); + g_task_run_in_thread (task, compress_task_thread_func); +} + +#if !defined (NAUTILUS_OMIT_SELF_CHECK) + +void +nautilus_self_check_file_operations (void) +{ + setlocale (LC_MESSAGES, "C"); + + + /* test the next duplicate name generator */ + EEL_CHECK_STRING_RESULT (get_duplicate_name (" (copy)", 1, -1, FALSE), " (another copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo", 1, -1, FALSE), "foo (copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name (".bashrc", 1, -1, FALSE), ".bashrc (copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name (".foo.txt", 1, -1, FALSE), ".foo (copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo", 1, -1, FALSE), "foo foo (copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo.txt", 1, -1, FALSE), "foo (copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo.txt", 1, -1, FALSE), "foo foo (copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo.txt txt", 1, -1, FALSE), "foo foo (copy).txt txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo...txt", 1, -1, FALSE), "foo.. (copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo...", 1, -1, FALSE), "foo... (copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo. (copy)", 1, -1, FALSE), "foo. (another copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (copy)", 1, -1, FALSE), "foo (another copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (copy).txt", 1, -1, FALSE), "foo (another copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (another copy)", 1, -1, FALSE), "foo (3rd copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (another copy).txt", 1, -1, FALSE), "foo (3rd copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (another copy).txt", 1, -1, FALSE), "foo foo (3rd copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (13th copy)", 1, -1, FALSE), "foo (14th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (13th copy).txt", 1, -1, FALSE), "foo (14th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (21st copy)", 1, -1, FALSE), "foo (22nd copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (21st copy).txt", 1, -1, FALSE), "foo (22nd copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (22nd copy)", 1, -1, FALSE), "foo (23rd copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (22nd copy).txt", 1, -1, FALSE), "foo (23rd copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (23rd copy)", 1, -1, FALSE), "foo (24th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (23rd copy).txt", 1, -1, FALSE), "foo (24th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (24th copy)", 1, -1, FALSE), "foo (25th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (24th copy).txt", 1, -1, FALSE), "foo (25th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (24th copy)", 1, -1, FALSE), "foo foo (25th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (24th copy).txt", 1, -1, FALSE), "foo foo (25th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo foo (100000000000000th copy).txt", 1, -1, FALSE), "foo foo (copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (10th copy)", 1, -1, FALSE), "foo (11th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (10th copy).txt", 1, -1, FALSE), "foo (11th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (11th copy)", 1, -1, FALSE), "foo (12th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (11th copy).txt", 1, -1, FALSE), "foo (12th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (12th copy)", 1, -1, FALSE), "foo (13th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (12th copy).txt", 1, -1, FALSE), "foo (13th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (110th copy)", 1, -1, FALSE), "foo (111th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (110th copy).txt", 1, -1, FALSE), "foo (111th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (122nd copy)", 1, -1, FALSE), "foo (123rd copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (122nd copy).txt", 1, -1, FALSE), "foo (123rd copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (123rd copy)", 1, -1, FALSE), "foo (124th copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("foo (123rd copy).txt", 1, -1, FALSE), "foo (124th copy).txt"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("dir.with.dots", 1, -1, TRUE), "dir.with.dots (copy)"); + EEL_CHECK_STRING_RESULT (get_duplicate_name ("dir (copy).dir", 1, -1, TRUE), "dir (another copy).dir"); + + setlocale (LC_MESSAGES, ""); +} + +#endif |