[gvfs/wip/rishi/goa: 2/4] Add GVfsBackendGoogle



commit 053490fcc48f3d05251d53721cd81695316575ab
Author: Debarshi Ray <debarshir gnome org>
Date:   Wed Oct 22 13:53:23 2014 +0200

    Add GVfsBackendGoogle
    
    Based on code written by Thibault Saunier.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=739008

 client/Makefile.am         |    1 +
 client/gdaemonvfs.c        |    2 +
 client/googleuri.c         |  194 ++++++
 configure.ac               |   23 +
 daemon/Makefile.am         |   20 +
 daemon/google.mount.in     |    4 +
 daemon/gvfsbackendgoogle.c | 1644 ++++++++++++++++++++++++++++++++++++++++++++
 daemon/gvfsbackendgoogle.h |   62 ++
 8 files changed, 1950 insertions(+), 0 deletions(-)
---
diff --git a/client/Makefile.am b/client/Makefile.am
index 8c7b76a..c9aa1f6 100644
--- a/client/Makefile.am
+++ b/client/Makefile.am
@@ -22,6 +22,7 @@ gvfsclientinclude_HEADERS = \
 
 URI_PARSER_SOURCES = \
        smburi.c \
+       googleuri.c \
        httpuri.c \
        afpuri.c \
        $(NULL)
diff --git a/client/gdaemonvfs.c b/client/gdaemonvfs.c
index 11435e8..758fd98 100644
--- a/client/gdaemonvfs.c
+++ b/client/gdaemonvfs.c
@@ -1488,6 +1488,7 @@ g_daemon_vfs_class_init (GDaemonVfsClass *class)
 /* Module API */
 
 void g_vfs_uri_mapper_smb_register (GIOModule *module);
+void g_vfs_uri_mapper_google_register (GIOModule *module);
 void g_vfs_uri_mapper_http_register (GIOModule *module);
 void g_vfs_uri_mapper_afp_register (GIOModule *module);
 
@@ -1523,6 +1524,7 @@ g_io_module_load (GIOModule *module)
   
   g_vfs_uri_mapper_register (module);
   g_vfs_uri_mapper_smb_register (module);
+  g_vfs_uri_mapper_google_register (module);
   g_vfs_uri_mapper_http_register (module);
   g_vfs_uri_mapper_afp_register (module);
 }
diff --git a/client/googleuri.c b/client/googleuri.c
new file mode 100644
index 0000000..3b6b640
--- /dev/null
+++ b/client/googleuri.c
@@ -0,0 +1,194 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* gvfs - extensions for gio
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * Author: Debarshi Ray <debarshir gnome org>
+ */
+
+#include <config.h>
+
+#include <gio/gio.h>
+#include <gvfsurimapper.h>
+#include <gvfsuriutils.h>
+
+typedef struct _GVfsUriMapperGoogle GVfsUriMapperGoogle;
+typedef struct _GVfsUriMapperGoogleClass GVfsUriMapperGoogleClass;
+
+struct _GVfsUriMapperGoogle
+{
+  GVfsUriMapper parent;
+};
+
+struct _GVfsUriMapperGoogleClass
+{
+  GVfsUriMapperClass parent_class;
+};
+
+GType g_vfs_uri_mapper_google_get_type (void);
+void  g_vfs_uri_mapper_google_register (GIOModule *module);
+
+G_DEFINE_DYNAMIC_TYPE (GVfsUriMapperGoogle, g_vfs_uri_mapper_google, G_VFS_TYPE_URI_MAPPER)
+
+static const gchar * const *
+google_get_handled_schemes (GVfsUriMapper *mapper)
+{
+  static const gchar *schemes[] = {
+    "google-drive",
+    NULL
+  };
+  return schemes;
+}
+
+static GMountSpec *
+google_from_uri (GVfsUriMapper  *mapper,
+                 const char     *uri_str,
+                 gchar         **path)
+{
+  GMountSpec *spec = NULL;
+  GDecodedUri *uri = NULL;
+  gboolean has_host = FALSE;
+  gboolean has_user = FALSE;
+  gchar *identity = NULL;
+
+  uri = g_vfs_decode_uri (uri_str);
+
+  if (uri == NULL)
+    goto out;
+
+  if (g_ascii_strncasecmp (uri->scheme, "google-drive", 12) != 0)
+    goto out;
+
+  spec = g_mount_spec_new ("google-drive");
+
+  if (uri->host && *uri->host)
+    {
+      g_mount_spec_set (spec, "host", uri->host);
+      has_host = TRUE;
+    }
+
+  if (uri->userinfo && *uri->userinfo)
+    {
+      g_mount_spec_set (spec, "user", uri->userinfo);
+      has_user = TRUE;
+    }
+
+  if (has_host && has_user)
+    {
+      identity = g_strconcat (uri->userinfo, "@", uri->host, NULL);
+      g_mount_spec_set (spec, "goa-identity", identity);
+    }
+
+  *path = uri->path;
+  uri->path = NULL;
+
+ out:
+  g_free (identity);
+  g_vfs_decoded_uri_free (uri);
+  return spec;
+}
+
+static GMountSpec *
+google_get_mount_spec_for_path (GVfsUriMapper *mapper,
+                                GMountSpec *spec,
+                                const gchar *old_path,
+                                const gchar *new_path)
+{
+  return NULL;
+}
+
+static const gchar * const *
+google_get_handled_mount_types (GVfsUriMapper *mapper)
+{
+  static const gchar *types[] = {
+    "google-drive",
+    NULL
+  };
+  return types;
+}
+
+static gchar *
+google_to_uri (GVfsUriMapper *mapper,
+               GMountSpec    *spec,
+               const gchar   *path,
+               gboolean       allow_utf8)
+{
+  GDecodedUri *uri = NULL;
+  const gchar *host;
+  const gchar *type;
+  const gchar *user;
+  gchar *res = NULL;
+
+  type = g_mount_spec_get (spec, "type");
+  if (g_strcmp0 (type, "google-drive") != 0)
+    goto out;
+
+  host = g_mount_spec_get (spec, "host");
+  user = g_mount_spec_get (spec, "user");
+
+  uri = g_vfs_decoded_uri_new ();
+  uri->scheme = g_strdup (type);
+  uri->host = g_strdup (host);
+  uri->userinfo = g_strdup (user);
+  uri->path = g_strdup (path);
+
+  res = g_vfs_encode_uri (uri, allow_utf8);
+
+ out:
+  g_vfs_decoded_uri_free (uri);
+  return res;
+}
+
+static const gchar *
+google_to_uri_scheme (GVfsUriMapper *mapper,
+                      GMountSpec    *spec)
+{
+  const gchar *type;
+
+  type = g_mount_spec_get (spec, "type");
+  return (g_strcmp0 (type, "google-drive") == 0) ? type : NULL;
+}
+
+static void
+g_vfs_uri_mapper_google_class_finalize (GVfsUriMapperGoogleClass *klass)
+{
+}
+
+static void
+g_vfs_uri_mapper_google_class_init (GVfsUriMapperGoogleClass *class)
+{
+  GVfsUriMapperClass *mapper_class = G_VFS_URI_MAPPER_CLASS (class);
+
+  mapper_class->get_handled_schemes = google_get_handled_schemes;
+  mapper_class->from_uri = google_from_uri;
+  mapper_class->get_mount_spec_for_path = google_get_mount_spec_for_path;
+  mapper_class->get_handled_mount_types = google_get_handled_mount_types;
+  mapper_class->to_uri = google_to_uri;
+  mapper_class->to_uri_scheme = google_to_uri_scheme;
+}
+
+static void
+g_vfs_uri_mapper_google_init (GVfsUriMapperGoogle *vfs)
+{
+}
+
+void
+g_vfs_uri_mapper_google_register (GIOModule *module)
+{
+  g_vfs_uri_mapper_google_register_type (G_TYPE_MODULE (module));
+}
diff --git a/configure.ac b/configure.ac
index 93e3374..6ffe08c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -411,6 +411,28 @@ AC_SUBST(GOA_CFLAGS)
 
 AM_CONDITIONAL(USE_GOA, [test "$msg_goa" = "yes"])
 
