1 /*
2 * Copyright (C) 2010, 2011 Igalia S.L.
3 * Copyright (C) 2011 Intel Corporation.
4 *
5 * Contact: Iago Toral Quiroga <itoral@igalia.com>
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public License
9 * as published by the Free Software Foundation; version 2.1 of
10 * the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20 * 02110-1301 USA
21 *
22 */
23
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif
27
28 #include <grilo.h>
29 #include <gio/gio.h>
30 #include <glib/gi18n-lib.h>
31 #include <string.h>
32 #include <stdlib.h>
33 #include <pls/grl-pls.h>
34
35 #include "grl-filesystem.h"
36
37 /* --------- Logging -------- */
38
39 #define GRL_LOG_DOMAIN_DEFAULT filesystem_log_domain
40 GRL_LOG_DOMAIN_STATIC(filesystem_log_domain);
41
42 /* -------- File info ------- */
43
44 #define FILE_ATTRIBUTES_FAST \
45 G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN
46
47 /* ---- Emission chunks ----- */
48
49 #define BROWSE_IDLE_CHUNK_SIZE 5
50
51 /* ---- Default root ---- */
52
53 #define DEFAULT_ROOT "file:///"
54
55 /* --- Plugin information --- */
56
57 #define SOURCE_ID "grl-filesystem"
58 #define SOURCE_NAME _("Filesystem")
59 #define SOURCE_DESC _("A source for browsing the filesystem")
60
61 /* --- Grilo Filesystem Private --- */
62
63 struct _GrlFilesystemSourcePrivate {
64 GList *chosen_uris;
65 guint max_search_depth;
66 gboolean handle_pls;
67 /* a mapping operation_id -> GCancellable to cancel this operation */
68 GHashTable *cancellables;
69 /* URI -> GFileMonitor */
70 GHashTable *monitors;
71 GCancellable *cancellable_monitors;
72 };
73
74 /* --- Data types --- */
75
76 typedef struct _RecursiveOperation RecursiveOperation;
77
78 typedef gboolean (*RecursiveOperationCb) (GFileInfo *file_info,
79 RecursiveOperation *operation);
80
81 typedef struct {
82 GrlSourceBrowseSpec *spec;
83 GList *entries;
84 GList *current;
85 const gchar *uri;
86 guint remaining;
87 GCancellable *cancellable;
88 guint id;
89 } BrowseIdleData;
90
91 struct _RecursiveOperation {
92 RecursiveOperationCb on_cancel;
93 RecursiveOperationCb on_finish;
94 RecursiveOperationCb on_dir;
95 RecursiveOperationCb on_file;
96 gpointer on_dir_data;
97 gpointer on_file_data;
98 GCancellable *cancellable;
99 GQueue *directories;
100 guint max_depth;
101 };
102
103 typedef struct {
104 guint depth;
105 GFile *directory;
106 gboolean handle_pls;
107 } RecursiveEntry;
108
109
110 static GrlFilesystemSource *grl_filesystem_source_new (void);
111
112 static void grl_filesystem_source_finalize (GObject *object);
113
114 gboolean grl_filesystem_plugin_init (GrlRegistry *registry,
115 GrlPlugin *plugin,
116 GList *configs);
117
118 static const GList *grl_filesystem_source_supported_keys (GrlSource *source);
119
120 static GrlCaps *grl_filesystem_source_get_caps (GrlSource *source,
121 GrlSupportedOps operation);
122 static void grl_filesystem_source_resolve (GrlSource *source,
123 GrlSourceResolveSpec *rs);
124
125 static void grl_filesystem_source_browse (GrlSource *source,
126 GrlSourceBrowseSpec *bs);
127
128 static void grl_filesystem_source_search (GrlSource *source,
129 GrlSourceSearchSpec *ss);
130
131 static gboolean grl_filesystem_test_media_from_uri (GrlSource *source,
132 const gchar *uri);
133
134 static void grl_filesystem_get_media_from_uri (GrlSource *source,
135 GrlSourceMediaFromUriSpec *mfus);
136
137 static void grl_filesystem_source_cancel (GrlSource *source,
138 guint operation_id);
139
140 static gboolean grl_filesystem_source_notify_change_start (GrlSource *source,
141 GError **error);
142
143 static gboolean grl_filesystem_source_notify_change_stop (GrlSource *source,
144 GError **error);
145
146 /* =================== Filesystem Plugin =============== */
147
148 gboolean
grl_filesystem_plugin_init(GrlRegistry * registry,GrlPlugin * plugin,GList * configs)149 grl_filesystem_plugin_init (GrlRegistry *registry,
150 GrlPlugin *plugin,
151 GList *configs)
152 {
153 GrlFilesystemSource *source;
154 GList *chosen_uris = NULL;
155 guint max_search_depth = GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT;
156 gboolean handle_pls = FALSE;
157
158 GRL_LOG_DOMAIN_INIT (filesystem_log_domain, "filesystem");
159
160 GRL_DEBUG ("filesystem_plugin_init");
161
162 /* Initialize i18n */
163 bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
164 bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
165
166 source = grl_filesystem_source_new ();
167
168 for (; configs; configs = g_list_next (configs)) {
169 GrlConfig *config = configs->data;
170 gchar *uri;
171
172 uri = grl_config_get_string (config, GRILO_CONF_CHOSEN_URI);
173 if (uri) {
174 chosen_uris = g_list_prepend (chosen_uris, uri);
175 }
176 if (grl_config_has_param (config, GRILO_CONF_MAX_SEARCH_DEPTH)) {
177 max_search_depth = (guint)grl_config_get_int (config, GRILO_CONF_MAX_SEARCH_DEPTH);
178 }
179 if (grl_config_has_param (config, GRILO_CONF_HANDLE_PLS)) {
180 handle_pls = grl_config_get_boolean (config, GRILO_CONF_HANDLE_PLS);
181 }
182 }
183 source->priv->chosen_uris = g_list_reverse (chosen_uris);
184 source->priv->max_search_depth = max_search_depth;
185 source->priv->handle_pls = handle_pls;
186
187 grl_registry_register_source (registry,
188 plugin,
189 GRL_SOURCE (source),
190 NULL);
191
192 return TRUE;
193 }
194
195 GRL_PLUGIN_DEFINE (GRL_MAJOR,
196 GRL_MINOR,
197 FILESYSTEM_PLUGIN_ID,
198 "Filesystem",
199 "A plugin for browsing the filesystem",
200 "Igalia S.L.",
201 VERSION,
202 "LGPL-2.1-or-later",
203 "http://www.igalia.com",
204 grl_filesystem_plugin_init,
205 NULL,
206 NULL);
207
208 /* ================== Filesystem GObject ================ */
209
210
G_DEFINE_TYPE_WITH_PRIVATE(GrlFilesystemSource,grl_filesystem_source,GRL_TYPE_SOURCE)211 G_DEFINE_TYPE_WITH_PRIVATE (GrlFilesystemSource, grl_filesystem_source, GRL_TYPE_SOURCE)
212
213 static GrlFilesystemSource *
214 grl_filesystem_source_new (void)
215 {
216 GRL_DEBUG ("grl_filesystem_source_new");
217 return g_object_new (GRL_FILESYSTEM_SOURCE_TYPE,
218 "source-id", SOURCE_ID,
219 "source-name", SOURCE_NAME,
220 "source-desc", SOURCE_DESC,
221 NULL);
222 }
223
224 static void
grl_filesystem_source_class_init(GrlFilesystemSourceClass * klass)225 grl_filesystem_source_class_init (GrlFilesystemSourceClass * klass)
226 {
227 GObjectClass *g_class = G_OBJECT_CLASS (klass);
228 GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
229
230 g_class->finalize = grl_filesystem_source_finalize;
231
232 source_class->supported_keys = grl_filesystem_source_supported_keys;
233 source_class->cancel = grl_filesystem_source_cancel;
234 source_class->get_caps = grl_filesystem_source_get_caps;
235 source_class->browse = grl_filesystem_source_browse;
236 source_class->search = grl_filesystem_source_search;
237 source_class->notify_change_start = grl_filesystem_source_notify_change_start;
238 source_class->notify_change_stop = grl_filesystem_source_notify_change_stop;
239 source_class->resolve = grl_filesystem_source_resolve;
240 source_class->test_media_from_uri = grl_filesystem_test_media_from_uri;
241 source_class->media_from_uri = grl_filesystem_get_media_from_uri;
242 }
243
244 static void
grl_filesystem_source_init(GrlFilesystemSource * source)245 grl_filesystem_source_init (GrlFilesystemSource *source)
246 {
247 source->priv = grl_filesystem_source_get_instance_private (source);
248 source->priv->cancellables = g_hash_table_new (NULL, NULL);
249 source->priv->monitors = g_hash_table_new_full (g_str_hash, g_str_equal,
250 g_free, g_object_unref);
251 }
252
253 static void
grl_filesystem_source_finalize(GObject * object)254 grl_filesystem_source_finalize (GObject *object)
255 {
256 GrlFilesystemSource *filesystem_source = GRL_FILESYSTEM_SOURCE (object);
257 g_list_free_full (filesystem_source->priv->chosen_uris, g_free);
258 g_hash_table_unref (filesystem_source->priv->cancellables);
259 g_hash_table_unref (filesystem_source->priv->monitors);
260 G_OBJECT_CLASS (grl_filesystem_source_parent_class)->finalize (object);
261 }
262
263 /* ======================= Utilities ==================== */
264
265 static void recursive_operation_next_entry (RecursiveOperation *operation);
266 static void add_monitor (GrlFilesystemSource *fs_source, GFile *dir);
267 static void cancel_monitors (GrlFilesystemSource *fs_source);
268
269 static gboolean
mime_is_video(const gchar * mime)270 mime_is_video (const gchar *mime)
271 {
272 return g_str_has_prefix (mime, "video/");
273 }
274
275 static gboolean
mime_is_audio(const gchar * mime)276 mime_is_audio (const gchar *mime)
277 {
278 return g_str_has_prefix (mime, "audio/");
279 }
280
281 static gboolean
mime_is_image(const gchar * mime)282 mime_is_image (const gchar *mime)
283 {
284 return g_str_has_prefix (mime, "image/");
285 }
286
287 static gboolean
mime_is_media(const gchar * mime,GrlTypeFilter filter)288 mime_is_media (const gchar *mime, GrlTypeFilter filter)
289 {
290 if (!mime)
291 return FALSE;
292 if (!strcmp (mime, "inode/directory"))
293 return TRUE;
294 if (filter & GRL_TYPE_FILTER_AUDIO &&
295 mime_is_audio (mime))
296 return TRUE;
297 if (filter & GRL_TYPE_FILTER_VIDEO &&
298 mime_is_video (mime))
299 return TRUE;
300 if (filter & GRL_TYPE_FILTER_IMAGE &&
301 mime_is_image (mime))
302 return TRUE;
303 return FALSE;
304 }
305
306 static gboolean
file_is_valid_content(GFileInfo * info,gboolean fast,GrlOperationOptions * options)307 file_is_valid_content (GFileInfo *info, gboolean fast, GrlOperationOptions *options)
308 {
309 const gchar *mime;
310 const gchar *mime_filter = NULL;
311 GValue *mime_filter_value = NULL;
312 GValue *min_date_value = NULL;
313 GValue *max_date_value = NULL;
314 GDateTime *min_date = NULL;
315 GDateTime *max_date = NULL;
316 GDateTime *file_date = NULL;
317 GrlTypeFilter type_filter;
318 gboolean is_media = TRUE;
319 GFileType type;
320
321 /* Ignore hidden files */
322 if (g_file_info_get_is_hidden (info)) {
323 is_media = FALSE;
324 goto end;
325 }
326
327 type = g_file_info_get_file_type (info);
328
329 /* Directories are always accepted */
330 if (type == G_FILE_TYPE_DIRECTORY) {
331 goto end;
332 }
333
334 type_filter = options? grl_operation_options_get_type_filter (options): GRL_TYPE_FILTER_ALL;
335
336 /* In fast mode we do not check mime-types, any non-hidden file is accepted */
337 if (fast) {
338 if (type_filter == GRL_TYPE_FILTER_NONE) {
339 is_media = FALSE;
340 }
341 goto end;
342 }
343
344 /* Filter by type */
345 mime = g_file_info_get_content_type (info);
346 if (!mime_is_media (mime, type_filter)) {
347 is_media = FALSE;
348 goto end;
349 }
350
351 /* Filter by mime */
352 mime_filter_value =
353 options? grl_operation_options_get_key_filter (options,
354 GRL_METADATA_KEY_MIME): NULL;
355 if (mime_filter_value) {
356 mime_filter = g_value_get_string (mime_filter_value);
357 }
358
359 if (mime_filter && g_strcmp0 (mime, mime_filter) != 0) {
360 is_media = FALSE;
361 goto end;
362 }
363
364 /* Filter by date */
365 if (options) {
366 grl_operation_options_get_key_range_filter (options,
367 GRL_METADATA_KEY_MODIFICATION_DATE,
368 &min_date_value,
369 &max_date_value);
370 }
371
372 if (min_date_value) {
373 min_date = g_date_time_ref (g_value_get_boxed (min_date_value));
374 }
375 if (max_date_value) {
376 max_date = g_date_time_ref (g_value_get_boxed (max_date_value));
377 }
378
379 if (min_date || max_date) {
380 GTimeVal time = {0,};
381
382 g_file_info_get_modification_time (info, &time);
383 file_date = g_date_time_new_from_timeval_utc (&time);
384 }
385
386 if (min_date && file_date && g_date_time_compare (min_date, file_date) > 0) {
387 is_media = FALSE;
388 goto end;
389 }
390
391 if (max_date && file_date && g_date_time_compare (max_date, file_date) < 0) {
392 is_media = FALSE;
393 goto end;
394 }
395
396 end:
397 g_clear_pointer (&file_date, g_date_time_unref);
398 g_clear_pointer (&min_date, g_date_time_unref);
399 g_clear_pointer (&max_date, g_date_time_unref);
400
401 return is_media;
402 }
403
404 static gboolean
browse_emit_idle(gpointer user_data)405 browse_emit_idle (gpointer user_data)
406 {
407 BrowseIdleData *idle_data;
408 guint count;
409 GrlFilesystemSource *fs_source;
410
411 GRL_DEBUG ("browse_emit_idle");
412
413 idle_data = (BrowseIdleData *) user_data;
414 fs_source = GRL_FILESYSTEM_SOURCE (idle_data->spec->source);
415
416 if (g_cancellable_is_cancelled (idle_data->cancellable)) {
417 GRL_DEBUG ("Browse operation %d (\"%s\") has been cancelled",
418 idle_data->id, idle_data->uri);
419 idle_data->spec->callback(idle_data->spec->source,
420 idle_data->id, NULL, 0,
421 idle_data->spec->user_data, NULL);
422 goto finish;
423 }
424
425 count = 0;
426 do {
427 gchar *uri;
428 GrlMedia *content;
429 GFile *file;
430 GrlOperationOptions *options = idle_data->spec->options;
431
432 uri = (gchar *) idle_data->current->data;
433 file = g_file_new_for_uri (uri);
434
435 content = grl_pls_file_to_media (NULL,
436 file,
437 NULL,
438 fs_source->priv->handle_pls,
439 options);
440 g_object_unref (file);
441
442 idle_data->spec->callback (idle_data->spec->source,
443 idle_data->spec->operation_id,
444 content,
445 idle_data->remaining--,
446 idle_data->spec->user_data,
447 NULL);
448
449 idle_data->current = g_list_next (idle_data->current);
450 count++;
451 } while (count < BROWSE_IDLE_CHUNK_SIZE && idle_data->current);
452
453 if (!idle_data->current)
454 goto finish;
455
456 return TRUE;
457
458 finish:
459 g_list_free_full (idle_data->entries, g_free);
460 g_hash_table_remove (fs_source->priv->cancellables,
461 GUINT_TO_POINTER (idle_data->id));
462 g_object_unref (idle_data->cancellable);
463 g_slice_free (BrowseIdleData, idle_data);
464 return FALSE;
465 }
466
467 static void
produce_from_uri(GrlSourceBrowseSpec * bs,const gchar * uri,GrlOperationOptions * options)468 produce_from_uri (GrlSourceBrowseSpec *bs, const gchar *uri, GrlOperationOptions *options)
469 {
470 GFile *file;
471 GFileEnumerator *e;
472 GFileInfo *info;
473 GError *error = NULL;
474 guint skip, count;
475 GList *entries = NULL;
476 GList *iter;
477
478 /* Open directory */
479 GRL_DEBUG ("Opening directory '%s'", uri);
480 file = g_file_new_for_uri (uri);
481 e = g_file_enumerate_children (file,
482 grl_pls_get_file_attributes (),
483 G_FILE_QUERY_INFO_NONE,
484 NULL,
485 &error);
486
487 if (!e) {
488 GRL_DEBUG ("Failed to open directory '%s': %s", uri, error->message);
489 bs->callback (bs->source, bs->operation_id, NULL, 0, bs->user_data, error);
490 g_error_free (error);
491 g_object_unref (file);
492 return;
493 }
494
495 /* Filter out media and directories */
496 while ((info = g_file_enumerator_next_file (e, NULL, NULL)) != NULL) {
497 if (file_is_valid_content (info, FALSE, options)) {
498 GFile *entry;
499 entry = g_file_get_child (file, g_file_info_get_name (info));
500 entries = g_list_prepend (entries, g_file_get_uri (entry));
501 g_object_unref (entry);
502 }
503 g_object_unref (info);
504 }
505
506 g_object_unref (e);
507 g_object_unref (file);
508
509 /* Apply skip and count */
510 skip = grl_operation_options_get_skip (bs->options);
511 count = grl_operation_options_get_count (bs->options);
512 iter = entries;
513 while (iter) {
514 gboolean remove;
515 GList *tmp;
516 if (skip > 0) {
517 skip--;
518 remove = TRUE;
519 } else if (count > 0) {
520 count--;
521 remove = FALSE;
522 } else {
523 remove = TRUE;
524 }
525 if (remove) {
526 tmp = iter;
527 iter = g_list_next (iter);
528 g_free (tmp->data);
529 entries = g_list_delete_link (entries, tmp);
530 } else {
531 iter = g_list_next (iter);
532 }
533 }
534
535 /* Emit results */
536 if (entries) {
537 guint id;
538
539 /* Use the idle loop to avoid blocking for too long */
540 BrowseIdleData *idle_data = g_slice_new (BrowseIdleData);
541 gint global_count = grl_operation_options_get_count (bs->options);
542 idle_data->spec = bs;
543 idle_data->remaining = global_count - count - 1;
544 idle_data->uri = uri;
545 idle_data->entries = entries;
546 idle_data->current = entries;
547 idle_data->cancellable = g_cancellable_new ();
548 idle_data->id = bs->operation_id;
549 g_hash_table_insert (GRL_FILESYSTEM_SOURCE (bs->source)->priv->cancellables,
550 GUINT_TO_POINTER (bs->operation_id),
551 idle_data->cancellable);
552
553 id = g_idle_add (browse_emit_idle, idle_data);
554 g_source_set_name_by_id (id, "[filesystem] browse_emit_idle");
555 } else {
556 /* No results */
557 bs->callback (bs->source,
558 bs->operation_id,
559 NULL,
560 0,
561 bs->user_data,
562 NULL);
563 }
564 }
565
566 static RecursiveEntry *
recursive_entry_new(guint depth,GFile * directory)567 recursive_entry_new (guint depth, GFile *directory)
568 {
569 RecursiveEntry *entry;
570
571 entry = g_slice_new(RecursiveEntry);
572 entry->depth = depth;
573 entry->directory = g_object_ref (directory);
574
575 return entry;
576 }
577
578 static void
recursive_entry_free(RecursiveEntry * entry)579 recursive_entry_free (RecursiveEntry *entry)
580 {
581 g_object_unref (entry->directory);
582 g_slice_free (RecursiveEntry, entry);
583 }
584
585 static RecursiveOperation *
recursive_operation_new(void)586 recursive_operation_new (void)
587 {
588 RecursiveOperation *operation;
589
590 operation = g_slice_new0 (RecursiveOperation);
591 operation->directories = g_queue_new ();
592 operation->cancellable = g_cancellable_new ();
593
594 return operation;
595 }
596
597 static void
recursive_operation_free(RecursiveOperation * operation)598 recursive_operation_free (RecursiveOperation *operation)
599 {
600 g_queue_foreach (operation->directories, (GFunc) recursive_entry_free, NULL);
601 g_queue_free (operation->directories);
602 g_object_unref (operation->cancellable);
603 g_slice_free (RecursiveOperation, operation);
604 }
605
606 static void
recursive_operation_got_file(GFileEnumerator * enumerator,GAsyncResult * res,RecursiveOperation * operation)607 recursive_operation_got_file (GFileEnumerator *enumerator, GAsyncResult *res, RecursiveOperation *operation)
608 {
609 GList *files;
610 GError *error = NULL;
611 gboolean continue_operation = TRUE;
612
613 GRL_DEBUG (__func__);
614
615 files = g_file_enumerator_next_files_finish (enumerator, res, &error);
616 if (error) {
617 if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
618 GRL_WARNING ("Got error: %s", error->message);
619 g_error_free (error);
620 goto finished;
621 }
622
623 if (files) {
624 GFileInfo *file_info;
625 RecursiveEntry *entry;
626
627 /* we assume there is only one GFileInfo in the list since that's what we ask
628 * for when calling g_file_enumerator_next_files_async() */
629 file_info = (GFileInfo *)files->data;
630 g_list_free (files);
631 /* Get the entry we are running now */
632 entry = g_queue_peek_head (operation->directories);
633 switch (g_file_info_get_file_type (file_info)) {
634 case G_FILE_TYPE_SYMBOLIC_LINK:
635 /* we're too afraid of infinite recursion to touch this for now */
636 break;
637 case G_FILE_TYPE_DIRECTORY:
638 {
639 if (entry->depth < operation->max_depth) {
640 GFile *subdir;
641 RecursiveEntry *subentry;
642
643 if (operation->on_dir) {
644 continue_operation = operation->on_dir(file_info, operation);
645 }
646
647 if (continue_operation) {
648 subdir = g_file_get_child (entry->directory,
649 g_file_info_get_name (file_info));
650 subentry = recursive_entry_new (entry->depth + 1, subdir);
651 g_queue_push_tail (operation->directories, subentry);
652 g_object_unref (subdir);
653 } else {
654 g_object_unref (file_info);
655 goto finished;
656 }
657 }
658 }
659 break;
660 case G_FILE_TYPE_REGULAR:
661 if (operation->on_file) {
662 continue_operation = operation->on_file(file_info, operation);
663 if (!continue_operation) {
664 g_object_unref (file_info);
665 goto finished;
666 }
667 }
668 break;
669 default:
670 /* this file is a weirdo, we ignore it */
671 break;
672 }
673 g_object_unref (file_info);
674 } else { /* end of enumerator */
675 goto finished;
676 }
677
678 g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
679 operation->cancellable,
680 (GAsyncReadyCallback)recursive_operation_got_file,
681 operation);
682
683 return;
684
685 finished:
686 /* we're done with this dir/enumerator, let's treat the next one */
687 g_object_unref (enumerator);
688 recursive_entry_free (g_queue_pop_head (operation->directories));
689 if (continue_operation) {
690 recursive_operation_next_entry (operation);
691 } else {
692 recursive_operation_free (operation);
693 }
694 }
695
696 static void
recursive_operation_got_entry(GFile * directory,GAsyncResult * res,RecursiveOperation * operation)697 recursive_operation_got_entry (GFile *directory, GAsyncResult *res, RecursiveOperation *operation)
698 {
699 GError *error = NULL;
700 GFileEnumerator *enumerator;
701
702 GRL_DEBUG (__func__);
703
704 enumerator = g_file_enumerate_children_finish (directory, res, &error);
705 if (error) {
706 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
707 g_error_free (error);
708 return;
709 }
710 GRL_WARNING ("Got error: %s", error->message);
711 g_error_free (error);
712 /* we couldn't get the children of this directory, but we probably have
713 * other directories to try */
714 recursive_entry_free (g_queue_pop_head (operation->directories));
715 recursive_operation_next_entry (operation);
716 return;
717 }
718
719 g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
720 operation->cancellable,
721 (GAsyncReadyCallback)recursive_operation_got_file,
722 operation);
723 }
724
725 static void
recursive_operation_next_entry(RecursiveOperation * operation)726 recursive_operation_next_entry (RecursiveOperation *operation)
727 {
728 RecursiveEntry *entry;
729
730 GRL_DEBUG (__func__);
731
732 if (g_cancellable_is_cancelled (operation->cancellable)) {
733 /* We've been cancelled! */
734 GRL_DEBUG ("Operation has been cancelled");
735 if (operation->on_cancel) {
736 operation->on_cancel(NULL, operation);
737 }
738 goto finished;
739 }
740
741 entry = g_queue_peek_head (operation->directories);
742 if (!entry) { /* We've crawled everything, before reaching count */
743 if (operation->on_finish) {
744 operation->on_finish (NULL, operation);
745 }
746 goto finished;
747 }
748
749 g_file_enumerate_children_async (entry->directory, G_FILE_ATTRIBUTE_STANDARD_TYPE ","
750 G_FILE_ATTRIBUTE_STANDARD_NAME ","
751 G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
752 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
753 G_PRIORITY_DEFAULT,
754 operation->cancellable,
755 (GAsyncReadyCallback)recursive_operation_got_entry,
756 operation);
757
758 return;
759
760 finished:
761 recursive_operation_free (operation);
762 }
763
764 static void
recursive_operation_initialize(RecursiveOperation * operation,GrlFilesystemSource * source)765 recursive_operation_initialize (RecursiveOperation *operation, GrlFilesystemSource *source)
766 {
767 GList *chosen_uris, *uri;
768
769 chosen_uris = source->priv->chosen_uris;
770 if (chosen_uris) {
771 for (uri = chosen_uris; uri; uri = g_list_next (uri)) {
772 GFile *directory = g_file_new_for_uri (uri->data);
773 g_queue_push_tail (operation->directories,
774 recursive_entry_new (0, directory));
775 add_monitor (source, directory);
776 g_object_unref (directory);
777 }
778 } else {
779 const gchar *home;
780 GFile *directory;
781
782 /* This is necessary for GLIB < 2.36 */
783 home = g_getenv ("HOME");
784 if (!home)
785 home = g_get_home_dir ();
786 directory = g_file_new_for_path (home);
787 g_queue_push_tail (operation->directories,
788 recursive_entry_new (0, directory));
789 add_monitor (source, directory);
790 g_object_unref (directory);
791 }
792 }
793
794 static gboolean
cancel_cb(GFileInfo * file_info,RecursiveOperation * operation)795 cancel_cb (GFileInfo *file_info, RecursiveOperation *operation)
796 {
797 GrlFilesystemSource *fs_source;
798
799 if (operation->on_file_data) {
800 GrlSourceSearchSpec *ss =
801 (GrlSourceSearchSpec *) operation->on_file_data;
802 fs_source = GRL_FILESYSTEM_SOURCE (ss->source);
803 g_hash_table_remove (fs_source->priv->cancellables,
804 GUINT_TO_POINTER (ss->operation_id));
805 ss->callback (ss->source, ss->operation_id, NULL, 0, ss->user_data, NULL);
806 }
807
808 if (operation->on_dir_data) {
809 /* Remove all monitors */
810 fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
811 cancel_monitors (fs_source);
812 }
813 return FALSE;
814 }
815
816 static gboolean
finish_cb(GFileInfo * file_info,RecursiveOperation * operation)817 finish_cb (GFileInfo *file_info, RecursiveOperation *operation)
818 {
819 if (operation->on_file_data) {
820 GrlSourceSearchSpec *ss =
821 (GrlSourceSearchSpec *) operation->on_file_data;
822 g_hash_table_remove (GRL_FILESYSTEM_SOURCE (ss->source)->priv->cancellables,
823 GUINT_TO_POINTER (ss->operation_id));
824 ss->callback (ss->source, ss->operation_id, NULL, 0, ss->user_data, NULL);
825 }
826
827 if (operation->on_dir_data) {
828 GRL_FILESYSTEM_SOURCE (operation->on_dir_data)->priv->cancellable_monitors = NULL;
829 }
830
831 return FALSE;
832 }
833
834 /* return TRUE if more files need to be returned, FALSE if we sent the count */
835 static gboolean
file_cb(GFileInfo * file_info,RecursiveOperation * operation)836 file_cb (GFileInfo *file_info, RecursiveOperation *operation)
837 {
838 gchar *needle = NULL;
839 gchar *haystack = NULL;
840 gchar *normalized_needle = NULL;
841 gchar *normalized_haystack = NULL;
842 GrlSourceSearchSpec *ss = operation->on_file_data;
843 gint remaining = -1;
844
845 GRL_DEBUG (__func__);
846
847 if (ss == NULL)
848 return FALSE;
849
850 if (ss->text) {
851 haystack = g_utf8_casefold (g_file_info_get_display_name (file_info), -1);
852 normalized_haystack = g_utf8_normalize (haystack, -1, G_NORMALIZE_ALL);
853
854 needle = g_utf8_casefold (ss->text, -1);
855 normalized_needle = g_utf8_normalize (needle, -1, G_NORMALIZE_ALL);
856 }
857
858 if (!ss->text ||
859 strstr (normalized_haystack, normalized_needle)) {
860 GrlMedia *media = NULL;
861 RecursiveEntry *entry;
862 GFile *file;
863 GFileInfo *info;
864
865 entry = g_queue_peek_head (operation->directories);
866 file = g_file_get_child (entry->directory,
867 g_file_info_get_name (file_info));
868
869 /* FIXME This is likely to block */
870 info = g_file_query_info (file, grl_pls_get_file_attributes (), G_FILE_QUERY_INFO_NONE, NULL, NULL);
871
872 if (file_is_valid_content (info, FALSE, ss->options)) {
873 guint skip = grl_operation_options_get_skip (ss->options);
874 if (skip) {
875 grl_operation_options_set_skip (ss->options, skip - 1);
876 } else {
877 gboolean handle_pls;
878 handle_pls = GRL_FILESYSTEM_SOURCE(ss->source)->priv->handle_pls;
879 media = grl_pls_file_to_media (NULL, file, info, handle_pls, ss->options);
880 }
881 }
882
883 g_object_unref (info);
884 g_object_unref (file);
885
886 if (media) {
887 gint count = grl_operation_options_get_count (ss->options);
888 count--;
889 grl_operation_options_set_count (ss->options, count);
890 if (count == 0) {
891 remaining = 0;
892 }
893 ss->callback (ss->source, ss->operation_id, media, remaining, ss->user_data, NULL);
894 }
895 }
896
897 g_free (haystack);
898 g_free (normalized_haystack);
899 g_free (needle);
900 g_free (normalized_needle);
901 return remaining == -1;
902 }
903
904 static void
notify_change(GrlSource * source,GFile * file,GrlSourceChangeType change)905 notify_change (GrlSource *source, GFile *file, GrlSourceChangeType change)
906 {
907 GrlMedia *media;
908 GrlOperationOptions *options;
909 GrlFilesystemSource *fs_source;
910
911 fs_source = GRL_FILESYSTEM_SOURCE (source);
912 options = grl_operation_options_new (NULL);
913 grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_FAST_ONLY);
914
915 media = grl_pls_file_to_media (NULL, file, NULL, fs_source->priv->handle_pls, options);
916 grl_source_notify_change (source, media, change, FALSE);
917 g_object_unref (media);
918 g_object_unref (options);
919 }
920
921 static void
directory_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event,gpointer data)922 directory_changed (GFileMonitor *monitor,
923 GFile *file,
924 GFile *other_file,
925 GFileMonitorEvent event,
926 gpointer data)
927 {
928 GrlSource *source = GRL_SOURCE (data);
929 GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (data);
930 GFileInfo *info = NULL;
931
932 /* Keep only signals we are interested in */
933 if (event != G_FILE_MONITOR_EVENT_CREATED &&
934 event != G_FILE_MONITOR_EVENT_CHANGED &&
935 event != G_FILE_MONITOR_EVENT_MOVED &&
936 event != G_FILE_MONITOR_EVENT_DELETED)
937 return;
938
939 /* File DELETED */
940 if (event == G_FILE_MONITOR_EVENT_DELETED) {
941 gchar *uri;
942
943 /* Avoid duplicated notification when a directory being monitored is
944 * deleted. The signal will be emitted by the monitor tracking its parent.
945 */
946 uri = g_file_get_uri (file);
947 if (g_hash_table_lookup (fs_source->priv->monitors, uri) != monitor)
948 notify_change (source, file, GRL_CONTENT_REMOVED);
949 g_free (uri);
950
951 goto out;
952 }
953
954 /* Query the file and leave if we are not interested in it */
955 info = g_file_query_info (file,
956 grl_pls_get_file_attributes (),
957 G_FILE_QUERY_INFO_NONE,
958 NULL, NULL);
959 if (!info || !file_is_valid_content (info, TRUE, NULL))
960 goto out;
961
962 /* File CHANGED */
963 if (event == G_FILE_MONITOR_EVENT_CHANGED) {
964 notify_change (source, file, GRL_CONTENT_CHANGED);
965 goto out;
966 }
967
968 /* File CREATED */
969 if (event == G_FILE_MONITOR_EVENT_CREATED) {
970 notify_change (source, file, GRL_CONTENT_ADDED);
971 if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
972 add_monitor (GRL_FILESYSTEM_SOURCE (source), file);
973 goto out;
974 }
975
976 /* File MOVED */
977 if (event == G_FILE_MONITOR_EVENT_MOVED) {
978 notify_change (source, file, GRL_CONTENT_REMOVED);
979 notify_change (source, other_file, GRL_CONTENT_ADDED);
980 goto out;
981 }
982
983 out:
984 g_clear_object (&info);
985 }
986
987 static void
cancel_monitors(GrlFilesystemSource * fs_source)988 cancel_monitors (GrlFilesystemSource *fs_source)
989 {
990 /* That table holds the only ref to our GFileMonitor, and dispose will
991 * cancel them. */
992 g_hash_table_remove_all (fs_source->priv->monitors);
993 }
994
995 static void
add_monitor(GrlFilesystemSource * fs_source,GFile * dir)996 add_monitor (GrlFilesystemSource *fs_source, GFile *dir)
997 {
998 GFileMonitor *monitor;
999 gchar *uri;
1000
1001 uri = g_file_get_uri (dir);
1002 if (g_hash_table_contains (fs_source->priv->monitors, uri))
1003 goto out;
1004
1005 monitor = g_file_monitor_directory (dir, G_FILE_MONITOR_SEND_MOVED, NULL, NULL);
1006 if (!monitor) {
1007 GRL_DEBUG ("Unable to set up monitor in %s\n", uri);
1008 goto out;
1009 }
1010
1011 /* transfer ownership of uri and monitor */
1012 g_hash_table_insert (fs_source->priv->monitors, uri, monitor);
1013 g_signal_connect (monitor,
1014 "changed",
1015 G_CALLBACK (directory_changed),
1016 fs_source);
1017 uri = NULL;
1018
1019 out:
1020 g_free (uri);
1021 }
1022
1023 static gboolean
directory_cb(GFileInfo * dir_info,RecursiveOperation * operation)1024 directory_cb (GFileInfo *dir_info, RecursiveOperation *operation)
1025 {
1026 RecursiveEntry *entry;
1027 GFile *dir;
1028 GrlFilesystemSource *fs_source;
1029
1030 fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
1031 entry = g_queue_peek_head (operation->directories);
1032 dir = g_file_get_child (entry->directory,
1033 g_file_info_get_name (dir_info));
1034
1035 add_monitor (fs_source, dir);
1036 g_object_unref (dir);
1037
1038 return TRUE;
1039 }
1040
1041 /* ================== API Implementation ================ */
1042
1043 static const GList *
grl_filesystem_source_supported_keys(GrlSource * source)1044 grl_filesystem_source_supported_keys (GrlSource *source)
1045 {
1046 static GList *keys = NULL;
1047 if (!keys) {
1048 keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
1049 GRL_METADATA_KEY_TITLE,
1050 GRL_METADATA_KEY_URL,
1051 GRL_METADATA_KEY_MIME,
1052 GRL_METADATA_KEY_MODIFICATION_DATE,
1053 GRL_METADATA_KEY_CHILDCOUNT,
1054 NULL);
1055 }
1056 return keys;
1057 }
1058
1059 static void
grl_filesystem_source_browse(GrlSource * source,GrlSourceBrowseSpec * bs)1060 grl_filesystem_source_browse (GrlSource *source,
1061 GrlSourceBrowseSpec *bs)
1062 {
1063 const gchar *id;
1064 GList *chosen_uris;
1065
1066 GRL_DEBUG (__FUNCTION__);
1067
1068 if (grl_pls_media_is_playlist (bs->container)) {
1069 grl_pls_browse_by_spec (source, NULL, bs);
1070 return;
1071 }
1072
1073 id = grl_media_get_id (bs->container);
1074 chosen_uris = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_uris;
1075 if (!id && chosen_uris) {
1076 guint remaining = g_list_length (chosen_uris);
1077
1078 if (remaining == 1) {
1079 produce_from_uri (bs, chosen_uris->data, bs->options);
1080 } else {
1081 for (; chosen_uris; chosen_uris = g_list_next (chosen_uris)) {
1082 GrlMedia *content;
1083 GFile *file;
1084
1085 file = g_file_new_for_uri ((gchar *) chosen_uris->data);
1086 content = grl_pls_file_to_media (NULL,
1087 file,
1088 NULL,
1089 GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls,
1090 bs->options);
1091 g_object_unref (file);
1092
1093 if (content) {
1094 bs->callback (source,
1095 bs->operation_id,
1096 content,
1097 --remaining,
1098 bs->user_data,
1099 NULL);
1100 }
1101 }
1102 }
1103 } else {
1104 produce_from_uri (bs, id ? id : DEFAULT_ROOT, bs->options);
1105 }
1106 }
1107
grl_filesystem_source_search(GrlSource * source,GrlSourceSearchSpec * ss)1108 static void grl_filesystem_source_search (GrlSource *source,
1109 GrlSourceSearchSpec *ss)
1110 {
1111 RecursiveOperation *operation;
1112 GrlFilesystemSource *fs_source;
1113
1114 GRL_DEBUG (__FUNCTION__);
1115
1116 fs_source = GRL_FILESYSTEM_SOURCE (source);
1117
1118 operation = recursive_operation_new ();
1119 operation->on_cancel = cancel_cb;
1120 operation->on_finish = finish_cb;
1121 operation->on_file = file_cb;
1122 operation->on_file_data = ss;
1123 operation->max_depth = fs_source->priv->max_search_depth;
1124 g_hash_table_insert (GRL_FILESYSTEM_SOURCE (source)->priv->cancellables,
1125 GUINT_TO_POINTER (ss->operation_id),
1126 operation->cancellable);
1127
1128 recursive_operation_initialize (operation, fs_source);
1129 recursive_operation_next_entry (operation);
1130 }
1131
1132 static void
grl_filesystem_source_resolve(GrlSource * source,GrlSourceResolveSpec * rs)1133 grl_filesystem_source_resolve (GrlSource *source,
1134 GrlSourceResolveSpec *rs)
1135 {
1136 GFile *file;
1137 const gchar *id;
1138 GFileInfo *info;
1139 GList *chosen_uris;
1140 GError *error = NULL;
1141
1142 GRL_DEBUG (__FUNCTION__);
1143
1144 id = grl_media_get_id (rs->media);
1145 chosen_uris = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_uris;
1146
1147 if (!id && chosen_uris) {
1148 guint len;
1149
1150 len = g_list_length (chosen_uris);
1151 if (len == 1) {
1152 file = g_file_new_for_uri (chosen_uris->data);
1153 } else {
1154 grl_media_set_title (rs->media, SOURCE_NAME);
1155 grl_media_set_childcount (rs->media, len);
1156 rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
1157 return;
1158 }
1159 } else {
1160 file = g_file_new_for_uri (id ? id : DEFAULT_ROOT);
1161 }
1162
1163 info = g_file_query_info (file, "", G_FILE_QUERY_INFO_NONE, NULL, &error);
1164 if (info != NULL) {
1165 grl_pls_file_to_media (rs->media, file, NULL, GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls, rs->options);
1166 rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
1167 g_object_unref (info);
1168 } else {
1169 GError *error_new = g_error_new (error->domain,
1170 error->code,
1171 _("File %s does not exist"),
1172 id);
1173 rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error_new);
1174 g_error_free (error_new);
1175 g_error_free (error);
1176 }
1177
1178 g_object_unref (file);
1179 }
1180
1181 static gboolean
is_supported_scheme(const char * scheme)1182 is_supported_scheme (const char *scheme)
1183 {
1184 GVfs *vfs;
1185 const gchar * const * schemes;
1186 guint i;
1187
1188 if (g_strcmp0(scheme, "file") == 0)
1189 return TRUE;
1190
1191 vfs = g_vfs_get_default ();
1192 schemes = g_vfs_get_supported_uri_schemes (vfs);
1193 for (i = 0; schemes[i] != NULL; i++) {
1194 if (strcmp (schemes[i], scheme) == 0)
1195 return TRUE;
1196 }
1197
1198 return FALSE;
1199 }
1200
1201 static gboolean
grl_filesystem_test_media_from_uri(GrlSource * source,const gchar * uri)1202 grl_filesystem_test_media_from_uri (GrlSource *source,
1203 const gchar *uri)
1204 {
1205 GFile *file;
1206 GFileInfo *info;
1207 gchar *scheme;
1208 gboolean ret = FALSE;
1209
1210 GRL_DEBUG (__FUNCTION__);
1211
1212 scheme = g_uri_parse_scheme (uri);
1213 ret = is_supported_scheme (scheme);
1214 g_free (scheme);
1215 if (!ret)
1216 return ret;
1217
1218 file = g_file_new_for_uri (uri);
1219 info = g_file_query_info (file, FILE_ATTRIBUTES_FAST, G_FILE_QUERY_INFO_NONE, NULL, NULL);
1220 g_object_unref (file);
1221
1222 if (!info)
1223 return FALSE;
1224
1225 ret = file_is_valid_content (info, TRUE, NULL);
1226
1227 g_object_unref (info);
1228
1229 return ret;
1230 }
1231
grl_filesystem_get_media_from_uri(GrlSource * source,GrlSourceMediaFromUriSpec * mfus)1232 static void grl_filesystem_get_media_from_uri (GrlSource *source,
1233 GrlSourceMediaFromUriSpec *mfus)
1234 {
1235 gchar *scheme;
1236 GError *error = NULL;
1237 gboolean ret = FALSE;
1238 GrlMedia *media;
1239 GFile *file;
1240
1241 GRL_DEBUG (__FUNCTION__);
1242
1243 scheme = g_uri_parse_scheme (mfus->uri);
1244 ret = is_supported_scheme (scheme);
1245 g_free (scheme);
1246 if (!ret) {
1247 error = g_error_new (GRL_CORE_ERROR,
1248 GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
1249 _("Cannot get media from %s"), mfus->uri);
1250 mfus->callback (source, mfus->operation_id, NULL, mfus->user_data, error);
1251 g_clear_error (&error);
1252 return;
1253 }
1254
1255 /* FIXME: this is a blocking call, not sure we want that in here */
1256 /* Note: we assume grl_pls_file_to_media() never returns NULL, which seems to be true */
1257 file = g_file_new_for_uri (mfus->uri);
1258 media = grl_pls_file_to_media (NULL, file, NULL, GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls, mfus->options);
1259 g_object_unref (file);
1260 mfus->callback (source, mfus->operation_id, media, mfus->user_data, NULL);
1261 }
1262
1263 static void
grl_filesystem_source_cancel(GrlSource * source,guint operation_id)1264 grl_filesystem_source_cancel (GrlSource *source, guint operation_id)
1265 {
1266 GCancellable *cancellable;
1267 GrlFilesystemSourcePrivate *priv;
1268
1269 priv = GRL_FILESYSTEM_SOURCE (source)->priv;
1270
1271 cancellable =
1272 G_CANCELLABLE (g_hash_table_lookup (priv->cancellables,
1273 GUINT_TO_POINTER (operation_id)));
1274 if (cancellable)
1275 g_cancellable_cancel (cancellable);
1276 }
1277
1278 static gboolean
grl_filesystem_source_notify_change_start(GrlSource * source,GError ** error)1279 grl_filesystem_source_notify_change_start (GrlSource *source,
1280 GError **error)
1281 {
1282 GrlFilesystemSource *fs_source;
1283 RecursiveOperation *operation;
1284
1285 GRL_DEBUG (__func__);
1286
1287 fs_source = GRL_FILESYSTEM_SOURCE (source);
1288 operation = recursive_operation_new ();
1289 operation->on_cancel = cancel_cb;
1290 operation->on_finish = finish_cb;
1291 operation->on_dir = directory_cb;
1292 operation->on_dir_data = fs_source;
1293 operation->max_depth = fs_source->priv->max_search_depth;
1294
1295 fs_source->priv->cancellable_monitors = operation->cancellable;
1296
1297 recursive_operation_initialize (operation, fs_source);
1298 recursive_operation_next_entry (operation);
1299
1300 return TRUE;
1301 }
1302
1303 static gboolean
grl_filesystem_source_notify_change_stop(GrlSource * source,GError ** error)1304 grl_filesystem_source_notify_change_stop (GrlSource *source,
1305 GError **error)
1306 {
1307 GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (source);
1308
1309 /* Check if notifying is being initialized */
1310 if (fs_source->priv->cancellable_monitors) {
1311 g_cancellable_cancel (fs_source->priv->cancellable_monitors);
1312 fs_source->priv->cancellable_monitors = NULL;
1313 } else {
1314 /* Cancel and remove all monitors */
1315 cancel_monitors (fs_source);
1316 }
1317
1318 return TRUE;
1319 }
1320
1321 static GrlCaps *
grl_filesystem_source_get_caps(GrlSource * source,GrlSupportedOps operation)1322 grl_filesystem_source_get_caps (GrlSource *source,
1323 GrlSupportedOps operation)
1324 {
1325 GList *keys;
1326 static GrlCaps *caps = NULL;
1327
1328 if (caps == NULL) {
1329 caps = grl_caps_new ();
1330 grl_caps_set_type_filter (caps, GRL_TYPE_FILTER_ALL);
1331 keys = grl_metadata_key_list_new (GRL_METADATA_KEY_MIME,
1332 NULL);
1333 grl_caps_set_key_filter (caps, keys);
1334 g_list_free (keys);
1335 keys = grl_metadata_key_list_new (GRL_METADATA_KEY_MODIFICATION_DATE,
1336 NULL);
1337 grl_caps_set_key_range_filter (caps, keys);
1338 g_list_free (keys);
1339 }
1340
1341 return caps;
1342 }
1343