gtksearchenginetracker.c 15.6 KB
Newer Older
1
/*
2
 * Copyright (C) 2009-2011 Nokia <ivan.frade@nokia.com>
3 4 5 6 7 8 9 10 11 12 13 14
 *
 * 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
Javier Jardón's avatar
Javier Jardón committed
15
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16
 *
17
 * Authors: Jürg Billeter <juerg.billeter@codethink.co.uk>
18
 *          Martyn Russell <martyn@lanedo.com>
19 20 21 22
 *
 * Based on nautilus-search-engine-tracker.c
 */

23
#include "config.h"
24

25 26
#include <string.h>

27 28
#include <gio/gio.h>
#include <gmodule.h>
Benjamin Otte's avatar
Benjamin Otte committed
29
#include <gdk/gdk.h>
30
#include <gtk/gtk.h>
31

32
#include "gtksearchenginetracker.h"
33

34 35 36 37 38 39 40 41 42 43 44 45 46 47
#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

48 49
/* 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
50
 * use filename matching instead. This doesn’t use the content of the
51 52
 * file however.
 */
Matthias Clasen's avatar
Matthias Clasen committed
53
#define FTS_MATCHING
54

55
struct _GtkSearchEngineTracker
56
{
57
  GtkSearchEngine parent;
58 59
  GDBusConnection *connection;
  GCancellable *cancellable;
60 61
  GtkQuery *query;
  gboolean query_pending;
62
  GPtrArray *indexed_locations;
63 64
};

65 66 67 68 69 70
struct _GtkSearchEngineTrackerClass
{
  GtkSearchEngineClass parent_class;
};

G_DEFINE_TYPE (GtkSearchEngineTracker, _gtk_search_engine_tracker, GTK_TYPE_SEARCH_ENGINE)
71 72 73 74 75

static void
finalize (GObject *object)
{
  GtkSearchEngineTracker *tracker;
76

77 78
  g_debug ("Finalizing GtkSearchEngineTracker");

79
  tracker = GTK_SEARCH_ENGINE_TRACKER (object);
80

81
  if (tracker->cancellable)
82
    {
83 84
      g_cancellable_cancel (tracker->cancellable);
      g_object_unref (tracker->cancellable);
85
    }
86

87 88
  g_clear_object (&tracker->query);
  g_clear_object (&tracker->connection);
89

90
  g_ptr_array_unref (tracker->indexed_locations);
91 92 93 94

  G_OBJECT_CLASS (_gtk_search_engine_tracker_parent_class)->finalize (object);
}