+dnl ****************************************************
+dnl *** Check if we should build with Google backend ***
+dnl ****************************************************
+AC_ARG_ENABLE(google, AS_HELP_STRING([--disable-google],[build without Google backend]))
+msg_google=no
+GOOGLE_LIBS=
+GOOGLE_CFLAGS=
+
+if test "x$enable_google" != "xno" ; then
+  PKG_CHECK_EXISTS(goa-1.0 >= 3.15.1 libgdata >= 0.13.1, msg_google=yes)
+
+  if test "x$msg_google" = "xyes"; then
+    PKG_CHECK_MODULES(GOOGLE, goa-1.0 libgdata)
+    AC_DEFINE(HAVE_GOOGLE, 1, [Define to 1 if Google is going to be built])
+  fi
+fi
+
+AC_SUBST(GOOGLE_LIBS)
+AC_SUBST(GOOGLE_CFLAGS)
+
+AM_CONDITIONAL(USE_GOOGLE, [test "$msg_google" = "yes"])
+
 dnl *************************
 dnl *** Check for gphoto2 ***
 dnl *************************
@@ -930,6 +952,7 @@ echo "
         hotplug backend:              $msg_hotplug_backend
 
        Blu-ray metadata support:     $msg_bluray
+       Google support:               $msg_google
         HTTP/WebDAV support:          $msg_http
        Samba support:                $msg_samba
        FUSE support:                 $msg_fuse
diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index 2d52f0a..c10a924 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -48,6 +48,12 @@ libexec_PROGRAMS=gvfsd gvfsd-sftp gvfsd-trash gvfsd-computer gvfsd-burn gvfsd-lo
 mount_in_files = sftp.mount.in ftp.mount.in ftps.mount.in trash.mount.in computer.mount.in burn.mount.in 
localtest.mount.in network.mount.in
 mount_DATA =  sftp.mount ftp.mount ftps.mount trash.mount computer.mount burn.mount localtest.mount 
network.mount
 
+mount_in_files +=google.mount.in
+if USE_GOOGLE
+mount_DATA += google.mount
+libexec_PROGRAMS += gvfsd-google
+endif
+
 mount_in_files +=recent.mount.in
 if USE_GTK
 mount_DATA += recent.mount
@@ -429,6 +435,20 @@ gvfsd_cdda_LDADD = $(libraries) $(CDDA_LIBS) $(HAL_LIBS) \
                   $(top_builddir)/common/libgvfscommon-hal.la
 endif
 
+gvfsd_google_SOURCES = \
+       gvfsbackendgoogle.c gvfsbackendgoogle.h \
+       daemon-main.c daemon-main.h \
+       daemon-main-generic.c
+
+gvfsd_google_CPPFLAGS = \
+       $(flags) \
+       -DBACKEND_HEADER=gvfsbackendgoogle.h \
+       -DDEFAULT_BACKEND_TYPE=google-drive \
+       -DBACKEND_TYPES='"google-drive", G_VFS_TYPE_BACKEND_GOOGLE,' \
+       $(GOOGLE_CFLAGS)
+
+gvfsd_google_LDADD = $(libraries) $(GOOGLE_LIBS)
+
 gvfsd_gphoto2_SOURCES = \
        gvfsbackendgphoto2.c gvfsbackendgphoto2.h \
        daemon-main.c daemon-main.h \
