/* * Copyright (C) 2005 Mr Jamie McCracken * * Nautilus 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. * * Nautilus 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; see the file COPYING. If not, * see . * * Author: Jamie McCracken * */ #include #include "nautilus-search-engine-tracker.h" #include "nautilus-search-engine-private.h" #include "nautilus-search-hit.h" #include "nautilus-search-provider.h" #include "nautilus-tracker-utilities.h" #define DEBUG_FLAG NAUTILUS_DEBUG_SEARCH #include "nautilus-debug.h" #include #include #include struct _NautilusSearchEngineTracker { GObject parent_instance; TrackerSparqlConnection *connection; NautilusQuery *query; gboolean query_pending; GQueue *hits_pending; gboolean recursive; gboolean fts_enabled; GCancellable *cancellable; }; enum { PROP_0, PROP_RUNNING, LAST_PROP }; static void nautilus_search_provider_init (NautilusSearchProviderInterface *iface); G_DEFINE_TYPE_WITH_CODE (NautilusSearchEngineTracker, nautilus_search_engine_tracker, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (NAUTILUS_TYPE_SEARCH_PROVIDER, nautilus_search_provider_init)) static void finalize (GObject *object) { NautilusSearchEngineTracker *tracker; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (object); if (tracker->cancellable) { g_cancellable_cancel (tracker->cancellable); g_clear_object (&tracker->cancellable); } g_clear_object (&tracker->query); g_queue_free_full (tracker->hits_pending, g_object_unref); /* This is a singleton, no need to unref. */ tracker->connection = NULL; G_OBJECT_CLASS (nautilus_search_engine_tracker_parent_class)->finalize (object); } #define BATCH_SIZE 100 static void check_pending_hits (NautilusSearchEngineTracker *tracker, gboolean force_send) { GList *hits = NULL; NautilusSearchHit *hit; DEBUG ("Tracker engine add hits"); if (!force_send && g_queue_get_length (tracker->hits_pending) < BATCH_SIZE) { return; } while ((hit = g_queue_pop_head (tracker->hits_pending))) { hits = g_list_prepend (hits, hit); } nautilus_search_provider_hits_added (NAUTILUS_SEARCH_PROVIDER (tracker), hits); g_list_free_full (hits, g_object_unref); } static void search_finished (NautilusSearchEngineTracker *tracker, GError *error) { DEBUG ("Tracker engine finished"); if (error == NULL) { check_pending_hits (tracker, TRUE); } else { g_queue_foreach (tracker->hits_pending, (GFunc) g_object_unref, NULL); g_queue_clear (tracker->hits_pending); } tracker->query_pending = FALSE; g_object_notify (G_OBJECT (tracker), "running"); if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { DEBUG ("Tracker engine error %s", error->message); nautilus_search_provider_error (NAUTILUS_SEARCH_PROVIDER (tracker), error->message); } else { nautilus_search_provider_finished (NAUTILUS_SEARCH_PROVIDER (tracker), NAUTILUS_SEARCH_PROVIDER_STATUS_NORMAL); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { DEBUG ("Tracker engine finished and cancelled"); } else { DEBUG ("Tracker engine finished correctly"); } } g_object_unref (tracker); } static void cursor_callback (GObject *object, GAsyncResult *result, gpointer user_data); static void cursor_next (NautilusSearchEngineTracker *tracker, TrackerSparqlCursor *cursor) { tracker_sparql_cursor_next_async (cursor, tracker->cancellable, cursor_callback, tracker); } static void cursor_callback (GObject *object, GAsyncResult *result, gpointer user_data) { NautilusSearchEngineTracker *tracker; GError *error = NULL; TrackerSparqlCursor *cursor; NautilusSearchHit *hit; const char *uri; const char *mtime_str; const char *atime_str; const char *ctime_str; const gchar *snippet; GTimeVal tv; gdouble rank, match; gboolean success; gchar *basename; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (user_data); cursor = TRACKER_SPARQL_CURSOR (object); success = tracker_sparql_cursor_next_finish (cursor, result, &error); if (!success) { search_finished (tracker, error); g_clear_error (&error); g_clear_object (&cursor); return; } /* We iterate result by result, not n at a time. */ uri = tracker_sparql_cursor_get_string (cursor, 0, NULL); rank = tracker_sparql_cursor_get_double (cursor, 1); mtime_str = tracker_sparql_cursor_get_string (cursor, 2, NULL); ctime_str = tracker_sparql_cursor_get_string (cursor, 3, NULL); atime_str = tracker_sparql_cursor_get_string (cursor, 4, NULL); basename = g_path_get_basename (uri); hit = nautilus_search_hit_new (uri); match = nautilus_query_matches_string (tracker->query, basename); nautilus_search_hit_set_fts_rank (hit, rank + match); g_free (basename); if (tracker->fts_enabled) { snippet = tracker_sparql_cursor_get_string (cursor, 5, NULL); if (snippet != NULL) { g_autofree gchar *escaped = NULL; g_autoptr (GString) buffer = NULL; /* Escape for markup, before adding our own markup. */ escaped = g_markup_escape_text (snippet, -1); buffer = g_string_new (escaped); g_string_replace (buffer, "_NAUTILUS_SNIPPET_DELIM_START_", "", 0); g_string_replace (buffer, "_NAUTILUS_SNIPPET_DELIM_END_", "", 0); nautilus_search_hit_set_fts_snippet (hit, buffer->str); } } if (g_time_val_from_iso8601 (mtime_str, &tv)) { GDateTime *date; date = g_date_time_new_from_timeval_local (&tv); nautilus_search_hit_set_modification_time (hit, date); g_date_time_unref (date); } else { g_warning ("unable to parse mtime: %s", mtime_str); } if (g_time_val_from_iso8601 (atime_str, &tv)) { GDateTime *date; date = g_date_time_new_from_timeval_local (&tv); nautilus_search_hit_set_access_time (hit, date); g_date_time_unref (date); } else { g_warning ("unable to parse atime: %s", atime_str); } if (g_time_val_from_iso8601 (ctime_str, &tv)) { GDateTime *date; date = g_date_time_new_from_timeval_local (&tv); nautilus_search_hit_set_creation_time (hit, date); g_date_time_unref (date); } else { g_warning ("unable to parse ctime: %s", ctime_str); } g_queue_push_head (tracker->hits_pending, hit); check_pending_hits (tracker, FALSE); /* Get next */ cursor_next (tracker, cursor); } static void query_callback (GObject *object, GAsyncResult *result, gpointer user_data) { NautilusSearchEngineTracker *tracker; TrackerSparqlConnection *connection; TrackerSparqlCursor *cursor; GError *error = NULL; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (user_data); connection = TRACKER_SPARQL_CONNECTION (object); cursor = tracker_sparql_connection_query_finish (connection, result, &error); if (error != NULL) { search_finished (tracker, error); g_error_free (error); } else { cursor_next (tracker, cursor); } } static gboolean search_finished_idle (gpointer user_data) { NautilusSearchEngineTracker *tracker = user_data; DEBUG ("Tracker engine finished idle"); search_finished (tracker, NULL); return FALSE; } /* This is used to compensate rank if fts:rank is not set (resp. fts:match is * not used). The value was determined experimentally. I am convinced that * fts:rank is currently always set to 5.0 in case of filename match. */ #define FILENAME_RANK "5.0" static void nautilus_search_engine_tracker_start (NautilusSearchProvider *provider) { NautilusSearchEngineTracker *tracker; gchar *query_text, *search_text, *location_uri, *downcase; GFile *location; GString *sparql; g_autoptr (GPtrArray) mimetypes = NULL; GPtrArray *date_range; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (provider); if (tracker->query_pending) { return; } DEBUG ("Tracker engine start"); g_object_ref (tracker); tracker->query_pending = TRUE; g_object_notify (G_OBJECT (provider), "running"); if (tracker->connection == NULL) { g_idle_add (search_finished_idle, provider); return; } tracker->fts_enabled = nautilus_query_get_search_content (tracker->query); query_text = nautilus_query_get_text (tracker->query); downcase = g_utf8_strdown (query_text, -1); search_text = tracker_sparql_escape_string (downcase); g_free (query_text); g_free (downcase); location = nautilus_query_get_location (tracker->query); location_uri = location ? g_file_get_uri (location) : NULL; mimetypes = nautilus_query_get_mime_types (tracker->query); sparql = g_string_new ("SELECT DISTINCT" " ?url" " xsd:double(COALESCE(?rank2, ?rank1)) AS ?rank" " nfo:fileLastModified(?file)" " nfo:fileCreated(?file)" " nfo:fileLastAccessed(?file)"); if (tracker->fts_enabled && *search_text) { g_string_append (sparql, "fts:snippet(?content," " '_NAUTILUS_SNIPPET_DELIM_START_'," " '_NAUTILUS_SNIPPET_DELIM_END_', " " '…'," " 20)"); } g_string_append (sparql, "FROM tracker:FileSystem "); if (tracker->fts_enabled) { g_string_append (sparql, "FROM tracker:Documents "); } g_string_append (sparql, "\nWHERE {" " ?file a nfo:FileDataObject;" " nfo:fileLastModified ?mtime;" " nfo:fileLastAccessed ?atime;" " nie:dataSource/tracker:available true;" " nie:url ?url." " OPTIONAL { ?file nfo:fileCreated ?ctime.}"); if (mimetypes->len > 0) { g_string_append (sparql, " ?content nie:isStoredAs ?file;" " nie:mimeType ?mime"); } if (tracker->fts_enabled && *search_text) { /* Use fts:match only for content search to not lose some filename results due to stop words. */ g_string_append_printf (sparql, " { " " ?content nie:isStoredAs ?file ." " ?content fts:match \"%s*\" ." " BIND(fts:rank(?content) AS ?rank1) ." " } UNION", search_text); } g_string_append_printf (sparql, " {" " ?file nfo:fileName ?filename ." " FILTER(fn:contains(fn:lower-case(?filename), '%s')) ." " BIND(" FILENAME_RANK " AS ?rank2) ." " }", search_text); g_string_append_printf (sparql, " . FILTER( "); if (!tracker->recursive) { g_string_append_printf (sparql, "tracker:uri-is-parent('%s', ?url)", location_uri); } else { /* STRSTARTS is faster than tracker:uri-is-descendant(). * See https://gitlab.gnome.org/GNOME/tracker/-/issues/243 */ g_string_append_printf (sparql, "STRSTARTS(?url, '%s/')", location_uri); } date_range = nautilus_query_get_date_range (tracker->query); if (date_range) { NautilusQuerySearchType type; gchar *initial_date_format; gchar *end_date_format; GDateTime *initial_date; GDateTime *end_date; GDateTime *shifted_end_date; initial_date = g_ptr_array_index (date_range, 0); end_date = g_ptr_array_index (date_range, 1); /* As we do for other searches, we want to make the end date inclusive. * For that, add a day to it */ shifted_end_date = g_date_time_add_days (end_date, 1); type = nautilus_query_get_search_type (tracker->query); initial_date_format = g_date_time_format_iso8601 (initial_date); end_date_format = g_date_time_format_iso8601 (shifted_end_date); g_string_append (sparql, " && "); if (type == NAUTILUS_QUERY_SEARCH_TYPE_LAST_ACCESS) { g_string_append_printf (sparql, "?atime >= \"%s\"^^xsd:dateTime", initial_date_format); g_string_append_printf (sparql, " && ?atime <= \"%s\"^^xsd:dateTime", end_date_format); } else if (type == NAUTILUS_QUERY_SEARCH_TYPE_LAST_MODIFIED) { g_string_append_printf (sparql, "?mtime >= \"%s\"^^xsd:dateTime", initial_date_format); g_string_append_printf (sparql, " && ?mtime <= \"%s\"^^xsd:dateTime", end_date_format); } else { g_string_append_printf (sparql, "?ctime >= \"%s\"^^xsd:dateTime", initial_date_format); g_string_append_printf (sparql, " && ?ctime <= \"%s\"^^xsd:dateTime", end_date_format); } g_free (initial_date_format); g_free (end_date_format); g_ptr_array_unref (date_range); } if (mimetypes->len > 0) { g_string_append (sparql, " && ("); for (gint i = 0; i < mimetypes->len; i++) { if (i != 0) { g_string_append (sparql, " || "); } g_string_append_printf (sparql, "fn:contains(?mime, '%s')", (gchar *) g_ptr_array_index (mimetypes, i)); } g_string_append (sparql, ")\n"); } g_string_append (sparql, ")} ORDER BY DESC (?rank)"); tracker->cancellable = g_cancellable_new (); tracker_sparql_connection_query_async (tracker->connection, sparql->str, tracker->cancellable, query_callback, tracker); g_string_free (sparql, TRUE); g_free (search_text); g_free (location_uri); g_object_unref (location); } static void nautilus_search_engine_tracker_stop (NautilusSearchProvider *provider) { NautilusSearchEngineTracker *tracker; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (provider); if (tracker->query_pending) { DEBUG ("Tracker engine stop"); g_cancellable_cancel (tracker->cancellable); g_clear_object (&tracker->cancellable); tracker->query_pending = FALSE; g_object_notify (G_OBJECT (provider), "running"); } } static void nautilus_search_engine_tracker_set_query (NautilusSearchProvider *provider, NautilusQuery *query) { g_autoptr (GFile) location = NULL; NautilusSearchEngineTracker *tracker; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (provider); location = nautilus_query_get_location (query); g_clear_object (&tracker->query); tracker->query = g_object_ref (query); tracker->recursive = is_recursive_search (NAUTILUS_SEARCH_ENGINE_TYPE_INDEXED, nautilus_query_get_recursive (query), location); } static gboolean nautilus_search_engine_tracker_is_running (NautilusSearchProvider *provider) { NautilusSearchEngineTracker *tracker; tracker = NAUTILUS_SEARCH_ENGINE_TRACKER (provider); return tracker->query_pending; } static void nautilus_search_provider_init (NautilusSearchProviderInterface *iface) { iface->set_query = nautilus_search_engine_tracker_set_query; iface->start = nautilus_search_engine_tracker_start; iface->stop = nautilus_search_engine_tracker_stop; iface->is_running = nautilus_search_engine_tracker_is_running; } static void nautilus_search_engine_tracker_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { NautilusSearchProvider *self = NAUTILUS_SEARCH_PROVIDER (object); switch (prop_id) { case PROP_RUNNING: { g_value_set_boolean (value, nautilus_search_engine_tracker_is_running (self)); } break; default: { G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } } static void nautilus_search_engine_tracker_class_init (NautilusSearchEngineTrackerClass *class) { GObjectClass *gobject_class; gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = finalize; gobject_class->get_property = nautilus_search_engine_tracker_get_property; /** * NautilusSearchEngine::running: * * Whether the search engine is running a search. */ g_object_class_override_property (gobject_class, PROP_RUNNING, "running"); } static void nautilus_search_engine_tracker_init (NautilusSearchEngineTracker *engine) { GError *error = NULL; engine->hits_pending = g_queue_new (); engine->connection = nautilus_tracker_get_miner_fs_connection (&error); if (error) { g_warning ("Could not establish a connection to Tracker: %s", error->message); g_error_free (error); } } NautilusSearchEngineTracker * nautilus_search_engine_tracker_new (void) { return g_object_new (NAUTILUS_TYPE_SEARCH_ENGINE_TRACKER, NULL); }