1 /* nautilus-tag-manager.c
2  *
3  * Copyright (C) 2017 Alexandru Pandelea <alexandru.pandelea@gmail.com>
4  * Copyright (C) 2020 Sam Thursfield <sam@afuera.me.uk>
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "nautilus-tag-manager.h"
21 #include "nautilus-file.h"
22 #include "nautilus-file-undo-operations.h"
23 #include "nautilus-file-undo-manager.h"
24 #include "nautilus-tracker-utilities.h"
25 #define DEBUG_FLAG NAUTILUS_DEBUG_TAG_MANAGER
26 #include "nautilus-debug.h"
27 
28 #include <gio/gunixinputstream.h>
29 #include <tracker-sparql.h>
30 
31 #include "config.h"
32 
33 struct _NautilusTagManager
34 {
35     GObject object;
36 
37     gboolean database_ok;
38     TrackerSparqlConnection *db;
39     TrackerNotifier *notifier;
40 
41     TrackerSparqlStatement *query_starred_files;
42     TrackerSparqlStatement *query_file_is_starred;
43 
44     GHashTable *starred_file_uris;
45     GFile *home;
46 
47     GCancellable *cancellable;
48 };
49 
50 G_DEFINE_TYPE (NautilusTagManager, nautilus_tag_manager, G_TYPE_OBJECT);
51 
52 typedef struct
53 {
54     NautilusTagManager *tag_manager;
55     GTask *task;
56     GList *selection;
57     gboolean star;
58 } UpdateData;
59 
60 enum
61 {
62     STARRED_CHANGED,
63     LAST_SIGNAL
64 };
65 
66 #define QUERY_STARRED_FILES \
67     "SELECT ?file " \
68     "{ " \
69     "    ?file a nautilus:File ; " \
70     "        nautilus:starred true . " \
71     "}"
72 
73 #define QUERY_FILE_IS_STARRED \
74     "ASK " \
75     "{ " \
76     "    ~file a nautilus:File ; " \
77     "        nautilus:starred true . " \
78     "}"
79 
80 static guint signals[LAST_SIGNAL];
81 
82 /* Limit to 10MB output from Tracker -- surely, nobody has over a million starred files. */
83 #define TRACKER2_MAX_IMPORT_BYTES 10 * 1024 * 1024
84 
85 static gchar *
tracker2_migration_stamp(void)86 tracker2_migration_stamp (void)
87 {
88     return g_build_filename (g_get_user_data_dir (), "nautilus", "tracker2-migration-complete", NULL);
89 }
90 
91 static void
start_query_or_update(TrackerSparqlConnection * db,GString * query,GAsyncReadyCallback callback,gpointer user_data,gboolean is_query,GCancellable * cancellable)92 start_query_or_update (TrackerSparqlConnection *db,
93                        GString                 *query,
94                        GAsyncReadyCallback      callback,
95                        gpointer                 user_data,
96                        gboolean                 is_query,
97                        GCancellable            *cancellable)
98 {
99     g_autoptr (GError) error = NULL;
100 
101     if (!db)
102     {
103         g_message ("nautilus-tag-manager: No Tracker connection");
104         return;
105     }
106 
107     if (is_query)
108     {
109         tracker_sparql_connection_query_async (db,
110                                                query->str,
111                                                cancellable,
112                                                callback,
113                                                user_data);
114     }
115     else
116     {
117         tracker_sparql_connection_update_async (db,
118                                                 query->str,
119                                                 cancellable,
120                                                 callback,
121                                                 user_data);
122     }
123 }
124 
125 static void
on_update_callback(GObject * object,GAsyncResult * result,gpointer user_data)126 on_update_callback (GObject      *object,
127                     GAsyncResult *result,
128                     gpointer      user_data)
129 {
130     TrackerSparqlConnection *db;
131     GError *error;
132     UpdateData *data;
133 
134     data = user_data;
135 
136     error = NULL;
137 
138     db = TRACKER_SPARQL_CONNECTION (object);
139 
140     tracker_sparql_connection_update_finish (db, result, &error);
141 
142     if (error == NULL)
143     {
144         /* FIXME: make sure data->tag_manager->starred_file_uris is up to date */
145 
146         if (!nautilus_file_undo_manager_is_operating ())
147         {
148             NautilusFileUndoInfo *undo_info;
149 
150             undo_info = nautilus_file_undo_info_starred_new (data->selection, data->star);
151             nautilus_file_undo_manager_set_action (undo_info);
152 
153             g_object_unref (undo_info);
154         }
155 
156         g_signal_emit_by_name (data->tag_manager, "starred-changed", nautilus_file_list_copy (data->selection));
157 
158         g_task_return_boolean (data->task, TRUE);
159         g_object_unref (data->task);
160     }
161     else if (error && error->code == G_IO_ERROR_CANCELLED)
162     {
163         g_error_free (error);
164     }
165     else
166     {
167         g_warning ("error updating tags: %s", error->message);
168         g_task_return_error (data->task, error);
169         g_object_unref (data->task);
170     }
171 
172     nautilus_file_list_free (data->selection);
173     g_free (data);
174 }
175 
176 /**
177  * nautilus_tag_manager_get_starred_files:
178  * @self: The tag manager singleton
179  *
180  * Returns: (element-type gchar*) (transfer container): A list of the starred urls.
181  */
182 GList *
nautilus_tag_manager_get_starred_files(NautilusTagManager * self)183 nautilus_tag_manager_get_starred_files (NautilusTagManager *self)
184 {
185     GHashTableIter starred_iter;
186     gchar *starred_uri;
187     GList *starred_file_uris = NULL;
188 
189     g_hash_table_iter_init (&starred_iter, self->starred_file_uris);
190     while (g_hash_table_iter_next (&starred_iter, (gpointer *) &starred_uri, NULL))
191     {
192         g_autoptr (GFile) file = g_file_new_for_uri (starred_uri);
193 
194         /* Skip files ouside $HOME, because we don't support starring these yet.
195          * See comment on nautilus_tag_manager_can_star_contents() */
196         if (g_file_has_prefix (file, self->home))
197         {
198             starred_file_uris = g_list_prepend (starred_file_uris, starred_uri);
199         }
200     }
201 
202     return starred_file_uris;
203 }
204 
205 static void
on_get_starred_files_cursor_callback(GObject * object,GAsyncResult * result,gpointer user_data)206 on_get_starred_files_cursor_callback (GObject      *object,
207                                       GAsyncResult *result,
208                                       gpointer      user_data)
209 {
210     TrackerSparqlCursor *cursor;
211     g_autoptr (GError) error = NULL;
212     const gchar *url;
213     gboolean success;
214     NautilusTagManager *self;
215     GList *changed_files;
216     NautilusFile *file;
217 
218     cursor = TRACKER_SPARQL_CURSOR (object);
219 
220     self = NAUTILUS_TAG_MANAGER (user_data);
221 
222     success = tracker_sparql_cursor_next_finish (cursor, result, &error);
223 
224     if (!success)
225     {
226         if (error != NULL)
227         {
228             g_warning ("Error on getting all tags cursor callback: %s", error->message);
229         }
230 
231         g_clear_object (&cursor);
232         return;
233     }
234 
235     url = tracker_sparql_cursor_get_string (cursor, 0, NULL);
236 
237     g_hash_table_add (self->starred_file_uris, g_strdup (url));
238 
239     file = nautilus_file_get_by_uri (url);
240 
241     if (file)
242     {
243         changed_files = g_list_prepend (NULL, file);
244 
245         g_signal_emit_by_name (self, "starred-changed", changed_files);
246 
247         nautilus_file_list_free (changed_files);
248     }
249     else
250     {
251         DEBUG ("File %s is starred but not found", url);
252     }
253 
254     tracker_sparql_cursor_next_async (cursor,
255                                       self->cancellable,
256                                       on_get_starred_files_cursor_callback,
257                                       self);
258 }
259 
260 static void
on_get_starred_files_query_callback(GObject * object,GAsyncResult * result,gpointer user_data)261 on_get_starred_files_query_callback (GObject      *object,
262                                      GAsyncResult *result,
263                                      gpointer      user_data)
264 {
265     TrackerSparqlCursor *cursor;
266     g_autoptr (GError) error = NULL;
267     TrackerSparqlStatement *statement;
268     NautilusTagManager *self;
269 
270     self = NAUTILUS_TAG_MANAGER (user_data);
271     statement = TRACKER_SPARQL_STATEMENT (object);
272 
273     cursor = tracker_sparql_statement_execute_finish (statement,
274                                                       result,
275                                                       &error);
276 
277     if (error != NULL)
278     {
279         if (error->code != G_IO_ERROR_CANCELLED)
280         {
281             g_warning ("Error on getting starred files: %s", error->message);
282         }
283     }
284     else
285     {
286         tracker_sparql_cursor_next_async (cursor,
287                                           self->cancellable,
288                                           on_get_starred_files_cursor_callback,
289                                           user_data);
290     }
291 }
292 
293 static void
nautilus_tag_manager_query_starred_files(NautilusTagManager * self,GCancellable * cancellable)294 nautilus_tag_manager_query_starred_files (NautilusTagManager *self,
295                                           GCancellable       *cancellable)
296 {
297     if (!self->database_ok)
298     {
299         g_message ("nautilus-tag-manager: No Tracker connection");
300         return;
301     }
302 
303     self->cancellable = cancellable;
304 
305     tracker_sparql_statement_execute_async (self->query_starred_files,
306                                             cancellable,
307                                             on_get_starred_files_query_callback,
308                                             self);
309 }
310 
311 static GString *
nautilus_tag_manager_delete_tag(NautilusTagManager * self,GList * selection)312 nautilus_tag_manager_delete_tag (NautilusTagManager *self,
313                                  GList              *selection)
314 {
315     GString *query;
316     NautilusFile *file;
317     GList *l;
318 
319     query = g_string_new ("DELETE DATA {");
320 
321     for (l = selection; l != NULL; l = l->next)
322     {
323         g_autofree gchar *uri = NULL;
324 
325         file = l->data;
326 
327         uri = nautilus_file_get_uri (file);
328         g_string_append_printf (query,
329                                 "    <%s> a nautilus:File ; "
330                                 "        nautilus:starred true . ",
331                                 uri);
332     }
333 
334     g_string_append (query, "}");
335 
336     return query;
337 }
338 
339 static GString *
nautilus_tag_manager_insert_tag(NautilusTagManager * self,GList * selection)340 nautilus_tag_manager_insert_tag (NautilusTagManager *self,
341                                  GList              *selection)
342 {
343     GString *query;
344     NautilusFile *file;
345     GList *l;
346 
347     query = g_string_new ("INSERT DATA {");
348 
349     for (l = selection; l != NULL; l = l->next)
350     {
351         g_autofree gchar *uri = NULL;
352 
353         file = l->data;
354 
355         uri = nautilus_file_get_uri (file);
356         g_string_append_printf (query,
357                                 "    <%s> a nautilus:File ; "
358                                 "        nautilus:starred true . ",
359                                 uri);
360     }
361 
362     g_string_append (query, "}");
363 
364     return query;
365 }
366 
367 gboolean
nautilus_tag_manager_file_is_starred(NautilusTagManager * self,const gchar * file_uri)368 nautilus_tag_manager_file_is_starred (NautilusTagManager *self,
369                                       const gchar        *file_uri)
370 {
371     return g_hash_table_contains (self->starred_file_uris, file_uri);
372 }
373 
374 void
nautilus_tag_manager_star_files(NautilusTagManager * self,GObject * object,GList * selection,GAsyncReadyCallback callback,GCancellable * cancellable)375 nautilus_tag_manager_star_files (NautilusTagManager  *self,
376                                  GObject             *object,
377                                  GList               *selection,
378                                  GAsyncReadyCallback  callback,
379                                  GCancellable        *cancellable)
380 {
381     GString *query;
382     g_autoptr (GError) error = NULL;
383     GTask *task;
384     UpdateData *update_data;
385 
386     DEBUG ("Starring %i files", g_list_length (selection));
387 
388     task = g_task_new (object, cancellable, callback, NULL);
389 
390     query = nautilus_tag_manager_insert_tag (self, selection);
391 
392     update_data = g_new0 (UpdateData, 1);
393     update_data->task = task;
394     update_data->tag_manager = self;
395     update_data->selection = nautilus_file_list_copy (selection);
396     update_data->star = TRUE;
397 
398     start_query_or_update (self->db,
399                            query,
400                            on_update_callback,
401                            update_data,
402                            FALSE,
403                            cancellable);
404 
405     g_string_free (query, TRUE);
406 }
407 
408 void
nautilus_tag_manager_unstar_files(NautilusTagManager * self,GObject * object,GList * selection,GAsyncReadyCallback callback,GCancellable * cancellable)409 nautilus_tag_manager_unstar_files (NautilusTagManager  *self,
410                                    GObject             *object,
411                                    GList               *selection,
412                                    GAsyncReadyCallback  callback,
413                                    GCancellable        *cancellable)
414 {
415     GString *query;
416     GTask *task;
417     UpdateData *update_data;
418 
419     DEBUG ("Unstarring %i files", g_list_length (selection));
420 
421     task = g_task_new (object, cancellable, callback, NULL);
422 
423     query = nautilus_tag_manager_delete_tag (self, selection);
424 
425     update_data = g_new0 (UpdateData, 1);
426     update_data->task = task;
427     update_data->tag_manager = self;
428     update_data->selection = nautilus_file_list_copy (selection);
429     update_data->star = FALSE;
430 
431     start_query_or_update (self->db,
432                            query,
433                            on_update_callback,
434                            update_data,
435                            FALSE,
436                            cancellable);
437 
438     g_string_free (query, TRUE);
439 }
440 
441 static void
on_tracker_notifier_events(TrackerNotifier * notifier,gchar * service,gchar * graph,GPtrArray * events,gpointer user_data)442 on_tracker_notifier_events (TrackerNotifier *notifier,
443                             gchar           *service,
444                             gchar           *graph,
445                             GPtrArray       *events,
446                             gpointer         user_data)
447 {
448     TrackerNotifierEvent *event;
449     NautilusTagManager *self;
450     int i;
451     const gchar *file_url;
452     GError *error = NULL;
453     TrackerSparqlCursor *cursor;
454     gboolean query_has_results = FALSE;
455     gboolean starred;
456     GList *changed_files;
457     NautilusFile *changed_file;
458 
459     self = NAUTILUS_TAG_MANAGER (user_data);
460 
461     for (i = 0; i < events->len; i++)
462     {
463         event = g_ptr_array_index (events, i);
464 
465         file_url = tracker_notifier_event_get_urn (event);
466         changed_file = NULL;
467 
468         DEBUG ("Got event for file %s", file_url);
469 
470         tracker_sparql_statement_bind_string (self->query_file_is_starred, "file", file_url);
471         cursor = tracker_sparql_statement_execute (self->query_file_is_starred,
472                                                    NULL,
473                                                    &error);
474 
475         if (cursor)
476         {
477             query_has_results = tracker_sparql_cursor_next (cursor, NULL, &error);
478         }
479 
480         if (error || !cursor || !query_has_results)
481         {
482             g_warning ("Couldn't query the starred files database: '%s'", error ? error->message : "(null error)");
483             g_clear_error (&error);
484             return;
485         }
486 
487         starred = tracker_sparql_cursor_get_boolean (cursor, 0);
488         if (starred)
489         {
490             gboolean inserted = g_hash_table_add (self->starred_file_uris, g_strdup (file_url));
491 
492             if (inserted)
493             {
494                 DEBUG ("Added %s to starred files list", file_url);
495                 changed_file = nautilus_file_get_by_uri (file_url);
496             }
497         }
498         else
499         {
500             gboolean removed = g_hash_table_remove (self->starred_file_uris, file_url);
501 
502             if (removed)
503             {
504                 DEBUG ("Removed %s from starred files list", file_url);
505                 changed_file = nautilus_file_get_by_uri (file_url);
506             }
507         }
508 
509         if (changed_file)
510         {
511             changed_files = g_list_prepend (NULL, changed_file);
512 
513             g_signal_emit_by_name (self, "starred-changed", changed_files);
514 
515             nautilus_file_list_free (changed_files);
516         }
517 
518         g_object_unref (cursor);
519     }
520 }
521 
522 static void
nautilus_tag_manager_finalize(GObject * object)523 nautilus_tag_manager_finalize (GObject *object)
524 {
525     NautilusTagManager *self;
526 
527     self = NAUTILUS_TAG_MANAGER (object);
528 
529     if (self->notifier != NULL)
530     {
531         g_signal_handlers_disconnect_by_func (self->notifier,
532                                               G_CALLBACK (on_tracker_notifier_events),
533                                               self);
534     }
535 
536     g_clear_object (&self->notifier);
537     g_clear_object (&self->db);
538     g_clear_object (&self->query_file_is_starred);
539     g_clear_object (&self->query_starred_files);
540 
541     g_hash_table_destroy (self->starred_file_uris);
542     g_clear_object (&self->home);
543 
544     G_OBJECT_CLASS (nautilus_tag_manager_parent_class)->finalize (object);
545 }
546 
547 static void
nautilus_tag_manager_class_init(NautilusTagManagerClass * klass)548 nautilus_tag_manager_class_init (NautilusTagManagerClass *klass)
549 {
550     GObjectClass *oclass;
551 
552     oclass = G_OBJECT_CLASS (klass);
553 
554     oclass->finalize = nautilus_tag_manager_finalize;
555 
556     signals[STARRED_CHANGED] = g_signal_new ("starred-changed",
557                                              NAUTILUS_TYPE_TAG_MANAGER,
558                                              G_SIGNAL_RUN_LAST,
559                                              0,
560                                              NULL,
561                                              NULL,
562                                              g_cclosure_marshal_VOID__POINTER,
563                                              G_TYPE_NONE,
564                                              1,
565                                              G_TYPE_POINTER);
566 }
567 
568 /**
569  * nautilus_tag_manager_get:
570  *
571  * Gets a reference to the tag manager.
572  *
573  * If used to initialize a struct field, make sure to release on finalization.
574  * If used to initialize a local variable, make sure to use g_autoptr().
575  *
576  * Returns: (transfer full): the #NautilusTagManager singleton object.
577  */
578 NautilusTagManager *
nautilus_tag_manager_get(void)579 nautilus_tag_manager_get (void)
580 {
581     static NautilusTagManager *tag_manager = NULL;
582 
583     if (tag_manager != NULL)
584     {
585         return g_object_ref (tag_manager);
586     }
587 
588     tag_manager = g_object_new (NAUTILUS_TYPE_TAG_MANAGER, NULL);
589     g_object_add_weak_pointer (G_OBJECT (tag_manager), (gpointer) & tag_manager);
590 
591     return tag_manager;
592 }
593 
594 static gboolean
setup_database(NautilusTagManager * self,GCancellable * cancellable,GError ** error)595 setup_database (NautilusTagManager  *self,
596                 GCancellable        *cancellable,
597                 GError             **error)
598 {
599     const gchar *datadir;
600     g_autofree gchar *store_path = NULL;
601     g_autofree gchar *ontology_path = NULL;
602     g_autoptr (GFile) store = NULL;
603     g_autoptr (GFile) ontology = NULL;
604 
605     /* Open private database to store nautilus:starred property. */
606 
607     datadir = NAUTILUS_DATADIR;
608 
609     store_path = g_build_filename (g_get_user_data_dir (), "nautilus", "tags", NULL);
610     ontology_path = g_build_filename (datadir, "ontology", NULL);
611 
612     store = g_file_new_for_path (store_path);
613     ontology = g_file_new_for_path (ontology_path);
614 
615     self->db = tracker_sparql_connection_new (TRACKER_SPARQL_CONNECTION_FLAGS_NONE,
616                                               store,
617                                               ontology,
618                                               cancellable,
619                                               error);
620 
621     if (*error)
622     {
623         return FALSE;
624     }
625 
626     /* Prepare reusable queries. */
627     self->query_file_is_starred = tracker_sparql_connection_query_statement (self->db,
628                                                                              QUERY_FILE_IS_STARRED,
629                                                                              cancellable,
630                                                                              error);
631 
632     if (*error)
633     {
634         return FALSE;
635     }
636 
637     self->query_starred_files = tracker_sparql_connection_query_statement (self->db,
638                                                                            QUERY_STARRED_FILES,
639                                                                            cancellable,
640                                                                            error);
641 
642     if (*error)
643     {
644         return FALSE;
645     }
646 
647     return TRUE;
648 }
649 
650 /* Initialize the tag mananger. */
651 void
nautilus_tag_manager_set_cancellable(NautilusTagManager * self,GCancellable * cancellable)652 nautilus_tag_manager_set_cancellable (NautilusTagManager *self,
653                                       GCancellable       *cancellable)
654 {
655     g_autoptr (GError) error = NULL;
656 
657     self->database_ok = setup_database (self, cancellable, &error);
658 
659     if (error)
660     {
661         g_warning ("Unable to initialize tag manager: %s", error->message);
662         return;
663     }
664 
665     self->notifier = tracker_sparql_connection_create_notifier (self->db);
666 
667     nautilus_tag_manager_query_starred_files (self, cancellable);
668 
669     g_signal_connect (self->notifier,
670                       "events",
671                       G_CALLBACK (on_tracker_notifier_events),
672                       self);
673 }
674 
675 static void
nautilus_tag_manager_init(NautilusTagManager * self)676 nautilus_tag_manager_init (NautilusTagManager *self)
677 {
678     self->starred_file_uris = g_hash_table_new_full (g_str_hash,
679                                                      g_str_equal,
680                                                      (GDestroyNotify) g_free,
681                                                      /* values are keys */
682                                                      NULL);
683     self->home = g_file_new_for_path (g_get_home_dir ());
684 }
685 
686 gboolean
nautilus_tag_manager_can_star_contents(NautilusTagManager * tag_manager,GFile * directory)687 nautilus_tag_manager_can_star_contents (NautilusTagManager *tag_manager,
688                                         GFile              *directory)
689 {
690     /* We only allow files to be starred inside the home directory for now.
691      * This avoids the starred files database growing too big.
692      * See https://gitlab.gnome.org/GNOME/nautilus/-/merge_requests/553#note_903108
693      */
694     return g_file_has_prefix (directory, tag_manager->home) || g_file_equal (directory, tag_manager->home);
695 }
696 
697 static void
update_moved_uris_callback(GObject * object,GAsyncResult * result,gpointer user_data)698 update_moved_uris_callback (GObject      *object,
699                             GAsyncResult *result,
700                             gpointer      user_data)
701 {
702     g_autoptr (GError) error = NULL;
703     g_autoptr (GPtrArray) new_uris = user_data;
704 
705     tracker_sparql_connection_update_finish (TRACKER_SPARQL_CONNECTION (object),
706                                              result,
707                                              &error);
708 
709     if (error != NULL && error->code != G_IO_ERROR_CANCELLED)
710     {
711         g_warning ("Error updating moved uris: %s", error->message);
712     }
713     else
714     {
715         g_autolist (NautilusFile) updated_files = NULL;
716         g_autoptr (NautilusTagManager) tag_manager = nautilus_tag_manager_get ();
717 
718         for (guint i = 0; i < new_uris->len; i++)
719         {
720             gchar *new_uri = g_ptr_array_index (new_uris, i);
721 
722             updated_files = g_list_prepend (updated_files, nautilus_file_get_by_uri (new_uri));
723         }
724 
725         g_signal_emit_by_name (tag_manager, "starred-changed", updated_files);
726     }
727 }
728 
729 /**
730  * nautilus_tag_manager_update_moved_uris:
731  * @self: The tag manager singleton
732  * @src: The original location as a #GFile
733  * @dest: The new location as a #GFile
734  *
735  * Checks whether the rename/move operation (@src to @dest) has modified
736  * the URIs of any starred files, and updates the database accordingly.
737  */
738 void
nautilus_tag_manager_update_moved_uris(NautilusTagManager * self,GFile * src,GFile * dest)739 nautilus_tag_manager_update_moved_uris (NautilusTagManager *self,
740                                         GFile              *src,
741                                         GFile              *dest)
742 {
743     GHashTableIter starred_iter;
744     gchar *starred_uri;
745     g_autoptr (GPtrArray) old_uris = NULL;
746     g_autoptr (GPtrArray) new_uris = NULL;
747     g_autoptr (GString) query = NULL;
748 
749     if (!self->database_ok)
750     {
751         g_message ("nautilus-tag-manager: No Tracker connection");
752         return;
753     }
754 
755     old_uris = g_ptr_array_new ();
756     new_uris = g_ptr_array_new_with_free_func (g_free);
757 
758     g_hash_table_iter_init (&starred_iter, self->starred_file_uris);
759     while (g_hash_table_iter_next (&starred_iter, (gpointer *) &starred_uri, NULL))
760     {
761         g_autoptr (GFile) starred_location = NULL;
762         g_autofree gchar *relative_path = NULL;
763 
764         starred_location = g_file_new_for_uri (starred_uri);
765 
766         if (g_file_equal (starred_location, src))
767         {
768             /* The moved file/folder is starred */
769             g_ptr_array_add (old_uris, starred_uri);
770             g_ptr_array_add (new_uris, g_file_get_uri (dest));
771             continue;
772         }
773 
774         relative_path = g_file_get_relative_path (src, starred_location);
775         if (relative_path != NULL)
776         {
777             /* The starred file/folder is descendant of the moved/renamed directory */
778             g_autoptr (GFile) new_location = NULL;
779 
780             new_location = g_file_resolve_relative_path (dest, relative_path);
781 
782             g_ptr_array_add (old_uris, starred_uri);
783             g_ptr_array_add (new_uris, g_file_get_uri (new_location));
784         }
785     }
786 
787     if (new_uris->len == 0)
788     {
789         /* No starred files are affected by this move/rename */
790         return;
791     }
792 
793     DEBUG ("Updating moved URI for %i starred files", new_uris->len);
794 
795     query = g_string_new ("DELETE DATA {");
796 
797     for (guint i = 0; i < old_uris->len; i++)
798     {
799         gchar *old_uri = g_ptr_array_index (old_uris, i);
800         g_string_append_printf (query,
801                                 "    <%s> a nautilus:File ; "
802                                 "        nautilus:starred true . ",
803                                 old_uri);
804     }
805 
806     g_string_append (query, "} ; INSERT DATA {");
807 
808     for (guint i = 0; i < new_uris->len; i++)
809     {
810         gchar *new_uri = g_ptr_array_index (new_uris, i);
811         g_string_append_printf (query,
812                                 "    <%s> a nautilus:File ; "
813                                 "        nautilus:starred true . ",
814                                 new_uri);
815     }
816 
817     g_string_append (query, "}");
818 
819     /* Forward the new_uris list to later pass in the ::files-changed signal.
820      * There is no need to pass the old_uris because the file model is updated
821      * independently; we need only inform the view where to display stars now.
822      */
823     tracker_sparql_connection_update_async (self->db,
824                                             query->str,
825                                             self->cancellable,
826                                             update_moved_uris_callback,
827                                             g_steal_pointer (&new_uris));
828 }
829 
830 static void
process_tracker2_data_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)831 process_tracker2_data_cb (GObject      *source_object,
832                           GAsyncResult *res,
833                           gpointer      user_data)
834 {
835     NautilusTagManager *self = NAUTILUS_TAG_MANAGER (source_object);
836     g_autofree gchar *path = tracker2_migration_stamp ();
837     g_autoptr (GError) error = NULL;
838 
839     tracker_sparql_connection_update_finish (self->db, res, &error);
840 
841     if (!error)
842     {
843         DEBUG ("Data migration was successful. Creating stamp %s", path);
844 
845         g_file_set_contents (path, "", -1, &error);
846         if (error)
847         {
848             g_warning ("Failed to create %s after migration: %s", path, error->message);
849         }
850     }
851     else
852     {
853         g_warning ("Error during data migration: %s", error->message);
854     }
855 }
856 
857 static void
process_tracker2_data(NautilusTagManager * self,GBytes * key_file_data)858 process_tracker2_data (NautilusTagManager *self,
859                        GBytes             *key_file_data)
860 {
861     g_autoptr (GKeyFile) key_file = NULL;
862     g_autoptr (GError) error = NULL;
863     gchar **groups, **group;
864     GList *selection = NULL;
865     NautilusFile *file;
866 
867     key_file = g_key_file_new ();
868     g_key_file_load_from_bytes (key_file,
869                                 key_file_data,
870                                 G_KEY_FILE_NONE,
871                                 &error);
872     g_bytes_unref (key_file_data);
873 
874     if (error)
875     {
876         g_warning ("Tracker 2 migration: Failed to parse key file data: %s", error->message);
877         return;
878     }
879 
880     groups = g_key_file_get_groups (key_file, NULL);
881 
882     for (group = groups; *group != NULL; group++)
883     {
884         file = nautilus_file_get_by_uri (*group);
885 
886         if (file)
887         {
888             DEBUG ("Tracker 2 migration: starring %s", *group);
889             selection = g_list_prepend (selection, file);
890         }
891         else
892         {
893             DEBUG ("Tracker 2 migration: couldn't get NautilusFile for %s", *group);
894         }
895     }
896 
897     nautilus_tag_manager_star_files (self,
898                                      G_OBJECT (self),
899                                      selection,
900                                      process_tracker2_data_cb,
901                                      self->cancellable);
902 
903     g_free (groups);
904 }
905 
906 static void
export_tracker2_data_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)907 export_tracker2_data_cb (GObject      *source_object,
908                          GAsyncResult *res,
909                          gpointer      user_data)
910 {
911     GInputStream *stream = G_INPUT_STREAM (source_object);
912     NautilusTagManager *self = NAUTILUS_TAG_MANAGER (user_data);
913     g_autoptr (GError) error = NULL;
914     GBytes *key_file_data;
915 
916     key_file_data = g_input_stream_read_bytes_finish (stream, res, &error);
917 
918     if (key_file_data)
919     {
920         process_tracker2_data (self, key_file_data);
921     }
922     else
923     {
924         g_warning ("Tracker2 migration: Failed to read data from pipe: %s", error->message);
925     }
926 }
927 
928 static void
child_watch_cb(GPid pid,gint status,gpointer user_data)929 child_watch_cb (GPid     pid,
930                 gint     status,
931                 gpointer user_data)
932 {
933     DEBUG ("Child %" G_PID_FORMAT " exited %s", pid,
934            g_spawn_check_exit_status (status, NULL) ? "normally" : "abnormally");
935     g_spawn_close_pid (pid);
936 }
937 
938 static void
export_tracker2_data(NautilusTagManager * self)939 export_tracker2_data (NautilusTagManager *self)
940 {
941     gchar *argv[] = {"tracker3", "export", "--2to3", "files-starred", "--keyfile", NULL};
942     gint stdout_fd;
943     GPid child_pid;
944     g_autoptr (GError) error = NULL;
945     gboolean success;
946     g_autoptr (GInputStream) stream = NULL;
947     GSpawnFlags flags;
948 
949     flags = G_SPAWN_DO_NOT_REAP_CHILD |
950             G_SPAWN_STDERR_TO_DEV_NULL |
951             G_SPAWN_SEARCH_PATH;
952     success = g_spawn_async_with_pipes (NULL,
953                                         argv,
954                                         NULL,
955                                         flags,
956                                         NULL,
957                                         NULL,
958                                         &child_pid,
959                                         NULL,
960                                         &stdout_fd,
961                                         NULL,
962                                         &error);
963     if (!success)
964     {
965         g_warning ("Tracker 2 migration: Couldn't run `tracker3`: %s", error->message);
966         return;
967     }
968 
969     g_child_watch_add (child_pid, child_watch_cb, NULL);
970 
971     stream = g_unix_input_stream_new (stdout_fd, TRUE);
972     g_input_stream_read_bytes_async (stream,
973                                      TRACKER2_MAX_IMPORT_BYTES,
974                                      G_PRIORITY_LOW,
975                                      self->cancellable,
976                                      export_tracker2_data_cb,
977                                      self);
978 }
979 
980 void
nautilus_tag_manager_maybe_migrate_tracker2_data(NautilusTagManager * self)981 nautilus_tag_manager_maybe_migrate_tracker2_data (NautilusTagManager *self)
982 {
983     g_autofree gchar *path = tracker2_migration_stamp ();
984 
985     if (g_file_test (path, G_FILE_TEST_EXISTS))
986     {
987         DEBUG ("Tracker 2 migration: already completed.");
988     }
989     else
990     {
991         DEBUG ("Tracker 2 migration: starting.");
992         export_tracker2_data (self);
993     }
994 }
995