/** * @file gnome-cmd-delete-dialog.cc * @copyright (C) 2001-2006 Marcus Bjurman\n * @copyright (C) 2007-2012 Piotr Eljasiak\n * @copyright (C) 2013-2023 Uwe Scholz\n * * @copyright 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. * * @copyright 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. * * @copyright You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "gnome-cmd-includes.h" #include "gnome-cmd-data.h" #include "gnome-cmd-dir.h" #include "gnome-cmd-file-list.h" #include "gnome-cmd-main-win.h" #include "utils.h" #include "dialogs/gnome-cmd-delete-dialog.h" using namespace std; #define DELETE_NONEMPTY_CANCEL 0 #define DELETE_NONEMPTY_SKIP 1 #define DELETE_NONEMPTY_DELETEALL 2 #define DELETE_NONEMPTY_DELETE 3 #define DELETE_ERROR_ACTION_ABORT 0 #define DELETE_ERROR_ACTION_RETRY 1 #define DELETE_ERROR_ACTION_SKIP 2 static gboolean perform_delete_operation_r(DeleteData *deleteData, GList *gnomeCmdFileGList); inline void cleanup (DeleteData *deleteData) { gnome_cmd_file_list_free (deleteData->gnomeCmdFiles); //Just set deletedGnomeCmdFiles back to null as it is a subset of gnomeCmdFiles g_list_free (deleteData->deletedGnomeCmdFiles); deleteData->deletedGnomeCmdFiles = nullptr; g_mutex_clear(&deleteData->mutex); if (deleteData->cancellable) g_object_unref (deleteData->cancellable); g_free (deleteData); } static void delete_progress_update (DeleteData *deleteData) { g_mutex_lock (&deleteData->mutex); if (deleteData->error) { deleteData->problem = TRUE; g_mutex_unlock (&deleteData->mutex); while (deleteData->problem_action == -1) g_thread_yield (); g_mutex_lock (&deleteData->mutex); deleteData->problemFileName = nullptr; g_clear_error (&(deleteData->error)); } if (deleteData->itemsDeleted > 0 && deleteData->itemsTotal > 0) { gfloat f = (gfloat)deleteData->itemsDeleted/(gfloat)deleteData->itemsTotal; g_free (deleteData->msg); deleteData->msg = g_strdup_printf (ngettext("Deleted %lu of %lu file", "Deleted %lu of %lu files", deleteData->itemsTotal), deleteData->itemsDeleted, deleteData->itemsTotal); if (f < 0.001f) f = 0.001f; if (f > 0.999f) f = 0.999f; deleteData->progress = f; } g_mutex_unlock (&deleteData->mutex); } static void on_cancel (GtkButton *btn, DeleteData *deleteData) { deleteData->stop = TRUE; if (deleteData->cancellable) g_cancellable_cancel (deleteData->cancellable); gtk_widget_set_sensitive (GTK_WIDGET (deleteData->progwin), FALSE); } static gboolean on_progwin_destroy (GtkWidget *win, DeleteData *deleteData) { deleteData->stop = TRUE; if (deleteData->cancellable) g_cancellable_cancel (deleteData->cancellable); return FALSE; } inline void create_delete_progress_win (DeleteData *deleteData) { GtkWidget *vbox; GtkWidget *bbox; GtkWidget *button; deleteData->progwin = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (deleteData->progwin), _("Deleting…")); gtk_window_set_policy (GTK_WINDOW (deleteData->progwin), FALSE, FALSE, FALSE); gtk_window_set_position (GTK_WINDOW (deleteData->progwin), GTK_WIN_POS_CENTER); gtk_widget_set_size_request (GTK_WIDGET (deleteData->progwin), 300, -1); g_signal_connect (deleteData->progwin, "destroy-event", G_CALLBACK (on_progwin_destroy), deleteData); vbox = create_vbox (deleteData->progwin, FALSE, 6); gtk_container_add (GTK_CONTAINER (deleteData->progwin), vbox); gtk_container_set_border_width (GTK_CONTAINER (vbox), 5); deleteData->proglabel = create_label (deleteData->progwin, ""); gtk_container_add (GTK_CONTAINER (vbox), deleteData->proglabel); deleteData->progbar = create_progress_bar (deleteData->progwin); gtk_container_add (GTK_CONTAINER (vbox), deleteData->progbar); bbox = create_hbuttonbox (deleteData->progwin); gtk_container_add (GTK_CONTAINER (vbox), bbox); button = create_stock_button_with_data (deleteData->progwin, GTK_STOCK_CANCEL, GTK_SIGNAL_FUNC (on_cancel), deleteData); gtk_widget_set_can_default (button, TRUE); gtk_container_add (GTK_CONTAINER (bbox), button); g_object_ref (deleteData->progwin); gtk_widget_show (deleteData->progwin); } /** * @brief This function is meant to be called from within the function perform_delete_operation_r(). * It will dive through a non-empty directory and tries to recursively delete all files and * folders inside of it. * * @param gnomeCmdDir Directory to go through and which should be deleted * @param deleteData DeleteData object * @return TRUE if deletion was performed successfully */ static gboolean perform_delete_subdirs(GnomeCmdDir *gnomeCmdDir, DeleteData *deleteData) { gboolean deleted = TRUE; for (GList *dirChildItem = gnome_cmd_dir_get_files (gnomeCmdDir); dirChildItem; dirChildItem = dirChildItem->next) { if (deleteData->problem_action == DELETE_ERROR_ACTION_ABORT) { deleteData->stop = TRUE; gnome_cmd_dir_unref (gnomeCmdDir); return FALSE; } if (deleteData->problem_action == DELETE_ERROR_ACTION_RETRY) { dirChildItem = dirChildItem->prev ? dirChildItem->prev : dirChildItem; deleteData->problem_action = -1; } if (deleteData->problem_action == DELETE_ERROR_ACTION_SKIP) { // just go on and set problem_action to the default value deleteData->problem_action = -1; } auto gnomeCmdFile = static_cast(dirChildItem->data); if (gnomeCmdFile->is_dotdot) continue; GList *childsList = nullptr; childsList = g_list_append(childsList, gnomeCmdFile); deleted &= perform_delete_operation_r (deleteData, childsList); g_list_free(childsList); } return deleted; } /** * This function recursively removes files of a given GnomeCmdFile list and stores * possible errors or the progress information in the deleteData object. */ static gboolean perform_delete_operation_r(DeleteData *deleteData, GList *gnomeCmdFileGList) { for (GList *gCmdFileGListItem = gnomeCmdFileGList; gCmdFileGListItem; gCmdFileGListItem = gCmdFileGListItem->next) { GError *tmpError = nullptr; if (deleteData->stop) { return FALSE; } auto gnomeCmdFile = static_cast(gCmdFileGListItem->data); g_return_val_if_fail (GNOME_CMD_IS_FILE(gnomeCmdFile), FALSE); g_return_val_if_fail (G_IS_FILE_INFO(gnomeCmdFile->gFileInfo), FALSE); auto filenameTmp = gnomeCmdFile->get_name(); if (gnomeCmdFile->is_dotdot || strcmp(filenameTmp, ".") == 0) continue; switch (deleteData->originAction) { case DeleteData::OriginAction::FORCE_DELETE: case DeleteData::OriginAction::MOVE: g_file_delete (gnomeCmdFile->gFile, deleteData->cancellable, &tmpError); break; case DeleteData::OriginAction::DELETE: default: gnome_cmd_data.options.deleteToTrash ? g_file_trash (gnomeCmdFile->gFile, deleteData->cancellable, &tmpError) : g_file_delete (gnomeCmdFile->gFile, deleteData->cancellable, &tmpError); break; } if (tmpError && g_error_matches (tmpError, G_IO_ERROR, G_IO_ERROR_NOT_EMPTY)) { g_error_free(tmpError); tmpError = nullptr; auto gnomeCmdDir = GNOME_CMD_DIR (gnomeCmdFile); gnome_cmd_dir_list_files (gnomeCmdDir, FALSE); if (perform_delete_subdirs(gnomeCmdDir, deleteData)) { // Now remove the directory itself, as it is finally empty GList *directory = nullptr; directory = g_list_append(directory, gnomeCmdFile); perform_delete_operation_r (deleteData, directory); g_list_free(directory); } else if (deleteData->stop) { return FALSE; } } else if (tmpError) { if (g_error_matches (tmpError, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_error_free(tmpError); else { g_warning ("Failed to delete %s: %s", gnomeCmdFile->get_name(), tmpError->message); g_propagate_error (&(deleteData->error), tmpError); deleteData->problemFileName = gnomeCmdFile->get_name(); delete_progress_update(deleteData); } return FALSE; } deleteData->deletedGnomeCmdFiles = g_list_append(deleteData->deletedGnomeCmdFiles, gnomeCmdFile); deleteData->itemsDeleted++; delete_progress_update(deleteData); if (deleteData->stop) return FALSE; } return TRUE; } static void perform_delete_operation (DeleteData *deleteData) { perform_delete_operation_r(deleteData, deleteData->gnomeCmdFiles); deleteData->deleteDone = TRUE; } static gboolean update_delete_status_widgets (DeleteData *deleteData) { g_mutex_lock (&deleteData->mutex); if (deleteData->progwin) { gtk_label_set_text (GTK_LABEL (deleteData->proglabel), deleteData->msg); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (deleteData->progbar), deleteData->progress); } if (deleteData->problem) { gchar *msg = g_strdup_printf (_("Error while deleting “%s”\n\n%s"), deleteData->problemFileName, deleteData->error->message); deleteData->problem_action = run_simple_dialog ( *main_win, TRUE, GTK_MESSAGE_ERROR, msg, _("Delete problem"), -1, _("Abort"), _("Retry"), _("Skip"), NULL); g_free (msg); deleteData->problem = FALSE; } g_mutex_unlock (&deleteData->mutex); if (deleteData->deleteDone) { g_thread_join (deleteData->thread); if (deleteData->error) gnome_cmd_show_message (*main_win, deleteData->error->message); if (deleteData->deletedGnomeCmdFiles) { for (GList *i = deleteData->deletedGnomeCmdFiles; i; i = i->next) { auto gnomeCmdFile = GNOME_CMD_FILE (i->data); auto gFile = gnomeCmdFile->get_gfile(); if (!g_file_query_exists (gFile, nullptr)) gnomeCmdFile->is_deleted(); } } if (deleteData->progwin) { gtk_widget_destroy (deleteData->progwin); } cleanup (deleteData); return FALSE; // returning FALSE here stops the timeout callbacks } return TRUE; } void do_delete (DeleteData *deleteData, gboolean showProgress = true) { g_return_if_fail(GNOME_CMD_IS_FILE(deleteData->gnomeCmdFiles->data)); g_mutex_init(&deleteData->mutex); deleteData->deleteDone = FALSE; deleteData->error = nullptr; deleteData->problem_action = -1; deleteData->itemsDeleted = 0; deleteData->deletedGnomeCmdFiles = nullptr; for(auto fileListItem = deleteData->gnomeCmdFiles; fileListItem; fileListItem = fileListItem->next) { auto gFile = GNOME_CMD_FILE(fileListItem->data)->gFile; g_return_if_fail(G_IS_FILE(gFile)); if (showProgress) { guint64 num_files = 0; guint64 num_dirs = 0; GError *error = nullptr; g_file_measure_disk_usage (gFile, G_FILE_MEASURE_NONE, nullptr, nullptr, nullptr, nullptr, &num_dirs, &num_files, &error); // If we cannot determine if gFile is empty or not, don't show progress dialog if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { showProgress = FALSE; g_error_free (error); error = nullptr; break; } deleteData->itemsTotal += num_files + num_dirs; } } if (showProgress) { create_delete_progress_win (deleteData); deleteData->cancellable = g_cancellable_new(); } else deleteData->itemsTotal = 0; deleteData->thread = g_thread_new (NULL, (GThreadFunc) perform_delete_operation, deleteData); g_timeout_add (gnome_cmd_data.gui_update_rate, (GSourceFunc) update_delete_status_widgets, deleteData); } /** * Remove a directory from the list of files to be deleted. * This happens as of user interaction. * The returned list has to be free'ed by g_list_free. */ static GList *remove_items_from_list_to_be_deleted(GList *files) { if (!gnome_cmd_data.options.confirm_delete) { return files; }; auto itemsToDelete = g_list_copy(files); gint dirCount = 0; gint guiResponse = -1; for (auto file = files; file; file = file->next) { auto gnomeCmdFile = (GnomeCmdFile*) file->data; if (!gnomeCmdFile->GetGfileAttributeBoolean(G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK) && gnomeCmdFile->GetGfileAttributeUInt32(G_FILE_ATTRIBUTE_STANDARD_TYPE) == G_FILE_TYPE_DIRECTORY) { GError *error; error = nullptr; guint64 num_dirs; guint64 num_files; gboolean canNotMeasure = FALSE; g_file_measure_disk_usage (gnomeCmdFile->gFile, G_FILE_MEASURE_NONE, nullptr, nullptr, nullptr, nullptr, &num_dirs, &num_files, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { // If we cannot determine if gFile is empty or not, we have to assume it is not empty canNotMeasure = TRUE; g_error_free (error); error = nullptr; } if (error) { g_message ("remove_items_from_list_to_be_deleted: g_file_measure_disk_usage failed: %s", error->message); gchar *msg = g_strdup_printf (_("Error while deleting: \n\n%s"), error->message); run_simple_dialog (*main_win, FALSE, GTK_MESSAGE_ERROR, msg, _("Delete problem"), -1, _("Abort"), NULL); g_free (msg); g_error_free (error); return 0; } if (num_dirs != 1 || num_files != 0 || canNotMeasure) // num_dirs = 1 -> this is the folder to be deleted { gchar *msg = NULL; msg = canNotMeasure ? g_strdup_printf (_("Do you really want to delete “%s”?"), gnomeCmdFile->get_name()) : g_strdup_printf (_("The directory “%s” is not empty. Do you really want to delete it?"), gnomeCmdFile->get_name()); guiResponse = run_simple_dialog (*main_win, FALSE, GTK_MESSAGE_WARNING, msg, _("Delete"), gnome_cmd_data.options.confirm_delete_default==GTK_BUTTONS_CANCEL ? 0 : 3, _("Cancel"), _("Skip"), dirCount++ == 0 ? _("Delete All") : _("Delete Remaining"), _("Delete"), nullptr); g_free(msg); if (guiResponse != DELETE_NONEMPTY_SKIP && guiResponse != DELETE_NONEMPTY_DELETEALL && guiResponse != DELETE_NONEMPTY_DELETE) { guiResponse = DELETE_NONEMPTY_CANCEL; // Set to zero for the case the user presses ESCAPE in the warning dialog) } if (guiResponse == DELETE_NONEMPTY_CANCEL || guiResponse == DELETE_NONEMPTY_DELETEALL) { break; } else if (guiResponse == DELETE_NONEMPTY_SKIP) { itemsToDelete = g_list_remove(itemsToDelete, file->data); continue; } else if (guiResponse == DELETE_NONEMPTY_DELETE) { continue; } else { break; } } } } if (guiResponse == DELETE_NONEMPTY_CANCEL) { g_list_free(itemsToDelete); return nullptr; } if (g_list_length(itemsToDelete) == 0) { return nullptr; // file list is empty } return itemsToDelete; } /** * Creates a delete dialog for the given list of GnomeCmdFiles */ void gnome_cmd_delete_dialog_show (GList *files, gboolean forceDelete) { g_return_if_fail (files != nullptr); gint response = 1; if (gnome_cmd_data.options.confirm_delete) { gchar *msg = nullptr; gint n_files = g_list_length (files); if (n_files == 1) { auto gnomeCmdFile = static_cast (g_list_nth_data (files, 0)); g_return_if_fail (GNOME_CMD_IS_FILE(gnomeCmdFile)); if (gnomeCmdFile->is_dotdot) return; msg = forceDelete || !gnome_cmd_data.options.deleteToTrash ? g_strdup_printf (_("Do you want to permanently delete “%s”?"), gnomeCmdFile->get_name()) : g_strdup_printf (_("Do you want to move “%s” to the trash can?"), gnomeCmdFile->get_name()); } else { msg = forceDelete || !gnome_cmd_data.options.deleteToTrash ? g_strdup_printf (ngettext("Do you want to permanently delete the selected file?", "Do you want to permanently delete the %d selected files?", n_files), n_files) : g_strdup_printf (ngettext("Do you want to move the selected file to the trash can?", "Do you want to move the %d selected files to the trash can?", n_files), n_files); } response = run_simple_dialog (*main_win, FALSE, forceDelete || !gnome_cmd_data.options.deleteToTrash ? GTK_MESSAGE_WARNING : GTK_MESSAGE_QUESTION, msg, _("Delete"), gnome_cmd_data.options.confirm_delete_default==GTK_BUTTONS_CANCEL ? 0 : 1, _("Cancel"), _("Delete"), NULL); g_free (msg); } if (response != 1) { g_list_free (files); return; } // eventually remove non-empty dirs from list files = remove_items_from_list_to_be_deleted(files); if (files == nullptr) return; DeleteData *deleteData = g_new0 (DeleteData, 1); deleteData->gnomeCmdFiles = files; deleteData->originAction = forceDelete ? DeleteData::OriginAction::FORCE_DELETE : DeleteData::OriginAction::DELETE; // Refing files for the delete procedure gnome_cmd_file_list_ref (deleteData->gnomeCmdFiles); do_delete (deleteData); }