/* 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 . * * Authors: Alexander Larsson * Ettore Perazzoli * Pavel Cisler */ #include #include #include #include #include #include #include #include #include "nautilus-file-operations.h" #include "nautilus-file-changes-queue.h" #include "nautilus-progress-info.h" #include #include #include #include #include #include #include #include "nautilus-dialog-utilities.h" #include "nautilus-error-reporting.h" #include "nautilus-fd-holder.h" #include "nautilus-file-changes-queue.h" #include "nautilus-file-conflict-dialog.h" #include "nautilus-file-operations-dbus-data.h" #include "nautilus-file-private.h" #include "nautilus-file-undo-manager.h" #include "nautilus-file-undo-operations.h" #include "nautilus-file-utilities.h" #include "nautilus-filename-utilities.h" #include "nautilus-operations-ui-manager.h" #include "nautilus-scheme.h" #include "nautilus-tag-manager.h" #include "nautilus-trash-monitor.h" #include "nautilus-ui-utilities.h" #ifdef GDK_WINDOWING_X11 #include #endif #ifdef GDK_WINDOWING_WAYLAND #include #endif typedef struct { GTimer *time; GtkWindow *parent_window; NautilusFileOperationsDBusData *dbus_data; guint inhibit_cookie; NautilusProgressInfo *progress; GCancellable *cancellable; guint64 last_dialog_timestamp; 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; 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; void *src_data; int length; gboolean new_mtime; GFile *created_file; NautilusCreateCallback done_callback; gpointer done_callback_data; } CreateJob; typedef struct { CommonJob common; NautilusFile *file; char *new_name; gboolean cancelled; gboolean success; NautilusOpRenameCallback done_callback; gpointer done_callback_data; } RenameJob; 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; 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_children; goffset num_bytes_children; } SourceDirInfo; typedef struct { int num_files; goffset num_bytes; goffset largest_file_bytes; int num_files_since_progress; OpKind op; GHashTable *scanned_dirs_info; } SourceInfo; typedef struct { int num_files; goffset num_bytes; OpKind op; guint64 last_report_time; int last_reported_files_left; /* * This is used when reporting progress for copy/move operations to not show * the remaining time. This is needed because some GVfs backends doesn't * report progress from those operations. Consequently it looks like that it * is hanged when the remaining time is not updated regularly. See: * https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/605 */ gboolean partial_progress; } TransferInfo; typedef struct { CommonJob common; GList *source_files; GFile *destination_directory; GList *output_files; gboolean destination_decided; gboolean extraction_failed; guint expected_total_files; gdouble base_progress; guint64 archive_compressed_size; guint64 total_compressed_size; gint total_files; NautilusExtractCallback done_callback; gpointer done_callback_data; } ExtractJob; typedef struct { CommonJob common; GList *source_files; GFile *output_file; AutoarFormat format; AutoarFilter filter; gchar *passphrase; guint64 total_size; guint total_files; GError *error; gboolean output_archive_exists; NautilusCreateCallback done_callback; gpointer done_callback_data; } CompressJob; typedef struct { CommonJob common; GFile *location; GFile *dest_dir; gboolean success; char *base_name; GdkTexture *texture; NautilusCopyCallback done_callback; gpointer done_callback_data; } SaveImageJob; static void source_info_clear (SourceInfo *source_info) { if (source_info->scanned_dirs_info != NULL) { g_hash_table_unref (source_info->scanned_dirs_info); } } G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (SourceInfo, source_info_clear) #define SOURCE_INFO_INIT { 0 } #define SECONDS_NEEDED_FOR_RELIABLE_TRANSFER_RATE 8 #define PROGRESS_NOTIFY_INTERVAL_USEC 100 * 1000 #define LONG_JOB_THRESHOLD_IN_SECONDS 2 #define INTERACTIVITY_DELAY_GRACE_PERIOD 1000 * 1000 #define MAXIMUM_DISPLAYED_FILE_NAME_LENGTH 50 #define MAXIMUM_FAT_FILE_SIZE G_MAXUINT32 #define IS_IO_ERROR(__error, KIND) (((__error)->domain == G_IO_ERROR && (__error)->code == G_IO_ERROR_ ## KIND)) static void scan_sources (GList *files, SourceInfo *source_info, CommonJob *job, OpKind kind); static void abort_job (CommonJob *job); 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); static gboolean is_dir (GFile *file, GCancellable *cancellable) { GFileType file_type; file_type = g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable); return file_type == G_FILE_TYPE_DIRECTORY; } /* 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 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, G_FILE_QUERY_INFO_NONE, 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 (basename == NULL) { return g_strdup (_("unknown")); } 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 = g_utf8_truncate_middle (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 g_utf8_truncate_middle (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, SCHEME_BURN) || g_file_has_uri_scheme (file, SCHEME_RECENT) || g_test_initialized ()) { 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; const char *heading; const char *body; const char *details; NautilusDialogResponse responses; gboolean delay_interactivity; NautilusDialogResponse 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; } DialogWithResponsesData; static void set_transient_for (GdkSurface *child_surface, const char *parent_handle) { GdkDisplay *display; const char *prefix; display = gdk_surface_get_display (child_surface); #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY (display)) { prefix = "x11:"; if (g_str_has_prefix (parent_handle, prefix)) { const char *handle; GdkSurface *surface; handle = parent_handle + strlen (prefix); surface = gdk_x11_surface_lookup_for_display (display, strtol (handle, NULL, 16)); if (surface != NULL) { gdk_toplevel_set_transient_for (GDK_TOPLEVEL (child_surface), surface); } } } #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_toplevel_set_transient_for_exported (GDK_TOPLEVEL (child_surface), (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_native_get_surface (gtk_widget_get_native (widget)), parent_handle); } static gboolean is_long_job (CommonJob *job) { double elapsed = nautilus_progress_info_get_total_elapsed_time (job->progress); return elapsed > LONG_JOB_THRESHOLD_IN_SECONDS ? TRUE : FALSE; } static void simple_dialog_cb (AdwAlertDialog *dialog, GAsyncResult *result, DialogWithResponsesData *data) { const char *response = adw_alert_dialog_choose_finish (dialog, result); data->result = nautilus_dialog_response_from_string (response); data->completed = TRUE; g_cond_signal (&data->cond); g_mutex_unlock (&data->mutex); } static gboolean do_run_simple_dialog (gpointer _data) { DialogWithResponsesData *data = _data; g_mutex_lock (&data->mutex); /* Create the dialog. */ AdwAlertDialog *dialog = nautilus_dialog_with_responses (data->heading, data->body, data->details, data->delay_interactivity, data->responses); if (data->dbus_data != NULL) { 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); } } adw_alert_dialog_choose (dialog, GTK_WIDGET (data->parent_window), NULL, (GAsyncReadyCallback) simple_dialog_cb, data); return FALSE; } /* NOTE: This frees the primary / secondary strings, in order to * avoid doing that everywhere. So, make sure they are strduped */ static NautilusDialogResponse run_dialog (CommonJob *job, const char *heading, const char *body, const char *details, NautilusDialogResponse responses) { g_timer_stop (job->time); gboolean delay_interactivity = is_long_job (job); if (delay_interactivity && job->last_dialog_timestamp != 0) { gint64 last_interactivity = g_get_monotonic_time () - job->last_dialog_timestamp; delay_interactivity = last_interactivity >= INTERACTIVITY_DELAY_GRACE_PERIOD; } DialogWithResponsesData *data = &(DialogWithResponsesData) { .parent_window = job->parent_window, .dbus_data = job->dbus_data, .heading = heading, .body = body, .details = details, .responses = responses, .delay_interactivity = delay_interactivity, .completed = FALSE }; g_mutex_init (&data->mutex); g_cond_init (&data->cond); 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); g_mutex_unlock (&data->mutex); g_mutex_clear (&data->mutex); g_cond_clear (&data->cond); job->last_dialog_timestamp = g_get_monotonic_time (); g_timer_continue (job->time); return data->result; } /** Returns: Whether skip was chosen. */ static gboolean show_skip_dialog (CommonJob *job, const char *heading, const char *body, const char *details, int total_operations, gboolean show_all) { NautilusDialogResponse responses = RESPONSE_CANCEL; if (total_operations > 1) { responses |= (show_all ? (RESPONSE_SKIP | RESPONSE_SKIP_ALL) : RESPONSE_SKIP); } NautilusDialogResponse response = run_dialog (job, heading, body, details, responses); if (response == RESPONSE_CANCEL) { abort_job (job); return FALSE; } else if (response == RESPONSE_SKIP_ALL) { job->skip_all_error = TRUE; return TRUE; } else if (response == RESPONSE_SKIP) { return TRUE; } else { g_assert_not_reached (); return FALSE; } } static void inhibit_power_manager (CommonJob *job, const char *message) { /* Since we might never initiate the app (in the case of testing), we can't * inhibit its power manager. This would emit a warning that terminates the * testing. So we avoid doing this by checking g_test_initialized (). */ if (g_test_initialized ()) { return; } GtkApplication *app = GTK_APPLICATION (g_application_get_default ()); job->inhibit_cookie = gtk_application_inhibit (app, 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); } static gboolean confirm_delete_from_trash (CommonJob *job, GList *files) { g_autofree char *prompt = NULL; int file_count; int response; 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 (_("Permanently Delete “%s”?"), basename); } else { prompt = g_strdup_printf (ngettext ("Permanently Delete %'d Selected Item?", "Permanently Delete %'d Selected Items?", file_count), file_count); } response = run_dialog (job, prompt, _("Permanently deleted items can not be restored"), NULL, RESPONSE_DELETE); return response == RESPONSE_DELETE; } static gboolean confirm_empty_trash (CommonJob *job) { int response = run_dialog (job, _("Empty Trash?"), _("All items in the Trash will be permanently deleted"), NULL, RESPONSE_EMPTY_TRASH); return response == RESPONSE_EMPTY_TRASH; } static gboolean confirm_delete_directly (CommonJob *job, GList *files) { g_autofree char *prompt = NULL; int file_count; int response; 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 (_("Permanently Delete “%s”?"), basename); } else { prompt = g_strdup_printf (ngettext ("Permanently Delete %'d Selected Item?", "Permanently Delete %'d Selected Items?", file_count), file_count); } response = run_dialog (job, prompt, _("Permanently deleted items can not be restored"), NULL, RESPONSE_DELETE); return response == RESPONSE_DELETE; } #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; gboolean is_clear_action; 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)) < PROGRESS_NOTIFY_INTERVAL_USEC && files_left > 0) { return; } transfer_info->last_report_time = now; /* Files from "Recent" are not deleted, only cleared from the File History. * This assumes recent files are not mixed with other files. */ is_clear_action = g_file_has_uri_scheme (delete_job->files->data, SCHEME_RECENT); if (source_info->num_files == 1) { g_autofree gchar *basename = NULL; if (files_left == 0) { if (is_clear_action) { /* Translators: This action removes a file from Recent */ status = _("Cleared “%s”"); } else { status = _("Deleted “%s”"); } } else { if (is_clear_action) { /* Translators: This action removes a file from Recent */ status = _("Clearing “%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), NULL); } else { if (files_left == 0) { if (is_clear_action) { /* Translators: This action removes file(s) from Recent */ status = ngettext ("Cleared %'d file", "Cleared %'d files", source_info->num_files); } else { status = ngettext ("Deleted %'d file", "Deleted %'d files", source_info->num_files); } } else { if (is_clear_action) { /* Translators: This action removes file(s) from Recent */ status = ngettext ("Clearing %'d file", "Clearing %'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), NULL); } 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 || transfer_rate == 0) { 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/s)" * * 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/s)", "(%d files/s)", (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) { if (!success && error == NULL) { /* Enumeration succeeded, but we've failed to delete at least one child. */ error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_EMPTY, _("Failed to delete all child files")); } 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; const char *primary; g_autofree char *secondary = NULL; char *details = NULL; 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 = _("Error while deleting."); basename = get_basename (file); if (is_dir (file, job->cancellable)) { secondary = IS_IO_ERROR (error, PERMISSION_DENIED) ? g_strdup_printf (_("You do not have sufficient permissions " "to delete the folder “%s”."), basename) : g_strdup_printf (_("There was an error deleting the " "folder “%s”."), basename); } else { secondary = IS_IO_ERROR (error, PERMISSION_DENIED) ? g_strdup_printf (_("You do not have sufficient permissions " "to delete the file “%s”."), basename) : g_strdup_printf (_("There was an error deleting the " "file “%s”."), basename); } details = error->message; show_skip_dialog (job, primary, secondary, details, source_info->num_files, source_info->num_files > transfer_info->num_files); } static void delete_files (CommonJob *job, GList *files, guint *files_skipped) { GList *l; GFile *file; g_auto (SourceInfo) source_info = SOURCE_INFO_INIT; 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; g_autofree gchar *details = NULL; 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)) < PROGRESS_NOTIFY_INTERVAL_USEC && 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), NULL); } 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), NULL); } 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 || transfer_rate == 0) { 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/s)" * * 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/s)", "(%d files/s)", (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 /** Returns: Whether file was trashed. */ static gboolean trash_file (CommonJob *job, GFile *file, SourceInfo *source_info, TransferInfo *transfer_info, GList **to_delete) { g_autoptr (GError) error = NULL; g_autofree char *primary = NULL; const char *secondary = NULL; const char *details = NULL; int response; g_autofree gchar *basename = NULL; if (should_skip_file (job, file)) { return FALSE; } 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 TRUE; } if (job->skip_all_error) { return FALSE; } if (job->delete_all) { *to_delete = g_list_prepend (*to_delete, file); return TRUE; } 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); if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { details = error->message; } else if (!g_file_is_native (file)) { secondary = _("This remote location does not support sending items to the trash."); } NautilusDialogResponse responses = RESPONSE_SKIP | RESPONSE_TRASHLESS_DELETE; if ((source_info->num_files - transfer_info->num_files) > 1) { responses |= RESPONSE_SKIP_ALL | RESPONSE_DELETE_ALL; } response = run_dialog (job, primary, secondary, details, responses); if (response == RESPONSE_CANCEL) { ((DeleteJob *) job)->user_cancel = TRUE; abort_job (job); } else if (response == RESPONSE_SKIP_ALL) { job->skip_all_error = TRUE; return FALSE; } else if (response == RESPONSE_SKIP) { return FALSE; } else if (response == RESPONSE_DELETE_ALL) { *to_delete = g_list_prepend (*to_delete, file); job->delete_all = TRUE; } else if (response == RESPONSE_DELETE) { *to_delete = g_list_prepend (*to_delete, file); } return TRUE; } static void source_info_remove_descendent_files_from_count (GFile *dir, SourceDirInfo *dir_info, SourceInfo *source_info) { GFile *other_dir; SourceDirInfo *other_dir_info; GHashTableIter dir_info_iter; source_info->num_files -= dir_info->num_files_children; source_info->num_bytes -= dir_info->num_bytes_children; g_hash_table_iter_init (&dir_info_iter, source_info->scanned_dirs_info); while (g_hash_table_iter_next (&dir_info_iter, (gpointer *) &other_dir, (gpointer *) &other_dir_info)) { g_assert (other_dir != NULL); g_assert (other_dir_info != NULL); if (other_dir_info != dir_info && g_file_has_parent (other_dir, dir)) { source_info_remove_descendent_files_from_count (other_dir, other_dir_info, source_info); } } } static void source_info_remove_file_from_count (GFile *file, CommonJob *job, SourceInfo *source_info) { g_autoptr (GFileInfo) file_info = NULL; SourceDirInfo *dir_info; 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); } dir_info = g_hash_table_lookup (source_info->scanned_dirs_info, file); if (dir_info != NULL) { source_info_remove_descendent_files_from_count (file, dir_info, source_info); } } static void trash_files (CommonJob *job, GList *files, guint *files_skipped) { GList *l; GFile *file; GList *to_delete; g_auto (SourceInfo) source_info = SOURCE_INFO_INIT; TransferInfo transfer_info; 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; if (!trash_file (job, file, &source_info, &transfer_info, &to_delete)) { (*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 (); } 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; guint files_skipped = 0; common = (CommonJob *) job; nautilus_progress_info_start (job->common.progress); must_confirm_delete_in_trash = FALSE; must_confirm_delete = FALSE; for (l = job->files; l != NULL; l = l->next) { file = l->data; if (job->try_trash && g_file_has_uri_scheme (file, SCHEME_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; 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 (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; gboolean trash_empty; gboolean pending_checks; 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; char *primary; gboolean unmounted; g_autoptr (GError) 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); } nautilus_show_ok_dialog (primary, error->message, GTK_WIDGET (data->parent_window)); g_free (primary); } } if (data->callback) { data->callback (data->callback_data); } unmount_data_free (data); } static void do_unmount (UnmountData *data) { GMountOperation *mount_op; nautilus_fd_holders_release_for_mount (data->mount); 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), data->mount); g_signal_connect (mount_op, "aborted", G_CALLBACK (show_unmount_progress_aborted_cb), data->mount); 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 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 AdwDialog * create_empty_trash_prompt (UnmountData *data) { AdwDialog *dialog; g_autofree gchar *name = g_mount_get_name (data->mount); dialog = adw_alert_dialog_new (_("Empty Trash Before Ejecting?"), NULL); adw_alert_dialog_format_body (ADW_ALERT_DIALOG (dialog), _("Empty the trash to free " "up space on “%s”. All trashed items will " "be permanently deleted."), name); adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog), "cancel", _("Cancel"), "do-not-empty", _("Do _Not Empty"), /* Translators: This is an action in a dialog to * clear trashed items. */ "empty-trash", _("_Empty"), NULL); adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "empty-trash"); adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "cancel"); adw_alert_dialog_set_response_appearance (ADW_ALERT_DIALOG (dialog), "empty-trash", ADW_RESPONSE_DESTRUCTIVE); return dialog; } static void empty_trash_for_unmount_done (gboolean success, gpointer user_data) { UnmountData *data = user_data; do_unmount (data); } static void empty_trash_prompt_cb (AdwDialog *dialog, char *response, gpointer user_data) { UnmountData *data = user_data; if (g_strcmp0 (response, "empty-trash") == 0) { GTask *task; EmptyTrashJob *job; job = op_job_new (EmptyTrashJob, data->parent_window, NULL); job->should_confirm = FALSE; job->trash_dirs = get_trash_dirs_for_mount (data->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); } else if (g_strcmp0 (response, "cancel") == 0) { if (data->callback) { data->callback (data->callback_data); } unmount_data_free (data); } else if (g_strcmp0 (response, "do-not-empty") == 0) { do_unmount (data); } } static void file_operations_trash_dir_checked (gpointer callback_data, gboolean is_empty) { UnmountData *data = callback_data; data->trash_empty &= is_empty; data->pending_checks -= 1; if (data->pending_checks != 0) { return; } if (data->trash_empty) { do_unmount (data); } else { AdwDialog *dialog = create_empty_trash_prompt (data); g_signal_connect (dialog, "response", G_CALLBACK (empty_trash_prompt_cb), data); adw_dialog_present (dialog, GTK_WIDGET (data->parent_window)); } } void nautilus_file_operations_unmount_mount_full (GtkWindow *parent_window, GMount *mount, GMountOperation *mount_operation, gboolean eject, NautilusUnmountCallback callback, gpointer callback_data) { g_autolist (GFile) trash_dirs = get_trash_dirs_for_mount (mount); guint number_dirs = g_list_length (trash_dirs); UnmountData *data; 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); data->trash_empty = TRUE; data->pending_checks = number_dirs; if (number_dirs == 0) { do_unmount (data); return; } for (GFileList *l = trash_dirs; l != NULL; l = l->next) { GFile *directory = l->data; nautilus_is_directory_empty (directory, file_operations_trash_dir_checked, data); } } 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; char *primary; char *name; gboolean success; success = TRUE; g_autoptr (GError) 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; nautilus_show_ok_dialog (primary, error->message, GTK_WIDGET (parent)); g_free (primary); } } 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_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, SourceDirInfo *dir_info) { goffset num_bytes = g_file_info_get_size (info); source_info->num_files += 1; source_info->num_bytes += num_bytes; source_info->largest_file_bytes = MAX (source_info->largest_file_bytes, num_bytes); if (dir_info != NULL) { dir_info->num_files_children += 1; dir_info->num_bytes_children += num_bytes; } 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) { GFileInfo *info; GFile *subdir; GFileEnumerator *enumerator; int response; SourceInfo saved_info; g_autolist (GFile) subdirs = NULL; SourceDirInfo *dir_info = NULL; gboolean skip_subdirs = FALSE; /* It is possible for this function to be called multiple times for * the same directory. * We pass a NULL SourceDirInfo into count_file() if this directory has * already been scanned once so that its children are not counted more * than once in the SourceDirInfo corresponding to this directory. */ if (!g_hash_table_contains (source_info->scanned_dirs_info, dir)) { dir_info = g_new0 (SourceDirInfo, 1); g_hash_table_insert (source_info->scanned_dirs_info, g_object_ref (dir), dir_info); } /* Stash a copy of the struct to restore state when retrying. Note that * this assumes the code below does not access any pointer member */ saved_info = *source_info; while (TRUE) { g_autoptr (GError) error = NULL; if (dir_info != NULL) { dir_info->num_files_children = 0; dir_info->num_bytes_children = 0; } 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) { while ((info = g_file_enumerator_next_file (enumerator, job->cancellable, &error)) != NULL) { count_file (info, job, source_info, dir_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)); subdirs = g_list_prepend (subdirs, subdir); } g_object_unref (info); } g_file_enumerator_close (enumerator, job->cancellable, NULL); g_object_unref (enumerator); if (error != NULL && !IS_IO_ERROR (error, CANCELLED)) { g_autofree gchar *basename = NULL; g_autofree gchar *primary = get_scan_primary (source_info->op); g_autofree gchar *secondary = NULL; const char *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_dialog (job, primary, secondary, details, RESPONSE_SKIP | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); skip_subdirs = TRUE; } else if (response == RESPONSE_RETRY) { g_clear_list (&subdirs, g_object_unref); *source_info = saved_info; continue; } else if (response == RESPONSE_SKIP) { skip_readdir_error (job, dir); } else { g_assert_not_reached (); } } } else if (job->skip_all_error) { skip_file (job, dir); skip_subdirs = TRUE; } else if (!IS_IO_ERROR (error, CANCELLED)) { g_autofree gchar *basename = NULL; g_autofree gchar *primary = get_scan_primary (source_info->op); g_autofree gchar *secondary = NULL; const char *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_dialog (job, primary, secondary, details, RESPONSE_SKIP | RESPONSE_SKIP_ALL | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); skip_subdirs = TRUE; } else if (response == RESPONSE_SKIP || response == RESPONSE_SKIP_ALL) { if (response == RESPONSE_SKIP_ALL) { job->skip_all_error = TRUE; } skip_file (job, dir); skip_subdirs = TRUE; } else if (response == RESPONSE_RETRY) { continue; } else { g_assert_not_reached (); } } break; } if (!skip_subdirs) { while (subdirs != NULL) { GList *l = subdirs; subdirs = g_list_remove_link (subdirs, l); /* Push to head, since we want depth-first */ g_queue_push_head_link (dirs, l); } } } static void scan_file (GFile *file, SourceInfo *source_info, CommonJob *job) { GFileInfo *info; GQueue *dirs; GFile *dir; int response; dirs = g_queue_new (); while (TRUE) { g_autoptr (GError) error = NULL; info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_TYPE "," G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (info) { count_file (info, job, source_info, NULL); /* 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) { skip_file (job, file); } else if (!IS_IO_ERROR (error, CANCELLED)) { g_autofree gchar *basename = NULL; g_autofree gchar *primary = get_scan_primary (source_info->op); g_autofree gchar *secondary = NULL; const char *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_dialog (job, primary, secondary, details, RESPONSE_SKIP | RESPONSE_SKIP_ALL | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_SKIP || response == RESPONSE_SKIP_ALL) { if (response == RESPONSE_SKIP_ALL) { job->skip_all_error = TRUE; } skip_file (job, file); } else if (response == RESPONSE_RETRY) { continue; } else { g_assert_not_reached (); } } break; } while (!job_aborted (job) && (dir = g_queue_pop_head (dirs)) != NULL) { scan_dir (dir, source_info, job, dirs); g_object_unref (dir); } /* Free all from queue if we exited early */ g_queue_foreach (dirs, (GFunc) g_object_unref, NULL); g_queue_free (dirs); } static void scan_sources (GList *files, SourceInfo *source_info, CommonJob *job, OpKind kind) { GList *l; GFile *file; source_info->op = kind; source_info->scanned_dirs_info = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, (GDestroyNotify) g_object_unref, (GDestroyNotify) g_free); 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); } /* 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, SourceInfo *source_info) { GFileInfo *info, *fsinfo; const char *fs_type; guint64 free_size; guint64 size_difference; int response; GFileType file_type; gboolean dest_is_symlink = FALSE; goffset required_size = (source_info != NULL) ? source_info->num_bytes : -1; if (dest_fs_id) { *dest_fs_id = NULL; } while (TRUE) { g_autoptr (GError) 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) { if (IS_IO_ERROR (error, CANCELLED)) { return; } g_autofree gchar *basename = get_basename (dest); g_autofree gchar *primary = g_strdup_printf (_("Error while copying to “%s”."), basename); const char *secondary; const char *details = NULL; if (IS_IO_ERROR (error, PERMISSION_DENIED)) { secondary = _("You do not have permissions to access the destination folder."); } else { secondary = _("There was an error getting information about the destination."); details = error->message; } response = run_dialog (job, primary, secondary, details, RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_RETRY) { continue; } 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); continue; } 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 = get_basename (dest); g_autofree char *heading = g_strdup_printf (_("Error while copying to “%s”."), basename); const char *body = _("The destination is not a folder."); nautilus_show_ok_dialog (heading, body, GTK_WIDGET (job->parent_window)); 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 "," G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, 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; } /* ramfs reports a free size, but that size is always 0. If we're copying to ramfs, * skip the free size check. */ fs_type = g_file_info_get_attribute_string (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE); if (required_size > 0 && g_strcmp0 (fs_type, "ramfs") != 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 < (guint64) required_size) { g_autofree gchar *basename = get_basename (dest); g_autofree gchar *formatted_size = NULL; g_autofree gchar *primary = g_strdup_printf (_("Error while copying to “%s”."), basename); const char *secondary; g_autofree char *details = NULL; size_difference = required_size - free_size; secondary = _("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_dialog (job, primary, secondary, details, RESPONSE_COPY_FORCE | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_RETRY) { continue; } else if (response == RESPONSE_COPY_FORCE) { /* We are forced to copy - just fall through ... */ } else { g_assert_not_reached (); } } } break; } if (!job_aborted (job) && source_info != NULL && source_info->largest_file_bytes > MAXIMUM_FAT_FILE_SIZE && g_strcmp0 (fs_type, "msdos") == 0) { response = run_dialog (job, _("File too Large for Destination"), _("Files bigger than 4.3 GB cannot be copied onto a FAT filesystem."), NULL, RESPONSE_PROCEED); if (response == RESPONSE_CANCEL) { abort_job (job); } } if (!job_aborted (job) && g_file_info_get_attribute_boolean (fsinfo, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) { g_autofree gchar *basename = get_basename (dest); g_autofree gchar *heading = g_strdup_printf (_("Error while copying to “%s”."), basename); const char *body = _("The destination is read-only."); nautilus_show_ok_dialog (heading, body, GTK_WIDGET (job->parent_window)); 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 *short_status; gchar *status; char *details; gchar *tmp; gchar *tmp_short; 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)) < PROGRESS_NOTIFY_INTERVAL_USEC && 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) { short_status = _("Moving “%s”"); status = _("Moving “%s” to “%s”"); } else { short_status = _("Moved “%s”"); status = _("Moved “%s” to “%s”"); } } else { if (files_left > 0) { short_status = _("Copying “%s”"); status = _("Copying “%s” to “%s”"); } else { short_status = _("Copied “%s”"); 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); tmp_short = g_strdup_printf (short_status, basename_fake_display_source); } 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); tmp_short = g_strdup_printf (short_status, basename_data); } nautilus_progress_info_take_status (job->progress, tmp, tmp_short); } 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), NULL); } } else if (copy_job->files != NULL) { if (copy_job->destination != NULL) { if (files_left > 0) { g_autofree gchar *basename = NULL; if (is_move) { short_status = ngettext ("Moving %'d file", "Moving %'d files", source_info->num_files); status = ngettext ("Moving %'d file to “%s”", "Moving %'d files to “%s”", source_info->num_files); } else { short_status = ngettext ("Copying %'d file", "Copying %'d files", source_info->num_files); 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); tmp_short = g_strdup_printf (short_status, source_info->num_files); nautilus_progress_info_take_status (job->progress, tmp, tmp_short); } else { g_autofree gchar *basename = NULL; if (is_move) { short_status = ngettext ("Moved %'d file", "Moved %'d files", source_info->num_files); status = ngettext ("Moved %'d file to “%s”", "Moved %'d files to “%s”", source_info->num_files); } else { short_status = ngettext ("Copied %'d file", "Copied %'d files", source_info->num_files); 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); tmp_short = g_strdup_printf (short_status, source_info->num_files); nautilus_progress_info_take_status (job->progress, tmp, tmp_short); } } 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) { short_status = ngettext ("Duplicating %'d file", "Duplicating %'d files", source_info->num_files); 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), g_strdup_printf (short_status, source_info->num_files)); } else { short_status = ngettext ("Duplicated %'d file", "Duplicated %'d files", source_info->num_files); 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_strdup_printf (short_status, source_info->num_files)); } 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 || !transfer_info->partial_progress) { 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 (4 kB/s)" * * 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/s)", "%s / %s \xE2\x80\x94 %s left (%s/s)", 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 (4 kB/s)" * * 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/s)", "%'d / %'d \xE2\x80\x94 %s left (%s/s)", 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 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; } } if (i > 0 && str[--i] == '.') { str[i] = replacement; success = TRUE; } 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 (/* 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") || /* msdos is returned for fat filesystems */ !strcmp (dest_fs_type, "msdos") || !strcmp (dest_fs_type, "exfat") || !strcmp (dest_fs_type, "cifs")) { gboolean ret; guint old_len = strlen (filename); ret = fat_str_replace (filename, '_'); for (guint 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, GCancellable *cancellable, const char *dest_fs_type, int count) { const char *editname, *end; char *basename, *new_name; GFileInfo *info; GFile *dest; int max_length; gboolean ignore_extension = FALSE; 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 "," G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NONE, cancellable, NULL); if (info != NULL) { ignore_extension = (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY); editname = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME); if (editname != NULL) { new_name = nautilus_filename_for_copy (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); g_assert (basename != NULL); if (g_utf8_validate (basename, -1, NULL)) { new_name = nautilus_filename_for_copy (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, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (info != NULL) { editname = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME); if (editname != NULL) { new_name = nautilus_filename_for_link (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 = nautilus_filename_for_link (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_NAME "," G_FILE_ATTRIBUTE_STANDARD_COPY_NAME "," G_FILE_ATTRIBUTE_TRASH_ORIG_PATH, G_FILE_QUERY_INFO_NONE, 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, SCHEME_TRASH) && g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TRASH_ORIG_PATH)) { 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 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 gboolean copy_move_file (CopyMoveJob *job, GFile *src, GFile *dest_dir, gboolean same_fs, gboolean unique_names, GFile *precomputed_dest, char **dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info, GHashTable *debuting_files, gboolean overwrite, gboolean reset_perms); 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) { GFile *new_dest, *dest_dir; int response; gboolean handled_invalid_filename; gboolean res; handled_invalid_filename = *dest_fs_type != NULL; while (TRUE) { /* First create the directory, then copy stuff to it before * copying the attributes, because we need to be sure we can write to it */ g_autoptr (GError) 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) { if (IS_IO_ERROR (error, CANCELLED)) { 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; return CREATE_DEST_DIR_RETRY; } else { g_object_unref (new_dest); } } } g_autofree char *secondary = NULL; const char *details = NULL; g_autofree gchar *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_dialog (job, _("Error while copying."), secondary, details, RESPONSE_SKIP | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_SKIP) { /* Skip: Do Nothing */ } else if (response == RESPONSE_RETRY) { continue; } else { g_assert_not_reached (); } return CREATE_DEST_DIR_FAILED; } break; } 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 reset_perms) { g_autoptr (GFileInfo) src_info = NULL; GFileInfo *info; GFile *src_file; GFileEnumerator *enumerator; int response; gboolean skip_error; CommonJob *job; GFileCopyFlags flags; job = (CommonJob *) copy_job; *skipped_file = FALSE; if (create_dest) { g_autofree char *attrs_to_read = NULL; 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)); } flags = G_FILE_COPY_NOFOLLOW_SYMLINKS; if (reset_perms) { flags |= G_FILE_COPY_TARGET_DEFAULT_PERMS; } if (copy_job->is_move) { flags |= G_FILE_COPY_ALL_METADATA; } /* Ignore errors here. Failure to copy metadata is not a hard error */ attrs_to_read = g_file_build_attribute_list_for_copy (*dest, flags, job->cancellable, NULL); if (attrs_to_read != NULL) { src_info = g_file_query_info (src, attrs_to_read, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, NULL); } } gboolean local_skipped_file = FALSE; g_autofree char *dest_fs_type = NULL; skip_error = should_skip_readdir_error (job, src); while (TRUE) { g_autoptr (GError) error = NULL; enumerator = g_file_enumerate_children (src, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, &error); if (enumerator) { 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)); local_skipped_file = !copy_move_file (copy_job, src_file, *dest, same_fs, FALSE, NULL, &dest_fs_type, source_info, transfer_info, NULL, FALSE, reset_perms); 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 != NULL && !IS_IO_ERROR (error, CANCELLED)) { g_autofree gchar *basename = NULL; const char *primary; g_autofree char *secondary = NULL; const char *details = NULL; if (copy_job->is_move) { primary = _("Error while moving."); } else { primary = _("Error while copying."); } 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_dialog (job, primary, secondary, details, RESPONSE_SKIP_FILES); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_SKIP_FILES) { /* Skip: Do Nothing */ local_skipped_file = TRUE; } else { g_assert_not_reached (); } } /* Count the copied directory as a file */ transfer_info->num_files++; info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, NULL); if (info != NULL) { transfer_info->num_bytes += g_file_info_get_size (info); g_object_unref (info); } 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_autofree gchar *basename = NULL; const char *primary; g_autofree char *secondary = NULL; const char *details = NULL; if (copy_job->is_move) { primary = _("Error while moving."); } else { primary = _("Error while copying."); } 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_dialog (job, primary, secondary, details, RESPONSE_SKIP | RESPONSE_RETRY); if (response == RESPONSE_CANCEL) { abort_job (job); } else if (response == RESPONSE_SKIP) { /* Skip: Do Nothing */ *skipped_file = TRUE; } else if (response == RESPONSE_RETRY) { continue; } else { g_assert_not_reached (); } } break; } if (src_info != NULL) { /* Ignore errors here. Failure to copy metadata is not a hard error */ g_file_set_attributes_from_info (*dest, src_info, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, job->cancellable, NULL); } if (!job_aborted (job) && copy_job->is_move && /* Don't delete source if there was a skipped file */ !*skipped_file && !local_skipped_file) { g_autoptr (GError) error = NULL; if (!g_file_delete (src, job->cancellable, &error)) { if (job->skip_all_error) { *skipped_file = TRUE; return TRUE; } g_autofree gchar *basename = get_basename (src); g_autofree char *primary = g_strdup_printf (_("Error while moving “%s”."), basename); const char *secondary = _("Could not remove the source folder."); const char *details = error->message; gboolean skip = show_skip_dialog (job, primary, secondary, details, source_info->num_files, source_info->num_files > transfer_info->num_files); *skipped_file = !skip; } } 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; if (current_num_bytes != 0 && current_num_bytes != total_num_bytes) { pdata->transfer_info->partial_progress = TRUE; } 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, gboolean dest_is_dir) { FileConflictResponse *response; g_autofree gchar *basename = NULL; g_autoptr (GFile) suggested_file = NULL; g_autofree gchar *suggestion = NULL; gboolean should_start_inactive; g_timer_stop (job->time); nautilus_progress_info_pause (job->progress); should_start_inactive = is_long_job (job); basename = g_file_get_basename (dest); suggested_file = nautilus_generate_unique_file_in_directory (dest_dir, basename, dest_is_dir); suggestion = g_file_get_basename (suggested_file); response = copy_move_conflict_ask_user_action (job->parent_window, should_start_inactive, src, dest, dest_dir, suggestion); 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; GFile *dest = NULL; job = (CommonJob *) copy_job; info = g_file_query_info (src, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, NULL, &error); if (info == NULL) { const char *heading = copy_job->is_move ? _("Error while moving.") : _("Error while copying."); nautilus_show_ok_dialog (heading, error->message, GTK_WIDGET (job->parent_window)); abort_job (job); } else { dest = get_target_file_for_display_name (dir, g_file_info_get_display_name (info)); } return dest; } /** * @debuting_files: only non-NULL for toplevel items * Returns: Whether file was copied/moved. */ static gboolean copy_move_file (CopyMoveJob *copy_job, GFile *src, GFile *dest_dir, gboolean same_fs, gboolean unique_names, GFile *precomputed_dest, char **dest_fs_type, SourceInfo *source_info, TransferInfo *transfer_info, GHashTable *debuting_files, gboolean overwrite, gboolean reset_perms) { g_autoptr (GFile) dest = NULL; GFile *new_dest; GFileCopyFlags flags; ProgressData pdata; gboolean would_recurse; CommonJob *job; gboolean res; int unique_name_nr; gboolean handled_invalid_filename; job = (CommonJob *) copy_job; if (should_skip_file (job, src)) { return FALSE; } 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, job->cancellable, *dest_fs_type, unique_name_nr++); } else if (precomputed_dest != NULL) { /* Set by the cross-filesystem move fallback when the user already * picked a new name during move_file_prepare. Reuse that exact * destination so the conflict dialog is not shown a second time. */ dest = g_object_ref (precomputed_dest); } 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 FALSE; } } else { dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); } /* Don't allow recursive move/copy into itself. * (We would get a file system error if we proceeded but it is nicer to * detect and report it at this level) */ if (test_dir_is_parent (dest_dir, src)) { if (job->skip_all_error) { return FALSE; } const char *primary = copy_job->is_move ? _("You cannot move a folder into itself.") : _("You cannot copy a folder into itself."); const char *secondary = _("The destination folder is inside the source folder."); show_skip_dialog (job, primary, secondary, NULL, source_info->num_files, source_info->num_files > transfer_info->num_files); return FALSE; } /* Don't allow copying over the source or one of the parents of the source. */ if (test_dir_is_parent (src, dest)) { if (job->skip_all_error) { return FALSE; } const char *primary = copy_job->is_move ? _("You cannot move a file over itself.") : _("You cannot copy a file over itself."); const char *secondary = _("The source file would be overwritten by the destination."); show_skip_dialog (job, primary, secondary, NULL, source_info->num_files, source_info->num_files > transfer_info->num_files); return FALSE; } while (TRUE) { g_autoptr (GError) error = NULL; flags = G_FILE_COPY_NOFOLLOW_SYMLINKS; if (overwrite) { flags |= G_FILE_COPY_OVERWRITE; } if (reset_perms) { 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) { g_hash_table_replace (debuting_files, g_object_ref (dest), GINT_TO_POINTER (!overwrite)); } 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); } return TRUE; } /* On smb shares INVALID_ARGUMENT is typically returned instead of INVALID_FILENAME * (i.e. FAT_FORBIDDEN_CHARACTER) except with '\' where NOT_DIRECTORY is returned */ if (!handled_invalid_filename && (IS_IO_ERROR (error, INVALID_FILENAME) || IS_IO_ERROR (error, INVALID_ARGUMENT) || IS_IO_ERROR (error, NOT_DIRECTORY))) { 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, job->cancellable, *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; continue; } 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; if (unique_names) { g_object_unref (dest); dest = get_unique_target_file (src, dest_dir, job->cancellable, *dest_fs_type, unique_name_nr++); continue; } source_is_directory = is_dir (src, job->cancellable); destination_is_directory = is_dir (dest, job->cancellable); 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; continue; } if ((is_merge && job->merge_all) || (!is_merge && job->replace_all)) { overwrite = TRUE; continue; } if (job->skip_all_conflict) { return FALSE; } response = handle_copy_move_conflict (job, src, dest, dest_dir, destination_is_directory); if (response->id == CONFLICT_RESPONSE_CANCEL) { 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); continue; } 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); continue; } 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; if (overwrite && would_recurse) { g_autoptr (GError) delete_error = NULL; /* Copying a dir onto file, first remove the file */ if (!g_file_delete (dest, job->cancellable, &delete_error) && !IS_IO_ERROR (delete_error, NOT_FOUND)) { if (job->skip_all_error) { return FALSE; } g_autofree gchar *basename = get_basename (src); g_autofree gchar *filename = NULL; g_autofree char *primary = NULL; g_autofree char *secondary = NULL; const char *details = NULL; 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 = delete_error->message; /* setting TRUE on show_all here, as we could have * another error on the same file later. */ show_skip_dialog (job, primary, secondary, details, source_info->num_files, TRUE); return FALSE; } 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; } gboolean skipped_file = FALSE; if (!copy_move_directory (copy_job, src, &dest, same_fs, would_recurse, dest_fs_type, source_info, transfer_info, debuting_files, &skipped_file, reset_perms)) { /* destination changed, since it was an invalid file name */ g_assert (*dest_fs_type != NULL); handled_invalid_filename = TRUE; continue; } return !skipped_file; } /* Other error */ else if (!IS_IO_ERROR (error, CANCELLED)) { if (job->skip_all_error) { return FALSE; } g_autofree gchar *basename = get_basename (src); g_autofree gchar *filename = get_truncated_parse_name (dest_dir); g_autofree char *primary = g_strdup_printf (_("Error while copying “%s”."), basename); g_autofree char *secondary = g_strdup_printf (_("There was an error copying the file into %s."), filename); const char *details = error->message; show_skip_dialog (job, primary, secondary, details, source_info->num_files, source_info->num_files > transfer_info->num_files); } break; } /* Either skipped or aborted, but same-same */ return FALSE; } 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; gboolean unique_names; GFile *source_dir; char *dest_fs_type; GFileInfo *inf; gboolean reset_perms; dest_fs_type = NULL; reset_perms = 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::type", NULL, NULL); if (inf != NULL) { /* Reset all permissions for isofs filesystems. If we didn't do this, we would * end up with unfortunate r-x permissions for all files copied from CDs that * don't have the POSIX permission extension. */ const char *source_fs_type; source_fs_type = g_file_info_get_attribute_string (inf, "filesystem::type"); reset_perms = g_strcmp0 (source_fs_type, "isofs") == 0; g_object_unref (inf); } g_object_unref (source_dir); } unique_names = (job->destination == NULL); 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); } g_autoptr (GFile) dest = (job->destination != NULL) ? g_object_ref (job->destination) : g_file_get_parent (src); if (dest) { if (!copy_move_file (job, src, dest, same_fs, unique_names, NULL, &dest_fs_type, source_info, transfer_info, job->debuting_files, FALSE, reset_perms)) { source_info_remove_file_from_count (src, common, source_info); report_copy_progress (job, source_info, transfer_info); } } } 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_clear_object (&job->fake_display_source); finalize_common ((CommonJob *) job); nautilus_file_changes_consume_changes (); } 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; g_auto (SourceInfo) source_info = SOURCE_INFO_INIT; TransferInfo transfer_info; g_autofree char *dest_fs_id = NULL; GFile *dest; job = task_data; common = &job->common; 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); 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 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), g_strdup (_("Preparing to move"))); 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; /* If set, the copy+delete move phase must use this destination * (e.g. after the user picked a new name in move_file_prepare). */ GFile *precomputed_dest; gboolean overwrite; } MoveFileCopyFallback; static void move_file_copy_fallback_free (MoveFileCopyFallback *fallback) { g_clear_object (&fallback->precomputed_dest); g_free (fallback); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (MoveFileCopyFallback, move_file_copy_fallback_free) static MoveFileCopyFallback * move_copy_file_callback_new (GFile *file, GFile *precomputed_dest, gboolean overwrite) { MoveFileCopyFallback *fallback; fallback = g_new (MoveFileCopyFallback, 1); fallback->file = file; fallback->precomputed_dest = precomputed_dest != NULL ? g_object_ref (precomputed_dest) : NULL; 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 total, int files_left) { GFile *new_dest; g_autoptr (GFile) dest = NULL; CommonJob *job; gboolean overwrite; 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)) { if (job->skip_all_error) { return; } const char *primary = move_job->is_move ? _("You cannot move a folder into itself.") : _("You cannot copy a folder into itself."); const char *secondary = _("The destination folder is inside the source folder."); show_skip_dialog (job, primary, secondary, NULL, total, files_left > 1); return; } /* Don't allow moving over the source or one of the parents of the source. */ if (test_dir_is_parent (src, dest)) { if (job->skip_all_error) { return; } const char *primary = move_job->is_move ? _("You cannot move a file over itself.") : _("You cannot copy a file over itself."); const char *secondary = _("The source file would be overwritten by the destination."); show_skip_dialog (job, primary, secondary, NULL, total, files_left > 1); return; } while (TRUE) { g_autoptr (GError) error = NULL; flags = G_FILE_COPY_NOFOLLOW_SYMLINKS | G_FILE_COPY_NO_FALLBACK_FOR_MOVE; if (overwrite) { flags |= G_FILE_COPY_OVERWRITE; } if (g_file_move (src, dest, flags, job->cancellable, NULL, NULL, &error)) { nautilus_file_changes_queue_file_moved (src, 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); } if (debuting_files != NULL) { g_hash_table_replace (debuting_files, g_steal_pointer (&dest), GINT_TO_POINTER (TRUE)); } return; } if (IS_IO_ERROR (error, INVALID_FILENAME) && !handled_invalid_filename) { handled_invalid_filename = TRUE; g_assert (*dest_fs_type == NULL); *dest_fs_type = query_fs_type (dest_dir, job->cancellable); new_dest = get_target_file (src, dest_dir, *dest_fs_type, same_fs); if (!g_file_equal (dest, new_dest)) { g_object_unref (dest); dest = new_dest; continue; } 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, job->cancellable); destination_is_directory = is_dir (dest, job->cancellable); 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; continue; } if ((is_merge && job->merge_all) || (!is_merge && job->replace_all)) { overwrite = TRUE; continue; } if (job->skip_all_conflict) { return; } response = handle_copy_move_conflict (job, src, dest, dest_dir, destination_is_directory); if (response->id == CONFLICT_RESPONSE_CANCEL) { 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); continue; } 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); continue; } 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)) { fallback = move_copy_file_callback_new (src, dest, overwrite); *fallback_files = g_list_prepend (*fallback_files, fallback); } else if (!IS_IO_ERROR (error, CANCELLED)) { /* Other error */ if (job->skip_all_error) { return; } g_autofree gchar *basename = get_basename (src); g_autofree gchar *filename = get_truncated_parse_name (dest_dir); g_autofree char *primary = g_strdup_printf (_("Error while moving “%s”."), basename); g_autofree char *secondary = g_strdup_printf (_("There was an error moving the file into %s."), filename); const char *details = error->message; show_skip_dialog (job, primary, secondary, details, total, files_left > 1); } break; } } 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; common = &job->common; int total = g_list_length (job->files); int left = total; report_preparing_move_progress (job, left); 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, total, left); report_preparing_move_progress (job, --left); } *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; MoveFileCopyFallback *fallback; common = &job->common; report_copy_progress (job, source_info, transfer_info); 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); } if (!copy_move_file (job, src, job->destination, same_fs, FALSE, fallback->precomputed_dest, dest_fs_type, source_info, transfer_info, job->debuting_files, fallback->overwrite, FALSE)) { 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 (); } 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; g_auto (SourceInfo) source_info = SOURCE_INFO_INIT; TransferInfo transfer_info; g_autofree char *dest_fs_id = NULL; g_autofree char *dest_fs_type = NULL; GList *fallback_files; job = task_data; 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, SCHEME_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); g_autolist (MoveFileCopyFallback) fallbacks = NULL; verify_destination (&job->common, job->destination, &dest_fs_id, NULL); if (job_aborted (common)) { return; } /* 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)) { return; } if (fallbacks == NULL) { gint total; total = g_list_length (job->files); memset (&source_info, 0, sizeof (source_info)); source_info.num_files = total; memset (&transfer_info, 0, sizeof (transfer_info)); transfer_info.num_files = total; report_copy_progress (job, &source_info, &transfer_info); return; } /* 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)) { return; } verify_destination (&job->common, job->destination, NULL, &source_info); if (job_aborted (common)) { return; } memset (&transfer_info, 0, sizeof (transfer_info)); move_files (job, fallbacks, dest_fs_id, &dest_fs_type, &source_info, &transfer_info); } 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), g_strdup (_("Creating links"))); 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 total, int files_left) { GFile *src_dir; GFile *new_dest; g_autoptr (GFile) dest = NULL; int count; char *path; gboolean not_local; CommonJob *common; 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); while (TRUE) { g_autoptr (GError) 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); 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; continue; } 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++); continue; } else if (error != NULL && !IS_IO_ERROR (error, CANCELLED)) { /* Other error */ if (common->skip_all_error) { return; } g_autofree gchar *basename = get_basename (src); g_autofree char *primary = g_strdup_printf (_("Error while creating link to %s."), basename); g_autofree char *secondary = NULL; const char *details = NULL; if (not_local) { secondary = g_strdup (_("Symbolic links only supported for local files")); } else if (IS_IO_ERROR (error, NOT_SUPPORTED)) { secondary = g_strdup (_("The target doesn’t support symbolic links.")); } 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; } show_skip_dialog (common, primary, secondary, details, total, files_left > 1); } break; } } 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 (); } 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; GList *l; job = task_data; common = &job->common; nautilus_progress_info_start (job->common.progress); verify_destination (&job->common, job->destination, NULL, NULL); if (job_aborted (common)) { return; } total = left = g_list_length (job->files); report_preparing_link_progress (job, total, left); 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, total, left); report_preparing_link_progress (job, total, --left); } } 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; if (job->done_callback) { job->done_callback (!job_aborted ((CommonJob *) job), job->done_callback_data); } finalize_common ((CommonJob *) job); } static void set_permissions_file (GFileInfo *info, GFile *file, SetPermissionsJob *job) { CommonJob *common; guint32 current; guint32 value; guint32 mask; common = (CommonJob *) job; nautilus_progress_info_pulse_progress (common->progress); 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 (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); } } #define PERMISSION_ATTRIBUTES \ G_FILE_ATTRIBUTE_STANDARD_NAME "," \ G_FILE_ATTRIBUTE_STANDARD_TYPE "," \ G_FILE_ATTRIBUTE_UNIX_MODE void nautilus_file_set_permissions_recursive (const char *directory_path, guint32 file_permissions, guint32 file_mask, guint32 dir_permissions, guint32 dir_mask, NautilusOpCallback callback, gpointer callback_data) { g_autoptr (GFile) directory = g_file_new_for_uri (directory_path); SetPermissionsJob *job; job = op_job_new (SetPermissionsJob, NULL, NULL); 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 (directory, file_permissions, file_mask, dir_permissions, dir_mask); } nautilus_progress_info_set_status (job->common.progress, _("Setting permissions"), NULL); nautilus_progress_info_start (job->common.progress); nautilus_iterate_directory_recursive (directory, G_MAXUINT, PERMISSION_ATTRIBUTES, FALSE, job->common.cancellable, (IterationFileCallback) set_permissions_file, set_permissions_task_done, job); } 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, SCHEME_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, SCHEME_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 (g_file_has_uri_scheme (dest, SCHEME_STARRED)) { g_autolist (NautilusFile) source_file_list = NULL; for (GList *l = locations; l != NULL; l = l->next) { source_file_list = g_list_prepend (source_file_list, nautilus_file_get (l->data)); } source_file_list = g_list_reverse (source_file_list); nautilus_tag_manager_star_files (nautilus_tag_manager_get (), G_OBJECT (parent_view), source_file_list, NULL, NULL, NULL); } else 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, SCHEME_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 (); } 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; g_autofree char *dest_fs_type = NULL; gboolean res; gboolean filename_is_utf8; void *data; gsize length; GFileOutputStream *out; gboolean handled_invalid_filename; int max_length; 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, NULL); 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; while (TRUE) { g_autoptr (GError) 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 | (job->new_mtime ? G_FILE_COPY_TARGET_DEFAULT_MODIFIED_TIME : 0), 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 = NULL; 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 { gboolean ignore_extension = job->src == NULL || is_dir (job->src, common->cancellable); new_filename = nautilus_filename_for_conflict (filename, count, max_length, ignore_extension); } 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); } continue; } } if (IS_IO_ERROR (error, EXISTS)) { gboolean ignore_extension = job->src == NULL || is_dir (job->src, common->cancellable); g_autofree gchar *filename2 = nautilus_filename_for_conflict (filename, ++count, max_length, ignore_extension); make_file_name_valid_for_dest_fs (filename2, dest_fs_type); g_clear_object (&dest); 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); } continue; } else if (!IS_IO_ERROR (error, CANCELLED)) { /* Other error */ g_autofree gchar *basename = get_basename (dest); g_autofree gchar *parse_name = NULL; g_autofree char *primary = NULL; g_autofree char *secondary = NULL; const char *details = error->message; 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); show_skip_dialog (common, primary, secondary, details, 1, FALSE); } } break; } } 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); } static void save_image_thread_func (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { SaveImageJob *job = task_data; g_autoptr (GBytes) bytes = NULL; g_autoptr (GError) output_error = NULL; g_autoptr (GFileOutputStream) stream = NULL; g_autofree gchar *basename = g_strconcat (job->base_name, ".png", NULL); gconstpointer bytes_buffer; size_t bytes_len; for (size_t i = 1; stream == NULL; i += 1) { g_autoptr (GError) stream_error = NULL; g_autofree gchar *filename = i == 1 ? g_strdup (basename) : nautilus_filename_for_conflict (basename, i, -1, FALSE); job->location = g_file_get_child (job->dest_dir, filename); stream = g_file_create (job->location, 0, job->common.cancellable, &stream_error); if (stream_error == NULL) { break; } else if (IS_IO_ERROR (stream_error, EXISTS)) { g_clear_object (&job->location); } else { return; } } nautilus_progress_info_set_progress (job->common.progress, .35, 1); nautilus_progress_info_set_status (job->common.progress, _("Saving image"), NULL); bytes = gdk_texture_save_to_png_bytes (job->texture); nautilus_progress_info_set_progress (job->common.progress, .65, 1); nautilus_progress_info_set_details (job->common.progress, ""); bytes_buffer = g_bytes_get_data (bytes, &bytes_len); g_output_stream_write_all (G_OUTPUT_STREAM (stream), bytes_buffer, bytes_len, NULL, job->common.cancellable, &output_error); if (output_error == NULL) { job->success = TRUE; nautilus_progress_info_set_progress (job->common.progress, 1, 1); nautilus_progress_info_set_status (job->common.progress, _("Saved image"), NULL); } } static void save_image_task_done (GObject *source_object, GAsyncResult *res, gpointer user_data) { SaveImageJob *job = user_data; g_autoptr (GHashTable) debuting_files = NULL; debuting_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); if (job->location != NULL) { g_hash_table_insert (debuting_files, g_object_ref (job->location), job->location); } if (job->done_callback != NULL) { job->done_callback (debuting_files, job->success, job->done_callback_data); } if (!job->success) { if (job->location != NULL) { g_file_delete (job->location, NULL, NULL); } nautilus_progress_info_set_status (job->common.progress, _("Failed to save image"), NULL); } g_free (job->base_name); g_clear_object (&job->location); g_clear_object (&job->dest_dir); g_clear_object (&job->texture); finalize_common ((CommonJob *) job); } static void clipboard_image_received_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { GdkClipboard *clipboard = GDK_CLIPBOARD (source_object); SaveImageJob *job = user_data; g_autoptr (GTask) task = NULL; g_autoptr (GdkTexture) texture = NULL; g_autoptr (GError) clipboard_error = NULL; if (job_aborted ((CommonJob *) job)) { save_image_task_done (NULL, NULL, job); return; } texture = gdk_clipboard_read_texture_finish (clipboard, res, &clipboard_error); if (clipboard_error != NULL) { save_image_task_done (NULL, NULL, job); return; } job->texture = g_object_ref (texture); nautilus_progress_info_set_progress (job->common.progress, .25, 1); task = g_task_new (NULL, NULL, save_image_task_done, job); g_task_set_task_data (task, job, NULL); g_task_run_in_thread (task, save_image_thread_func); } static void finish_rename (RenameJob *job, gboolean stop_timer); static void rename_task_done (GObject *source_object, GAsyncResult *res, gpointer user_data) { RenameJob *job = user_data; if (job->done_callback) { g_autoptr (GFile) renamed_location = nautilus_file_get_location (job->file); job->done_callback (renamed_location, job->success, job->done_callback_data); } finish_rename (job, !job->cancelled); g_clear_object (&job->file); g_free (job->new_name); finalize_common ((CommonJob *) job); nautilus_file_changes_consume_changes (); } static void rename_callback (NautilusFile *file, GFile *result_location, GError *error, gpointer callback_data) { RenameJob *job = callback_data; g_assert (NAUTILUS_IS_FILE (job->file)); job->cancelled = FALSE; if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { job->cancelled = TRUE; } else if (error != NULL) { if (!g_test_initialized ()) { /* If rename failed, notify the user. */ GtkWidget *parent = GTK_WIDGET (((CommonJob *) job)->parent_window); nautilus_report_error_renaming_file (job->file, job->new_name, error, parent); } } else { job->success = TRUE; } rename_task_done (NULL, NULL, job); } static void cancel_rename_callback (gpointer callback_data) { RenameJob *job = callback_data; nautilus_file_cancel (job->file, rename_callback, job); } static void finish_rename (RenameJob *job, gboolean stop_timer) { /* Cancel both the rename and the timed wait. */ nautilus_file_cancel (job->file, rename_callback, NULL); if (stop_timer) { nautilus_ui_timed_wait_stop (cancel_rename_callback, job); } } void nautilus_file_operations_rename (GFile *location, const char *new_name, GtkWidget *parent_view, NautilusFileOperationsDBusData *dbus_data, NautilusOpRenameCallback done_callback, gpointer done_callback_data) { g_return_if_fail (G_IS_FILE (location)); g_return_if_fail (new_name != NULL); RenameJob *job = op_job_new (RenameJob, parent_view != NULL ? GTK_WINDOW (gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW)) : NULL, dbus_data); NautilusFile *file = nautilus_file_get (location); g_autofree char *wait_message = NULL; g_autofree char *uri = NULL; job->file = file; job->new_name = strdup (new_name); job->done_callback = done_callback; job->done_callback_data = done_callback_data; finish_rename (job, TRUE); /* Start the timed wait to cancel the rename. */ wait_message = g_strdup_printf (_("Renaming “%s” to “%s”."), nautilus_file_get_display_name (file), new_name); nautilus_ui_timed_wait_start (cancel_rename_callback, job, wait_message, NULL); /* FIXME bugzilla.gnome.org 42395: Parent this? */ uri = nautilus_file_get_uri (file); g_debug ("Renaming file %s to %s", uri, new_name); /* Start the rename. */ nautilus_file_rename (file, new_name, rename_callback, job); } void nautilus_file_operations_paste_image_from_clipboard (GtkWidget *parent_view, NautilusFileOperationsDBusData *dbus_data, const char *parent_dir_uri, NautilusCopyCallback done_callback, gpointer done_callback_data) { SaveImageJob *job; GtkWindow *parent_window = NULL; GdkClipboard *clipboard = gtk_widget_get_clipboard (parent_view); if (parent_view) { parent_window = (GtkWindow *) gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } job = op_job_new (SaveImageJob, parent_window, dbus_data); /* Translators: This is used to auto-generate a file name for pasted images from * the clipboard i.e. "Pasted image.png", "Pasted image 1.png", ... */ job->base_name = g_strdup (_("Pasted image")); job->dest_dir = g_file_new_for_uri (parent_dir_uri); job->location = NULL; job->done_callback = done_callback; job->done_callback_data = done_callback_data; nautilus_progress_info_start (job->common.progress); nautilus_progress_info_set_status (job->common.progress, _("Retrieving clipboard data"), NULL); gdk_clipboard_read_texture_async (clipboard, job->common.cancellable, clipboard_image_received_callback, job); } void nautilus_file_operations_save_image_from_texture (GtkWidget *parent_view, NautilusFileOperationsDBusData *dbus_data, const char *parent_dir_uri, const char *base_name, GdkTexture *texture, NautilusCopyCallback done_callback, gpointer done_callback_data) { SaveImageJob *job; GtkWindow *parent_window = NULL; g_autoptr (GTask) task = NULL; if (parent_view) { parent_window = (GtkWindow *) gtk_widget_get_ancestor (parent_view, GTK_TYPE_WINDOW); } job = op_job_new (SaveImageJob, parent_window, dbus_data); /* Translators: This is used to auto-generate a file name for saved images * i.e. "Dropped image.png", "Dropped image 1.png", ... */ job->base_name = g_strdup (base_name); job->dest_dir = g_file_new_for_uri (parent_dir_uri); job->texture = g_object_ref (texture); job->done_callback = done_callback; job->done_callback_data = done_callback_data; nautilus_progress_info_start (job->common.progress); nautilus_progress_info_set_status (job->common.progress, _("Retrieving image data"), NULL); task = g_task_new (NULL, NULL, save_image_task_done, job); g_task_set_task_data (task, job, NULL); g_task_run_in_thread (task, save_image_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); job->new_mtime = TRUE; 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 void *initial_contents, gsize 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_memdup2 (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, SCHEME_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 (SCHEME_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 = user_data; gboolean success = !extract_job->extraction_failed && !job_aborted ((CommonJob *) extract_job); if (extract_job->done_callback) { extract_job->done_callback (success ? extract_job->output_files : NULL, 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 (); } 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; gboolean dest_is_dir; nautilus_progress_info_set_details (extract_job->common.progress, _("Verifying destination")); /* This doesn't work when the archive has a single empty directory, but * that's okay since it's a dumb archive. */ dest_is_dir = extract_job->expected_total_files > 1; basename = g_file_get_basename (destination); decided_destination = nautilus_generate_unique_file_in_directory (extract_job->destination_directory, basename, dest_is_dir); if (job_aborted ((CommonJob *) extract_job)) { g_object_unref (decided_destination); return NULL; } /* The extract_job->destination_decided variable signalizes whether the * extract_job->output_files list already contains the final location as * its first link. There is no way to get this over the AutoarExtractor * API currently. */ extract_job->output_files = g_list_prepend (extract_job->output_files, decided_destination); extract_job->destination_decided = TRUE; 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), NULL); 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 (4 kB/s)" * * 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/s)", "%s / %s \xE2\x80\x94 %s left (%s/s)", 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; GFile *destination; gint remaining_files; g_autofree gchar *basename = NULL; g_autofree gchar *heading = 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; } extract_job->extraction_failed = TRUE; /* It is safe to use extract_job->output_files->data only when the * extract_job->destination_decided variable was set, see comment in the * extract_job_on_decide_destination function. */ if (extract_job->destination_decided) { destination = extract_job->output_files->data; delete_file_recursively (destination, NULL, NULL, NULL); extract_job->output_files = g_list_delete_link (extract_job->output_files, extract_job->output_files); g_object_unref (destination); } if (extract_job->common.skip_all_error) { return; } basename = get_basename (source_file); nautilus_progress_info_take_status (extract_job->common.progress, g_strdup_printf (_("Error extracting “%s”"), basename), NULL); remaining_files = g_list_length (g_list_find_custom (extract_job->source_files, source_file, (GCompareFunc) g_file_equal)) - 1; heading = g_strdup_printf (_("There was an error while extracting “%s”."), basename), show_skip_dialog ((CommonJob *) extract_job, heading, error->message, NULL, extract_job->total_files, remaining_files > 1); } 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 gchar * extract_job_on_request_passphrase (AutoarExtractor *extractor, gpointer user_data) { ExtractJob *extract_job = user_data; GtkWindow *parent_window; GFile *source_file; g_autofree gchar *basename = NULL; gchar *passphrase; parent_window = extract_job->common.parent_window; source_file = autoar_extractor_get_source_file (extractor); basename = get_basename (source_file); passphrase = extract_ask_passphrase (parent_window, basename); if (passphrase == NULL) { abort_job ((CommonJob *) extract_job); } return passphrase; } static void extract_job_on_scanned (AutoarExtractor *extractor, guint total_files, gpointer user_data) { g_autoptr (GFileInfo) fsinfo = NULL; guint64 free_size; guint64 total_size = autoar_extractor_get_total_size (extractor); GFile *output_file = autoar_extractor_get_output_file (extractor); ExtractJob *extract_job = user_data; extract_job->expected_total_files = total_files; fsinfo = g_file_query_filesystem_info (output_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) { GFile *source_file = autoar_extractor_get_source_file (extractor); g_autofree gchar *basename = get_basename (source_file); g_autofree char *primary = g_strdup_printf (_("Not enough free space to extract “%s”"), basename); GtkWidget *parent = GTK_WIDGET (extract_job->common.parent_window); nautilus_progress_info_take_status (extract_job->common.progress, g_strdup_printf (_("Error extracting “%s”"), basename), NULL); nautilus_show_ok_dialog (primary, NULL, parent); abort_job ((CommonJob *) extract_job); } } static void report_extract_final_progress (ExtractJob *extract_job) { char *short_status; 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); /* extract_job->source_files variable is used intentionally here instead of the * extract_job->total_files variable to avoid printing wrong basename in * the case of skipped files. */ if (extract_job->source_files != NULL && extract_job->source_files->next == NULL) { GFile *source_file; g_autofree gchar *basename = NULL; source_file = G_FILE (extract_job->source_files->data); basename = get_basename (source_file); short_status = g_strdup_printf (_("Extracted “%s”"), basename); status = g_strdup_printf (_("Extracted “%s” to “%s”"), basename, basename_dest); } else { short_status = g_strdup_printf (ngettext ("Extracted %'d file", "Extracted %'d files", extract_job->total_files), extract_job->total_files); status = g_strdup_printf (ngettext ("Extracted %'d file to “%s”", "Extracted %'d files to “%s”", extract_job->total_files), extract_job->total_files, basename_dest); } nautilus_progress_info_take_status (extract_job->common.progress, status, short_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)); nautilus_progress_info_set_progress (extract_job->common.progress, 1, 1); } static void extract_task_thread_func (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { ExtractJob *extract_job = task_data; GList *l; 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")); extract_job->total_files = g_list_length (extract_job->source_files); archive_compressed_sizes = g_malloc0_n (extract_job->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_USEC); 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); g_signal_connect (extractor, "request-passphrase", G_CALLBACK (extract_job_on_request_passphrase), extract_job); extract_job->archive_compressed_size = archive_compressed_sizes[i]; extract_job->destination_decided = FALSE; extract_job->extraction_failed = FALSE; autoar_extractor_start (extractor, extract_job->common.cancellable); g_signal_handlers_disconnect_by_data (extractor, extract_job); if (!extract_job->extraction_failed) { extract_job->base_progress += (gdouble) extract_job->archive_compressed_size / (gdouble) extract_job->total_compressed_size; } else { extract_job->total_files--; extract_job->base_progress *= extract_job->total_compressed_size; extract_job->total_compressed_size -= extract_job->archive_compressed_size; extract_job->base_progress /= extract_job->total_compressed_size; } } if (!job_aborted ((CommonJob *) extract_job)) { report_extract_final_progress (extract_job); } 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; gboolean success = compress_job->source_files != NULL && compress_job->error == NULL && !job_aborted (&compress_job->common) && compress_job->output_archive_exists; if (compress_job->done_callback) { compress_job->done_callback (success ? compress_job->output_file : NULL, success, compress_job->done_callback_data); } g_object_unref (compress_job->output_file); g_clear_error (&compress_job->error); g_list_free_full (compress_job->source_files, g_object_unref); g_free (compress_job->passphrase); finalize_common ((CommonJob *) compress_job); nautilus_file_changes_consume_changes (); } 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 *short_status; 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)); short_status = g_strdup_printf (_("Compressing “%s”"), basename_data); status = g_strdup_printf (_("Compressing “%s” into “%s”"), basename_data, basename_output_file); } else { short_status = g_strdup_printf (ngettext ("Compressing %'d file", "Compressing %'d files", compress_job->total_files), compress_job->total_files); 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, short_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 (4 kB/s)" * * 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/s)", "%s / %s \xE2\x80\x94 %s left (%s/s)", 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 (4 kB/s)" * * 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/s)", "%'d / %'d \xE2\x80\x94 %s left (%s/s)", 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 *short_status; char *status; g_autofree gchar *basename_output_file = NULL; compress_job->error = g_error_copy (error); 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)); short_status = g_strdup_printf (_("Error compressing “%s”"), basename_data); status = g_strdup_printf (_("Error compressing “%s” into “%s”"), basename_data, basename_output_file); } else { short_status = g_strdup_printf (ngettext ("Error compressing %'d file", "Error compressing %'d files", compress_job->total_files), compress_job->total_files); 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, short_status); nautilus_show_ok_dialog (_("There was an error while compressing files."), error->message, GTK_WIDGET (compress_job->common.parent_window)); 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 *short_status; 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)); short_status = g_strdup_printf (_("Compressed “%s”"), basename_data); status = g_strdup_printf (_("Compressed “%s” into “%s”"), basename_data, basename_output_file); } else { short_status = g_strdup_printf (ngettext ("Compressed %'d file", "Compressed %'d files", compress_job->total_files), compress_job->total_files); 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, short_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; g_auto (SourceInfo) source_info = SOURCE_INFO_INIT; g_autoptr (AutoarCompressor) compressor = NULL; GList *l; 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; /* take out files that should be skipped */ l = compress_job->source_files; while (l != NULL) { GList *next = l->next; if (should_skip_file ((CommonJob *) compress_job, l->data)) { compress_job->source_files = g_list_remove_link (compress_job->source_files, l); g_list_free_full (l, g_object_unref); } l = next; } if (compress_job->source_files == NULL) { g_clear_object (&compress_job->common.undo_info); return; } compressor = autoar_compressor_new (compress_job->source_files, compress_job->output_file, compress_job->format, compress_job->filter, FALSE); if (compress_job->passphrase && compress_job->passphrase[0] != '\0') { autoar_compressor_set_passphrase (compressor, compress_job->passphrase); } autoar_compressor_set_output_is_dest (compressor, TRUE); autoar_compressor_set_notify_interval (compressor, PROGRESS_NOTIFY_INTERVAL_USEC); 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->output_archive_exists = g_file_query_exists (compress_job->output_file, NULL); /* There is nothing to undo if the output was not created */ if (!compress_job->output_archive_exists) { g_clear_object (&compress_job->common.undo_info); } } void nautilus_file_operations_compress (GList *files, GFile *output, AutoarFormat format, AutoarFilter filter, const gchar *passphrase, 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->passphrase = g_strdup (passphrase); 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, passphrase); } 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); }