/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * GThumb * * Copyright (C) 2010 Free Software Foundation, 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 . */ #include #include #include #include #include #include #include "gth-find-duplicates.h" #include "gth-folder-chooser-dialog.h" #define GET_WIDGET(x) (_gtk_builder_get_widget (self->priv->builder, (x))) #define BUFFER_SIZE 4096 #define SELECT_COMMAND_ID_DATA "delete-command-id" #define PULSE_DELAY 50 enum { FILE_LIST_COLUMN_FILE = 0, FILE_LIST_COLUMN_CHECKED, FILE_LIST_COLUMN_FILENAME, FILE_LIST_COLUMN_POSITION, FILE_LIST_COLUMN_LAST_MODIFIED, FILE_LIST_COLUMN_VISIBLE, FILE_LIST_COLUMN_LAST_MODIFIED_TIME, }; typedef enum { SELECT_LEAVE_NEWEST, SELECT_LEAVE_OLDEST, SELECT_BY_FOLDER, SELECT_ALL, SELECT_NONE } SelectID; typedef struct { const char *display_name; SelectID id; } SelectCommand; SelectCommand select_commands[] = { { N_("leave the newest duplicates"), SELECT_LEAVE_NEWEST }, { N_("leave the oldest duplicates"), SELECT_LEAVE_OLDEST }, { N_("by folder…"), SELECT_BY_FOLDER }, { N_("all files"), SELECT_ALL }, { N_("no file"), SELECT_NONE } }; struct _GthFindDuplicatesPrivate { GthBrowser *browser; GtkWidget *dialog; GFile *location; gboolean recursive; GthTest *test; GtkBuilder *builder; GtkWidget *duplicates_list; GtkWidget *select_button; GtkWidget *select_menu; GString *attributes; GCancellable *cancellable; gboolean io_operation; gboolean closing; GthFileSource *file_source; int n_duplicates; goffset duplicates_size; int n_files; int n_file; GList *files; GList *directories; GFile *current_directory; GthFileData *current_file; guchar buffer[BUFFER_SIZE]; GChecksum *checksum; GInputStream *file_stream; GHashTable *duplicated; gulong folder_changed_id; guint pulse_event_id; }; G_DEFINE_TYPE_WITH_CODE (GthFindDuplicates, gth_find_duplicates, G_TYPE_OBJECT, G_ADD_PRIVATE (GthFindDuplicates)) typedef struct { GthFileData *file_data; GList *files; goffset total_size; int n_files; } DuplicatedData; static DuplicatedData * duplicated_data_new (void) { DuplicatedData *d_data; d_data = g_new0 (DuplicatedData, 1); d_data->file_data = NULL; d_data->files = 0; d_data->total_size = 0; d_data->n_files = 0; return d_data; } static void duplicated_data_free (DuplicatedData *d_data) { _g_object_list_unref (d_data->files); _g_object_unref (d_data->file_data); g_free (d_data); } static void gth_find_duplicates_finalize (GObject *object) { GthFindDuplicates *self; self = GTH_FIND_DUPLICATES (object); if (self->priv->pulse_event_id != 0) g_source_remove (self->priv->pulse_event_id); if (self->priv->folder_changed_id != 0) g_signal_handler_disconnect (gth_main_get_default_monitor (), self->priv->folder_changed_id); g_object_unref (self->priv->location); _g_object_unref (self->priv->test); _g_object_unref (self->priv->builder); if (self->priv->attributes != NULL) g_string_free (self->priv->attributes, TRUE); g_object_unref (self->priv->cancellable); _g_object_unref (self->priv->file_source); _g_object_list_unref (self->priv->files); _g_object_list_unref (self->priv->directories); _g_object_unref (self->priv->current_file); _g_object_unref (self->priv->current_directory); if (self->priv->checksum != NULL) g_checksum_free (self->priv->checksum); _g_object_unref (self->priv->file_stream); g_hash_table_unref (self->priv->duplicated); G_OBJECT_CLASS (gth_find_duplicates_parent_class)->finalize (object); } static void gth_find_duplicates_class_init (GthFindDuplicatesClass *klass) { GObjectClass *object_class; object_class = (GObjectClass*) klass; object_class->finalize = gth_find_duplicates_finalize; } static void gth_find_duplicates_init (GthFindDuplicates *self) { self->priv = gth_find_duplicates_get_instance_private (self); self->priv->test = NULL; self->priv->builder = NULL; self->priv->attributes = NULL; self->priv->io_operation = FALSE; self->priv->n_duplicates = 0; self->priv->duplicates_size = 0; self->priv->file_source = NULL; self->priv->files = NULL; self->priv->directories = NULL; self->priv->current_directory = NULL; self->priv->current_file = NULL; self->priv->checksum = NULL; self->priv->file_stream = NULL; self->priv->duplicated = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) duplicated_data_free); self->priv->cancellable = g_cancellable_new (); self->priv->folder_changed_id = 0; } static void update_file_list_sensitivity (GthFindDuplicates *self) { GtkTreeModel *model; GtkTreeIter iter; gboolean one_active = FALSE; model = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); if (gtk_tree_model_get_iter_first (model, &iter)) { do { gboolean active; gboolean visible; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_CHECKED, &active, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (active && visible) { one_active = TRUE; break; } } while (gtk_tree_model_iter_next (model, &iter)); } gtk_widget_set_sensitive (GET_WIDGET ("view_button"), one_active); gtk_widget_set_sensitive (GET_WIDGET ("delete_button"), one_active); } static void update_file_list_selection_info (GthFindDuplicates *self) { GtkTreeModel *model; GtkTreeIter iter; int n_files; goffset total_size; char *size_formatted; char *text; n_files = 0; total_size = 0; model = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); if (gtk_tree_model_get_iter_first (model, &iter)) { do { GthFileData *file_data; gboolean active; gboolean visible; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, FILE_LIST_COLUMN_CHECKED, &active, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (active && visible) { n_files += 1; total_size += g_file_info_get_size (file_data->info); } _g_object_unref (file_data); } while (gtk_tree_model_iter_next (model, &iter)); } size_formatted = g_format_size (total_size); text = g_strdup_printf (g_dngettext (NULL, "%d file (%s)", "%d files (%s)", n_files), n_files, size_formatted); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("total_files_label")), text); g_free (text); g_free (size_formatted); } static GList * get_duplicates_file_data_list (GthFindDuplicates *self) { GtkWidget *duplicates_view; GList *items; GList *file_data_list; duplicates_view = gth_file_list_get_view (GTH_FILE_LIST (self->priv->duplicates_list)); items = gth_file_selection_get_selected (GTH_FILE_SELECTION (duplicates_view)); file_data_list = gth_file_list_get_files (GTH_FILE_LIST (self->priv->duplicates_list), items); if (file_data_list == NULL) file_data_list = gth_file_store_get_visibles (GTH_FILE_STORE (gth_file_view_get_model (GTH_FILE_VIEW (duplicates_view)))); _gtk_tree_path_list_free (items); return file_data_list; } #if 0 static void duplicates_list_view_selection_changed_cb (GthFileView *fileview, gpointer user_data) { GthFindDuplicates *self = user_data; GList *file_data_list; GList *scan; file_data_list = get_duplicates_file_data_list (self); gtk_list_store_clear (GTK_LIST_STORE (GET_WIDGET ("files_liststore"))); for (scan = file_data_list; scan; scan = scan->next) { GthFileData *selected_file_data = (GthFileData *) scan->data; const char *checksum; DuplicatedData *d_data; GList *scan_duplicated; checksum = g_file_info_get_attribute_string (selected_file_data->info, "find-duplicates::checksum"); d_data = g_hash_table_lookup (self->priv->duplicated, checksum); g_return_if_fail (d_data != NULL); for (scan_duplicated = d_data->files; scan_duplicated; scan_duplicated = scan_duplicated->next) { GthFileData *file_data = scan_duplicated->data; GFile *parent; char *parent_name; GtkTreeIter iter; parent = g_file_get_parent (file_data->file); if (parent != NULL) parent_name = g_file_get_parse_name (parent); else parent_name = NULL; gtk_list_store_append (GTK_LIST_STORE (GET_WIDGET ("files_liststore")), &iter); gtk_list_store_set (GTK_LIST_STORE (GET_WIDGET ("files_liststore")), &iter, FILE_LIST_COLUMN_FILE, file_data, FILE_LIST_COLUMN_CHECKED, TRUE, FILE_LIST_COLUMN_FILENAME, g_file_info_get_display_name (file_data->info), FILE_LIST_COLUMN_POSITION, parent_name, FILE_LIST_COLUMN_LAST_MODIFIED, g_file_info_get_attribute_string (file_data->info, "gth::file::display-mtime"), -1); g_free (parent_name); g_object_unref (parent); } } update_file_list_sensitivity (self); update_file_list_selection_info (self); _g_object_list_unref (file_data_list); } #endif static void duplicates_list_view_selection_changed_cb (GthFileView *fileview, gpointer user_data) { GthFindDuplicates *self = user_data; GList *file_data_list; GHashTable *selected_files; GtkTreeModel *files_treemodel; GtkTreeModel *files_treemodelfilter; GtkTreeIter iter; GList *scan; selected_files = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); file_data_list = get_duplicates_file_data_list (self); for (scan = file_data_list; scan; scan = scan->next) { GthFileData *selected_file_data = (GthFileData *) scan->data; const char *checksum; DuplicatedData *d_data; GList *scan_duplicated; checksum = g_file_info_get_attribute_string (selected_file_data->info, "find-duplicates::checksum"); d_data = g_hash_table_lookup (self->priv->duplicated, checksum); g_return_if_fail (d_data != NULL); for (scan_duplicated = d_data->files; scan_duplicated; scan_duplicated = scan_duplicated->next) { GthFileData *file_data = scan_duplicated->data; g_hash_table_insert (selected_files, g_object_ref (file_data->file), GINT_TO_POINTER (1)); } } files_treemodel = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); files_treemodelfilter = GTK_TREE_MODEL (GET_WIDGET ("files_treemodelfilter")); g_object_ref (files_treemodelfilter); gtk_tree_view_set_model (GTK_TREE_VIEW (GET_WIDGET ("files_treeview")), NULL); /* to avoid excessive recomputation */ if (gtk_tree_model_get_iter_first (files_treemodel, &iter)) { do { GthFileData *file_data; gtk_tree_model_get (files_treemodel, &iter, FILE_LIST_COLUMN_FILE, &file_data, -1); gtk_list_store_set (GTK_LIST_STORE (files_treemodel), &iter, FILE_LIST_COLUMN_VISIBLE, (g_hash_table_lookup (selected_files, file_data->file) != NULL), -1); g_object_unref (file_data); } while (gtk_tree_model_iter_next (files_treemodel, &iter)); } gtk_tree_view_set_model (GTK_TREE_VIEW (GET_WIDGET ("files_treeview")), files_treemodelfilter); g_object_unref (files_treemodelfilter); update_file_list_sensitivity (self); update_file_list_selection_info (self); _g_object_list_unref (file_data_list); g_hash_table_unref (selected_files); } static void files_tree_view_selection_changed_cb (GtkTreeSelection *tree_selection, gpointer user_data) { GthFindDuplicates *self = user_data; GtkTreeModel *model; GtkTreeIter iter; GthFileData *file_data; const char *checksum; DuplicatedData *d_data; if (! gtk_tree_selection_get_selected (tree_selection, &model, &iter)) return; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, -1); checksum = g_file_info_get_attribute_string (file_data->info, "find-duplicates::checksum"); d_data = g_hash_table_lookup (self->priv->duplicated, checksum); if (d_data != NULL) { GtkWidget *duplicates_view; int pos; duplicates_view = gth_file_list_get_view (GTH_FILE_LIST (self->priv->duplicates_list)); pos = gth_file_store_get_pos (GTH_FILE_STORE (gth_file_view_get_model (GTH_FILE_VIEW (duplicates_view))), d_data->file_data->file); if (pos >= 0) { /* FIXME */ /*g_signal_handlers_block_by_data (duplicates_view, self); gth_file_selection_select (GTH_FILE_SELECTION (duplicates_view), pos);*/ gth_file_view_scroll_to (GTH_FILE_VIEW (duplicates_view), pos, 0.5); /*g_signal_handlers_unblock_by_data (duplicates_view, self);*/ } } g_object_unref (file_data); } static void update_total_duplicates_label (GthFindDuplicates *self) { char *size_formatted; char *text; size_formatted = g_format_size (self->priv->duplicates_size); text = g_strdup_printf (g_dngettext (NULL, "%d file (%s)", "%d files (%s)", self->priv->n_duplicates), self->priv->n_duplicates, size_formatted); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("total_duplicates_label")), text); g_free (text); g_free (size_formatted); } static void folder_changed_cb (GthMonitor *monitor, GFile *parent, GList *list, int position, GthMonitorEvent event, gpointer user_data) { GthFindDuplicates *self = user_data; GList *file_scan; if (event != GTH_MONITOR_EVENT_DELETED) return; for (file_scan = list; file_scan; file_scan = file_scan->next) { GFile *file = file_scan->data; GList *values; GList *scan; values = g_hash_table_get_values (self->priv->duplicated); for (scan = values; scan; scan = scan->next) { DuplicatedData *d_data = scan->data; GList *link; char *text; GList *singleton; link = gth_file_data_list_find_file (d_data->files, file); if (link == NULL) continue; d_data->files = g_list_remove_link (d_data->files, link); d_data->n_files -= 1; d_data->total_size -= g_file_info_get_size (d_data->file_data->info); text = g_strdup_printf (g_dngettext (NULL, "%d duplicate", "%d duplicates", d_data->n_files - 1), d_data->n_files - 1); g_file_info_set_attribute_string (d_data->file_data->info, "find-duplicates::n-duplicates", text); g_free (text); singleton = g_list_append (NULL, d_data->file_data); if (d_data->n_files <= 1) gth_file_list_delete_files (GTH_FILE_LIST (self->priv->duplicates_list), singleton); else gth_file_list_update_files (GTH_FILE_LIST (self->priv->duplicates_list), singleton); g_list_free (singleton); self->priv->n_duplicates -= 1; self->priv->duplicates_size -= g_file_info_get_size (d_data->file_data->info); update_total_duplicates_label (self); _g_object_list_unref (link); } g_list_free (values); } duplicates_list_view_selection_changed_cb (NULL, self); update_file_list_sensitivity (self); update_file_list_selection_info (self); } static void after_checksums (GthFindDuplicates *self) { self->priv->folder_changed_id = g_signal_connect (gth_main_get_default_monitor (), "folder-changed", G_CALLBACK (folder_changed_cb), self); gtk_notebook_set_current_page (GTK_NOTEBOOK (GET_WIDGET ("pages_notebook")), (self->priv->n_duplicates > 0) ? 0 : 1); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("progress_label")), _("Search completed")); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("search_details_label")), ""); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (GET_WIDGET ("search_progressbar")), 1.0); gtk_widget_set_sensitive (GET_WIDGET ("stop_button"), FALSE); duplicates_list_view_selection_changed_cb (NULL, self); } static void start_next_checksum (GthFindDuplicates *self); static void _file_list_add_file (GthFindDuplicates *self, GthFileData *file_data) { GFile *parent; char *parent_name; GTimeVal timeval; GtkTreeIter iter; parent = g_file_get_parent (file_data->file); if (parent != NULL) parent_name = g_file_get_parse_name (parent); else parent_name = NULL; g_file_info_get_modification_time (file_data->info, &timeval); gtk_list_store_append (GTK_LIST_STORE (GET_WIDGET ("files_liststore")), &iter); gtk_list_store_set (GTK_LIST_STORE (GET_WIDGET ("files_liststore")), &iter, FILE_LIST_COLUMN_FILE, file_data, FILE_LIST_COLUMN_CHECKED, TRUE, FILE_LIST_COLUMN_FILENAME, g_file_info_get_display_name (file_data->info), FILE_LIST_COLUMN_POSITION, parent_name, FILE_LIST_COLUMN_LAST_MODIFIED, g_file_info_get_attribute_string (file_data->info, "gth::file::display-mtime"), FILE_LIST_COLUMN_VISIBLE, TRUE, FILE_LIST_COLUMN_LAST_MODIFIED_TIME, timeval.tv_sec, -1); g_free (parent_name); g_object_unref (parent); } static void file_input_stream_read_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GthFindDuplicates *self = user_data; GError *error = NULL; gssize buffer_size; self->priv->io_operation = FALSE; if (self->priv->closing) { gtk_widget_destroy (self->priv->dialog); return; } buffer_size = g_input_stream_read_finish (G_INPUT_STREAM (source), result, &error); if (buffer_size < 0) { start_next_checksum (self); return; } else if (buffer_size == 0) { const char *checksum; DuplicatedData *d_data; self->priv->n_file += 1; g_object_unref (self->priv->file_stream); self->priv->file_stream = NULL; checksum = g_checksum_get_string (self->priv->checksum); g_file_info_set_attribute_string (self->priv->current_file->info, "find-duplicates::checksum", checksum); d_data = g_hash_table_lookup (self->priv->duplicated, checksum); if (d_data == NULL) { d_data = duplicated_data_new (); g_hash_table_insert (self->priv->duplicated, g_strdup (checksum), d_data); } if (d_data->file_data == NULL) d_data->file_data = g_object_ref (self->priv->current_file); d_data->files = g_list_prepend (d_data->files, g_object_ref (self->priv->current_file)); d_data->n_files += 1; d_data->total_size += g_file_info_get_size (self->priv->current_file->info); if (d_data->n_files > 1) { char *text; GList *singleton; text = g_strdup_printf (g_dngettext (NULL, "%d duplicate", "%d duplicates", d_data->n_files - 1), d_data->n_files - 1); g_file_info_set_attribute_string (d_data->file_data->info, "find-duplicates::n-duplicates", text); g_free (text); singleton = g_list_append (NULL, d_data->file_data); if (d_data->n_files == 2) { gth_file_list_add_files (GTH_FILE_LIST (self->priv->duplicates_list), singleton, -1); _file_list_add_file (self, d_data->file_data); /* add the first one as well */ } else gth_file_list_update_files (GTH_FILE_LIST (self->priv->duplicates_list), singleton); _file_list_add_file (self, self->priv->current_file); g_list_free (singleton); self->priv->n_duplicates += 1; self->priv->duplicates_size += g_file_info_get_size (d_data->file_data->info); update_total_duplicates_label (self); } duplicates_list_view_selection_changed_cb (NULL, self); start_next_checksum (self); return; } self->priv->io_operation = TRUE; g_checksum_update (self->priv->checksum, self->priv->buffer, buffer_size); g_input_stream_read_async (self->priv->file_stream, self->priv->buffer, BUFFER_SIZE, G_PRIORITY_DEFAULT, self->priv->cancellable, file_input_stream_read_ready_cb, self); } static void read_current_file_ready_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GthFindDuplicates *self = user_data; GError *error = NULL; self->priv->io_operation = FALSE; if (self->priv->closing) { gtk_widget_destroy (self->priv->dialog); return; } if (self->priv->file_stream != NULL) g_object_unref (self->priv->file_stream); self->priv->file_stream = (GInputStream *) g_file_read_finish (G_FILE (source), result, &error); if (self->priv->file_stream == NULL) { start_next_checksum (self); return; } self->priv->io_operation = TRUE; g_input_stream_read_async (self->priv->file_stream, self->priv->buffer, BUFFER_SIZE, G_PRIORITY_DEFAULT, self->priv->cancellable, file_input_stream_read_ready_cb, self); } static void start_next_checksum (GthFindDuplicates *self) { GList *link; char *text; int n_remaining; link = self->priv->files; if (link == NULL) { after_checksums (self); return; } self->priv->files = g_list_remove_link (self->priv->files, link); _g_object_unref (self->priv->current_file); self->priv->current_file = (GthFileData *) link->data; g_list_free (link); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("progress_label")), _("Searching for duplicates")); n_remaining = self->priv->n_files - self->priv->n_file; text = g_strdup_printf (g_dngettext (NULL, "%d file remaining", "%d files remaining", n_remaining), n_remaining); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("search_details_label")), text); g_free (text); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (GET_WIDGET ("search_progressbar")), (double) (self->priv->n_file + 1) / (self->priv->n_files + 1)); if (self->priv->checksum == NULL) self->priv->checksum = g_checksum_new (G_CHECKSUM_MD5); else g_checksum_reset (self->priv->checksum); self->priv->io_operation = TRUE; g_file_read_async (self->priv->current_file->file, G_PRIORITY_DEFAULT, self->priv->cancellable, read_current_file_ready_cb, self); } /* -- search_directory -- */ static void done_func (GObject *object, GError *error, gpointer user_data) { GthFindDuplicates *self = user_data; GList *scan; GHashTable *file_sizes; GList *possible_duplicates; g_source_remove (self->priv->pulse_event_id); self->priv->pulse_event_id = 0; self->priv->io_operation = FALSE; if (self->priv->closing) { gtk_widget_destroy (self->priv->dialog); return; } if ((error != NULL) && ! g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { _gtk_error_dialog_from_gerror_show (GTK_WINDOW (self->priv->browser), _("Could not perform the operation"), error); gtk_widget_destroy (self->priv->dialog); return; } /* ignore files with an unique size */ file_sizes = g_hash_table_new_full (g_int64_hash, g_int64_equal, NULL, NULL); for (scan = self->priv->files; scan; scan = scan->next) { GthFileData *file_data = scan->data; gpointer value; int n_files; gint64 size; size = g_file_info_get_size (file_data->info); value = g_hash_table_lookup (file_sizes, &size); n_files = (value == NULL) ? 0 : GPOINTER_TO_INT (value); n_files += 1; g_hash_table_insert (file_sizes, &size, GINT_TO_POINTER (n_files)); } possible_duplicates = NULL; for (scan = self->priv->files; scan; scan = scan->next) { GthFileData *file_data = scan->data; gint64 size; gpointer value; int n_files; size = g_file_info_get_size (file_data->info); value = g_hash_table_lookup (file_sizes, &size); if (value == NULL) continue; n_files = GPOINTER_TO_INT (value); if (n_files > 1) possible_duplicates = g_list_prepend (possible_duplicates, g_object_ref (file_data)); } g_hash_table_destroy (file_sizes); /* start computing checksums */ _g_object_list_unref (self->priv->files); self->priv->files = possible_duplicates; self->priv->n_files = g_list_length (self->priv->files); self->priv->n_file = 0; start_next_checksum (self); } static void for_each_file_func (GFile *file, GFileInfo *info, gpointer user_data) { GthFindDuplicates *self = user_data; GthFileData *file_data; if (g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR) return; file_data = gth_file_data_new (file, info); if (gth_test_match (self->priv->test, file_data)) self->priv->files = g_list_prepend (self->priv->files, g_object_ref (file_data)); g_object_unref (file_data); } static DirOp start_dir_func (GFile *directory, GFileInfo *info, GError **error, gpointer user_data) { GthFindDuplicates *self = user_data; _g_object_unref (self->priv->current_directory); self->priv->current_directory = g_object_ref (directory); return DIR_OP_CONTINUE; } static gboolean pulse_progressbar_cb (gpointer user_data) { GthFindDuplicates *self = user_data; gtk_progress_bar_pulse (GTK_PROGRESS_BAR (GET_WIDGET ("search_progressbar"))); return TRUE; } static void search_directory (GthFindDuplicates *self, GFile *directory) { gtk_widget_set_sensitive (GET_WIDGET ("stop_button"), TRUE); self->priv->io_operation = TRUE; gtk_label_set_text (GTK_LABEL (GET_WIDGET ("progress_label")), _("Getting the file list")); gtk_label_set_text (GTK_LABEL (GET_WIDGET ("search_details_label")), ""); gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (GET_WIDGET ("search_progressbar")), 0.0); self->priv->pulse_event_id = g_timeout_add (PULSE_DELAY, pulse_progressbar_cb, self); gth_file_source_for_each_child (self->priv->file_source, directory, self->priv->recursive, self->priv->attributes->str, start_dir_func, for_each_file_func, done_func, self); } static void find_duplicates_dialog_destroy_cb (GtkWidget *dialog, gpointer user_data) { g_object_unref (GTH_FIND_DUPLICATES (user_data)); } static void close_button_clicked_cb (GtkButton *button, gpointer user_data) { GthFindDuplicates *self = user_data; if (! self->priv->io_operation) { gtk_widget_destroy (self->priv->dialog); } else { self->priv->closing = TRUE; g_cancellable_cancel (self->priv->cancellable); } } static void file_cellrenderertoggle_toggled_cb (GtkCellRendererToggle *cell_renderer, char *path, gpointer user_data) { GthFindDuplicates *self = user_data; GtkTreeModel *model; GtkTreePath *filter_path; GtkTreePath *child_path; GtkTreeIter iter; gboolean active; model = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); filter_path = gtk_tree_path_new_from_string (path); child_path = gtk_tree_model_filter_convert_path_to_child_path (GTK_TREE_MODEL_FILTER (GET_WIDGET ("files_treemodelfilter")), filter_path); if (! gtk_tree_model_get_iter (model, &iter, child_path)) { gtk_tree_path_free (child_path); gtk_tree_path_free (filter_path); return; } gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_CHECKED, &active, -1); gtk_list_store_set (GTK_LIST_STORE (model), &iter, FILE_LIST_COLUMN_CHECKED, ! active, -1); update_file_list_sensitivity (self); update_file_list_selection_info (self); gtk_tree_path_free (child_path); gtk_tree_path_free (filter_path); } static void _file_list_set_sort_column_id (GthFindDuplicates *self, GtkTreeViewColumn *treeviewcolumn, int column) { int old_column; GtkSortType order; GList *columns; GList *scan; gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE (GET_WIDGET ("files_liststore")), &old_column, &order); if (column == old_column) order = (order == GTK_SORT_ASCENDING) ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING; else order = GTK_SORT_ASCENDING; gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (GET_WIDGET ("files_liststore")), column, order); columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (GET_WIDGET ("files_treeview"))); for (scan = columns; scan; scan = scan->next) { GtkTreeViewColumn *c = scan->data; gtk_tree_view_column_set_sort_indicator (c, c == treeviewcolumn); } g_list_free (columns); gtk_tree_view_column_set_sort_order (treeviewcolumn, order); } static void file_treeviewcolumn_clicked_cb (GtkTreeViewColumn *treeviewcolumn, gpointer user_data) { GthFindDuplicates *self = user_data; _file_list_set_sort_column_id (self, treeviewcolumn, FILE_LIST_COLUMN_FILENAME); } static void modified_treeviewcolumn_clicked_cb (GtkTreeViewColumn *treeviewcolumn, gpointer user_data) { GthFindDuplicates *self = user_data; _file_list_set_sort_column_id (self, treeviewcolumn, FILE_LIST_COLUMN_LAST_MODIFIED_TIME); } static void position_treeviewcolumn_clicked_cb (GtkTreeViewColumn *treeviewcolumn, gpointer user_data) { GthFindDuplicates *self = user_data; _file_list_set_sort_column_id (self, treeviewcolumn, FILE_LIST_COLUMN_POSITION); } static GList * get_selected_files (GthFindDuplicates *self) { GtkTreeModel *model; GtkTreeIter iter; GList *list; model = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); if (! gtk_tree_model_get_iter_first (model, &iter)) return NULL; list = NULL; do { GthFileData *file_data; gboolean active; gboolean visible; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, FILE_LIST_COLUMN_CHECKED, &active, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (active && visible) list = g_list_prepend (list, g_object_ref (file_data)); g_object_unref (file_data); } while (gtk_tree_model_iter_next (model, &iter)); return g_list_reverse (list); } static void view_button_clicked_cb (GtkWidget *button, gpointer user_data) { GthFindDuplicates *self = user_data; GList *file_data_list; GList *file_list; GthCatalog *catalog; GFile *catalog_file; file_data_list = get_selected_files (self); if (file_data_list == NULL) return; file_list = gth_file_data_list_to_file_list (file_data_list); catalog = gth_catalog_new (); catalog_file = gth_catalog_file_from_relative_path (_("Duplicates"), ".catalog"); gth_catalog_set_file (catalog, catalog_file); gth_catalog_set_file_list (catalog, file_list); gth_catalog_save (catalog); gth_browser_go_to (self->priv->browser, catalog_file, NULL); g_object_unref (catalog_file); g_object_unref (catalog); _g_object_list_unref (file_list); _g_object_list_unref (file_data_list); } static void delete_button_clicked_cb (GtkWidget *button, gpointer user_data) { GthFindDuplicates *self = user_data; GList *file_data_list; file_data_list = get_selected_files (self); if (file_data_list != NULL) { gth_file_mananger_delete_files (GTK_WINDOW (self->priv->dialog), file_data_list); _g_object_list_unref (file_data_list); } } static void select_files_leaving_one (GthFindDuplicates *self, GtkTreeModel *model, SelectID selection_type) { GHashTable *newest_files; GList *file_data_list; GList *scan; GtkTreeIter iter; newest_files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); file_data_list = get_duplicates_file_data_list (self); for (scan = file_data_list; scan; scan = scan->next) { GthFileData *selected_file_data = (GthFileData *) scan->data; const char *checksum; DuplicatedData *d_data; GList *scan_duplicated; GthFileData *newest_file = NULL; checksum = g_file_info_get_attribute_string (selected_file_data->info, "find-duplicates::checksum"); d_data = g_hash_table_lookup (self->priv->duplicated, checksum); g_return_if_fail (d_data != NULL); for (scan_duplicated = d_data->files; scan_duplicated; scan_duplicated = scan_duplicated->next) { GthFileData *file_data = scan_duplicated->data; if (newest_file != NULL) { GTimeVal *t_newest_file; GTimeVal *t_file_data; gboolean is_newest = FALSE; t_newest_file = gth_file_data_get_modification_time (newest_file); t_file_data = gth_file_data_get_modification_time (file_data); switch (selection_type) { case SELECT_LEAVE_NEWEST: is_newest = _g_time_val_cmp (t_file_data, t_newest_file) > 0; break; case SELECT_LEAVE_OLDEST: is_newest = _g_time_val_cmp (t_file_data, t_newest_file) < 0; break; default: break; } if (is_newest) { g_object_unref (newest_file); newest_file = g_object_ref (file_data); } } else newest_file = g_object_ref (file_data); } g_hash_table_insert (newest_files, g_strdup (checksum), newest_file); } if (gtk_tree_model_get_iter_first (model, &iter)) { do { GthFileData *file_data; gboolean visible; const char *checksum; GthFileData *newest_file; gboolean active; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (visible) { checksum = g_file_info_get_attribute_string (file_data->info, "find-duplicates::checksum"); newest_file = g_hash_table_lookup (newest_files, checksum); active = ((newest_file == NULL) || ! g_file_equal (newest_file->file, file_data->file)); gtk_list_store_set (GTK_LIST_STORE (model), &iter, FILE_LIST_COLUMN_CHECKED, active, -1); } g_object_unref (file_data); } while (gtk_tree_model_iter_next (model, &iter)); } _g_object_list_unref (file_data_list); g_hash_table_unref (newest_files); } static void select_menu_item_activate_cb (GtkMenuItem *menu_item, gpointer user_data) { GthFindDuplicates *self = user_data; SelectID id; GtkTreeModel *model; GtkTreeIter iter; model = GTK_TREE_MODEL (GET_WIDGET ("files_liststore")); id = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (menu_item), SELECT_COMMAND_ID_DATA)); switch (id) { case SELECT_ALL: case SELECT_NONE: if (gtk_tree_model_get_iter_first (model, &iter)) { do { gboolean visible; gtk_tree_model_get (GTK_TREE_MODEL (model), &iter, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (visible) gtk_list_store_set (GTK_LIST_STORE (model), &iter, FILE_LIST_COLUMN_CHECKED, (id == SELECT_ALL), -1); } while (gtk_tree_model_iter_next (model, &iter)); } break; case SELECT_BY_FOLDER: { GHashTable *folders_table; GList *folders = NULL; GtkWidget *dialog; GHashTable *selected_folders = NULL; folders_table = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, NULL); if (gtk_tree_model_get_iter_first (model, &iter)) { do { GthFileData *file_data; gboolean visible; GFile *folder; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (visible) { folder = g_file_get_parent (file_data->file); if (folder != NULL) { if (g_hash_table_lookup (folders_table, folder) == NULL) g_hash_table_insert (folders_table, g_object_ref (folder), GINT_TO_POINTER (1)); g_object_unref (folder); } } g_object_unref (file_data); } while (gtk_tree_model_iter_next (model, &iter)); folders = g_hash_table_get_keys (folders_table); } dialog = gth_folder_chooser_dialog_new (folders); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (self->priv->dialog)); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); gtk_widget_show (dialog); switch (gtk_dialog_run (GTK_DIALOG (dialog))) { case GTK_RESPONSE_OK: selected_folders = gth_folder_chooser_dialog_get_selected (GTH_FOLDER_CHOOSER_DIALOG (dialog)); break; default: break; } gtk_widget_destroy (dialog); if (selected_folders != NULL) { if (gtk_tree_model_get_iter_first (model, &iter)) { do { GthFileData *file_data; gboolean visible; gtk_tree_model_get (model, &iter, FILE_LIST_COLUMN_FILE, &file_data, FILE_LIST_COLUMN_VISIBLE, &visible, -1); if (visible) { GFile *parent; gboolean active; parent = g_file_get_parent (file_data->file); active = (parent != NULL) && g_hash_table_lookup (selected_folders, parent) != NULL; gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, active, -1); _g_object_unref (parent); } g_object_unref (file_data); } while (gtk_tree_model_iter_next (model, &iter)); } g_hash_table_unref (selected_folders); } g_list_free (folders); g_hash_table_unref (folders_table); } break; case SELECT_LEAVE_NEWEST: case SELECT_LEAVE_OLDEST: select_files_leaving_one (self, model, id); break; } update_file_list_sensitivity (self); update_file_list_selection_info (self); } void gth_find_duplicates_exec (GthBrowser *browser, GFile *location, gboolean recursive, const char *filter) { GthFindDuplicates *self; GSettings *settings; const char *test_attributes; int i; g_return_if_fail (location != NULL); self = (GthFindDuplicates *) g_object_new (GTH_TYPE_FIND_DUPLICATES, NULL); settings = g_settings_new (GTHUMB_BROWSER_SCHEMA); self->priv->browser = browser; self->priv->location = g_object_ref (location); self->priv->recursive = recursive; if (filter != NULL) self->priv->test = gth_main_get_registered_object (GTH_TYPE_TEST, filter); self->priv->file_source = gth_main_get_file_source (self->priv->location); gth_file_source_set_cancellable (self->priv->file_source, self->priv->cancellable); self->priv->attributes = g_string_new (g_settings_get_boolean (settings, PREF_BROWSER_FAST_FILE_TYPE) ? GFILE_STANDARD_ATTRIBUTES_WITH_FAST_CONTENT_TYPE : GFILE_STANDARD_ATTRIBUTES_WITH_CONTENT_TYPE); g_string_append (self->priv->attributes, ",gth::file::display-size"); test_attributes = gth_test_get_attributes (self->priv->test); if (test_attributes[0] != '\0') { g_string_append (self->priv->attributes, ","); g_string_append (self->priv->attributes, test_attributes); } self->priv->builder = _gtk_builder_new_from_file ("find-duplicates-dialog.ui", "find_duplicates"); self->priv->dialog = g_object_new (GTK_TYPE_DIALOG, "title", _("Find Duplicates"), "transient-for", GTK_WINDOW (self->priv->browser), "modal", FALSE, "destroy-with-parent", FALSE, "use-header-bar", _gtk_settings_get_dialogs_use_header (), NULL); gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (self->priv->dialog))), _gtk_builder_get_widget (self->priv->builder, "dialog_content")); gtk_dialog_add_buttons (GTK_DIALOG (self->priv->dialog), _GTK_LABEL_CLOSE, GTK_RESPONSE_CLOSE, NULL); self->priv->duplicates_list = gth_file_list_new (gth_grid_view_new (), GTH_FILE_LIST_MODE_NORMAL, FALSE); gtk_tree_model_filter_set_visible_column (GTK_TREE_MODEL_FILTER (GET_WIDGET ("files_treemodelfilter")), FILE_LIST_COLUMN_VISIBLE); gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (GET_WIDGET ("files_liststore")), FILE_LIST_COLUMN_FILENAME, GTK_SORT_ASCENDING); gth_file_selection_set_selection_mode (GTH_FILE_SELECTION (gth_file_list_get_view (GTH_FILE_LIST (self->priv->duplicates_list))), GTK_SELECTION_MULTIPLE); gth_file_list_set_caption (GTH_FILE_LIST (self->priv->duplicates_list), "find-duplicates::n-duplicates,gth::file::display-size"); gth_file_list_set_thumb_size (GTH_FILE_LIST (self->priv->duplicates_list), 112); gtk_widget_set_size_request (self->priv->duplicates_list, 750, 300); gtk_widget_show (self->priv->duplicates_list); gtk_box_pack_start (GTK_BOX (GET_WIDGET ("duplicates_list_box")), self->priv->duplicates_list, TRUE, TRUE, 0); self->priv->select_button = gtk_menu_button_new (); gtk_container_add (GTK_CONTAINER (self->priv->select_button), gtk_label_new (_("Select"))); gtk_widget_show_all (self->priv->select_button); gtk_box_pack_start (GTK_BOX (GET_WIDGET ("select_button_box")), self->priv->select_button, FALSE, FALSE, 0); self->priv->select_menu = gtk_menu_new (); for (i = 0; i < G_N_ELEMENTS (select_commands); i++) { SelectCommand *command = &select_commands[i]; GtkWidget *menu_item; menu_item = gtk_menu_item_new_with_label (_(command->display_name)); g_object_set_data (G_OBJECT (menu_item), SELECT_COMMAND_ID_DATA, GINT_TO_POINTER (command->id)); gtk_widget_show (menu_item); g_signal_connect (menu_item, "activate", G_CALLBACK (select_menu_item_activate_cb), self); gtk_menu_shell_append (GTK_MENU_SHELL (self->priv->select_menu), menu_item); } gtk_menu_button_set_popup (GTK_MENU_BUTTON (self->priv->select_button), self->priv->select_menu); g_object_unref (settings); g_signal_connect (self->priv->dialog, "destroy", G_CALLBACK (find_duplicates_dialog_destroy_cb), self); g_signal_connect (gtk_dialog_get_widget_for_response (GTK_DIALOG (self->priv->dialog), GTK_RESPONSE_CLOSE), "clicked", G_CALLBACK (close_button_clicked_cb), self); g_signal_connect_swapped (GET_WIDGET ("stop_button"), "clicked", G_CALLBACK (g_cancellable_cancel), self->priv->cancellable); g_signal_connect (gth_file_list_get_view (GTH_FILE_LIST (self->priv->duplicates_list)), "file-selection-changed", G_CALLBACK (duplicates_list_view_selection_changed_cb), self); g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (GET_WIDGET ("files_treeview"))), "changed", G_CALLBACK (files_tree_view_selection_changed_cb), self); g_signal_connect (GET_WIDGET ("file_cellrenderertoggle"), "toggled", G_CALLBACK (file_cellrenderertoggle_toggled_cb), self); g_signal_connect (GET_WIDGET ("file_treeviewcolumn"), "clicked", G_CALLBACK (file_treeviewcolumn_clicked_cb), self); g_signal_connect (GET_WIDGET ("modified_treeviewcolumn"), "clicked", G_CALLBACK (modified_treeviewcolumn_clicked_cb), self); g_signal_connect (GET_WIDGET ("position_treeviewcolumn"), "clicked", G_CALLBACK (position_treeviewcolumn_clicked_cb), self); g_signal_connect (GET_WIDGET ("view_button"), "clicked", G_CALLBACK (view_button_clicked_cb), self); g_signal_connect (GET_WIDGET ("delete_button"), "clicked", G_CALLBACK (delete_button_clicked_cb), self); gtk_widget_show (self->priv->dialog); search_directory (self, self->priv->location); }