/* * Copyright (C) 2009-2011 Nokia * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * * Authors: Jürg Billeter * Martyn Russell * * Based on nautilus-search-engine-tracker.c */ #include "config.h" #include #include #include #include #include #include "gtksearchenginetracker.h" #define DBUS_SERVICE_RESOURCES "org.freedesktop.Tracker1" #define DBUS_PATH_RESOURCES "/org/freedesktop/Tracker1/Resources" #define DBUS_INTERFACE_RESOURCES "org.freedesktop.Tracker1.Resources" #define DBUS_SERVICE_STATUS "org.freedesktop.Tracker1" #define DBUS_PATH_STATUS "/org/freedesktop/Tracker1/Status" #define DBUS_INTERFACE_STATUS "org.freedesktop.Tracker1.Status" /* Time in second to wait for service before deciding it's not available */ #define WAIT_TIMEOUT_SECONDS 1 /* Time in second to wait for query results to come back */ #define QUERY_TIMEOUT_SECONDS 10 /* If defined, we use fts:match, this has to be enabled in Tracker to * work which it usually is. The alternative is to undefine it and * use filename matching instead. This doesn’t use the content of the * file however. */ #define FTS_MATCHING struct _GtkSearchEngineTracker { GtkSearchEngine parent; GDBusConnection *connection; GCancellable *cancellable; GtkQuery *query; gboolean query_pending; GPtrArray *indexed_locations; }; struct _GtkSearchEngineTrackerClass { GtkSearchEngineClass parent_class; }; G_DEFINE_TYPE (GtkSearchEngineTracker, _gtk_search_engine_tracker, GTK_TYPE_SEARCH_ENGINE) static void finalize (GObject *object) { GtkSearchEngineTracker *tracker; g_debug ("Finalizing GtkSearchEngineTracker"); tracker = GTK_SEARCH_ENGINE_TRACKER (object); if (tracker->cancellable) { g_cancellable_cancel (tracker->cancellable); g_object_unref (tracker->cancellable); } g_clear_object (&tracker->query); g_clear_object (&tracker->connection); g_ptr_array_unref (tracker->indexed_locations); G_OBJECT_CLASS (_gtk_search_engine_tracker_parent_class)->finalize (object); } static GDBusConnection * get_connection (void) { GDBusConnection *connection; GError *error = NULL; GVariant *reply; /* Normally I hate sync calls with UIs, but we need to return NULL * or a GtkSearchEngine as a result of this function. */ connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); if (error) { g_debug ("Couldn't connect to D-Bus session bus, %s", error->message); g_error_free (error); return NULL; } /* If connection is set, we know it worked. */ g_debug ("Finding out if Tracker is available via D-Bus..."); /* We only wait 1 second max, we expect it to be very fast. If we * don't get a response by then, clearly we're replaying a journal * or cleaning up the DB internally. Either way, services is not * available. * * We use the sync call here because we don't expect to be waiting * long enough to block UI painting. */ reply = g_dbus_connection_call_sync (connection, DBUS_SERVICE_STATUS, DBUS_PATH_STATUS, DBUS_INTERFACE_STATUS, "Wait", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, WAIT_TIMEOUT_SECONDS * 1000, NULL, &error); if (error) { g_debug ("Tracker is not available, %s", error->message); g_error_free (error); g_object_unref (connection); return NULL; } g_variant_unref (reply); g_debug ("Tracker is ready"); return connection; } static void get_query_results (GtkSearchEngineTracker *engine, const gchar *sparql, GAsyncReadyCallback callback, gpointer user_data) { g_dbus_connection_call (engine->connection, DBUS_SERVICE_RESOURCES, DBUS_PATH_RESOURCES, DBUS_INTERFACE_RESOURCES, "SparqlQuery", g_variant_new ("(s)", sparql), NULL, G_DBUS_CALL_FLAGS_NONE, QUERY_TIMEOUT_SECONDS * 1000, engine->cancellable, callback, user_data); } /* Stolen from libtracker-sparql */ static gchar * sparql_escape_string (const gchar *literal) { GString *str; const gchar *p; g_return_val_if_fail (literal != NULL, NULL); str = g_string_new (""); p = literal; while (TRUE) { gsize len; if (!((*p) != '\0')) break; len = strcspn ((const gchar *) p, "\t\n\r\b\f\"\\"); g_string_append_len (str, (const gchar *) p, (gssize) ((glong) len)); p = p + len; switch (*p) { case '\t': g_string_append (str, "\\t"); break; case '\n': g_string_append (str, "\\n"); break; case '\r': g_string_append (str, "\\r"); break; case '\b': g_string_append (str, "\\b"); break; case '\f': g_string_append (str, "\\f"); break; case '"': g_string_append (str, "\\\""); break; case '\\': g_string_append (str, "\\\\"); break; default: continue; } p++; } return g_string_free (str, FALSE); } static void sparql_append_string_literal (GString *sparql, const gchar *str, gboolean glob, gboolean is_dir_uri, gboolean quoted) { gchar *s; s = sparql_escape_string (str); g_string_append_c (sparql, '"'); if (quoted) g_string_append (sparql, "\\\""); g_string_append (sparql, s); if (is_dir_uri) g_string_append_c (sparql, '/'); if (quoted) g_string_append (sparql, "\\\""); if (glob) g_string_append_c (sparql, '*'); g_string_append_c (sparql, '"'); g_free (s); } static void sparql_append_string_literal_lower_case (GString *sparql, const gchar *str) { gchar *s; s = g_utf8_strdown (str, -1); sparql_append_string_literal (sparql, s, FALSE, FALSE, FALSE); g_free (s); } static void query_callback (GObject *object, GAsyncResult *res, gpointer user_data) { GtkSearchEngineTracker *tracker; GList *hits; GVariant *reply; GVariant *r; GVariantIter iter; GError *error = NULL; gint i, n; GtkSearchHit *hit; tracker = GTK_SEARCH_ENGINE_TRACKER (user_data); tracker->query_pending = FALSE; reply = g_dbus_connection_call_finish (tracker->connection, res, &error); if (error) { _gtk_search_engine_error (GTK_SEARCH_ENGINE (tracker), error->message); g_error_free (error); g_object_unref (tracker); return; } if (!reply) { _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker), FALSE); g_object_unref (tracker); return; } r = g_variant_get_child_value (reply, 0); g_variant_iter_init (&iter, r); n = g_variant_iter_n_children (&iter); hit = g_new (GtkSearchHit, n); hits = NULL; for (i = 0; i < n; i++) { GVariant *v; const gchar **strv; v = g_variant_iter_next_value (&iter); strv = g_variant_get_strv (v, NULL); hit[i].file = g_file_new_for_uri (strv[0]); hit[i].info = NULL; g_free (strv); hits = g_list_prepend (hits, &hit[i]); } _gtk_search_engine_hits_added (GTK_SEARCH_ENGINE (tracker), hits); _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker), i > 0); g_list_free (hits); for (i = 0; i < n; i++) g_object_unref (hit[i].file); g_free (hit); g_variant_unref (reply); g_variant_unref (r); g_object_unref (tracker); } static void gtk_search_engine_tracker_start (GtkSearchEngine *engine) { GtkSearchEngineTracker *tracker; const gchar *search_text; GFile *location; GString *sparql; gboolean recursive; tracker = GTK_SEARCH_ENGINE_TRACKER (engine); if (tracker->query_pending) { g_debug ("Attempt to start a new search while one is pending, doing nothing"); return; } if (tracker->query == NULL) { g_debug ("Attempt to start a new search with no GtkQuery, doing nothing"); return; } search_text = gtk_query_get_text (tracker->query); location = gtk_query_get_location (tracker->query); recursive = _gtk_search_engine_get_recursive (engine); sparql = g_string_new ("SELECT nie:url(?urn) " "WHERE {" " ?urn a nfo:FileDataObject ;" " tracker:available true ; " " nfo:belongsToContainer ?parent; "); #ifdef FTS_MATCHING /* Using FTS: */ g_string_append (sparql, "fts:match "); sparql_append_string_literal (sparql, search_text, TRUE, FALSE, TRUE); #endif g_string_append (sparql, ". FILTER (BOUND(nie:url(?urn)) && "); g_string_append (sparql, "fn:contains(fn:lower-case(nfo:fileName(?urn)),"); sparql_append_string_literal_lower_case (sparql, search_text); g_string_append (sparql, ")"); if (location) { gchar *location_uri = g_file_get_uri (location); g_string_append (sparql, " && "); if (recursive) { g_string_append (sparql, "fn:starts-with(nie:url(?urn),"); sparql_append_string_literal (sparql, location_uri, FALSE, TRUE, FALSE); g_string_append (sparql, ")"); } else { g_string_append (sparql, "nie:url(?parent) = "); sparql_append_string_literal (sparql, location_uri, FALSE, FALSE, FALSE); } g_free (location_uri); } g_string_append (sparql, ")"); #ifdef FTS_MATCHING g_string_append (sparql, " } ORDER BY DESC(fts:rank(?urn)) DESC(nie:url(?urn))"); #else /* FTS_MATCHING */ g_string_append (sparql, "} ORDER BY DESC(nie:url(?urn)) DESC(nfo:fileName(?urn))"); #endif /* FTS_MATCHING */ tracker->query_pending = TRUE; g_debug ("SearchEngineTracker: query: %s", sparql->str); get_query_results (tracker, sparql->str, query_callback, g_object_ref (tracker)); g_string_free (sparql, TRUE); } static void gtk_search_engine_tracker_stop (GtkSearchEngine *engine) { GtkSearchEngineTracker *tracker; tracker = GTK_SEARCH_ENGINE_TRACKER (engine); if (tracker->query && tracker->query_pending) { g_cancellable_cancel (tracker->cancellable); tracker->query_pending = FALSE; } } static void gtk_search_engine_tracker_set_query (GtkSearchEngine *engine, GtkQuery *query) { GtkSearchEngineTracker *tracker; tracker = GTK_SEARCH_ENGINE_TRACKER (engine); if (query) g_object_ref (query); if (tracker->query) g_object_unref (tracker->query); tracker->query = query; } static void _gtk_search_engine_tracker_class_init (GtkSearchEngineTrackerClass *class) { GObjectClass *gobject_class; GtkSearchEngineClass *engine_class; gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = finalize; engine_class = GTK_SEARCH_ENGINE_CLASS (class); engine_class->set_query = gtk_search_engine_tracker_set_query; engine_class->start = gtk_search_engine_tracker_start; engine_class->stop = gtk_search_engine_tracker_stop; } static void get_indexed_locations (GtkSearchEngineTracker *engine); static void _gtk_search_engine_tracker_init (GtkSearchEngineTracker *engine) { engine->cancellable = g_cancellable_new (); engine->query_pending = FALSE; engine->indexed_locations = g_ptr_array_new_with_free_func (g_object_unref); get_indexed_locations (engine); } GtkSearchEngine * _gtk_search_engine_tracker_new (void) { GtkSearchEngineTracker *engine; GDBusConnection *connection; g_debug ("--"); connection = get_connection (); if (!connection) return NULL; g_debug ("Creating GtkSearchEngineTracker..."); engine = g_object_new (GTK_TYPE_SEARCH_ENGINE_TRACKER, NULL); engine->connection = connection; return GTK_SEARCH_ENGINE (engine); } #define TRACKER_SCHEMA "org.freedesktop.Tracker.Miner.Files" #define TRACKER_KEY_RECURSIVE_DIRECTORIES "index-recursive-directories" static const gchar * get_user_special_dir_if_not_home (GUserDirectory idx) { const gchar *path; path = g_get_user_special_dir (idx); if (g_strcmp0 (path, g_get_home_dir ()) == 0) return NULL; return path; } static const gchar * path_from_tracker_dir (const gchar *value) { const gchar *path; if (g_strcmp0 (value, "&DESKTOP") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DESKTOP); else if (g_strcmp0 (value, "&DOCUMENTS") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOCUMENTS); else if (g_strcmp0 (value, "&DOWNLOAD") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOWNLOAD); else if (g_strcmp0 (value, "&MUSIC") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_MUSIC); else if (g_strcmp0 (value, "&PICTURES") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PICTURES); else if (g_strcmp0 (value, "&PUBLIC_SHARE") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PUBLIC_SHARE); else if (g_strcmp0 (value, "&TEMPLATES") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_TEMPLATES); else if (g_strcmp0 (value, "&VIDEOS") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_VIDEOS); else if (g_strcmp0 (value, "$HOME") == 0) path = g_get_home_dir (); else path = value; return path; } static void get_indexed_locations (GtkSearchEngineTracker *engine) { GSettingsSchemaSource *source; GSettingsSchema *schema; GSettings *settings; gchar **locations; gint i; GFile *location; const gchar *path; source = g_settings_schema_source_get_default (); schema = g_settings_schema_source_lookup (source, TRACKER_SCHEMA, FALSE); if (!schema) return; settings = g_settings_new_full (schema, NULL, NULL); g_settings_schema_unref (schema); locations = g_settings_get_strv (settings, TRACKER_KEY_RECURSIVE_DIRECTORIES); for (i = 0; locations[i] != NULL; i++) { path = path_from_tracker_dir (locations[i]); if (path == NULL) continue; location = g_file_new_for_path (path); g_ptr_array_add (engine->indexed_locations, location); } g_strfreev (locations); g_object_unref (settings); } gboolean _gtk_search_engine_tracker_is_indexed (GFile *location, gpointer data) { GtkSearchEngineTracker *engine = data; gint i; GFile *place; for (i = 0; i < engine->indexed_locations->len; i++) { place = g_ptr_array_index (engine->indexed_locations, i); if (g_file_equal (location, place) || g_file_has_prefix (location, place)) return TRUE; } return FALSE; }