95 96
static GDBusConnection *
get_connection (void)
97
{
98
  GDBusConnection *connection;
99
  GError *error = NULL;
100
  GVariant *reply;
101

102 103 104 105
  /* 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);
106

107 108
  if (error)
    {
109
      g_debug ("Couldn't connect to D-Bus session bus, %s", error->message);
110
      g_error_free (error);
111
      return NULL;
112 113
    }

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  /* 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);
136

137 138 139 140 141 142 143
  if (error)
    {
      g_debug ("Tracker is not available, %s", error->message);
      g_error_free (error);
      g_object_unref (connection);
      return NULL;
    }
144

145
  g_variant_unref (reply);
146

147
  g_debug ("Tracker is ready");
148

149
  return connection;
150
}
151

152
static void
153 154 155 156
get_query_results (GtkSearchEngineTracker *engine,
                   const gchar            *sparql,
                   GAsyncReadyCallback     callback,
                   gpointer                user_data)
157
{
158
  g_dbus_connection_call (engine->connection,
159 160 161 162 163 164 165 166
                          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,
167
                          engine->cancellable,
168 169 170
                          callback,
                          user_data);
}
171

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
/* 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);
 }

227 228
static void
sparql_append_string_literal (GString     *sparql,
229
                              const gchar *str,
230
                              gboolean     glob,
231 232
                              gboolean     is_dir_uri,
                              gboolean     quoted)
233 234
{
  gchar *s;
Jürg Billeter's avatar
Jürg Billeter committed
235

236
  s = sparql_escape_string (str);
237

238
  g_string_append_c (sparql, '"');
239 240
  if (quoted)
    g_string_append (sparql, "\\\"");
241
  g_string_append (sparql, s);
242

243 244
  if (is_dir_uri)
    g_string_append_c (sparql, '/');
245 246
  if (quoted)
    g_string_append (sparql, "\\\"");
247 248
  if (glob)
    g_string_append_c (sparql, '*');
249
  g_string_append_c (sparql, '"');
250

251
  g_free (s);
252 253
}

254 255 256 257 258 259 260
static void
sparql_append_string_literal_lower_case (GString     *sparql,
                                         const gchar *str)
{
  gchar *s;

  s = g_utf8_strdown (str, -1);
261
  sparql_append_string_literal (sparql, s, FALSE, FALSE, FALSE);
262 263 264
  g_free (s);
}

265 266 267 268 269 270 271 272 273 274 275 276
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;
277
  GtkSearchHit *hit;
278 279 280

  tracker = GTK_SEARCH_ENGINE_TRACKER (user_data);

281
  tracker->query_pending = FALSE;
282

283
  reply = g_dbus_connection_call_finish (tracker->connection, res, &error);
284 285 286 287
  if (error)
    {
      _gtk_search_engine_error (GTK_SEARCH_ENGINE (tracker), error->message);
      g_error_free (error);
288
      g_object_unref (tracker);
289 290 291 292 293 294
      return;
    }

  if (!reply)
    {
      _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
295
      g_object_unref (tracker);
296 297 298 299 300 301
      return;
    }

  r = g_variant_get_child_value (reply, 0);
  g_variant_iter_init (&iter, r);
  n = g_variant_iter_n_children (&iter);
302 303
  hit = g_new (GtkSearchHit, n);
  hits = NULL;
304 305 306 307 308 309 310
  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);
311
      hit[i].file = g_file_new_for_uri (strv[0]);
312
      hit[i].info = NULL;
313
      g_free (strv);
314
      hits = g_list_prepend (hits, &hit[i]);
315 316 317 318 319
    }

  _gtk_search_engine_hits_added (GTK_SEARCH_ENGINE (tracker), hits);
  _gtk_search_engine_finished (GTK_SEARCH_ENGINE (tracker));
  g_list_free (hits);
320
  g_free (hit);
321 322 323
  g_variant_unref (reply);
  g_variant_unref (r);

324
  g_object_unref (tracker);
325 326
}

327 328 329 330
static void
gtk_search_engine_tracker_start (GtkSearchEngine *engine)
{
  GtkSearchEngineTracker *tracker;
331
  const gchar *search_text;
332
  GFile *location;
333
  GString *sparql;
334
  gboolean recursive;
335 336 337

  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);

338
  if (tracker->query_pending)
339 340 341 342
    {
      g_debug ("Attempt to start a new search while one is pending, doing nothing");
      return;
    }
343

344
  if (tracker->query == NULL)
345 346 347 348
    {
      g_debug ("Attempt to start a new search with no GtkQuery, doing nothing");
      return;
    }
349

350
  search_text = gtk_query_get_text (tracker->query);
351
  location = gtk_query_get_location (tracker->query);
352
  recursive = _gtk_search_engine_get_recursive (engine);
353

354 355 356
  sparql = g_string_new ("SELECT nie:url(?urn) "
                         "WHERE {"
                         "  ?urn a nfo:FileDataObject ;"
357
                         "  tracker:available true ; "
358
                         "  nfo:belongsToContainer ?parent; ");
359 360 361 362

#ifdef FTS_MATCHING
  /* Using FTS: */
  g_string_append (sparql, "fts:match ");
363
  sparql_append_string_literal (sparql, search_text, TRUE, FALSE, TRUE);
364 365
#endif

366
  g_string_append (sparql, ". FILTER (BOUND(nie:url(?urn)) && ");
367 368

  g_string_append (sparql, "fn:contains(fn:lower-case(nfo:fileName(?urn)),");
369
  sparql_append_string_literal_lower_case (sparql, search_text);
370
  g_string_append (sparql, ")");
371

372
  if (location)
373
    {
374
      gchar *location_uri = g_file_get_uri (location);
375 376
      g_string_append (sparql, " && ");
      if (recursive)
377 378
        {
          g_string_append (sparql, "fn:starts-with(nie:url(?urn),");
379
          sparql_append_string_literal (sparql, location_uri, FALSE, TRUE, FALSE);
380 381
          g_string_append (sparql, ")");
        }
382
      else
383 384
        {
          g_string_append (sparql, "nie:url(?parent) = ");
385
          sparql_append_string_literal (sparql, location_uri, FALSE, FALSE, FALSE);
386
        }
387
      g_free (location_uri);
388
    }
389

390
  g_string_append (sparql, ")");
391

392 393 394 395
#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))");
396 397
#endif /* FTS_MATCHING */

398
  tracker->query_pending = TRUE;
399

Matthias Clasen's avatar
Matthias Clasen committed
400 401
  g_debug ("SearchEngineTracker: query: %s", sparql->str);

402
  get_query_results (tracker, sparql->str, query_callback, g_object_ref (tracker));
403 404

  g_string_free (sparql, TRUE);
405 406 407 408 409 410
}

