/* nautilus-tag-manager.c * * Copyright (C) 2017 Alexandru Pandelea * * 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 "nautilus-tag-manager.h" #include "nautilus-file.h" #include "nautilus-file-undo-operations.h" #include "nautilus-file-undo-manager.h" #include struct _NautilusTagManager { GObject object; TrackerNotifier *notifier; GError *notifier_error; GHashTable *favorite_files; GCancellable *cancellable; }; G_DEFINE_TYPE (NautilusTagManager, nautilus_tag_manager, G_TYPE_OBJECT); static NautilusTagManager *tag_manager = NULL; typedef enum { GET_FAVORITE_FILES, GET_IDS_FOR_URLS } OperationType; typedef struct { GTask *task; GList *selection; GHashTable *ids; GObject *object; GAsyncReadyCallback callback; GCancellable *cancellable; } InsertTaskData; typedef struct { NautilusTagManager *tag_manager; GTask *task; GList *selection; gboolean star; GHashTable *ids; } UpdateData; enum { FAVORITES_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; static const gchar* nautilus_tag_manager_file_with_id_changed_url (GHashTable *hash_table, gint64 id, const gchar *url) { GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, hash_table); while (g_hash_table_iter_next (&iter, &key, &value)) { if ((gint64) value == id && g_strcmp0 (url, key) != 0) { return key; } } return NULL; } void destroy_insert_task_data (gpointer data) { InsertTaskData *task_data; task_data = data; nautilus_file_list_free (task_data->selection); g_free (data); } static GString* add_selection_filter (GList *selection, GString *query) { NautilusFile *file; GList *l; gchar *uri; g_string_append (query, " FILTER(?url IN ("); for (l = selection; l != NULL; l = l->next) { file = l->data; uri = nautilus_file_get_uri (file); g_string_append_printf (query, "'%s'", uri); if (l->next != NULL) { g_string_append (query, ", "); } g_free (uri); } g_string_append (query, "))"); return query; } static void start_query_or_update (GString *query, GAsyncReadyCallback callback, gpointer user_data, gboolean is_query, GCancellable *cancellable) { g_autoptr (GError) error = NULL; TrackerSparqlConnection *connection; connection = tracker_sparql_connection_get (cancellable, &error); if (!connection) { if (error) { g_warning ("Error on getting connection: %s", error->message); } return; } if (is_query) { tracker_sparql_connection_query_async (connection, query->str, cancellable, callback, user_data); } else { tracker_sparql_connection_update_async (connection, query->str, G_PRIORITY_DEFAULT, cancellable, callback, user_data); } g_object_unref (connection); } static void on_query_callback (GObject *object, GAsyncResult *result, gpointer user_data, GAsyncReadyCallback callback, OperationType op_type, GCancellable *cancellable) { TrackerSparqlCursor *cursor; g_autoptr (GError) error = NULL; TrackerSparqlConnection *connection; GTask *task; task = user_data; connection = TRACKER_SPARQL_CONNECTION (object); cursor = tracker_sparql_connection_query_finish (connection, result, &error); if (error != NULL) { if (error->code != G_IO_ERROR_CANCELLED) { if (op_type == GET_FAVORITE_FILES) { g_warning ("Error on getting favorite files: %s", error->message); } else if (op_type == GET_IDS_FOR_URLS) { g_warning ("Error on getting id for url: %s", error->message); g_task_return_pointer (task, g_task_get_task_data (task), NULL); g_object_unref (task); } else { g_warning ("Error on getting query callback: %s", error->message); } } } else { tracker_sparql_cursor_next_async (cursor, cancellable, callback, user_data); } } static void on_update_callback (GObject *object, GAsyncResult *result, gpointer user_data) { TrackerSparqlConnection *connection; GError *error; UpdateData *data; gint64 *id; GList *l; gchar *uri; data = user_data; error = NULL; connection = TRACKER_SPARQL_CONNECTION (object); tracker_sparql_connection_update_finish (connection, result, &error); if (error == NULL || (error != NULL && error->code != G_IO_ERROR_CANCELLED)) { for (l = data->selection; l != NULL; l = l->next) { uri = nautilus_file_get_uri (NAUTILUS_FILE (l->data)); if (data->star) { if (g_hash_table_contains (data->ids, uri)) { id = g_new0 (gint64, 1); *id = (gint64) g_hash_table_lookup (data->ids, uri); g_hash_table_insert (data->tag_manager->favorite_files, nautilus_file_get_uri (NAUTILUS_FILE (l->data)), id); } } else { g_hash_table_remove (data->tag_manager->favorite_files, uri); } g_free (uri); } if (!nautilus_file_undo_manager_is_operating ()) { NautilusFileUndoInfo *undo_info; undo_info = nautilus_file_undo_info_favorites_new (data->selection, data->star); nautilus_file_undo_manager_set_action (undo_info); g_object_unref (undo_info); } g_signal_emit_by_name (data->tag_manager, "favorites-changed", nautilus_file_list_copy (data->selection)); g_task_return_boolean (data->task, TRUE); g_object_unref (data->task); } else if (error && error->code == G_IO_ERROR_CANCELLED) { g_error_free (error); } else { g_task_return_error (data->task, error); g_object_unref (data->task); g_warning ("error updating tags: %s", error->message); } if (data->ids) { g_hash_table_destroy (data->ids); } nautilus_file_list_free (data->selection); g_free (data); } static gboolean get_query_status (TrackerSparqlCursor *cursor, GAsyncResult *result, OperationType op_type, gpointer user_data) { gboolean success; GTask *task; g_autoptr (GError) error = NULL; task = user_data; success = tracker_sparql_cursor_next_finish (cursor, result, &error); if (!success) { if (error) { g_warning ("Error on getting all tags cursor callback: %s", error->message); } g_clear_object (&cursor); if (error == NULL || (error != NULL && error->code != G_IO_ERROR_CANCELLED)) { if (op_type == GET_IDS_FOR_URLS) { g_task_return_pointer (task, g_task_get_task_data (task), NULL); g_object_unref (task); } } } return success; } GList* nautilus_tag_manager_get_favorite_files (NautilusTagManager *self) { return g_hash_table_get_keys (self->favorite_files); } static void on_get_favorite_files_cursor_callback (GObject *object, GAsyncResult *result, gpointer user_data) { TrackerSparqlCursor *cursor; const gchar *url; gint64 *id; gboolean success; NautilusTagManager *self; GList *changed_files; NautilusFile *file; cursor = TRACKER_SPARQL_CURSOR (object); self = NAUTILUS_TAG_MANAGER (user_data); success = get_query_status (cursor, result, GET_FAVORITE_FILES, NULL); if (!success) { return; } id = g_new0 (gint64, 1); url = tracker_sparql_cursor_get_string (cursor, 0, NULL); *id = tracker_sparql_cursor_get_integer (cursor, 1); g_hash_table_insert (self->favorite_files, g_strdup (url), id); file = nautilus_file_get_by_uri (url); changed_files = g_list_prepend (NULL, file); g_signal_emit_by_name (self, "favorites-changed", changed_files); nautilus_file_list_free (changed_files); tracker_sparql_cursor_next_async (cursor, self->cancellable, on_get_favorite_files_cursor_callback, self); } static void on_get_favorite_files_query_callback (GObject *object, GAsyncResult *result, gpointer user_data) { NautilusTagManager *self; self = NAUTILUS_TAG_MANAGER (user_data); on_query_callback (object, result, user_data, on_get_favorite_files_cursor_callback, GET_FAVORITE_FILES, self->cancellable); } static void nautilus_tag_manager_query_favorite_files (NautilusTagManager *self, GCancellable *cancellable) { GString *query; self->cancellable = cancellable; query = g_string_new ("SELECT ?url tracker:id(?urn) WHERE { ?urn nie:url ?url ; nao:hasTag nao:predefined-tag-favorite}"); start_query_or_update (query, on_get_favorite_files_query_callback, self, TRUE, cancellable); g_string_free (query, TRUE); } static gpointer nautilus_tag_manager_gpointer_task_finish (GObject *source_object, GAsyncResult *res, GError **error) { g_return_val_if_fail (g_task_is_valid (res, source_object), FALSE); return g_task_propagate_pointer (G_TASK (res), error); } static GString* nautilus_tag_manager_delete_tag (NautilusTagManager *self, GList *selection, GString *query) { g_string_append (query, "DELETE { ?urn nao:hasTag nao:predefined-tag-favorite }" "WHERE { ?urn a nfo:FileDataObject ; nie:url ?url ."); query = add_selection_filter (selection, query); g_string_append (query, "}\n"); return query; } static GString* nautilus_tag_manager_insert_tag (NautilusTagManager *self, GList *selection, GString *query) { g_string_append (query, "INSERT { ?urn nao:hasTag nao:predefined-tag-favorite }" "WHERE { ?urn a nfo:FileDataObject ; nie:url ?url ."); query = add_selection_filter (selection, query); g_string_append (query, "}\n"); return query; } gboolean nautilus_tag_manager_file_is_favorite (NautilusTagManager *self, const gchar *file_name) { return g_hash_table_contains (self->favorite_files, file_name); } static void on_get_file_ids_for_urls_cursor_callback (GObject *object, GAsyncResult *result, gpointer user_data) { TrackerSparqlCursor *cursor; GTask *task; gint64 *id; const gchar *url; gboolean success; GList *l; gchar *file_url; InsertTaskData *data; task = user_data; data = g_task_get_task_data (task); cursor = TRACKER_SPARQL_CURSOR (object); success = get_query_status (cursor, result, GET_IDS_FOR_URLS, task); if (!success) { return; } id = g_new0 (gint64, 1); url = tracker_sparql_cursor_get_string (cursor, 0, NULL); *id = tracker_sparql_cursor_get_integer (cursor, 1); for (l = data->selection; l != NULL; l = l->next) { file_url = nautilus_file_get_uri (NAUTILUS_FILE (l->data)); if (g_strcmp0 (file_url, url) == 0) { g_hash_table_insert (data->ids, g_strdup (url), id); g_free (file_url); break; } g_free (file_url); } tracker_sparql_cursor_next_async (cursor, g_task_get_cancellable (task), on_get_file_ids_for_urls_cursor_callback, task); } static void on_get_file_ids_for_urls_query_callback (GObject *object, GAsyncResult *result, gpointer user_data) { GTask *task; task = user_data; on_query_callback (object, result, user_data, on_get_file_ids_for_urls_cursor_callback, GET_IDS_FOR_URLS, g_task_get_cancellable (task)); } static void nautilus_tag_manager_get_file_ids_for_urls (GObject *object, GList *selection, GTask *task) { GString *query; query = g_string_new ("SELECT ?url tracker:id(?urn) WHERE { ?urn nie:url ?url; ."); query = add_selection_filter (selection, query); g_string_append (query, "}\n"); start_query_or_update (query, on_get_file_ids_for_urls_query_callback, task, TRUE, g_task_get_cancellable (task)); g_string_free (query, TRUE); } static void on_star_files_callback (GObject *object, GAsyncResult *res, gpointer user_data) { NautilusTagManager *self; GString *query; InsertTaskData *data; g_autoptr (GError) error = NULL; GTask *task; UpdateData *update_data; self = NAUTILUS_TAG_MANAGER (object); data = nautilus_tag_manager_gpointer_task_finish (object, res, &error); task = g_task_new (data->object, data->cancellable, data->callback, NULL); query = g_string_new (""); query = nautilus_tag_manager_insert_tag (self, data->selection, query); update_data = g_new0 (UpdateData, 1); update_data->task = task; update_data->tag_manager = self; update_data->selection = nautilus_file_list_copy (data->selection); update_data->star = TRUE; update_data->ids = data->ids; /* the ids hash table is now owned by the update_data, * so it will be freed by it. */ destroy_insert_task_data (data); start_query_or_update (query, on_update_callback, update_data, FALSE, g_task_get_cancellable (task)); g_string_free (query, TRUE); } void nautilus_tag_manager_star_files (NautilusTagManager *self, GObject *object, GList *selection, GAsyncReadyCallback callback, GCancellable *cancellable) { GTask *task; InsertTaskData *data; data = g_new0 (InsertTaskData, 1); data->selection = nautilus_file_list_copy (selection); data->ids = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_free); data->callback = callback; data->object = object; data->cancellable = cancellable; task = g_task_new (self, cancellable, on_star_files_callback, NULL); g_task_set_task_data (task, data, NULL); nautilus_tag_manager_get_file_ids_for_urls (G_OBJECT (self), selection, task); } void nautilus_tag_manager_unstar_files (NautilusTagManager *self, GObject *object, GList *selection, GAsyncReadyCallback callback, GCancellable *cancellable) { GString *query; GTask *task; UpdateData *update_data; task = g_task_new (object, cancellable, callback, NULL); query = g_string_new (""); query = nautilus_tag_manager_delete_tag (self, selection, query); update_data = g_new0 (UpdateData, 1); update_data->task = task; update_data->tag_manager = self; update_data->selection = nautilus_file_list_copy (selection); update_data->star = FALSE; start_query_or_update (query, on_update_callback, update_data, FALSE, cancellable); g_string_free (query, TRUE); } void on_tracker_notifier_events(TrackerNotifier *notifier, GPtrArray *events, gpointer user_data) { TrackerNotifierEvent *event; NautilusTagManager *self; int i; const gchar *location_uri; const gchar *new_location_uri; GError *error = NULL; TrackerSparqlConnection *connection; TrackerSparqlCursor *cursor; GString *query; gboolean query_has_results; gint64 *id; GList *changed_files; NautilusFile *file; self = NAUTILUS_TAG_MANAGER (user_data); for (i = 0; i < events->len; i++) { event = g_ptr_array_index (events, i); location_uri = tracker_notifier_event_get_location (event); query = g_string_new (""); g_string_append_printf (query, "SELECT ?url WHERE { ?urn nie:url ?url; nao:hasTag nao:predefined-tag-favorite . FILTER (tracker:id(?urn) = %ld)}", tracker_notifier_event_get_id (event)); /* check if the file changed it's location and update hash table if so */ new_location_uri = nautilus_tag_manager_file_with_id_changed_url (self->favorite_files, tracker_notifier_event_get_id (event), location_uri); if (new_location_uri) { id = g_new0 (gint64, 1); *id = tracker_notifier_event_get_id (event); g_hash_table_remove (self->favorite_files, new_location_uri); g_hash_table_insert (self->favorite_files, g_strdup (location_uri), id); file = nautilus_file_get_by_uri (location_uri); changed_files = g_list_prepend (NULL, file); g_signal_emit_by_name (self, "favorites-changed", changed_files); nautilus_file_list_free (changed_files); } connection = tracker_sparql_connection_get (NULL, &error); if (!connection) { g_printerr ("Couldn't obtain a direct connection to the Tracker store: %s", error ? error->message : "unknown error"); g_clear_error (&error); return; } cursor = tracker_sparql_connection_query (connection, query->str, NULL, &error); if (error) { g_printerr ("Couldn't query the Tracker Store: '%s'", error->message); g_clear_error (&error); return; } if (cursor) { query_has_results = tracker_sparql_cursor_next (cursor, NULL, &error); /* if no results are found, then the file isn't marked as favorite. * If needed, update the hashtable. */ if (!query_has_results && location_uri && g_hash_table_contains (self->favorite_files, location_uri)) { g_hash_table_remove (self->favorite_files, location_uri); file = nautilus_file_get_by_uri (location_uri); changed_files = g_list_prepend (NULL, file); g_signal_emit_by_name (self, "favorites-changed", changed_files); nautilus_file_list_free (changed_files); } else if (query_has_results && location_uri && !g_hash_table_contains (self->favorite_files, location_uri)) { id = g_new0 (gint64, 1); *id = tracker_notifier_event_get_id (event); g_hash_table_insert (self->favorite_files, g_strdup (location_uri), id); file = nautilus_file_get_by_uri (location_uri); changed_files = g_list_prepend (NULL, file); g_signal_emit_by_name (self, "favorites-changed", changed_files); nautilus_file_list_free (changed_files); } g_object_unref (cursor); } g_object_unref (connection); g_string_free (query, TRUE); } } static void nautilus_tag_manager_finalize (GObject *object) { NautilusTagManager *self; self = NAUTILUS_TAG_MANAGER (object); g_signal_handlers_disconnect_by_func (self->notifier, G_CALLBACK (on_tracker_notifier_events), self); g_clear_object (&self->notifier); g_hash_table_destroy (self->favorite_files); G_OBJECT_CLASS (nautilus_tag_manager_parent_class)->finalize (object); } static void nautilus_tag_manager_class_init (NautilusTagManagerClass *klass) { GObjectClass *oclass; oclass = G_OBJECT_CLASS (klass); oclass->finalize = nautilus_tag_manager_finalize; signals[FAVORITES_CHANGED] = g_signal_new ("favorites-changed", NAUTILUS_TYPE_TAG_MANAGER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); } NautilusTagManager* nautilus_tag_manager_get () { if (tag_manager != NULL) { return g_object_ref (tag_manager); } tag_manager = g_object_new (NAUTILUS_TYPE_TAG_MANAGER, NULL); g_object_add_weak_pointer (G_OBJECT (tag_manager), (gpointer)&tag_manager); return tag_manager; } void nautilus_tag_manager_set_cancellable (NautilusTagManager *tag_manager, GCancellable *cancellable) { nautilus_tag_manager_query_favorite_files (tag_manager, cancellable); tag_manager->notifier = tracker_notifier_new (NULL, TRACKER_NOTIFIER_FLAG_QUERY_LOCATION, cancellable, &tag_manager->notifier_error); g_signal_connect (tag_manager->notifier, "events", G_CALLBACK (on_tracker_notifier_events), tag_manager); } static void nautilus_tag_manager_init (NautilusTagManager *self) { self->favorite_files = g_hash_table_new_full (g_str_hash, g_str_equal, (GDestroyNotify) g_free, (GDestroyNotify) g_free); }