diff --git a/daemon/google.mount.in b/daemon/google.mount.in
new file mode 100644
index 0000000..059b228
--- /dev/null
+++ b/daemon/google.mount.in
@@ -0,0 +1,4 @@
+[Mount]
+Type=google-drive
+Exec= libexecdir@/gvfsd-google
+AutoMount=false
diff --git a/daemon/gvfsbackendgoogle.c b/daemon/gvfsbackendgoogle.c
new file mode 100644
index 0000000..fa9f219
--- /dev/null
+++ b/daemon/gvfsbackendgoogle.c
@@ -0,0 +1,1644 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* gvfs - extensions for gio
+ *
+ * Copyright (C) 2009 Thibault Saunier
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * Author: Debarshi Ray <debarshir gnome org>
+ *         Thibault Saunier <saunierthibault gmail com>
+ */
+
+#include <config.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <gdata/gdata.h>
+#include <goa/goa.h>
+
+#include "gvfsbackendgoogle.h"
+#include "gvfsjobcloseread.h"
+#include "gvfsjobcreatemonitor.h"
+#include "gvfsjobenumerate.h"
+#include "gvfsjobopenforread.h"
+#include "gvfsjobread.h"
+#include "gvfsjobseekread.h"
+#include "gvfsjobsetdisplayname.h"
+#include "gvfsmonitor.h"
+
+struct _GVfsBackendGoogle
+{
+  GVfsBackend parent;
+  GDataDocumentsService *service;
+  GHashTable *entries;
+  GHashTable *lookaside;
+  GHashTable *monitors;
+  GRecMutex mutex;
+  GoaClient *client;
+  gboolean entries_stale;
+  guint entries_stale_timeout;
+};
+
+struct _GVfsBackendGoogleClass
+{
+  GVfsBackendClass parent_class;
+};
+
+G_DEFINE_TYPE(GVfsBackendGoogle, g_vfs_backend_google, G_VFS_TYPE_BACKEND)
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+#define CATEGORY_SCHEMA_KIND "http://schemas.google.com/g/2005#kind";
+#define CATEGORY_SCHEMA_KIND_FILE "http://schemas.google.com/docs/2007#file";
+
+#define MAX_RESULTS_INITIAL 50
+#define MAX_RESULTS_FACTOR 2
+
+#define REBUILD_ENTRIES_TIMEOUT 60 /* s */
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+emit_event_internal (GVfsMonitor       *monitor,
+                     const gchar       *filename,
+                     GFileMonitorEvent  event)
+{
+  const gchar *monitored_path;
+
+  monitored_path = g_object_get_data (G_OBJECT (monitor), "g-vfs-backend-google-path");
+  if (g_str_has_prefix (filename, monitored_path))
+    g_vfs_monitor_emit_event (monitor, event, filename, NULL);
+}
+
+static void
+emit_create_event (gpointer monitor,
+                   gpointer unused,
+                   gpointer filename)
+{
+  emit_event_internal (G_VFS_MONITOR (monitor), filename, G_FILE_MONITOR_EVENT_CREATED);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+is_folder_or_root (const gchar *filename,
+                   GDataEntry  *entry,
+                   gboolean    *out_is_folder,
+                   gboolean    *out_is_root)
+{
+  gboolean is_folder = FALSE;
+  gboolean is_root = FALSE;
+
+  if (g_strcmp0 (filename, "/") == 0 && entry == NULL)
+    {
+      is_folder = TRUE;
+      is_root = TRUE;
+    }
+
+  if (entry != NULL && GDATA_IS_DOCUMENTS_FOLDER (entry))
+    is_folder = TRUE;
+
+  if (out_is_folder != NULL)
+    *out_is_folder = is_folder;
+
+  if (out_is_root != NULL)
+    *out_is_root = is_root;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gchar *
+get_content_type_from_entry (GDataEntry *entry)
+{
+  gchar *ret_val = NULL;
+
+  if (!GDATA_IS_DOCUMENTS_DOCUMENT (entry))
+    goto out;
+
+  if (GDATA_IS_DOCUMENTS_DRAWING (entry))
+    {
+      ret_val = g_strdup ("image/png");
+    }
+  else if (GDATA_IS_DOCUMENTS_PDF (entry))
+    {
+      ret_val = g_strdup ("application/pdf");
+    }
+  else if (GDATA_IS_DOCUMENTS_PRESENTATION (entry))
+    {
+      ret_val = g_strdup ("application/vnd.ms-powerpoint");
+    }
+  else if (GDATA_IS_DOCUMENTS_SPREADSHEET (entry))
+    {
+      ret_val = g_strdup ("application/vnd.oasis.opendocument.spreadsheet");
+    }
+  else if (GDATA_IS_DOCUMENTS_TEXT (entry))
+    {
+      ret_val = g_strdup ("application/vnd.oasis.opendocument.text");
+    }
+  else
+    {
+      GList *categories;
+      GList *l;
+
+      categories = gdata_entry_get_categories (entry);
+      for (l = categories; l != NULL; l = l->next)
+        {
+          GDataCategory *category = GDATA_CATEGORY (l->data);
+          const gchar *scheme;
+          const gchar *term;
+
+          scheme = gdata_category_get_scheme (category);
+          term = gdata_category_get_term (category);
+          if (g_strcmp0 (scheme, CATEGORY_SCHEMA_KIND) == 0 &&
+              g_strcmp0 (term, CATEGORY_SCHEMA_KIND_FILE) == 0)
+            {
+              ret_val = g_strdup (gdata_category_get_label (category));
+              break;
+            }
+        }
+    }
+
+ out:
+  return ret_val;
+}
+
+static gchar *
+get_export_format_from_entry (GDataEntry *entry)
+{
+  gchar *ret_val = NULL;
+
+  if (!GDATA_IS_DOCUMENTS_DOCUMENT (entry))
+    goto out;
+
+  if (GDATA_IS_DOCUMENTS_DRAWING (entry))
+    {
+      ret_val = g_strdup (GDATA_DOCUMENTS_DRAWING_PNG);
+    }
+  else if (GDATA_IS_DOCUMENTS_PDF (entry))
+    {
+      ret_val = g_strdup (GDATA_DOCUMENTS_TEXT_PDF);
+    }
+  else if (GDATA_IS_DOCUMENTS_PRESENTATION (entry))
+    {
+      ret_val = g_strdup (GDATA_DOCUMENTS_PRESENTATION_PPT);
+    }
+  else if (GDATA_IS_DOCUMENTS_SPREADSHEET (entry))
+    {
+      ret_val = g_strdup (GDATA_DOCUMENTS_SPREADSHEET_ODS);
+    }
+  else if (GDATA_IS_DOCUMENTS_TEXT (entry))
+    {
+      ret_val = g_strdup (GDATA_DOCUMENTS_TEXT_ODT);
+    }
+  else
+    {
+      GList *categories;
+      GList *l;
+      const gchar *content_type = NULL;
+
+      categories = gdata_entry_get_categories (entry);
+      for (l = categories; l != NULL; l = l->next)
+        {
+          GDataCategory *category = GDATA_CATEGORY (l->data);
+          const gchar *scheme;
+          const gchar *term;
+
+          scheme = gdata_category_get_scheme (category);
+          term = gdata_category_get_term (category);
+          if (g_strcmp0 (scheme, CATEGORY_SCHEMA_KIND) == 0 &&
+              g_strcmp0 (term, CATEGORY_SCHEMA_KIND_FILE) == 0)
+            {
+              content_type = gdata_category_get_label (category);
+              break;
+            }
+        }
+
+      if (g_strcmp0 (content_type, "image/jpeg") == 0)
+        ret_val = g_strdup ("jpeg");
+      else if (g_strcmp0 (content_type, "image/png") == 0)
+        ret_val = g_strdup ("png");
+    }
+
+ out:
+  return ret_val;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+build_file_info (GVfsBackendGoogle      *self,
+                 const gchar            *filename,
+                 GDataEntry             *entry,
+                 GFileInfo              *info,
+                 GFileAttributeMatcher  *matcher,
+                 gboolean                is_symlink,
+                 const gchar            *symlink_name,
+                 GError                **error)
+{
+  GFileType file_type;
+  GList *authors;
+  gboolean is_folder;
+  gboolean is_root;
+  const gchar *etag;
+  const gchar *id;
+  const gchar *name;
+  const gchar *title;
+  gchar *content_type = NULL;
+  gint64 atime;
+  gint64 ctime;
+  gint64 mtime;
+
+  is_folder_or_root (filename, entry, &is_folder, &is_root);
+  if (entry == NULL && !is_root)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or directory"));
+      goto out;
+    }
+
+  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, !is_root);
+  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, !is_root);
+  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE);
+
+  if (is_folder)
+    {
+      content_type = g_strdup ("inode/directory");
+      file_type = G_FILE_TYPE_DIRECTORY;
+    }
+  else
+    {
+      content_type = get_content_type_from_entry (entry);
+      file_type = G_FILE_TYPE_REGULAR;
+    }
+
+  if (is_symlink)
+    {
+      file_type = G_FILE_TYPE_SYMBOLIC_LINK;
+      g_file_info_set_is_symlink (info, TRUE);
+      g_file_info_set_symlink_target (info, filename);
+    }
+
+  if (content_type != NULL)
+    {
+      GIcon *icon;
+
+      g_file_info_set_content_type (info, content_type);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, content_type);
+
+      icon = g_content_type_get_icon (content_type);
+      g_file_info_set_icon (info, icon);
+      g_object_unref (icon);
+    }
+
+  g_file_info_set_file_type (info, file_type);
+
+  if (is_root)
+    goto out;
+
+  if (is_symlink)
+    {
+      name = symlink_name;
+    }
+  else
+    {
+      /* We need the document-id, not id or resource-id, because
+       * gdata_documents_entry_get_path returns a path based on it.
+       */
+      G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+      name = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (entry));
+      G_GNUC_END_IGNORE_DEPRECATIONS;
+    }
+
+  g_file_info_set_name (info, name);
+
+  title = gdata_entry_get_title (entry);
+  g_file_info_set_display_name (info, title);
+  g_file_info_set_edit_name (info, title);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME, title);
+
+  atime = gdata_documents_entry_get_last_viewed (GDATA_DOCUMENTS_ENTRY (entry));
+  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_ACCESS, (guint64) atime);
+
+  ctime = gdata_entry_get_published (entry);
+  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CREATED, (guint64) ctime);
+
+  mtime = gdata_entry_get_updated (entry);
+  g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED, (guint64) mtime);
+
+  authors = gdata_entry_get_authors (entry);
+  if (authors != NULL)
+    {
+      GDataAuthor *author = GDATA_AUTHOR (authors->data);
+      const gchar *email_address;
+      const gchar *name;
+
+      email_address = gdata_author_get_email_address (author);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_USER, email_address);
+
+      name = gdata_author_get_name (author);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_USER_REAL, name);
+    }
+
+  etag = gdata_entry_get_etag (entry);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, etag);
+
+  id = gdata_entry_get_id (entry);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ID_FILE, id);
+
+ out:
+  g_free (content_type);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static GoaClient *
+get_goa_client_sync (GError **error)
+{
+  static GoaClient *client = NULL;
+  static GError *_error = NULL;
+  static volatile gsize initialized = 0;
+
+  if (g_once_init_enter (&initialized))
+    {
+      client = goa_client_new_sync (NULL, &_error);
+      g_once_init_leave (&initialized, 1);
+    }
+
+  if (_error != NULL && error != NULL)
+    *error = g_error_copy (_error);
+
+  return client;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gchar *
+get_parent_basename (const gchar *filename)
+{
+  gchar *parent;
+  gchar *ret_val;
+
+  parent = g_path_get_dirname (filename);
+  ret_val = g_path_get_basename (parent);
+
+  g_free (parent);
+  return ret_val;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+rebuild_entries (GVfsBackendGoogle  *self,
+                 GCancellable       *cancellable,
+                 GError            **error)
+{
+  GDataDocumentsFeed *feed = NULL;
+  GDataDocumentsQuery *query = NULL;
+  GError *local_error;
+  gboolean found_new;
+  gboolean succeeded_once = FALSE;
+  guint max_results = MAX_RESULTS_INITIAL;
+
+  /* The paging mechanism based on start-index and
+   * gdata_query_next_page is not working at the moment. If we start
+   * with (1, 50) and then call gdata_query_next_page, we get a feed
+   * for (1, 50) instead of (51, 50). The result is the same if we
+   * create a new query instead of calling gdata_query_next_page.
+   *
+   * Basically, the start-index always stays at 1.
+   *
+   * The only way I could get it to work was to keep increasing the
+   * max-results and keep the start-index at 1.
+   */
+
+  do
+    {
+      GList *entries;
+      GList *l;
+
+      found_new = FALSE;
+
+      query = gdata_documents_query_new_with_limits (NULL, 1, max_results);
+      gdata_documents_query_set_show_folders (query, TRUE);
+
+      local_error = NULL;
+      feed = gdata_documents_service_query_documents (self->service, query, cancellable, NULL, NULL, 
&local_error);
+      if (local_error != NULL)
+        {
+          if (succeeded_once)
+            {
+              g_warning ("Unable to query: %s", local_error->message);
+              g_error_free (local_error);
+            }
+          else
+            {
+              g_propagate_error (error, local_error);
+            }
+
+          goto out;
+        }
+
+      if (!succeeded_once)
+        {
+          g_hash_table_remove_all (self->entries);
+          succeeded_once = TRUE;
+        }
+
+      entries = gdata_feed_get_entries (GDATA_FEED (feed));
+      for (l = entries; l != NULL; l = l->next)
+        {
+          GDataEntry *entry = GDATA_ENTRY (l->data);
+          const gchar *document_id;
+
+          /* We need the document-id, not id or resource-id, because
+           * gdata_documents_entry_get_path returns a path based on it.
+           */
+          G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+          document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (entry));
+          G_GNUC_END_IGNORE_DEPRECATIONS;
+
+          if (g_hash_table_lookup (self->entries, document_id) == NULL)
+            {
+              g_hash_table_insert (self->entries, g_strdup (document_id), g_object_ref (entry));
+              found_new = TRUE;
+            }
+        }
+
+      max_results *= MAX_RESULTS_FACTOR;
+      g_clear_object (&feed);
+      g_clear_object (&query);
+    } while (found_new);
+
+ out:
+  g_clear_object (&feed);
+  g_clear_object (&query);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+remove_monitor_weak_ref (gpointer monitor,
+                         gpointer unused,
+                         gpointer monitors)
+{
+  g_object_weak_unref (G_OBJECT (monitor), (GWeakNotify) g_hash_table_remove, monitors);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+close_read_cb (GObject      *source_object,
+               GAsyncResult *res,
+               gpointer      user_data)
+{
+  GError *error;
+  GInputStream *stream = G_INPUT_STREAM (source_object);
+  GVfsJobCloseRead *job = G_VFS_JOB_CLOSE_READ (user_data);
+
+  error = NULL;
+  g_input_stream_close_finish (stream, res, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_object_unref (job);
+  g_debug ("- close_read\n");
+}
+
+static gboolean
+g_vfs_backend_google_close_read (GVfsBackend       *_self,
+                                 GVfsJobCloseRead  *job,
+                                 GVfsBackendHandle  handle)
+{
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GInputStream *stream = G_INPUT_STREAM (handle);
+
+  g_debug ("+ close_read: %p\n", handle);
+
+  g_input_stream_close_async (stream, G_PRIORITY_DEFAULT, cancellable, close_read_cb, g_object_ref (job));
+
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+g_vfs_backend_google_create_dir_monitor (GVfsBackend          *_self,
+                                         GVfsJobCreateMonitor *job,
+                                         const gchar          *filename,
+                                         GFileMonitorFlags     flags)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataEntry *entry;
+  GError *error;
+  GVfsMonitor *monitor = NULL;
+  gboolean is_folder;
+  gboolean is_root;
+  gchar *document_id = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+
+  document_id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, document_id);
+  is_folder_or_root (filename, entry, NULL, &is_root);
+
+  if (entry == NULL && !is_root)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      entry = g_hash_table_lookup (self->entries, document_id);
+      if (entry == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+    }
+
+  is_folder_or_root (filename, entry, &is_folder, NULL);
+  if (!is_folder)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY, _("The file is not a 
directory"));
+      goto out;
+    }
+
+  monitor = g_vfs_monitor_new (_self);
+  g_object_set_data_full (G_OBJECT (monitor), "g-vfs-backend-google-path", g_strdup (filename), g_free);
+  g_hash_table_add (self->monitors, monitor);
+  g_object_weak_ref (G_OBJECT (monitor), (GWeakNotify) g_hash_table_remove, self->monitors);
+  g_vfs_job_create_monitor_set_monitor (job, monitor);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&monitor);
+  g_free (document_id);
+  g_rec_mutex_unlock (&self->mutex);
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_delete (GVfsBackend   *_self,
+                             GVfsJobDelete *job,
+                             const gchar   *filename)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataAuthorizationDomain *auth_domain;
+  GDataEntry *entry;
+  GError *error;
+  gboolean is_root;
+  const gchar *real_filename;
+  gchar *document_id = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ delete: %s\n", filename);
+
+  real_filename = g_hash_table_lookup (self->lookaside, filename);
+  if (real_filename != NULL)
+    filename = real_filename;
+
+  is_folder_or_root (filename, NULL, NULL, &is_root);
+  if (is_root)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_NOT_SUPPORTED,
+                        _("Can not delete the root directory"));
+      goto out;
+    }
+
+  document_id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, document_id);
+
+  if (entry == NULL)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      entry = g_hash_table_lookup (self->entries, document_id);
+      if (entry == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+    }
+
+  auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+
+  error = NULL;
+  gdata_service_delete_entry (GDATA_SERVICE (self->service), auth_domain, entry, cancellable, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_hash_table_remove (self->entries, document_id);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_free (document_id);
+  g_debug ("- delete\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+rebuild_entries_timeout_cb (gpointer user_data)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (user_data);
+
+  self->entries_stale = TRUE;
+  self->entries_stale_timeout = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+g_vfs_backend_google_enumerate (GVfsBackend           *_self,
+                                GVfsJobEnumerate      *job,
+                                const gchar           *filename,
+                                GFileAttributeMatcher *matcher,
+                                GFileQueryInfoFlags    flags)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GError *error;
+  GList *entries = NULL;
+  GList *l;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ enumerate: %s\n", filename);
+
+  if (self->entries_stale_timeout == 0)
+    {
+      self->entries_stale_timeout = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+                                                                REBUILD_ENTRIES_TIMEOUT,
+                                                                rebuild_entries_timeout_cb,
+                                                                g_object_ref (self),
+                                                                g_object_unref);
+    }
+
+  if (self->entries_stale)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      self->entries_stale = FALSE;
+    }
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+  entries = g_hash_table_get_values (self->entries);
+  for (l = entries; l != NULL; l = l->next)
+    {
+      GDataEntry *entry = GDATA_ENTRY (l->data);
+      gchar *parent_path;
+      gchar *path;
+
+      path = gdata_documents_entry_get_path (GDATA_DOCUMENTS_ENTRY (entry));
+      parent_path = g_path_get_dirname (path);
+      if (g_strcmp0 (filename, parent_path) == 0)
+        {
+          GFileInfo *info;
+
+          info = g_file_info_new ();
+          build_file_info (self, path, entry, info, matcher, FALSE, NULL, NULL);
+          g_vfs_job_enumerate_add_info (job, info);
+          g_object_unref (info);
+        }
+
+      g_free (parent_path);
+      g_free (path);
+    }
+
+  g_vfs_job_enumerate_done (job);
+
+ out:
+  g_list_free (entries);
+  g_debug ("- enumerate\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_make_directory (GVfsBackend          *_self,
+                                     GVfsJobMakeDirectory *job,
+                                     const gchar          *filename)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataAuthorizationDomain *auth_domain;
+  GDataDocumentsFolder *folder = NULL;
+  GDataDocumentsFolder *parent = NULL;
+  GDataEntry *new_entry = NULL;
+  GError *error;
+  gboolean is_root;
+  const gchar *document_id;
+  gchar *parent_id = NULL;
+  gchar *parent_path = NULL;
+  gchar *path = NULL;
+  gchar *title = NULL;
+  gchar *upload_uri = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ make_directory: %s\n", filename);
+
+  is_folder_or_root (filename, NULL, NULL, &is_root);
+  if (is_root)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_NOT_SUPPORTED,
+                        _("Can not create root directory"));
+      goto out;
+    }
+
+  parent_path = g_path_get_dirname (filename);
+  is_folder_or_root (parent_path, NULL, NULL, &is_root);
+  if (!is_root)
+    {
+      parent_id = g_path_get_basename (parent_path);
+      parent = g_hash_table_lookup (self->entries, parent_id);
+
+      if (parent == NULL)
+        {
+          error = NULL;
+          rebuild_entries (self, cancellable, &error);
+          if (error != NULL)
+            {
+              g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+              g_error_free (error);
+              goto out;
+            }
+
+          parent = g_hash_table_lookup (self->entries, parent_id);
+          if (parent == NULL)
+            {
+              g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+              goto out;
+            }
+        }
+    }
+
+  upload_uri = gdata_documents_service_get_upload_uri (parent);
+  if (upload_uri == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("Upload URI not found"));
+      goto out;
+    }
+
+  folder = gdata_documents_folder_new (NULL);
+  title = g_path_get_basename (filename);
+  gdata_entry_set_title (GDATA_ENTRY (folder), title);
+
+  auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+
+  error = NULL;
+  new_entry = gdata_service_insert_entry (GDATA_SERVICE (self->service),
+                                          auth_domain,
+                                          upload_uri,
+                                          GDATA_ENTRY (folder),
+                                          cancellable,
+                                          &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  /* We need the document-id, not id or resource-id, because
+   * gdata_documents_entry_get_path returns a path based on it.
+   */
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (new_entry));
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  g_hash_table_insert (self->entries, g_strdup (document_id), g_object_ref (new_entry));
+
+  path = gdata_documents_entry_get_path (GDATA_DOCUMENTS_ENTRY (new_entry));
+  g_hash_table_insert (self->lookaside, g_strdup (filename), g_strdup (path));
+
+  self->entries_stale = TRUE;
+  //g_hash_table_foreach (self->monitors, emit_create_event, (gpointer) path);
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&new_entry);
+  g_clear_object (&folder);
+  g_free (parent_id);
+  g_free (parent_path);
+  g_free (path);
+  g_free (title);
+  g_free (upload_uri);
+  g_debug ("- make_directory\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_mount (GVfsBackend  *_self,
+                            GVfsJobMount *job,
+                            GMountSpec   *spec,
+                            GMountSource *source,
+                            gboolean      is_automount)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GList *accounts = NULL;
+  GList *l;
+  gboolean account_found = FALSE;
+  const gchar *mount_identity;
+
+  g_debug ("+ mount\n");
+
+  if (self->client == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Failed to connect to GOA"));
+      goto out;
+    }
+
+  mount_identity = g_mount_spec_get (spec, "goa-identity");
+  if (mount_identity == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Invalid mount spec"));
+      goto out;
+    }
+
+  accounts = goa_client_get_accounts (self->client);
+  for (l = accounts; l != NULL && !account_found; l = l->next)
+    {
+      GoaAccount *account;
+      GoaObject *object = GOA_OBJECT (l->data);
+      gchar *account_identity;
+      gchar *provider_type;
+
+      account = goa_object_get_account (object);
+      account_identity = goa_account_dup_identity (account);
+      provider_type = goa_account_dup_provider_type (account);
+
+      if (g_strcmp0 (provider_type, "google") == 0 &&
+          g_strcmp0 (account_identity, mount_identity) == 0)
+        {
+          GDataGoaAuthorizer *authorizer;
+
+          authorizer = gdata_goa_authorizer_new (object);
+
+          if (self->service != NULL)
+            {
+              g_warning ("Expected NULL service, but found %p", self->service);
+              g_clear_object (&self->service);
+            }
+
+          self->service = gdata_documents_service_new (GDATA_AUTHORIZER (authorizer));
+
+          account_found = TRUE;
+          g_object_unref (authorizer);
+        }
+
+      g_free (provider_type);
+      g_free (account_identity);
+      g_object_unref (account);
+    }
+
+  if (!account_found)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_INVALID_ARGUMENT,
+                        _("Failed to find a matching GOA account: %s"),
+                        mount_identity);
+      goto out;
+    }
+
+  g_vfs_backend_set_mount_spec (_self, spec);
+  g_vfs_backend_set_display_name (_self, mount_identity);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_list_free_full (accounts, g_object_unref);
+  g_debug ("- mount\n");
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_open_for_read (GVfsBackend        *_self,
+                                    GVfsJobOpenForRead *job,
+                                    const gchar        *filename)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataEntry *entry;
+  GDataDownloadStream *stream;
+  GError *error;
+  gboolean is_folder;
+  gboolean is_root;
+  gchar *document_id = NULL;
+  gchar *export_format = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ open_for_read: %s\n", filename);
+
+  document_id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, document_id);
+
+  is_folder_or_root (filename, entry, NULL, &is_root);
+  if (is_root)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("Can't open directory"));
+      goto out;
+    }
+
+  if (entry == NULL)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      entry = g_hash_table_lookup (self->entries, document_id);
+      if (entry == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+    }
+
+  is_folder_or_root (filename, entry, &is_folder, NULL);
+  if (is_folder)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("Can't open directory"));
+      goto out;
+    }
+
+  export_format = get_export_format_from_entry (entry);
+  if (export_format == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Not supported"));
+      goto out;
+    }
+
+  error = NULL;
+  stream = gdata_documents_document_download (GDATA_DOCUMENTS_DOCUMENT (entry),
+                                              self->service,
+                                              export_format,
+                                              cancellable,
+                                              &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_object_set_data_full (G_OBJECT (stream), "g-vfs-backend-google-entry", g_object_ref (entry), 
g_object_unref);
+
+  g_vfs_job_open_for_read_set_handle (job, stream);
+  g_vfs_job_open_for_read_set_can_seek (job, TRUE);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_free (export_format);
+  g_free (document_id);
+  g_debug ("- open_for_read\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_push (GVfsBackend           *_self,
+                           GVfsJobPush           *job,
+                           const gchar           *destination,
+                           const gchar           *local_path,
+                           GFileCopyFlags         flags,
+                           gboolean               remove_source,
+                           GFileProgressCallback  progress_callback,
+                           gpointer               progress_callback_data)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataDocumentsDocument *document = NULL;
+  GDataDocumentsDocument *new_document = NULL;
+  GDataDocumentsFolder *parent = NULL;
+  GDataUploadStream *ostream = NULL;
+  GError *error;
+  GFile *local_file = NULL;
+  GFileInputStream *istream = NULL;
+  GFileInfo *info = NULL;
+  gboolean is_root;
+  const gchar *content_type;
+  const gchar *document_id;
+  const gchar *real_parent_path;
+  const gchar *title;
+  gchar *parent_id = NULL;
+  gchar *parent_path = NULL;
+  gchar *path = NULL;
+  gchar *upload_uri = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ push: %s -> %s\n", local_path, destination);
+
+  local_file = g_file_new_for_path (local_path);
+
+  error = NULL;
+  info = g_file_query_info (local_file,
+                            G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE","
+                            G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                            G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                            G_FILE_QUERY_INFO_NONE,
+                            cancellable,
+                            &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_WOULD_RECURSE,
+                        _("Can't recursively copy directory"));
+      goto out;
+    }
+
+  error = NULL;
+  istream = g_file_read (local_file, cancellable, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  parent_path = g_path_get_dirname (destination);
+  real_parent_path = g_hash_table_lookup (self->lookaside, parent_path);
+  if (real_parent_path != NULL)
+    {
+      g_free (parent_path);
+      parent_path = g_strdup (real_parent_path);
+    }
+
+  is_folder_or_root (parent_path, NULL, NULL, &is_root);
+  if (!is_root)
+    {
+      parent_id = g_path_get_basename (parent_path);
+      parent = g_hash_table_lookup (self->entries, parent_id);
+
+      if (parent == NULL)
+        {
+          error = NULL;
+          rebuild_entries (self, cancellable, &error);
+          if (error != NULL)
+            {
+              g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+              g_error_free (error);
+              goto out;
+            }
+
+          parent = g_hash_table_lookup (self->entries, parent_id);
+          if (parent == NULL)
+            {
+              g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+              goto out;
+            }
+        }
+    }
+
+  upload_uri = gdata_documents_service_get_upload_uri (parent);
+  if (upload_uri == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("Upload URI not found"));
+      goto out;
+    }
+
+  document = gdata_documents_document_new (NULL);
+  title = g_file_info_get_display_name (info);
+  gdata_entry_set_title (GDATA_ENTRY (document), title);
+
+  content_type = g_file_info_get_content_type (info);
+
+  error = NULL;
+  ostream = gdata_documents_service_upload_document (self->service,
+                                                     document,
+                                                     title,
+                                                     content_type,
+                                                     parent,
+                                                     cancellable,
+                                                     &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  error = NULL;
+  g_output_stream_splice (G_OUTPUT_STREAM (ostream),
+                          G_INPUT_STREAM (istream),
+                          G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+                          cancellable,
+                          &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  error = NULL;
+  new_document = gdata_documents_service_finish_upload (self->service, ostream, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  /* We need the document-id, not id or resource-id, because
+   * gdata_documents_entry_get_path returns a path based on it.
+   */
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (new_document));
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  g_hash_table_insert (self->entries, g_strdup (document_id), g_object_ref (new_document));
+
+  path = gdata_documents_entry_get_path (GDATA_DOCUMENTS_ENTRY (new_document));
+  g_hash_table_insert (self->lookaside, g_strdup (destination), g_strdup (path));
+
+  self->entries_stale = TRUE;
+  g_hash_table_foreach (self->monitors, emit_create_event, (gpointer) path);
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&document);
+  g_clear_object (&info);
+  g_clear_object (&istream);
+  g_clear_object (&local_file);
+  g_clear_object (&new_document);
+  g_clear_object (&ostream);
+  g_free (parent_id);
+  g_free (parent_path);
+  g_free (path);
+  g_free (upload_uri);
+  g_debug ("- push\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+g_vfs_backend_google_query_fs_info (GVfsBackend           *_self,
+                                    GVfsJobQueryFsInfo    *job,
+                                    const gchar           *filename,
+                                    GFileInfo             *info,
+                                    GFileAttributeMatcher *matcher)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GMountSpec *spec;
+  const gchar *type;
+
+  g_debug ("+ query_fs_info: %s\n", filename);
+
+  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, FALSE);
+
+  spec = g_vfs_backend_get_mount_spec (_self);
+  type = g_mount_spec_get_type (spec);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, type);
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+  g_debug ("- query_fs_info\n");
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_query_info (GVfsBackend           *_self,
+                                 GVfsJobQueryInfo      *job,
+                                 const gchar           *filename,
+                                 GFileQueryInfoFlags    flags,
+                                 GFileInfo             *info,
+                                 GFileAttributeMatcher *matcher)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataEntry *entry;
+  GError *error;
+  gboolean is_root;
+  gboolean is_symlink = FALSE;
+  const gchar *real_filename;
+  gchar *document_id = NULL;
+  gchar *symlink_name = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ query_info: %s\n", filename);
+
+  real_filename = g_hash_table_lookup (self->lookaside, filename);
+  if (real_filename != NULL)
+    {
+      is_symlink = TRUE;
+      symlink_name = g_path_get_basename (filename);
+      filename = real_filename;
+    }
+
+  document_id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, document_id);
+  is_folder_or_root (filename, entry, NULL, &is_root);
+
+  if (entry == NULL && !is_root)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      entry = g_hash_table_lookup (self->entries, document_id);
+    }
+
+  error = NULL;
+  build_file_info (self, filename, entry, info, matcher, is_symlink, symlink_name, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_free (document_id);
+  g_free (symlink_name);
+  g_debug ("- query_info\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_query_info_on_read (GVfsBackend           *_self,
+                                         GVfsJobQueryInfoRead  *job,
+                                         GVfsBackendHandle      handle,
+                                         GFileInfo             *info,
+                                         GFileAttributeMatcher *matcher)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GDataEntry *entry;
+  GError *error;
+  GInputStream *stream = G_INPUT_STREAM (handle);
+  gchar *path = NULL;
+
+  g_debug ("+ query_info_on_read: %p\n", handle);
+
+  entry = g_object_get_data (G_OBJECT (stream), "g-vfs-backend-google-entry");
+  path = gdata_documents_entry_get_path (GDATA_DOCUMENTS_ENTRY (entry));
+
+  error = NULL;
+  build_file_info (self, path, entry, info, matcher, FALSE, NULL, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_free (path);
+  g_debug ("- query_info_on_read\n");
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+g_vfs_backend_google_seek_on_read (GVfsBackend       *_self,
+                                   GVfsJobSeekRead   *job,
+                                   GVfsBackendHandle  handle,
+                                   goffset            offset,
+                                   GSeekType          type)
+{
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GError *error;
+  GInputStream *stream = G_INPUT_STREAM (handle);
+  goffset cur_offset;
+
+  g_debug ("+ seek_on_read: %p\n", handle);
+
+  error = NULL;
+  g_seekable_seek (G_SEEKABLE (stream), offset, type, cancellable, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  cur_offset = g_seekable_tell (G_SEEKABLE (stream));
+  g_vfs_job_seek_read_set_offset (job, cur_offset);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_debug ("- seek_on_read\n");
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_set_display_name (GVfsBackend           *_self,
+                                       GVfsJobSetDisplayName *job,
+                                       const gchar           *filename,
+                                       const gchar           *display_name)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataAuthorizationDomain *auth_domain;
+  GDataEntry *entry;
+  GDataEntry *new_entry = NULL;
+  GError *error;
+  gboolean is_root;
+  const gchar *new_document_id;
+  const gchar *real_filename;
+  gchar *document_id = NULL;
+  gchar *path = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  g_debug ("+ set_display_name: %s, %s\n", filename, display_name);
+
+  real_filename = g_hash_table_lookup (self->lookaside, filename);
+  if (real_filename != NULL)
+    filename = real_filename;
+
+  is_folder_or_root (filename, NULL, NULL, &is_root);
+  if (is_root)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_NOT_SUPPORTED,
+                        _("Can not rename the root directory"));
+      goto out;
+    }
+
+  document_id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, document_id);
+
+  if (entry == NULL)
+    {
+      error = NULL;
+      rebuild_entries (self, cancellable, &error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          goto out;
+        }
+
+      entry = g_hash_table_lookup (self->entries, document_id);
+      if (entry == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+    }
+
+  gdata_entry_set_title (entry, display_name);
+
+  auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+
+  error = NULL;
+  new_entry = gdata_service_update_entry (GDATA_SERVICE (self->service), auth_domain, entry, cancellable, 
&error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_hash_table_remove (self->entries, document_id);
+
+  /* We need the document-id, not id or resource-id, because
+   * gdata_documents_entry_get_path returns a path based on it.
+   */
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  new_document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (new_entry));
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  g_hash_table_insert (self->entries, g_strdup (new_document_id), g_object_ref (new_entry));
+
+  path = gdata_documents_entry_get_path (GDATA_DOCUMENTS_ENTRY (new_entry));
+  g_vfs_job_set_display_name_set_new_path (job, path);
+
+  g_warn_if_fail (g_strcmp0 (filename, path) == 0);
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&new_entry);
+  g_free (document_id);
+  g_free (path);
+  g_debug ("- set_display_name\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+read_cb (GObject      *source_object,
+         GAsyncResult *res,
+         gpointer      user_data)
+{
+  GError *error;
+  GInputStream *stream = G_INPUT_STREAM (source_object);
+  GVfsJobRead *job = G_VFS_JOB_READ (user_data);
+  gssize nread;
+
+  error = NULL;
+  nread = g_input_stream_read_finish (stream, res, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  g_vfs_job_read_set_size (job, (gsize) nread);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_object_unref (job);
+  g_debug ("- read\n");
+}
+
+static gboolean
+g_vfs_backend_google_read (GVfsBackend       *_self,
+                           GVfsJobRead       *job,
+                           GVfsBackendHandle  handle,
+                           gchar             *buffer,
+                           gsize              bytes_requested)
+{
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GInputStream *stream = G_INPUT_STREAM (handle);
+
+  g_debug ("+ read: %p\n", handle);
+
+  g_input_stream_read_async (stream,
+                             buffer,
+                             bytes_requested,
+                             G_PRIORITY_DEFAULT,
+                             cancellable,
+                             read_cb,
+                             g_object_ref (job));
+
+  return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_dispose (GObject *_self)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+
+  if (self->entries_stale_timeout != 0)
+    {
+      g_source_remove (self->entries_stale_timeout);
+      self->entries_stale_timeout = 0;
+    }
+
+  g_clear_object (&self->service);
+  g_clear_object (&self->client);
+  g_clear_pointer (&self->entries, (GDestroyNotify) g_hash_table_unref);
+
+  G_OBJECT_CLASS (g_vfs_backend_google_parent_class)->dispose (_self);
+}
+
+static void
+g_vfs_backend_google_finalize (GObject *_self)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+
+  g_hash_table_unref (self->lookaside);
+
+  g_hash_table_foreach (self->monitors, remove_monitor_weak_ref, self->monitors);
+  g_hash_table_unref (self->monitors);
+
+  g_rec_mutex_clear (&self->mutex);
+
+  G_OBJECT_CLASS (g_vfs_backend_google_parent_class)->finalize (_self);
+}
+
+static void
+g_vfs_backend_google_class_init (GVfsBackendGoogleClass * klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass);
+
+  gobject_class->dispose = g_vfs_backend_google_dispose;
+  gobject_class->finalize = g_vfs_backend_google_finalize;
+
+  backend_class->try_close_read = g_vfs_backend_google_close_read;
+  backend_class->try_create_dir_monitor = g_vfs_backend_google_create_dir_monitor;
+  backend_class->delete = g_vfs_backend_google_delete;
+  backend_class->enumerate = g_vfs_backend_google_enumerate;
+  backend_class->make_directory = g_vfs_backend_google_make_directory;
+  backend_class->mount = g_vfs_backend_google_mount;
+  backend_class->open_for_read = g_vfs_backend_google_open_for_read;
+  backend_class->push = g_vfs_backend_google_push;
+  backend_class->try_query_fs_info = g_vfs_backend_google_query_fs_info;
+  backend_class->query_info = g_vfs_backend_google_query_info;
+  backend_class->query_info_on_read = g_vfs_backend_google_query_info_on_read;
+  backend_class->try_seek_on_read = g_vfs_backend_google_seek_on_read;
+  backend_class->set_display_name = g_vfs_backend_google_set_display_name;
+  backend_class->try_read = g_vfs_backend_google_read;
+}
+
+static void
+g_vfs_backend_google_init (GVfsBackendGoogle *self)
+{
+  GError *error;
+
+  self->entries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+  self->lookaside = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  self->monitors = g_hash_table_new (NULL, NULL);
+  g_rec_mutex_init (&self->mutex);
+  self->entries_stale = TRUE;
+
+  error = NULL;
+  self->client = get_goa_client_sync (&error);
+  if (self->client == NULL)
+    {
+      g_warning ("Failed to connect to GOA: %s", error->message);
+      g_error_free (error);
+    }
+}
+
+GVfsBackend *
+g_vfs_backend_google_new (void)
+{
+  return g_object_new (G_VFS_TYPE_BACKEND_GOOGLE, NULL);
+}
diff --git a/daemon/gvfsbackendgoogle.h b/daemon/gvfsbackendgoogle.h
new file mode 100644
index 0000000..21b209f
--- /dev/null
+++ b/daemon/gvfsbackendgoogle.h
@@ -0,0 +1,62 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* gvfs - extensions for gio
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * Author: Debarshi Ray <debarshir gnome org>
+ */
+
+#ifndef __G_VFS_BACKEND_GOOGLE_H__
+#define __G_VFS_BACKEND_GOOGLE_H__
+
+#include <gvfsbackend.h>
+
+G_BEGIN_DECLS
+
+#define G_VFS_TYPE_BACKEND_GOOGLE (g_vfs_backend_google_get_type())
+
+#define G_VFS_BACKEND_GOOGLE(o) \
+  (G_TYPE_CHECK_INSTANCE_CAST((o), \
+   G_VFS_TYPE_BACKEND_GOOGLE, GVfsBackendGoogle))
+
+#define G_VFS_BACKEND_GOOGLE_CLASS(k) \
+  (G_TYPE_CHECK_CLASS_CAST((k), \
+   G_VFS_TYPE_BACKEND_GOOGLE, GVfsBackendGoogleClass))
+
+#define G_VFS_IS_BACKEND_GOOGLE(o) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((o), \
+   G_VFS_TYPE_BACKEND_GOOGLE))
+
+#define G_VFS_IS_BACKEND_GOOGLE_CLASS(k) \
+  (G_TYPE_CHECK_CLASS_TYPE((k), \
+   G_VFS_TYPE_BACKEND_GOOGLE))
+
+#define G_VFS_BACKEND_GOOGLE_GET_CLASS(o) \
+  (G_TYPE_INSTANCE_GET_CLASS((o), \
+   G_VFS_TYPE_BACKEND_GOOGLE, GVfsBackendGoogleClass))
+
+typedef struct _GVfsBackendGoogle GVfsBackendGoogle;
+typedef struct _GVfsBackendGoogleClass GVfsBackendGoogleClass;
+
+GType g_vfs_backend_google_get_type (void) G_GNUC_CONST;
+
+GVfsBackend *g_vfs_backend_google_new (void);
+
+G_END_DECLS
+
+#endif /* __G_VFS_BACKEND_GOOGLE_H__ */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]