static void
gtk_search_engine_tracker_stop (GtkSearchEngine *engine)
{
  GtkSearchEngineTracker *tracker;
411

412
  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
413

414
  if (tracker->query && tracker->query_pending)
415
    {
416 417
      g_cancellable_cancel (tracker->cancellable);
      tracker->query_pending = FALSE;
418 419 420 421
    }
}

static void
422
gtk_search_engine_tracker_set_query (GtkSearchEngine *engine,
423
                                     GtkQuery        *query)
424 425
{
  GtkSearchEngineTracker *tracker;
426

427
  tracker = GTK_SEARCH_ENGINE_TRACKER (engine);
428 429

  if (query)
430 431
    g_object_ref (query);

432 433
  if (tracker->query)
    g_object_unref (tracker->query);
434

435
  tracker->query = query;
436 437 438 439 440 441 442
}

static void
_gtk_search_engine_tracker_class_init (GtkSearchEngineTrackerClass *class)
{
  GObjectClass *gobject_class;
  GtkSearchEngineClass *engine_class;
443

444 445
  gobject_class = G_OBJECT_CLASS (class);
  gobject_class->finalize = finalize;
446

447 448 449 450 451 452
  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;
}

453 454
static void get_indexed_locations (GtkSearchEngineTracker *engine);

455 456 457
static void
_gtk_search_engine_tracker_init (GtkSearchEngineTracker *engine)
{
458 459 460
  engine->cancellable = g_cancellable_new ();
  engine->query_pending = FALSE;
  engine->indexed_locations = g_ptr_array_new_with_free_func (g_object_unref);
461 462 463

  get_indexed_locations (engine);
}
464 465 466 467 468

GtkSearchEngine *
_gtk_search_engine_tracker_new (void)
{
  GtkSearchEngineTracker *engine;
469
  GDBusConnection *connection;
470

471
  g_debug ("--");
472

473 474 475
  connection = get_connection ();
  if (!connection)
    return NULL;
476

477
  g_debug ("Creating GtkSearchEngineTracker...");
478

479 480
  engine = g_object_new (GTK_TYPE_SEARCH_ENGINE_TRACKER, NULL);

481
  engine->connection = connection;
482

483 484
  return GTK_SEARCH_ENGINE (engine);
}
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552

#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]);
553 554 555
      if (path == NULL)
        continue;

556
      location = g_file_new_for_path (path);
557
      g_ptr_array_add (engine->indexed_locations, location);
558 559 560 561 562 563 564 565 566 567 568 569 570 571
    }

  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;

572
  for (i = 0; i < engine->indexed_locations->len; i++)
573
    {
574
      place = g_ptr_array_index (engine->indexed_locations, i);
575 576 577 578 579 580
      if (g_file_equal (location, place) || g_file_has_prefix (location, place))
        return TRUE;
    }

  return FALSE;
}