[gvfs/wip/rishi/goa: 3/5] Add GVfsBackendGoogle



commit f28aa631de022d8c7aa67b4cae9b81c5f305b033
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 | 2253 ++++++++++++++++++++++++++++++++++++++++++++
 daemon/gvfsbackendgoogle.h |   62 ++
 8 files changed, 2559 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..3d60a4d 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.17.1 libgdata >= 0.17.2, 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 ddfc1c9..1be228f 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
@@ -430,6 +436,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..e34f08a
--- /dev/null
+++ b/daemon/gvfsbackendgoogle.c
@@ -0,0 +1,2253 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* gvfs - extensions for gio
+ *
+ * Copyright (C) 2009 Thibault Saunier
+ * Copyright (C) 2014, 2015 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 <string.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 "gvfsicon.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;
+  GDataEntry *root;
+  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 CONTENT_TYPE_PREFIX_GOOGLE "application/vnd.google-apps"
+
+#define MAX_RESULTS 50
+
+#define REBUILD_ENTRIES_TIMEOUT 60 /* s */
+
+#define URI_PREFIX "https://www.googleapis.com/drive/v2/files/";
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gchar *
+escape_id (const gchar *id)
+{
+  return g_strconcat ("_", id, NULL);
+}
+
+static gchar *
+escape_filename (const gchar *filename)
+{
+  GString *escaped_filename;
+  gboolean separator_added = FALSE;
+  gchar *basename = NULL;
+  gchar *dirname = NULL;
+  gsize len;
+
+  len = strlen (filename);
+  escaped_filename = g_string_sized_new (len);
+  basename = g_path_get_basename (filename);
+  dirname = g_path_get_dirname (filename);
+
+  while (g_strcmp0 (basename, dirname) != 0)
+    {
+      gchar *escaped_basename;
+      gchar *tmp = NULL;
+
+      escaped_basename = escape_id (basename);
+      g_string_prepend (escaped_filename, escaped_basename);
+      g_string_prepend_c (escaped_filename, G_DIR_SEPARATOR);
+      separator_added = TRUE;
+      g_free (escaped_basename);
+
+      g_free (basename);
+      basename = g_path_get_basename (dirname);
+
+      tmp = dirname;
+      dirname = g_path_get_dirname (dirname);
+      g_free (tmp);
+    }
+
+  if (!separator_added)
+    g_string_prepend_c (escaped_filename, G_DIR_SEPARATOR);
+
+  g_free (basename);
+  g_free (dirname);
+  return g_string_free (escaped_filename, FALSE);
+}
+
+/* Takes a filename as supplied by the application and returns a value
+ * that is either:
+ * (a) guaranteed to be made up of persistent, server-defined IDs, or
+ * (b) can be used to look-up such a persistent filename through the
+ *     lookaside cache
+ */
+static gchar *
+unescape_filename (GVfsBackendGoogle *self,
+                   const gchar       *filename,
+                   gboolean          *out_is_volatile)
+{
+  GString *unescaped_filename;
+  gboolean is_volatile;
+  gboolean separator_added = FALSE;
+  gchar *basename = NULL;
+  gchar *dirname = NULL;
+  gsize len;
+
+  /* If a path is in the lookaside cache then it is volatile, even if
+   * it looks like /_id10/_id2, and we don't perform the usual
+   * unescaping operation. See the copy job for an explanation.
+   */
+  if (g_hash_table_lookup (self->lookaside, filename) != NULL)
+    {
+      is_volatile = TRUE;
+      unescaped_filename = g_string_new (filename);
+      goto out;
+    }
+  else
+    {
+      is_volatile = FALSE;
+    }
+
+  len = strlen (filename);
+  unescaped_filename = g_string_sized_new (len);
+  basename = g_path_get_basename (filename);
+  dirname = g_path_get_dirname (filename);
+
+  while (g_strcmp0 (basename, dirname) != 0)
+    {
+      const gchar *unescaped_basename;
+      gchar *tmp = NULL;
+
+      if (g_str_has_prefix (basename, "_") && !g_str_has_prefix (basename, "__"))
+        {
+          unescaped_basename = basename + 1;
+        }
+      else
+        {
+          unescaped_basename = basename;
+          is_volatile = TRUE;
+        }
+
+      g_string_prepend (unescaped_filename, unescaped_basename);
+      g_string_prepend_c (unescaped_filename, G_DIR_SEPARATOR);
+      separator_added = TRUE;
+
+      g_free (basename);
+      basename = g_path_get_basename (dirname);
+
+      tmp = dirname;
+      dirname = g_path_get_dirname (dirname);
+      g_free (tmp);
+    }
+
+  if (!separator_added)
+    g_string_prepend_c (unescaped_filename, G_DIR_SEPARATOR);
+
+ out:
+  if (out_is_volatile != NULL)
+    *out_is_volatile = is_volatile;
+  g_free (basename);
+  g_free (dirname);
+  return g_string_free (unescaped_filename, FALSE);
+}
+
+/* Specifically meant for differentiating between /_id1/_id2 and
+ * /_id1/foo when creating a file or directory, and unescaping the
+ * dirname.
+ */
+static gchar *
+unescape_basename_and_map_dirname (GVfsBackendGoogle *self,
+                                   const gchar       *filename,
+                                   gboolean          *out_is_volatile)
+{
+  gboolean is_volatile;
+  gboolean is_dirname_volatile;
+  const gchar *unescaped_basename;
+  gchar *basename = NULL;
+  gchar *dirname = NULL;
+  gchar *ret_val = NULL;
+  gchar *unescaped_dirname = NULL;
+
+  basename = g_path_get_basename (filename);
+  if (g_str_has_prefix (basename, "_") && !g_str_has_prefix (basename, "__"))
+    {
+      unescaped_basename = basename + 1;
+      is_volatile = FALSE;
+    }
+  else
+    {
+      unescaped_basename = basename;
+      is_volatile = TRUE;
+    }
+
+  dirname = g_path_get_dirname (filename);
+  unescaped_dirname = unescape_filename (self, dirname, &is_dirname_volatile);
+  if (is_dirname_volatile)
+    {
+      const gchar *real_dirname;
+
+      real_dirname = g_hash_table_lookup (self->lookaside, unescaped_dirname);
+      if (real_dirname == NULL)
+        goto out;
+
+      g_free (unescaped_dirname);
+      unescaped_dirname = g_strdup (real_dirname);
+    }
+
+  ret_val = g_build_filename (unescaped_dirname, unescaped_basename, NULL);
+
+  if (out_is_volatile != NULL)
+    *out_is_volatile = is_volatile;
+
+ out:
+  g_free (unescaped_dirname);
+  g_free (dirname);
+  g_free (basename);
+  return ret_val;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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))
+    {
+      gchar *escaped_filename = NULL;
+
+      escaped_filename = escape_filename (filename);
+      g_vfs_monitor_emit_event (monitor, event, escaped_filename, NULL);
+      g_free (escaped_filename);
+    }
+}
+
+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
+emit_delete_event (gpointer monitor,
+                   gpointer unused,
+                   gpointer filename)
+{
+  emit_event_internal (G_VFS_MONITOR (monitor), filename, G_FILE_MONITOR_EVENT_DELETED);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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)
+{
+  GList *categories;
+  GList *l;
+  gchar *ret_val = NULL;
+
+  categories = gdata_entry_get_categories (entry);
+  for (l = categories; l != NULL; l = l->next)
+    {
+      GDataCategory *category = GDATA_CATEGORY (l->data);
+      const gchar *scheme;
+
+      scheme = gdata_category_get_scheme (category);
+      if (g_strcmp0 (scheme, CATEGORY_SCHEMA_KIND) == 0)
+        {
+          ret_val = g_strdup (gdata_category_get_label (category));
+          break;
+        }
+    }
+
+  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;
+  GMountSpec *spec;
+  gboolean content_type_override = FALSE;
+  gboolean is_folder;
+  gboolean is_root;
+  const gchar *etag;
+  const gchar *id;
+  const gchar *name;
+  const gchar *title;
+  gchar *escaped_name = NULL;
+  gchar *content_type = NULL;
+  gchar *copy_name = NULL;
+  gint64 atime;
+  gint64 ctime;
+  gint64 mtime;
+  gsize i;
+
+  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_is_symlink (info, is_symlink);
+  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_STANDARD_IS_VOLATILE, is_symlink);
+
+  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;
+    }
+
+  spec = g_vfs_backend_get_mount_spec (G_VFS_BACKEND (self));
+
+  if (is_symlink)
+    {
+      const gchar *mount_identity;
+      const gchar *type;
+      gchar *escaped_filename = NULL;
+      gchar *target_uri = NULL;
+
+      file_type = G_FILE_TYPE_SYMBOLIC_LINK;
+
+      type = g_mount_spec_get (spec, "type");
+      mount_identity = g_mount_spec_get (spec, "goa-identity");
+      escaped_filename = escape_filename (filename);
+      target_uri = g_strdup_printf ("%s://%s%s", type, mount_identity, escaped_filename);
+      g_file_info_set_symlink_target (info, target_uri);
+      g_free (target_uri);
+      g_free (escaped_filename);
+    }
+
+  if (content_type != NULL)
+    {
+      GIcon *icon;
+      const gchar *virtual_content_type = content_type;
+
+      /* We want native Drive content to open in the browser. We
+       * accomplish this by creating fake Link-type desktop files.
+       */
+      if (g_str_has_prefix (content_type, CONTENT_TYPE_PREFIX_GOOGLE))
+        {
+          content_type_override = TRUE;
+          virtual_content_type = "application/x-desktop";
+        }
+
+      g_file_info_set_content_type (info, virtual_content_type);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, 
virtual_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;
+
+  id = gdata_entry_get_id (entry);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ID_FILE, id);
+
+  if (is_symlink)
+    {
+      name = symlink_name;
+    }
+  else
+    {
+      escaped_name = escape_id (id);
+      name = escaped_name;
+    }
+
+  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);
+
+  if (content_type_override)
+    copy_name = g_strconcat (title, ".desktop", NULL);
+  else
+    copy_name = g_strdup (title);
+
+  /* Sanitize copy-name by replacing slashes with dashes. This is
+   * what nautilus does (for desktop files).
+   */
+  for (i = 0; copy_name[i] != '\0'; i++)
+    {
+      if (copy_name[i] == '/')
+        copy_name[i] = '-';
+    }
+
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME, copy_name);
+
+  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 *author_name;
+      const gchar *email_address;
+
+      author_name = gdata_author_get_name (author);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_USER_REAL, author_name);
+
+      email_address = gdata_author_get_email_address (author);
+      g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_USER, email_address);
+    }
+
+  etag = gdata_entry_get_etag (entry);
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, etag);
+
+  if (!is_folder)
+    {
+      const gchar *thumbnail_uri;
+
+      thumbnail_uri = gdata_documents_document_get_thumbnail_uri (GDATA_DOCUMENTS_DOCUMENT (entry));
+      if (thumbnail_uri != NULL && thumbnail_uri[0] != '\0')
+        {
+          GIcon *preview;
+
+          preview = g_vfs_icon_new (spec, thumbnail_uri);
+          g_file_info_set_attribute_object (info, G_FILE_ATTRIBUTE_PREVIEW_ICON, G_OBJECT (preview));
+          g_object_unref (preview);
+        }
+    }
+
+ out:
+  g_free (copy_name);
+  g_free (escaped_name);
+  g_free (content_type);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+find_root (GVfsBackendGoogle *self, GCancellable *cancellable, GError **error)
+{
+  GDataAuthorizationDomain *auth_domain;
+
+  if (self->root != NULL)
+    return;
+
+  auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+  self->root = gdata_service_query_single_entry (GDATA_SERVICE (self->service),
+                                                 auth_domain,
+                                                 "root",
+                                                 NULL,
+                                                 GDATA_TYPE_DOCUMENTS_FOLDER,
+                                                 cancellable,
+                                                 error);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gchar *
+get_parent_id (GDataEntry *entry)
+{
+  GList *l;
+  GList *links = NULL;
+  gchar *ret_val = NULL;
+
+  links = gdata_entry_look_up_links (entry, GDATA_LINK_PARENT);
+  for (l = links; l != NULL; l = l->next)
+    {
+      GDataLink *link = GDATA_LINK (l->data);
+      const gchar *uri;
+
+      /* HACK: GDataLink does not have the ID, only the URI. Extract
+       * the ID from the GDataLink:uri by removing the prefix. Ignore
+       * links which don't have the prefix.
+       */
+      uri = gdata_link_get_uri (link);
+      if (g_str_has_prefix (uri, URI_PREFIX))
+        {
+          gsize uri_prefix_len;
+
+          uri_prefix_len = strlen (URI_PREFIX);
+          ret_val = g_strdup (uri + uri_prefix_len);
+          break;
+        }
+    }
+
+  g_list_free (links);
+  return ret_val;
+}
+
+static gchar *
+get_entry_path (GVfsBackendGoogle *self, GDataEntry *entry)
+{
+  GString *path;
+  const gchar *base_id;
+  const gchar *root_id;
+  gchar *id = NULL;
+  gchar *ret_val = NULL;
+
+  base_id = gdata_entry_get_id (entry);
+  path = g_string_new (base_id);
+  g_string_prepend_c (path, '/');
+
+  id = get_parent_id (entry);
+  root_id = gdata_entry_get_id (self->root);
+
+  while (id != NULL)
+    {
+      GDataEntry *parent_entry;
+
+      /* The root folder itself has an ID, so path can become
+       * /root/folder1/folder2/file. Instead, we want it to be
+       * /folder1/folder2/file.
+       */
+
+      if (g_strcmp0 (id, root_id) == 0)
+        break;
+
+      parent_entry = g_hash_table_lookup (self->entries, id);
+      if (parent_entry == NULL)
+        goto out;
+
+      g_string_prepend (path, id);
+      g_string_prepend_c (path, '/');
+
+      g_free (id);
+      id = get_parent_id (parent_entry);
+    }
+
+
+  ret_val = g_strdup (path->str);
+
+ out:
+  g_free (id);
+  g_string_free (path, TRUE);
+  return ret_val;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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 succeeded_once = FALSE;
+
+  local_error = NULL;
+  find_root (self, cancellable, &local_error);
+  if (local_error != NULL)
+    {
+      g_propagate_error (error, local_error);
+      goto out;
+    }
+
+  query = gdata_documents_query_new_with_limits (NULL, 1, MAX_RESULTS);
+  gdata_documents_query_set_show_folders (query, TRUE);
+
+  while (TRUE)
+    {
+      GList *entries;
+      GList *l;
+
+      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);
+            }
+
+          break;
+        }
+
+      if (!succeeded_once)
+        {
+          g_hash_table_remove_all (self->entries);
+          succeeded_once = TRUE;
+        }
+
+      entries = gdata_feed_get_entries (GDATA_FEED (feed));
+      if (entries == NULL)
+        break;
+
+      for (l = entries; l != NULL; l = l->next)
+        {
+          GDataEntry *entry = GDATA_ENTRY (l->data);
+          const gchar *id;
+
+          id = gdata_entry_get_id (entry);
+          g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (entry));
+        }
+
+      gdata_query_next_page (GDATA_QUERY (query));
+      g_clear_object (&feed);
+    }
+
+ 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 = NULL;
+  GInputStream *stream = G_INPUT_STREAM (source_object);
+  GVfsJobCloseRead *job = G_VFS_JOB_CLOSE_READ (user_data);
+
+  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 void
+g_vfs_backend_google_copy (GVfsBackend           *_self,
+                           GVfsJobCopy           *job,
+                           const gchar           *source,
+                           const gchar           *destination,
+                           GFileCopyFlags         flags,
+                           GFileProgressCallback  progress_callback,
+                           gpointer               progress_callback_data)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataDocumentsEntry *new_entry = NULL;
+  GDataDocumentsFolder *destination_parent;
+  GDataEntry *source_entry;
+  GError *error;
+  gboolean is_folder;
+  gboolean is_root;
+  gboolean is_volatile_source;
+  const gchar *id;
+  const gchar *real_destination_parent_path;
+  const gchar *real_parent_path;
+  const gchar *real_source;
+  gchar *destination_parent_id = NULL;
+  gchar *destination_parent_path = NULL;
+  gchar *path = NULL;
+  gchar *source_id = NULL;
+  gchar *unescaped_destination = NULL;
+  gchar *unescaped_source = NULL;
+
+  /* The destination is always volatile. It will look like /_id1/_id2
+   * -> /_id10/_id2, whereas in reality it will be /_id1/_id2 ->
+   * /_id10/_id20. This will confuse our escaping logic when
+   * /_id10/_id2 is passed to a any subsequent job, and make it think
+   * that it is a non-volatile path. Hence, we add /_id10/_id2 to the
+   * lookaside cache at the end of the copy so that subsequent jobs
+   * can recognize it as a volatile path.
+   */
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_destination = unescape_basename_and_map_dirname (self, destination, NULL);
+  unescaped_source = unescape_filename (self, source, &is_volatile_source);
+  g_debug ("+ copy: %s (%s, %d) -> %s (%s, 1)\n",
+           source,
+           unescaped_source,
+           is_volatile_source,
+           destination,
+           unescaped_destination);
+  source = unescaped_source;
+
+  error = NULL;
+  find_root (self, cancellable, &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  if (is_volatile_source)
+    {
+      real_source = g_hash_table_lookup (self->lookaside, source);
+      if (real_source == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      source = real_source;
+    }
+
+  source_id = g_path_get_basename (source);
+  source_entry = g_hash_table_lookup (self->entries, source_id);
+
+  destination_parent_path = g_path_get_dirname (unescaped_destination);
+  real_destination_parent_path = g_hash_table_lookup (self->lookaside, destination_parent_path);
+  if (real_destination_parent_path != NULL)
+    {
+      g_free (destination_parent_path);
+      destination_parent_path = g_strdup (real_destination_parent_path);
+    }
+
+  is_folder_or_root (destination_parent_path, NULL, NULL, &is_root);
+  if (is_root)
+    {
+      destination_parent = GDATA_DOCUMENTS_FOLDER (self->root);
+    }
+  else
+    {
+      destination_parent_id = g_path_get_basename (destination_parent_path);
+      destination_parent = g_hash_table_lookup (self->entries, destination_parent_id);
+    }
+
+  if (source_entry == NULL || destination_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;
+        }
+    }
+
+  source_entry = g_hash_table_lookup (self->entries, source_id);
+  is_folder_or_root (source, source_entry, &is_folder, NULL);
+  if (is_folder)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job),
+                        G_IO_ERROR,
+                        G_IO_ERROR_WOULD_RECURSE,
+                        _("Can't recursively copy directory"));
+      goto out;
+    }
+
+  destination_parent = g_hash_table_lookup (self->entries, destination_parent_id);
+  if (source_entry == NULL || destination_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;
+    }
+
+  error = NULL;
+  new_entry = gdata_documents_service_add_entry_to_folder (self->service,
+                                                           GDATA_DOCUMENTS_ENTRY (source_entry),
+                                                           destination_parent,
+                                                           cancellable,
+                                                           &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  id = gdata_entry_get_id (GDATA_ENTRY (new_entry));
+  g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (new_entry));
+
+  path = get_entry_path (self, GDATA_ENTRY (new_entry));
+  if (path != NULL)
+    {
+      g_debug ("  insert lookaside: %s -> %s\n", destination, path);
+      g_hash_table_insert (self->lookaside, g_strdup (destination), g_strdup (path));
+      g_hash_table_foreach (self->monitors, emit_create_event, (gpointer) path);
+    }
+
+  self->entries_stale = TRUE;
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&new_entry);
+  g_free (path);
+  g_free (destination_parent_id);
+  g_free (destination_parent_path);
+  g_free (source_id);
+  g_free (unescaped_source);
+  g_free (unescaped_destination);
+  g_debug ("- copy\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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;
+  gboolean is_volatile;
+  gchar *id = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ create_dir_monitor: %s (%s, %d)\n", filename, unescaped_filename, is_volatile);
+  filename = unescaped_filename;
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      filename = real_filename;
+    }
+
+  id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, 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, 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 (id);
+  g_free (unescaped_filename);
+  g_debug ("- create_dir_monitor\n");
+  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;
+  gboolean is_volatile;
+  gchar *path = NULL;
+  gchar *id = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ delete: %s (%s, %d)\n", filename, unescaped_filename, is_volatile);
+  filename = unescaped_filename;
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      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;
+    }
+
+  id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, 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, 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 ();
+  path = get_entry_path (self, entry);
+
+  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, id);
+  if (path != NULL)
+    g_hash_table_foreach (self->monitors, emit_delete_event, (gpointer) path);
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_free (path);
+  g_free (id);
+  g_free (unescaped_filename);
+  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;
+  GDataEntry *entry;
+  GError *error;
+  GHashTableIter iter;
+  gboolean is_volatile;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ enumerate: %s (%s, %d)\n", filename, unescaped_filename, is_volatile);
+  filename = unescaped_filename;
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      filename = real_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));
+
+  g_hash_table_iter_init (&iter, self->entries);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &entry))
+    {
+      gchar *path;
+
+      path = get_entry_path (self, entry);
+      if (path != NULL)
+        {
+          gchar *parent_path;
+
+          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_free (unescaped_filename);
+  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;
+  GDataDocumentsEntry *new_entry = NULL;
+  GDataDocumentsFolder *folder = NULL;
+  GDataDocumentsFolder *parent;
+  GDataEntry *source_entry = NULL;
+  GError *error;
+  gboolean is_display_name;
+  gboolean is_root;
+  gboolean needs_rebuild = FALSE;
+  const gchar *id;
+  const gchar *real_parent_path;
+  gchar *parent_id = NULL;
+  gchar *parent_path = NULL;
+  gchar *path = NULL;
+  gchar *source_id = NULL;
+  gchar *title = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_basename_and_map_dirname (self, filename, &is_display_name);
+  g_debug ("+ make_directory: %s (%s, %d)\n", filename, unescaped_filename, is_display_name);
+
+  if (unescaped_filename == 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 (unescaped_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;
+    }
+
+  error = NULL;
+  find_root (self, 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 (unescaped_filename);
+  is_folder_or_root (parent_path, NULL, NULL, &is_root);
+  if (is_root)
+    {
+      parent = GDATA_DOCUMENTS_FOLDER (self->root);
+    }
+  else
+    {
+      parent_id = g_path_get_basename (parent_path);
+      parent = g_hash_table_lookup (self->entries, parent_id);
+      if (parent == NULL)
+        needs_rebuild = TRUE;
+    }
+
+  if (is_display_name)
+    {
+      title = g_path_get_basename (unescaped_filename);
+    }
+  else
+    {
+      source_id = g_path_get_basename (unescaped_filename);
+      source_entry = g_hash_table_lookup (self->entries, source_id);
+      if (source_entry == NULL)
+        needs_rebuild = TRUE;
+    }
+
+  if (needs_rebuild)
+    {
+      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;
+    }
+
+  if (!is_display_name)
+    {
+      source_entry = g_hash_table_lookup (self->entries, source_id);
+      if (source_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;
+        }
+
+      title = g_strdup (gdata_entry_get_title (source_entry));
+    }
+
+  folder = gdata_documents_folder_new (NULL);
+  gdata_entry_set_title (GDATA_ENTRY (folder), title);
+
+  error = NULL;
+  new_entry = gdata_documents_service_add_entry_to_folder (self->service,
+                                                           GDATA_DOCUMENTS_ENTRY (folder),
+                                                           parent,
+                                                           cancellable,
+                                                           &error);
+  if (error != NULL)
+    {
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      goto out;
+    }
+
+  id = gdata_entry_get_id (GDATA_ENTRY (new_entry));
+  g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (new_entry));
+
+  path = get_entry_path (self, GDATA_ENTRY (new_entry));
+  if (path != NULL)
+    {
+      g_debug ("  insert lookaside: %s -> %s\n", filename, path);
+      g_hash_table_insert (self->lookaside, g_strdup (filename), g_strdup (path));
+      g_hash_table_foreach (self->monitors, emit_create_event, (gpointer) path);
+    }
+
+  self->entries_stale = TRUE;
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&new_entry);
+  g_clear_object (&folder);
+  g_free (source_id);
+  g_free (parent_id);
+  g_free (parent_path);
+  g_free (path);
+  g_free (title);
+  g_free (unescaped_filename);
+  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);
+  GError *error;
+  GList *accounts = NULL;
+  GList *l;
+  gboolean account_found = FALSE;
+  const gchar *mount_identity;
+
+  g_debug ("+ mount\n");
+
+  if (self->client == NULL)
+    {
+      error = NULL;
+      self->client = get_goa_client_sync (&error);
+      if (error != NULL)
+        {
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          g_error_free (error);
+          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;
+  GInputStream *stream;
+  GError *error;
+  gboolean is_folder;
+  gboolean is_root;
+  gboolean is_volatile;
+  gchar *content_type = NULL;
+  gchar *id = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ open_for_read: %s (%s, %d)\n", filename, unescaped_filename, is_volatile);
+  filename = unescaped_filename;
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      filename = real_filename;
+    }
+
+  id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, 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, 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;
+    }
+
+  content_type = get_content_type_from_entry (entry);
+  if (content_type == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Unknown content type"));
+      goto out;
+    }
+
+  /* We want native Drive content to open in the browser. We
+   * accomplish this by creating fake Link-type desktop files.
+   */
+  if (g_str_has_prefix (content_type, CONTENT_TYPE_PREFIX_GOOGLE))
+    {
+      GDataLink *alternate;
+      GKeyFile *file;
+      const gchar *title;
+      const gchar *uri;
+      gchar *data;
+      gsize length;
+
+      file = g_key_file_new ();
+
+      title = gdata_entry_get_title (entry);
+      g_key_file_set_string (file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, title);
+      g_key_file_set_string (file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TYPE, "Link");
+
+      alternate = gdata_entry_look_up_link (entry, GDATA_LINK_ALTERNATE);
+      uri = gdata_link_get_uri (alternate);
+      g_key_file_set_string (file, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, uri);
+
+      data = g_key_file_to_data (file, &length, NULL);
+      stream = g_memory_input_stream_new_from_data (data, (gssize) length, g_free);
+
+      g_key_file_free (file);
+    }
+  else
+    {
+      GDataAuthorizationDomain *auth_domain;
+      const gchar *uri;
+
+      auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+      uri = gdata_entry_get_content_uri (entry);
+      stream = gdata_download_stream_new (GDATA_SERVICE (self->service), auth_domain, uri, cancellable);
+    }
+
+  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 (content_type);
+  g_free (id);
+  g_free (unescaped_filename);
+  g_debug ("- open_for_read\n");
+  g_rec_mutex_unlock (&self->mutex);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+g_vfs_backend_google_open_icon_for_read (GVfsBackend            *_self,
+                                         GVfsJobOpenIconForRead *job,
+                                         const gchar            *icon_id)
+{
+  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
+  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
+  GDataAuthorizationDomain *auth_domain;
+  GInputStream *stream;
+
+  g_debug ("+ open_icon_for_read: %s\n", icon_id);
+
+  auth_domain = gdata_documents_service_get_primary_authorization_domain ();
+  stream = gdata_download_stream_new (GDATA_SERVICE (self->service), auth_domain, icon_id, cancellable);
+  if (stream == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Failed to initiate download"));
+      goto out;
+    }
+
+  g_vfs_job_open_for_read_set_handle (G_VFS_JOB_OPEN_FOR_READ (job), stream);
+  g_vfs_job_open_for_read_set_can_seek (G_VFS_JOB_OPEN_FOR_READ (job), TRUE);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_debug ("- open_icon_for_read\n");
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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 *id;
+  const gchar *real_parent_path;
+  const gchar *title;
+  gchar *parent_id = NULL;
+  gchar *parent_path = NULL;
+  gchar *path = NULL;
+  gchar *unescaped_destination = NULL;
+
+  /* The destination is always volatile. It will look like /foo/bar
+   * -> /_id1/bar, whereas in reality it will be /foo/bar ->
+   * /_id1/_id2.
+   */
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_destination = unescape_basename_and_map_dirname (self, destination, NULL);
+  g_debug ("+ push: %s -> %s (%s, 1)\n", local_path, destination, unescaped_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 (unescaped_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;
+            }
+        }
+    }
+
+  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;
+    }
+
+  id = gdata_entry_get_id (GDATA_ENTRY (new_document));
+  g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (new_document));
+
+  path = get_entry_path (self, GDATA_ENTRY (new_document));
+  if (path != NULL)
+    {
+      g_debug ("  insert lookaside: %s -> %s\n", destination, path);
+      g_hash_table_insert (self->lookaside, g_strdup (destination), g_strdup (path));
+      g_hash_table_foreach (self->monitors, emit_create_event, (gpointer) path);
+    }
+
+  self->entries_stale = TRUE;
+
+  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 (unescaped_destination);
+  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;
+  gboolean is_volatile;
+  gchar *id = NULL;
+  gchar *symlink_name = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ query_info: %s (%s, %d), %d\n", filename, unescaped_filename, is_volatile, flags);
+  filename = unescaped_filename;
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      filename = real_filename;
+      if ((flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS) != 0)
+        {
+          is_symlink = TRUE;
+          symlink_name = g_path_get_basename (filename);
+        }
+    }
+
+  id = g_path_get_basename (filename);
+  entry = g_hash_table_lookup (self->entries, 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, 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 (id);
+  g_free (symlink_name);
+  g_free (unescaped_filename);
+  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 = get_entry_path (self, entry);
+  if (path == NULL)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or directory"));
+      goto out;
+    }
+
+  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 void
+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");
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+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;
+  gboolean is_volatile;
+  const gchar *new_id;
+  gchar *id = NULL;
+  gchar *unescaped_filename = NULL;
+
+  g_rec_mutex_lock (&self->mutex);
+  unescaped_filename = unescape_filename (self, filename, &is_volatile);
+  g_debug ("+ set_display_name: %s (%s, %d), %s\n", filename, unescaped_filename, is_volatile, display_name);
+
+  if (is_volatile)
+    {
+      const gchar *real_filename;
+
+      real_filename = g_hash_table_lookup (self->lookaside, unescaped_filename);
+      if (real_filename == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or 
directory"));
+          goto out;
+        }
+
+      g_free (unescaped_filename);
+      unescaped_filename = g_strdup (real_filename);
+    }
+
+  is_folder_or_root (unescaped_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;
+    }
+
+  id = g_path_get_basename (unescaped_filename);
+  entry = g_hash_table_lookup (self->entries, 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, 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, id);
+
+  new_id = gdata_entry_get_id (new_entry);
+  g_hash_table_insert (self->entries, g_strdup (new_id), g_object_ref (new_entry));
+
+  g_vfs_job_set_display_name_set_new_path (job, filename);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+ out:
+  g_clear_object (&new_entry);
+  g_free (id);
+  g_free (unescaped_filename);
+  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->root);
+  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->copy = g_vfs_backend_google_copy;
+  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->open_icon_for_read = g_vfs_backend_google_open_icon_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->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)
+{
+  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;
+}
+
+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]