/* -*- 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 "gvfsjobcopy.h"
#include "gvfsjobcreatemonitor.h"
#include "gvfsjobenumerate.h"
#include "gvfsjobopenforread.h"
#include "gvfsjobopenforwrite.h"
#include "gvfsjobqueryfsinfo.h"
#include "gvfsjobread.h"
#include "gvfsjobseekread.h"
#include "gvfsjobsetdisplayname.h"
#include "gvfsjobwrite.h"
#include "gvfsmonitor.h"
#include "gvfsdaemonutils.h"

struct _GVfsBackendGoogle
{
  GVfsBackend parent;
  GDataDocumentsService *service;
  GDataEntry *root;
  GDataEntry *home;
  GDataEntry *shared_with_me_dir;
  GDataEntry *shared_drives_dir;
  GHashTable *entries; /* gchar *entry_id -> GDataEntry */
  GHashTable *dir_entries; /* DirEntriesKey -> GDataEntry */
  GHashTable *dir_timestamps; /* gchar *entry_id -> gint64 *timestamp */
  GHashTable *monitors;
  GList *dir_collisions;
  GList *shared_drives;
  GRecMutex mutex; /* guards cache */
  GoaClient *client;
  gchar *account_identity;
};

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 REBUILD_ENTRIES_TIMEOUT 60 /* s */

#define URI_PREFIX "https://www.googleapis.com/drive/v2/files/"

#define SOURCE_ID_PROPERTY_KEY "GVfsSourceID"
#define PARENT_ID_PROPERTY_KEY "GVfsParentID"

#define ROOT_ID "GVfsRoot"
#define SHARED_WITH_ME_ID "GVfsSharedWithMe"
#define SHARED_DRIVES_ID "GVfsSharedDrives"
/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  gchar *title_or_id;
  gchar *parent_id;
} DirEntriesKey;

typedef struct
{
  GDataEntry *document;
  GDataUploadStream *stream;
  gchar *filename;
  gchar *entry_path;
} WriteHandle;

static GDataEntry *resolve_dir (GVfsBackendGoogle  *self,
                                const gchar        *filename,
                                GCancellable       *cancellable,
                                gchar             **out_basename,
                                gchar             **out_path,
                                GError            **error);

/* ---------------------------------------------------------------------------------------------------- */

static DirEntriesKey *
dir_entries_key_new (const gchar *title_or_id, const gchar *parent_id)
{
  DirEntriesKey *k;

  k = g_slice_new0 (DirEntriesKey);
  k->title_or_id = g_strdup (title_or_id);
  k->parent_id = g_strdup (parent_id);
  return k;
}

static void
dir_entries_key_free (gpointer data)
{
  DirEntriesKey *k = (DirEntriesKey *) data;

  if (k == NULL)
    return;

  g_free (k->title_or_id);
  g_free (k->parent_id);
  g_slice_free (DirEntriesKey, k);
}

static guint
entries_in_folder_hash (gconstpointer key)
{
  DirEntriesKey *k = (DirEntriesKey *) key;
  guint hash1;
  guint hash2;

  hash1 = g_str_hash (k->title_or_id);
  hash2 = g_str_hash (k->parent_id);
  return hash1 ^ hash2;
}

static gboolean
entries_in_folder_equal (gconstpointer a, gconstpointer b)
{
  DirEntriesKey *k_a = (DirEntriesKey *) a;
  DirEntriesKey *k_b = (DirEntriesKey *) b;

  if (g_strcmp0 (k_a->title_or_id, k_b->title_or_id) == 0 &&
      g_strcmp0 (k_a->parent_id, k_b->parent_id) == 0)
    return TRUE;

  return FALSE;
}

/* ---------------------------------------------------------------------------------------------------- */

static void
log_dir_entries (GVfsBackendGoogle *self)
{
  GHashTableIter iter;
  GDataEntry *entry;
  DirEntriesKey *key;
  GList *l;

  if (!g_getenv ("GVFS_GOOGLE_DEBUG"))
    return;

  g_hash_table_iter_init (&iter, self->dir_entries);
  while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &entry))
    {
      g_debug ("  Real ID = %s, (%s, %s) -> %p, %d\n",
               gdata_entry_get_id (GDATA_ENTRY (entry)),
               key->title_or_id,
               key->parent_id,
               entry,
               ((GObject *) entry)->ref_count);
    }

  for (l = self->dir_collisions; l != NULL; l = l->next)
    {
      entry = GDATA_ENTRY (l->data);
      g_debug ("* Real ID = %s, (%s) -> %p, %d\n",
               gdata_entry_get_id (entry),
               gdata_entry_get_title (entry),
               entry,
               ((GObject *) entry)->ref_count);
    }
}

/* ---------------------------------------------------------------------------------------------------- */

static WriteHandle *
write_handle_new (GDataEntry *document, GDataUploadStream *stream, const gchar *filename, const gchar *entry_path)
{
  WriteHandle *handle;

  handle = g_slice_new0 (WriteHandle);

  if (document != NULL)
    handle->document = g_object_ref (document);

  if (stream != NULL)
    {
      handle->stream = g_object_ref (stream);
      if (handle->document == NULL)
        handle->document = g_object_ref (gdata_upload_stream_get_entry (stream));
    }

  handle->filename = g_strdup (filename);
  handle->entry_path = g_strdup (entry_path);

  return handle;
}

static void
write_handle_free (gpointer data)
{
  WriteHandle *handle = (WriteHandle *) data;

  if (handle == NULL)
    return;

  g_clear_object (&handle->document);
  g_clear_object (&handle->stream);
  g_free (handle->filename);
  g_free (handle->entry_path);
  g_slice_free (WriteHandle, handle);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
sanitize_error (GError **error)
{
  if (g_error_matches (*error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED) ||
      g_error_matches (*error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_FORBIDDEN))
    {
      g_warning ("%s", (*error)->message);
      g_clear_error (error);
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, _("Permission denied"));
    }
  else if (g_error_matches (*error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_NOT_FOUND))
    {
      g_warning ("%s", (*error)->message);
      g_clear_error (error);
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("Target object doesn’t exist"));
    }
}

/* ---------------------------------------------------------------------------------------------------- */

static void
emit_event_internal (GVfsMonitor       *monitor,
                     const gchar       *entry_path,
                     GFileMonitorEvent  event)
{
  const gchar *monitored_path;
  gchar *parent_path;

  if (entry_path == NULL)
    return;

  monitored_path = g_object_get_data (G_OBJECT (monitor), "g-vfs-backend-google-path");
  parent_path = g_path_get_dirname (entry_path);

  if (g_strcmp0 (parent_path, monitored_path) == 0)
    {
      g_debug ("  emit event %d on parent directory for %s\n", event, entry_path);
      g_vfs_monitor_emit_event (monitor, event, entry_path, NULL);
    }
  else if (g_strcmp0 (entry_path, monitored_path) == 0)
    {
      g_debug ("  emit event %d on file %s\n", event, entry_path);
      g_vfs_monitor_emit_event (monitor, event, entry_path, NULL);
    }

  g_free (parent_path);
}

static void
emit_attribute_changed_event (gpointer monitor,
                              gpointer unused,
                              gpointer entry_path)
{
  emit_event_internal (G_VFS_MONITOR (monitor), entry_path, G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED);
}

static void
emit_changed_event (gpointer monitor,
                    gpointer unused,
                    gpointer entry_path)
{
  emit_event_internal (G_VFS_MONITOR (monitor), entry_path, G_FILE_MONITOR_EVENT_CHANGED);
}

static void
emit_changes_done_event (gpointer monitor,
                         gpointer unused,
                         gpointer entry_path)
{
  emit_event_internal (G_VFS_MONITOR (monitor), entry_path, G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT);
}

static void
emit_create_event (gpointer monitor,
                   gpointer unused,
                   gpointer entry_path)
{
  emit_event_internal (G_VFS_MONITOR (monitor), entry_path, G_FILE_MONITOR_EVENT_CREATED);
}

static void
emit_delete_event (gpointer monitor,
                   gpointer unused,
                   gpointer entry_path)
{
  emit_event_internal (G_VFS_MONITOR (monitor), entry_path, G_FILE_MONITOR_EVENT_DELETED);
}

/* ---------------------------------------------------------------------------------------------------- */

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 GList *
get_parent_ids (GVfsBackendGoogle *self,
                GDataEntry        *entry)
{
  GList *l;
  GList *links = NULL;
  GList *ids = 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))
        {
          const gchar *id;
          gsize uri_prefix_len;

          uri_prefix_len = strlen (URI_PREFIX);
          id = uri + uri_prefix_len;
          if (id[0] != '\0')
            {
              ids = g_list_prepend (ids, g_strdup (id));
            }
        }
    }

  g_list_free (links);
  return ids;
}

/* ---------------------------------------------------------------------------------------------------- */

static gint64
count_files_in_directory_with_title (GVfsBackendGoogle *self,
                                     const gchar       *entry_title,
                                     GDataEntry        *directory)
{
  gint64 num_same_title_files = 0;
  GHashTableIter iter;
  GDataEntry *entry;
  DirEntriesKey *key;

  g_hash_table_iter_init (&iter, self->dir_entries);
  while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &entry))
    {
      if (g_strcmp0 (entry_title, gdata_entry_get_title (entry)) == 0 &&
          g_strcmp0 (key->parent_id, gdata_entry_get_id (GDATA_ENTRY (directory))) == 0 &&
          g_strcmp0 (key->title_or_id, gdata_entry_get_id (entry)) == 0)
        num_same_title_files++;
    }

  return num_same_title_files;
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
is_owner (GVfsBackendGoogle *self, GDataEntry *entry)
{
  GList *l;
  GDataGoaAuthorizer *goa_authorizer;
  GoaAccount *account;
  const gchar *account_identity;

  goa_authorizer = GDATA_GOA_AUTHORIZER (gdata_service_get_authorizer (GDATA_SERVICE (self->service)));
  account = goa_object_peek_account (gdata_goa_authorizer_get_goa_object (goa_authorizer));
  account_identity = goa_account_get_identity (account);

  for (l = gdata_entry_get_authors (entry); l != NULL; l = l->next)
    {
      GDataAuthor *author = GDATA_AUTHOR (l->data);

      if (g_strcmp0 (gdata_author_get_email_address (author), account_identity) == 0) {
          return TRUE;
      }
    }

  return FALSE;
}

static gboolean
is_shared_with_me (GDataEntry *entry)
{
  return gdata_documents_entry_get_shared_with_me_date (GDATA_DOCUMENTS_ENTRY (entry)) > 0;
}

/* ---------------------------------------------------------------------------------------------------- */

static const gchar *
_gdata_documents_entry_get_property_value (GDataDocumentsEntry *entry,
                                           const gchar         *key)
{
  GList *l = NULL;
  GList *document_properties;
  GDataDocumentsProperty *property;

  property = gdata_documents_property_new (key);
  gdata_documents_property_set_visibility (property, GDATA_DOCUMENTS_PROPERTY_VISIBILITY_PRIVATE);

  document_properties = gdata_documents_entry_get_document_properties (GDATA_DOCUMENTS_ENTRY (entry));
  l = g_list_find_custom (document_properties, property, (GCompareFunc) gdata_comparable_compare);

  g_clear_object (&property);
  if (l && l->data)
    return gdata_documents_property_get_value (GDATA_DOCUMENTS_PROPERTY (l->data));
  else
    return NULL;
}

/* ---------------------------------------------------------------------------------------------------- */

/* Since, we now store both the mappings (title, parent_id) -> GDataEntry
 * and (volatile_entry_id, parent_id) -> GDataEntry in the dir_entries hash
 * table, so a case may arise that some (entry_id, parent_id) matches some
 * other entry's (volatile_entry_id, parent_id) tuple. This means that we have
 * a collision on the volatile entry with some other entry's actual ID.
 *
 * The below helper function checks if dir_entries contains a value
 * corresponding to key specified by argument k. If such a value exists, it
 * then checks if both the entries returned have the same ID or not. In case
 * that they have same ID, both entries are guaranteed to be same whereas if the
 * two IDs differ, it means that a collision has ocurred. In this case, we return
 * the ID of the volatile_entry.
 *
 * Semantically, we may have returned a boolean value, but in the case of a
 * collision, we need the volatile entry's ID to log meaningful debug output.
 * So, we decide to return string or NULL. */
static const gchar *
get_volatile_entry_id (GVfsBackendGoogle *self, GDataEntry *entry, DirEntriesKey *k)
{
  GDataEntry *child = NULL;
  const gchar *child_id;

  if ((child = g_hash_table_lookup (self->dir_entries, k)) != NULL)
    {
      child_id = gdata_entry_get_id (child);

      /* Be sure that we are not matching title/id of volatile file. This
       * needs to specifically be taken care of while having files with
       * multiple titles, otherwise we may remove an entry which belonged
       * to some other file and errorneously decrement the ref count of
       * that entry (causing seg-faults later). */
      if (g_strcmp0 (gdata_entry_get_id (entry), child_id) == 0)
        return NULL;
      else
        return child_id;
    }

  return NULL;
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
insert_entry_full (GVfsBackendGoogle *self,
                   GDataEntry        *entry,
                   gboolean           track_dir_collisions)
{
  DirEntriesKey *k;
  GDataEntry *old_entry;
  GDataEntry *volatile_entry = NULL;
  gboolean retval = TRUE;
  gboolean insert_title = TRUE;
  const gchar *id;
  const gchar *old_id;
  const gchar *title;
  const gchar *source_id_property;
  GList *parent_ids, *l, *ll;

  id = gdata_entry_get_id (entry);
  title = gdata_entry_get_title (entry);

  g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (entry));

  source_id_property = _gdata_documents_entry_get_property_value (GDATA_DOCUMENTS_ENTRY (entry),
                                                                  SOURCE_ID_PROPERTY_KEY);

  parent_ids = get_parent_ids (self, entry);
  for (l = parent_ids; l != NULL; l = l->next)
    {
      const gchar *parent_id_property;
      gchar *parent_id = l->data;

      k = dir_entries_key_new (id, parent_id);
      old_entry = g_hash_table_lookup (self->dir_entries, k);

      /* Below case happens when I have an entry (say "entry1") which has a GVfsSourceID
       * property (because of a copy operation performed earlier) and the entry
       * corresponding to value of GVfsSourceID property (say "entry2") is moved to the same
       * parent (i.e. the parent of "entry1"), then the entries will collide on
       * the title as well as there will be a contention for inserting real ID
       * of "entry2" vs volatile entry of "entry1".
       *
       * So, we give priority to real ID of "entry1" to maintain the condition that each
       * entry's real ID must be in dir_entries hash table. */
      if (old_entry != NULL && g_strcmp0 (gdata_entry_get_id (old_entry), id) != 0)
        {
          if ((ll = g_list_find (self->dir_collisions, old_entry)) == NULL)
            {
              self->dir_collisions = g_list_prepend (self->dir_collisions, g_object_ref (old_entry));
              g_debug ("  insert_entry: Ejected volatile  (colliding real)  (%s, (%s, %s)) -> %p, %d\n", id, k->title_or_id, k->parent_id, old_entry, ((GObject *)old_entry)->ref_count);
            }
        }

      g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));
      g_debug ("  insert_entry: Inserted real     (%s, %s) -> %p\n", id, parent_id, entry);

      k = dir_entries_key_new (title, parent_id);
      old_entry = g_hash_table_lookup (self->dir_entries, k);
      if (old_entry != NULL)
        {
          old_id = gdata_entry_get_id (old_entry);
          g_debug ("  title collision of %s with: (%s, %s) -> %p\n", id, title, parent_id, old_entry);
          if (g_strcmp0 (old_id, title) == 0)
            {
              insert_title = FALSE;
            }
          else
            {
              /* If the collision is not due to the title matching the ID
               * of an earlier GDataEntry, then it is due to duplicate
               * titles.
               */
              if (g_strcmp0 (old_id, id) < 0)
                insert_title = FALSE;
            }
        }

      if (insert_title)
        {
          if (old_entry != NULL && track_dir_collisions)
            {
              if ((ll = g_list_find (self->dir_collisions, old_entry)) == NULL)
                {
                  self->dir_collisions = g_list_prepend (self->dir_collisions, g_object_ref (old_entry));

                  g_debug ("  insert_entry: Ejected title    (%s, (%s, %s)) -> %p, %d\n", old_id, title, parent_id, old_entry, ((GObject *)old_entry)->ref_count);
                }
            }

          g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));
          g_debug ("  insert_entry: Inserted title    (%s, %s) -> %p\n", title, parent_id, entry);
        }
      else
        {
          if (track_dir_collisions)
            {
              if ((ll = g_list_find (self->dir_collisions, entry)) == NULL)
                {
                  self->dir_collisions = g_list_prepend (self->dir_collisions, g_object_ref (entry));
                  g_debug ("  insert_entry: Skipped title    (%s, %s, %s) -> %p, %d\n", id, title, parent_id, entry, ((GObject *)entry)->ref_count);
                }
            }

          dir_entries_key_free (k);
        }

      parent_id_property = _gdata_documents_entry_get_property_value (GDATA_DOCUMENTS_ENTRY (entry),
                                                                      PARENT_ID_PROPERTY_KEY);

      if (source_id_property && (g_strcmp0 (parent_id_property, parent_id) == 0))
        {
          k = dir_entries_key_new (source_id_property, parent_id);
          volatile_entry = g_hash_table_lookup (self->dir_entries, k);

          if (volatile_entry == NULL)
            {
              g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));
              g_debug ("  insert_entry: Inserted volatile (%s, %s) -> %p\n", source_id_property, parent_id, entry);
            }
          else
            {
              /* This case only occurs in the following scenario:
               * Suppose I have a file with title "t1" and ID "id1". Suppose we
               * wish to copy this file "t1" over to some folder "folder1".
               * Now, firstly I touch (create) a new file in "folder1" and I
               * set the title of this new file to "id1". So, this file has its
               * ID (say "id2") and title "id1".
               *
               * Now, we copy the file "id1" into "folder1". When we're trying
               * to do so, the volatile path entry which needs to be inserted
               * consists of mapping `("id1", "folder1_ID") -> GDataEntry` but
               * since we had already created the file with title "id1" inside
               * of "folder1", we get a collision of a volatile entry with some
               * file's actual title. If we ignore this case here, the code
               * will definitely work and the copy operation will succeed but
               * when `query_info` is performed on the entry
               * `("id1", "folder1_ID") -> GDataEntry`, we will get the older
               * file and not the newer one (which nautilus tries to find). So,
               * we treat this case just like that of a title collision.
               *
               * This case shouldn't normally happen since it entails that file
               * names be completely gibberish (like IDs), but for the sake of
               * completeness, we handle it here. */
              if (track_dir_collisions)
                {
                  /* The below string comparison is used to check if the
                   * (id, parent_id) tuple corresponds to some other entry's real ID or
                   * of volatile entry. It may so happen that some file which is to
                   * be moved/copied will have a volatile entry kept inside
                   * GVfsSourceID GDataDocumentsProperty, and that this ID equals
                   * to some other file's real ID. The highest precedence is given
                   * to real ID, hence we put the volatile entry (here, `id`) into
                   * the dir_collisions list. The inherent knowledge here is that
                   * two different entries can never have same real ID. */
                  if (g_strcmp0 (k->title_or_id, gdata_entry_get_id (volatile_entry)) == 0)
                    {
                      if ((ll = g_list_find (self->dir_collisions, entry)) == NULL)
                        {
                          self->dir_collisions = g_list_prepend (self->dir_collisions, g_object_ref (entry));
                          g_debug ("  insert_entry: Inserted volatile to dir_collisions (%s, (%s, %s)) -> %p, %d\n",
                                   gdata_entry_get_id (volatile_entry),
                                   source_id_property,
                                   parent_id,
                                   volatile_entry,
                                   ((GObject *)entry)->ref_count);
                        }
                    }
                  else
                    {
                      /* (id, parent_id) tuple maps to some other entry (here volatile_entry's)
                       * real ID. So, we prepend `entry` to dir_collisions instead of
                       * ejecting volatile_entry. */

                      if ((ll = g_list_find (self->dir_collisions, entry)) == NULL)
                        {
                          self->dir_collisions = g_list_prepend (self->dir_collisions, g_object_ref (volatile_entry));
                          g_debug ("  insert_entry: Ejected volatile  (%s, (%s, %s)) -> %p, %d\n",
                                   gdata_entry_get_id (volatile_entry),
                                   source_id_property,
                                   parent_id,
                                   volatile_entry,
                                   ((GObject *)volatile_entry)->ref_count);
                        }

                      g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));
                      g_debug ("  insert_entry: Inserted volatile (%s, %s) -> %p, %d\n", source_id_property, parent_id, entry, ((GObject *)entry)->ref_count);
                    }
                }
            }
        }

      retval &= insert_title && (!source_id_property ||  !volatile_entry);
    }
  g_list_free_full (parent_ids, g_free);

  return retval;
}

static void
insert_entry (GVfsBackendGoogle *self,
              GDataEntry        *entry)
{
  gint64 *timestamp;

  timestamp = g_new (gint64, 1);
  *timestamp =  g_get_real_time ();
  g_object_set_data_full (G_OBJECT (entry), "timestamp", timestamp, g_free);

  insert_entry_full (self, entry, TRUE);
}

static void
insert_custom_entry (GVfsBackendGoogle *self,
                     GDataEntry        *entry,
                     const gchar       *parent_id)
{
  DirEntriesKey *k;
  const gchar *id;
  const gchar *title;

  id = gdata_entry_get_id (entry);
  title = gdata_entry_get_title (entry);

  g_hash_table_insert (self->entries, g_strdup (id), g_object_ref (entry));

  k = dir_entries_key_new (id, parent_id);
  g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));

  k = dir_entries_key_new (title, parent_id);
  g_hash_table_insert (self->dir_entries, k, g_object_ref (entry));
}

static void
remove_entry_full (GVfsBackendGoogle *self,
                   GDataEntry        *entry,
                   gboolean           restore_dir_collisions)
{
  DirEntriesKey *k;
  GList *l;
  const gchar *id;
  const gchar *title;
  GList *parent_ids, *ll;

  id = gdata_entry_get_id (entry);
  title = gdata_entry_get_title (entry);

  g_hash_table_remove (self->entries, id);

  if (is_shared_with_me (entry))
    g_hash_table_remove (self->dir_timestamps, SHARED_WITH_ME_ID);

  parent_ids = get_parent_ids (self, entry);
  for (ll = parent_ids; ll != NULL; ll = ll->next)
    {
      const gchar *source_id_property = NULL;
      const gchar *parent_id_property = NULL;
      const gchar *volatile_entry_id = NULL;
      gchar *parent_id = ll->data;

      source_id_property = _gdata_documents_entry_get_property_value (GDATA_DOCUMENTS_ENTRY (entry),
                                                                      SOURCE_ID_PROPERTY_KEY);

      g_hash_table_remove (self->dir_timestamps, parent_id);

      k = dir_entries_key_new (id, parent_id);
      if (g_hash_table_remove (self->dir_entries, k))
        g_debug ("  remove_entry: Removed real      (%s, %s) -> %p\n", id, parent_id, entry);
      dir_entries_key_free (k);

      k = dir_entries_key_new (title, parent_id);

      if ((volatile_entry_id = get_volatile_entry_id (self, entry, k)) != NULL)
        g_debug ("  skipping removal of %s as it is volatile path for %s\n", id, volatile_entry_id);
      else if (g_hash_table_remove (self->dir_entries, k))
        g_debug ("  remove_entry: Removed title     (%s, %s) -> %p\n", title, parent_id, entry);

      dir_entries_key_free (k);

      parent_id_property = _gdata_documents_entry_get_property_value (GDATA_DOCUMENTS_ENTRY (entry),
                                                                      PARENT_ID_PROPERTY_KEY);
      if (source_id_property && (g_strcmp0 (parent_id_property, parent_id) == 0))
        {
          k = dir_entries_key_new (source_id_property, parent_id);

          if ((volatile_entry_id = get_volatile_entry_id (self, entry, k)) != NULL)
            g_debug ("  skipping removal of %s as it is volatile path for %s\n", id, volatile_entry_id);
          else if (g_hash_table_remove (self->dir_entries, k))
            g_debug ("  remove_entry: Removed volatile  (%s, %s) -> %p\n", source_id_property, parent_id, entry);

          dir_entries_key_free (k);
        }

      /* Lets remove the entry from the dir_collisions list as well. It's
       * guaranteed that at a time, only 1 entry will exist in dir_collisions */
      l = g_list_find (self->dir_collisions, entry);
      if (l != NULL)
        {
          self->dir_collisions = g_list_remove_link (self->dir_collisions, l);
          g_object_unref (entry);
          g_list_free (l);
        }

      if (restore_dir_collisions)
        {
          for (l = self->dir_collisions; l != NULL; l = l->next)
            {
              GDataEntry *colliding_entry = GDATA_ENTRY (l->data);

              if (insert_entry_full (self, colliding_entry, FALSE))
                {
                  self->dir_collisions = g_list_remove_link (self->dir_collisions, l);
                  g_debug ("  remove_entry: Restored %p\n", colliding_entry);
                  g_list_free (l);
                  g_object_unref (colliding_entry);
                }
            }
        }
    }
  g_list_free_full (parent_ids, g_free);
}

static void
remove_entry (GVfsBackendGoogle *self,
              GDataEntry        *entry)
{
  remove_entry_full (self, entry, TRUE);
}

static void
remove_dir (GVfsBackendGoogle *self,
            GDataEntry        *parent)
{
  GHashTableIter iter;
  GDataEntry *entry;
  gchar *parent_id;
  GList *l;

  /* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
  parent_id = g_strdup (gdata_entry_get_id (parent));

  g_hash_table_remove (self->dir_timestamps, parent_id);

  g_hash_table_iter_init (&iter, self->entries);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &entry))
    {
      DirEntriesKey *k;
      const gchar *volatile_entry_id;

      k = dir_entries_key_new (gdata_entry_get_id (entry), parent_id);

      if (g_hash_table_lookup (self->dir_entries, k) != NULL)
        {
          if ((volatile_entry_id = get_volatile_entry_id (self, entry, k)) != NULL)
            {
              g_debug ("Skipping removal of (%s, %s) as it is volatile path for %s\n",
                       k->title_or_id,
                       k->parent_id,
                       volatile_entry_id);
              dir_entries_key_free (k);
              continue;
            }

          g_object_ref (entry);
          g_hash_table_iter_remove (&iter);
          remove_entry_full (self, entry, FALSE);
          g_object_unref (entry);
        }

      dir_entries_key_free (k);
    }

  for (l = self->dir_collisions; l != NULL; l = l->next)
  {
    GDataEntry *colliding_entry = GDATA_ENTRY (l->data);

    if (insert_entry_full (self, colliding_entry, FALSE))
      {
        self->dir_collisions = g_list_remove_link (self->dir_collisions, l);
        g_debug ("  remove_entry: Restored %p\n", colliding_entry);
        g_list_free (l);
        g_object_unref (colliding_entry);
      }
  }

  g_free (parent_id);
}

static gboolean
is_entry_valid (GDataEntry *entry)
{
  gint64 *timestamp;

  timestamp = g_object_get_data (G_OBJECT (entry), "timestamp");
  if (timestamp == NULL)
    return TRUE;

  return (g_get_real_time () - *timestamp < REBUILD_ENTRIES_TIMEOUT * G_USEC_PER_SEC);
}

static gboolean
is_dir_listing_valid (GVfsBackendGoogle *self, GDataEntry *parent)
{
  gint64 *timestamp;

  if (parent == self->root)
    return TRUE;

  timestamp = g_hash_table_lookup (self->dir_timestamps, gdata_entry_get_id (parent));
  if (timestamp != NULL)
    return (g_get_real_time () - *timestamp < REBUILD_ENTRIES_TIMEOUT * G_USEC_PER_SEC);

  return FALSE;
}

static void
rebuild_shared_drives_dir (GVfsBackendGoogle  *self,
                           GCancellable       *cancellable,
                           GError            **error)

{
  GDataAuthorizationDomain *auth_domain;
  GList *l;
  gint64 *timestamp;

  auth_domain = gdata_documents_service_get_primary_authorization_domain ();

  remove_dir (self, self->shared_drives_dir);

  for (l = self->shared_drives; l != NULL; l = l->next)
    {
      GDataDocumentsDrive *drive = GDATA_DOCUMENTS_DRIVE (l->data);
      GDataEntry *entry = NULL;

      entry = gdata_service_query_single_entry (GDATA_SERVICE (self->service),
                                                auth_domain,
                                                gdata_entry_get_id (GDATA_ENTRY (drive)),
                                                NULL,
                                                GDATA_TYPE_DOCUMENTS_FOLDER,
                                                cancellable,
                                                error);
      if (entry == NULL)
        return;

      /* Replace "My Drive" title by the real name of the Drive. */
      gdata_entry_set_title (entry, gdata_documents_drive_get_name (drive));

      insert_custom_entry (self, entry, SHARED_DRIVES_ID);

      g_object_unref (entry);
    }

  timestamp = g_new (gint64, 1);
  *timestamp = g_get_real_time ();
  g_hash_table_insert (self->dir_timestamps, SHARED_DRIVES_ID, timestamp);
}

static void
rebuild_dir (GVfsBackendGoogle  *self,
             GDataEntry         *parent,
             GCancellable       *cancellable,
             GError            **error)
{
  GDataDocumentsFeed *feed = NULL;
  GDataDocumentsQuery *query = NULL;
  gboolean succeeded_once = FALSE;
  gchar *search;
  gchar *parent_id;

  if (parent == self->shared_drives_dir)
    {
      rebuild_shared_drives_dir (self, cancellable, error);
      return;
    }

  /* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
  parent_id = g_strdup (gdata_entry_get_id (parent));

  if (parent == self->shared_with_me_dir)
    search = g_strdup ("sharedWithMe");
  else
    search = g_strdup_printf ("'%s' in parents", parent_id);
  query = gdata_documents_query_new_with_limits (search, 1, G_MAXUINT);
  gdata_documents_query_set_show_folders (query, TRUE);
  g_free (search);

  while (TRUE)
    {
      GError *local_error;
      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)
        {
          sanitize_error (&local_error);
          g_propagate_error (error, local_error);

          goto out;
        }

      if (!succeeded_once)
        {
          gint64 *timestamp;

          remove_dir (self, parent);

          timestamp = g_new (gint64, 1);
          *timestamp = g_get_real_time ();
          g_hash_table_insert (self->dir_timestamps, g_strdup (parent_id), timestamp);

          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);
          insert_entry (self, entry);
        }

      gdata_query_next_page (GDATA_QUERY (query));
      g_clear_object (&feed);
    }

 out:
  g_clear_object (&feed);
  g_clear_object (&query);
  g_free (parent_id);
}

/* ---------------------------------------------------------------------------------------------------- */

static GDataEntry *
resolve_child (GVfsBackendGoogle  *self,
               GDataEntry         *parent,
               const gchar        *basename,
               GCancellable       *cancellable,
               GError            **error)
{
  DirEntriesKey *k;
  GDataEntry *entry;
  const gchar *parent_id;
  GError *local_error = NULL;
  gboolean is_shared_with_me_dir = (parent == self->shared_with_me_dir);

  parent_id = gdata_entry_get_id (parent);
  k = dir_entries_key_new (basename, parent_id);

  if (is_shared_with_me_dir)
    entry = g_hash_table_lookup (self->entries, basename);
  else
    entry = g_hash_table_lookup (self->dir_entries, k);

  if ((entry == NULL && !is_dir_listing_valid (self, parent)) ||
      (entry != NULL && !is_entry_valid (entry)))
    {
      rebuild_dir (self, parent, cancellable, &local_error);
      if (local_error != NULL)
        {
          g_propagate_error (error, local_error);
          goto out;
        }

      if (is_shared_with_me_dir)
        entry = g_hash_table_lookup (self->entries, basename);
      else
        entry = g_hash_table_lookup (self->dir_entries, k);
    }

  if (entry == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file or directory"));
      goto out;
    }

 out:
  dir_entries_key_free (k);

  return entry;
}

static GDataEntry *
resolve (GVfsBackendGoogle  *self,
         const gchar        *filename,
         GCancellable       *cancellable,
         gchar             **out_path,
         GError            **error)
{
  GDataEntry *parent;
  GDataEntry *ret_val = NULL;
  GError *local_error;
  gchar *basename = NULL;

  g_assert (filename && filename[0] == '/');

  if (g_strcmp0 (filename, "/") == 0)
    {
      ret_val = self->root;

      if (out_path != NULL)
        *out_path = g_strdup ("/");

      goto out;
    }

  local_error = NULL;
  parent = resolve_dir (self, filename, cancellable, &basename, out_path, &local_error);
  if (local_error != NULL)
    {
      g_propagate_error (error, local_error);
      goto out;
    }

  ret_val = resolve_child (self, parent, basename, cancellable, &local_error);
  if (ret_val == NULL)
    {
      /* This fallback provides volatile entries for URIs which was used
       * before My Drive folder was added in the root. */
      if (parent == self->root)
        {
          g_clear_error (&local_error);
          ret_val = resolve_child (self, self->home, basename, cancellable, &local_error);
          if (ret_val != NULL && out_path != NULL)
            {
              gchar *tmp;
              tmp = g_build_path ("/", *out_path, gdata_entry_get_id (self->home), NULL);
              g_free (*out_path);
              *out_path = tmp;
            }
        }

      if (ret_val == NULL)
        {
          g_propagate_error (error, local_error);
          goto out;
        }
    }

  if (out_path != NULL)
    {
      gchar *tmp;
      tmp = g_build_path ("/", *out_path, gdata_entry_get_id (ret_val), NULL);
      g_free (*out_path);
      *out_path = tmp;
    }

 out:
  g_free (basename);
  return ret_val;
}

static GDataEntry *
resolve_dir (GVfsBackendGoogle  *self,
             const gchar        *filename,
             GCancellable       *cancellable,
             gchar             **out_basename,
             gchar             **out_path,
             GError            **error)
{
  GDataEntry *parent;
  GDataEntry *ret_val = NULL;
  GError *local_error;
  gchar *basename = NULL;
  gchar *parent_path = NULL;

  basename = g_path_get_basename (filename);
  parent_path = g_path_get_dirname (filename);

  local_error = NULL;
  parent = resolve (self, parent_path, cancellable, out_path, &local_error);
  if (local_error != NULL)
    {
      g_propagate_error (error, local_error);
      goto out;
    }

  if (!GDATA_IS_DOCUMENTS_FOLDER (parent))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY, _("The file is not a directory"));
      goto out;
    }

  if (out_basename != NULL)
    {
      *out_basename = basename;
      basename = NULL;
    }

  ret_val = parent;

 out:
  g_free (basename);
  g_free (parent_path);
  return ret_val;
}

/* ---------------------------------------------------------------------------------------------------- */

static const char *
get_extension_offset (const char *title)
{
  const gchar *end;
  const gchar *end2;

  end = strrchr (title, '.');

  if (end != NULL && end != title)
    {
      if (g_strcmp0 (end, ".gz") == 0 ||
          g_strcmp0 (end, ".bz2") == 0 ||
          g_strcmp0 (end, ".sit") == 0 ||
          g_strcmp0 (end, ".zip") == 0 ||
          g_strcmp0 (end, ".Z") == 0)
        {
          end2 = end - 1;
          while (end2 > title && *end2 != '.')
            end2--;

          if (end2 != title)
            end = end2;
        }
    }

  return end;
}

static gchar *
generate_copy_name (GVfsBackendGoogle *self, GDataEntry *entry, const gchar *entry_path)
{
  GDataEntry *existing_entry;
  GDataEntry *parent;
  const gchar *id;
  const gchar *title;
  gchar *extension = NULL;
  const gchar *extension_offset;
  gchar *ret_val = NULL;
  gchar *title_without_extension = NULL;

  title = gdata_entry_get_title (entry);

  parent = resolve_dir (self, entry_path, NULL, NULL, NULL, NULL);
  if (parent == NULL)
    goto out;

  existing_entry = resolve_child (self, parent, title, NULL, NULL);
  if (existing_entry == entry)
    goto out;

  title_without_extension = g_strdup (title);
  extension_offset = get_extension_offset (title_without_extension);
  if (extension_offset != NULL && extension_offset != title_without_extension)
    {
      extension = g_strdup (extension_offset);
      title_without_extension[extension_offset - title_without_extension] = '\0';
    }

  id = gdata_entry_get_id (entry);
  ret_val = g_strdup_printf ("%s - %s%s", title_without_extension, id, (extension == NULL) ? "" : extension);

 out:
  if (ret_val == NULL)
    ret_val = g_strdup (title);
  g_free (extension);
  g_free (title_without_extension);
  return ret_val;
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
is_native_file (GDataEntry *entry)
{
  gchar *content_type;
  gboolean ret = FALSE;

  content_type = get_content_type_from_entry (entry);
  if (content_type != NULL && g_str_has_prefix (content_type, CONTENT_TYPE_PREFIX_GOOGLE))
    ret = TRUE;

  g_free (content_type);

  return ret;
}

static void
build_file_info (GVfsBackendGoogle      *self,
                 GDataEntry             *entry,
                 GFileQueryInfoFlags     flags,
                 GFileInfo              *info,
                 GFileAttributeMatcher  *matcher,
                 const gchar            *filename,
                 const gchar            *entry_path,
                 GError                **error)
{
  GFileType file_type;
  GList *authors;
  gboolean is_folder = FALSE;
  gboolean is_root = FALSE;
  gboolean is_symlink = FALSE;
  const gchar *etag;
  const gchar *id;
  const gchar *name;
  const gchar *title;
  gchar *escaped_name = NULL;
  gchar *content_type = NULL;
  gchar *copy_name = NULL;
  gchar *symlink_name = NULL;
  gint64 atime;
  gint64 ctime;
  gint64 mtime;
  gsize i;
  gboolean is_shared_with_me_dir = (entry == self->shared_with_me_dir);
  gboolean is_home = (entry == self->home);
  gboolean is_shared_drives_dir = (entry == self->shared_drives_dir);
  gboolean can_edit;

  if (GDATA_IS_DOCUMENTS_FOLDER (entry))
    is_folder = TRUE;

  if (entry == self->root)
    is_root = TRUE;

  if (filename != NULL && g_strcmp0 (entry_path, filename) != 0) /* volatile */
    {
      is_symlink = TRUE;
      symlink_name = g_path_get_basename (filename);
    }

  /* TODO: It is not always possible to rename, delete, or list children.
   * However, the proper implementation of gdata_documents_entry_can_rename/
   * _delete/_list_children would require port to Google Drive API v3. */
  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME,
                                     !is_root && !is_home && !is_shared_with_me_dir && !is_shared_drives_dir);

  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, is_folder);

  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);
  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE,
                                     !is_root && !is_home && !is_shared_with_me_dir && !is_shared_drives_dir);

  can_edit = gdata_documents_entry_can_edit (GDATA_DOCUMENTS_ENTRY (entry));
  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE,
                                     !is_root && !is_shared_with_me_dir && !is_shared_drives_dir && can_edit);
  g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_READ, TRUE);

  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;

      /* We want native Drive content to open in the browser. */
      if (is_native_file (entry))
        {
          GDataLink *alternate;
          const gchar *uri;

          file_type = G_FILE_TYPE_SHORTCUT;
          alternate = gdata_entry_look_up_link (entry, GDATA_LINK_ALTERNATE);
          uri = gdata_link_get_uri (alternate);
          g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, uri);
        }
      else
        {
          goffset size;

          size = gdata_documents_entry_get_file_size (GDATA_DOCUMENTS_ENTRY (entry));
          g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_SIZE, (guint64) size);
        }
    }

  if (is_symlink)
    {
      if (flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
        {
          g_free (content_type);
          content_type = g_strdup ("inode/symlink");
          file_type = G_FILE_TYPE_SYMBOLIC_LINK;
        }

      g_file_info_set_symlink_target (info, entry_path);
    }

  if (content_type != NULL)
    {
      GIcon *icon;
      GIcon *symbolic_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);

      if (is_home)
        {
          icon = g_themed_icon_new_with_default_fallbacks ("user-home");
          symbolic_icon = g_themed_icon_new_with_default_fallbacks ("user-home-symbolic");
        }
      else if (is_shared_with_me_dir)
        {
          icon = g_themed_icon_new_with_default_fallbacks ("folder-publicshare");
          symbolic_icon = g_themed_icon_new_with_default_fallbacks ("folder-publicshare-symbolic");
        }
      else if (is_shared_drives_dir)
        {
          icon = g_themed_icon_new_with_default_fallbacks ("folder-remote");
          symbolic_icon = g_themed_icon_new_with_default_fallbacks ("folder-remote-symbolic");
        }
      else
        {
          icon = g_content_type_get_icon (content_type);
          symbolic_icon = g_content_type_get_symbolic_icon (content_type);
        }

      g_file_info_set_icon (info, icon);
      g_file_info_set_symbolic_icon (info, symbolic_icon);

      g_object_unref (icon);
      g_object_unref (symbolic_icon);
    }

  g_file_info_set_file_type (info, file_type);

  id = gdata_entry_get_id (entry);
  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ID_FILE, id);

  if (is_root)
    name = "/";
  else if (is_symlink)
    name = symlink_name;
  else
    name = id;

  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 (is_root || is_home || is_shared_with_me_dir || is_shared_drives_dir)
    goto out;

  copy_name = generate_copy_name (self, entry, entry_path);

  /* 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;
          GMountSpec *spec;

          spec = g_vfs_backend_get_mount_spec (G_VFS_BACKEND (self));
          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 (symlink_name);
  g_free (copy_name);
  g_free (escaped_name);
  g_free (content_type);
}

/* ---------------------------------------------------------------------------------------------------- */

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
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 *dummy_source_entry = NULL;
  GDataDocumentsEntry *new_entry = NULL;
  GDataEntry *destination_parent;
  GDataEntry *source_parent;
  GDataEntry *existing_entry;
  GDataEntry *source_entry;
  GError *error;
  GType source_entry_type;
  const gchar *etag;
  const gchar *id;
  const gchar *source_parent_id;
  const gchar *destination_parent_id;
  const gchar *summary;
  const gchar *title;
  const gchar *dummy_entry_filename;
  gboolean will_overwrite = FALSE;
  gchar *destination_basename = NULL;
  gchar *entry_path = NULL;
  goffset size;
  gchar *parent_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ copy: %s -> %s, %d\n", source, destination, flags);
  log_dir_entries (self);

  if (flags & G_FILE_COPY_BACKUP)
    {
      /* Return G_IO_ERROR_NOT_SUPPORTED instead of
       * G_IO_ERROR_CANT_CREATE_BACKUP to proceed with the GIO
       * fallback copy.
       */
      g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  error = NULL;
  source_entry = resolve (self, source, cancellable, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  source_parent = resolve_dir (self, source, cancellable, NULL, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  destination_parent = resolve_dir (self, destination, cancellable, &destination_basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (source_entry == self->root || source_parent == self->root || source_parent == self->shared_drives_dir ||
      destination_parent == self->root || destination_parent == self->shared_with_me_dir ||
      destination_parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  id = gdata_entry_get_id (source_entry);
  title = gdata_entry_get_title (source_entry);
  source_parent_id = gdata_entry_get_id (source_parent);
  destination_parent_id = gdata_entry_get_id (destination_parent);

  g_debug ("  wants overwrite: %d\n", flags & G_FILE_COPY_OVERWRITE);

  existing_entry = resolve_child (self, destination_parent, destination_basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      gint64 num_same_title_files;

      num_same_title_files = count_files_in_directory_with_title (self,
                                                                  destination_basename,
                                                                  destination_parent);

      g_debug ("  count of files with title same as destination_basename: %ld\n", num_same_title_files);

      if (flags & G_FILE_COPY_OVERWRITE)
        {
          if (num_same_title_files > 1)
            {
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }

          /* If the destination-basename matches that of some other entry, we
           * further check if the files have the same ID or not. If the source
           * file as well as the destination file have same ID, we fail the
           * operation. */
          if (g_strcmp0 (gdata_entry_get_id (existing_entry), gdata_entry_get_id (source_entry)) == 0)
            {
              /* We return G_IO_ERROR_FAILED below instead of
               * G_IO_ERROR_NOT_SUPPORTED because _NOT_SUPPORTED implies that the
               * operation hasn't been implemented yet, and hence GIO takes a
               * fallback to perform the operation. So, for copy operation with
               * overwrite, it uses a (replace + read + write) fallback which in
               * turn overwrites the file even though we don't support overwrites.
               *
               * For a concrete case see this discussion:
               * https://gitlab.gnome.org/GNOME/gvfs/merge_requests/58#note_584083
               */
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }
          else if (g_strcmp0 (gdata_entry_get_id (existing_entry), destination_basename) == 0 ||
                   g_strcmp0 (gdata_entry_get_title (existing_entry), destination_basename) == 0)
            will_overwrite = TRUE;
          else
            {
              /* Neither the ID nor the title of existing_entry matches the destination_basename.
               * This can only happen when destination_basename corresponds to the volatile entry
               * being pointed to by some other file. We simply ignore it and let the copy
               * operation to happen. */
            }
        }
      else
        {
          /* We just check for two conditions here (out of a total of 3 we checked above when
           * G_FILE_COPY_OVERWRITE is specified). The first condition "num_same_title_files > 0"
           * checks if we have multiple files with same destination_name in the destination
           * directory, whereas the second condition specifically checks for the case when we have a
           * file whose ID is same as that of source file (i.e. the same file with multiple
           * parents).
           *
           * In the case our destination_name matches the volatile entry of some other file, we
           * simply ignore it and let the copy operation to take place. */
          if (num_same_title_files > 0 ||
              g_strcmp0 (gdata_entry_get_id (existing_entry), destination_basename) == 0)
            {
              g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
              goto out;
            }
        }
    }

  /* For the cases like `gio copy id1 ./folder/`, the operation actually resolves to its longer form
   * `gio copy id1 ./folder/id1`. We cater such cases differently and check if another file with the
   * same title as that of file with real ID id1 exists in the destination folder. This check allows
   * us to be as similar to POSIX filesystem operations as possible. This way, we can make
   * title-based copy-overwrite operations to work in nautilus. */
  if (g_strcmp0 (destination_basename, id) == 0)
    {
      existing_entry = resolve_child (self, destination_parent, title, cancellable, NULL);
      if (existing_entry != NULL)
        {
          gint64 num_same_title_files;

          num_same_title_files = count_files_in_directory_with_title (self, title, destination_parent);
          g_debug ("  count of same title files: %ld\n", num_same_title_files);

          if (flags & G_FILE_COPY_OVERWRITE)
            {
              if (num_same_title_files > 1)
                {
                  /* We return G_IO_ERROR_FAILED below instead of
                   * G_IO_ERROR_NOT_SUPPORTED because _NOT_SUPPORTED implies that the
                   * operation hasn't been implemented yet, and hence GIO takes a
                   * fallback to perform the operation. So, for copy operation with
                   * overwrite, it uses a (replace + read + write) fallback which in
                   * turn overwrites the file even though we don't support overwrites.
                   *
                   * For a concrete case see this discussion:
                   * https://gitlab.gnome.org/GNOME/gvfs/merge_requests/58#note_584083
                   */
                  g_vfs_job_failed_literal (G_VFS_JOB (job),
                                            G_IO_ERROR,
                                            G_IO_ERROR_FAILED,
                                            _("Operation not supported"));
                  goto out;
                }

              if (GDATA_IS_DOCUMENTS_FOLDER (existing_entry))
                {
                  if (GDATA_IS_DOCUMENTS_FOLDER (source_entry))
                    {
                      g_vfs_job_failed (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_WOULD_MERGE,
                                        _("Can’t copy directory over directory"));
                      goto out;
                    }
                  else
                    {
                      g_vfs_job_failed (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_IS_DIRECTORY,
                                        _("Can’t copy file over directory"));
                      goto out;
                    }
                }

              will_overwrite = (num_same_title_files == 1);
            }
          else
            {
              /* If the title matches the volatile path of some
               * other file (given by existing_entry) and if the titles are
               * different, move operation should be allowed. In that case, we will
               * be having two different entries (each with different titles), but
               * the stored volatile path (in GDataDocumentsProperty) will be same.
               *
               * This isn't an issue since we've already handled those extra
               * entries in insert_entry_full().
               */
              if (num_same_title_files > 0)
                {
                  g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
                  goto out;
                }
            }
        }
    }

  g_debug ("  will overwrite: %d\n", will_overwrite);

  /* We again resolve the source_entry after checking existing_entry. This is
   * because when we try to find existing_entry, resolve_child is called which
   * internally calls rebuild_dir function. Now, if between the initial
   * resolution of source_entry and the resolution of existing_entry, the
   * source_entry gets invalidated (possibly due to elapsing of
   * REBUILD_ENTRIES_TIMEOUT seconds), rebuild_dir will update the entry
   * internally in the structures but will free our source_entry. This results
   * in a segfault in the below step.
   *
   * This case was observed when doing the following set of operations:
   *    copy --> rename copied file (but don't refresh nautilus) --> copy renamed file
   *    in same directory */
  source_entry = resolve (self, source, cancellable, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (GDATA_IS_DOCUMENTS_FOLDER (source_entry))
    {
      g_vfs_job_failed (G_VFS_JOB (job),
                        G_IO_ERROR,
                        G_IO_ERROR_WOULD_RECURSE,
                        _("Can’t recursively copy directory"));
      goto out;
    }

  id = gdata_entry_get_id (source_entry);
  etag = gdata_entry_get_etag (source_entry);
  summary = gdata_entry_get_summary (source_entry);
  title = gdata_entry_get_title (source_entry);

  source_entry_type = G_OBJECT_TYPE (source_entry);

  /* When a file is copied to the same folder, Google Drive provides a "Make a
   * copy" option which creates a new file and changes its title from "Foobar.pdf"
   * to "Copy of Foobar". But instead here, nautilus does the heavy-lifting and
   * creates the destination file name as "Foobar (copy).pdf".
   *
   * Moreover, just after copy operation, a query_info operation is performed
   * and it needs the ("Foobar (copy).pdf", destination_parent_id) -> Entry mapping
   * in the cache. Hence, we set the new entry's filename conditionally. */
  if (g_strcmp0 (destination_basename, id) == 0)
    dummy_entry_filename = gdata_entry_get_title (source_entry);
  else
    {
      /* Case occurs when trying to overwrite an existing file using destination_basename
       * set to ID2 in `gio copy ID1 ./folder/ID2`. We need to retain the title of the
       * source file instead of setting dummy_entry_filename to destination_basename. */
      if (will_overwrite && existing_entry &&
          g_strcmp0 (gdata_entry_get_id (existing_entry), destination_basename) == 0)
        dummy_entry_filename = title;
      else
        dummy_entry_filename = destination_basename;
    }

  dummy_source_entry = g_object_new (source_entry_type,
                                     "etag", etag,
                                     "id", id,
                                     "summary", summary,
                                     "title", dummy_entry_filename,
                                     NULL);

  /*
   * When a file is copied inside the same parent, we don't add any
   * GDataDocumentsProperty on it. This is done so that whenever the new_entry
   * gets inserted into the cache, it doesn't overwrite the mapping
   * (source_id, source_parent) -> (GDataEntry of source)
   * with (source_id, source_parent) -> (GDataEntry of new_entry).
   *
   * Moreover, in this case, just after the copy operation, nautilus performs a
   * query_info searching for the new title of the file (to check whether file
   * has actually been created). Our new_entry already has its title set to
   * destination_basename, hence we don't need to add any extra property here.
   */
  if (g_strcmp0 (source_parent_id, destination_parent_id) != 0)
    {
      GDataDocumentsProperty *source_id_property, *parent_id_property;

      source_id_property = gdata_documents_property_new (SOURCE_ID_PROPERTY_KEY);
      gdata_documents_property_set_visibility (source_id_property, GDATA_DOCUMENTS_PROPERTY_VISIBILITY_PRIVATE);
      gdata_documents_property_set_value (source_id_property, id);

      parent_id_property = gdata_documents_property_new (PARENT_ID_PROPERTY_KEY);
      gdata_documents_property_set_visibility (parent_id_property, GDATA_DOCUMENTS_PROPERTY_VISIBILITY_PRIVATE);
      gdata_documents_property_set_value (parent_id_property, destination_parent_id);

      gdata_documents_entry_add_documents_property (GDATA_DOCUMENTS_ENTRY (dummy_source_entry), source_id_property);
      gdata_documents_entry_add_documents_property (GDATA_DOCUMENTS_ENTRY (dummy_source_entry), parent_id_property);

      g_clear_object (&source_id_property);
      g_clear_object (&parent_id_property);
    }

  error = NULL;
  new_entry = gdata_documents_service_add_entry_to_folder (self->service,
                                                           dummy_source_entry,
                                                           GDATA_DOCUMENTS_FOLDER (destination_parent),
                                                           cancellable,
                                                           &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (GDATA_ENTRY (new_entry)), NULL);
  g_debug ("  new entry path: %s\n", entry_path);

  insert_entry (self, GDATA_ENTRY (new_entry));
  g_hash_table_foreach (self->monitors, emit_create_event, entry_path);

  if (will_overwrite)
    {
      GDataDocumentsEntry *entry_to_remove = NULL;
      guint parent_ids_len;
      GList *parent_ids;
      gchar *existing_entry_path = NULL;

      /* The internal ref count has to be increased before removing the
       * existing_entry since remove_entry_full calls g_object_unref() internally */
      g_object_ref (existing_entry);
      remove_entry (self, existing_entry);

      parent_ids = get_parent_ids (self, existing_entry);
      parent_ids_len = g_list_length (parent_ids);
      if (parent_ids_len > 1 || !is_owner (self, GDATA_ENTRY (existing_entry)))
        {
          /* gdata_documents_service_remove_entry_from_folder () returns the
           * updated entry variable provided as argument with an increased ref.
           * The ref count after the next line shall be 2. */
          entry_to_remove = gdata_documents_service_remove_entry_from_folder (self->service,
                                                                              GDATA_DOCUMENTS_ENTRY (existing_entry),
                                                                              GDATA_DOCUMENTS_FOLDER (destination_parent),
                                                                              cancellable,
                                                                              &error);
        }
      else
        {
          GDataAuthorizationDomain *auth_domain;

          auth_domain = gdata_documents_service_get_primary_authorization_domain ();
          gdata_service_delete_entry (GDATA_SERVICE (self->service), auth_domain, existing_entry, cancellable, &error);
        }

      if (error != NULL)
        {
          sanitize_error (&error);
          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
          g_error_free (error);
          g_clear_object (&entry_to_remove);
          goto out;
        }

      existing_entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (existing_entry), NULL);
      g_hash_table_foreach (self->monitors, emit_delete_event, existing_entry_path);

      g_object_unref (existing_entry);
      g_clear_object (&entry_to_remove);
    }

  size = gdata_documents_entry_get_file_size (new_entry);
  g_vfs_job_progress_callback (size, size, job);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_clear_object (&dummy_source_entry);
  g_clear_object (&new_entry);
  g_free (destination_basename);
  g_free (entry_path);
  g_free (parent_path);
  log_dir_entries (self);
  g_debug ("- copy\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_move (GVfsBackend           *_self,
                           GVfsJobMove           *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;
  GDataAuthorizationDomain *auth_domain;
  GDataEntry *new_entry = NULL;
  GDataEntry *destination_parent;
  GDataEntry *source_parent;
  GDataEntry *existing_entry;
  GDataEntry *source_entry = NULL;
  GDataLink *source_parent_link = NULL;
  GDataLink *destination_parent_link = NULL;
  GError *error = NULL;
  gboolean will_overwrite = FALSE;
  const gchar *source_id;
  const gchar *source_parent_id;
  const gchar *destination_parent_id;
  const gchar *title;
  gchar *source_parent_link_uri = NULL;
  gchar *destination_parent_link_uri = NULL;
  gchar *destination_basename = NULL;
  gchar *entry_path = NULL;
  gchar *parent_path = NULL;
  goffset size;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ move: %s -> %s, %d\n", source, destination, flags);
  log_dir_entries (self);

  if (flags & G_FILE_COPY_BACKUP)
    {
      /* Return G_IO_ERROR_NOT_SUPPORTED instead of
       * G_IO_ERROR_CANT_CREATE_BACKUP to proceed with the GIO
       * fallback copy.
       */
      g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  source_entry = resolve (self, source, cancellable, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  source_parent = resolve_dir (self, source, cancellable, NULL, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  destination_parent = resolve_dir (self, destination, cancellable, &destination_basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (source_entry == self->root || source_parent == self->root || source_parent == self->shared_drives_dir ||
      destination_parent == self->root || destination_parent == self->shared_with_me_dir ||
      destination_parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  source_id = gdata_entry_get_id (source_entry);
  source_parent_id = gdata_entry_get_id (source_parent);
  destination_parent_id = gdata_entry_get_id (destination_parent);
  title = gdata_entry_get_title (source_entry);
  auth_domain = gdata_documents_service_get_primary_authorization_domain ();

  g_debug ("  wants overwrite: %d\n", flags & G_FILE_COPY_OVERWRITE);

  existing_entry = resolve_child (self, destination_parent, destination_basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      const gchar *existing_entry_id;
      gint64 num_same_title_files;

      existing_entry_id = gdata_entry_get_id (existing_entry);
      num_same_title_files = count_files_in_directory_with_title (self,
                                                                  destination_basename,
                                                                  destination_parent);

      g_debug ("  count of files with title same as destination_basename: %ld\n", num_same_title_files);

      /* We don't support overwrites, so we don't need to care
       * about G_IO_ERROR_IS_DIRECTORY and G_IO_ERROR_WOULD_MERGE.
       *
       * Moreover, we don't need to take care of G_IO_ERROR_WOULD_RECURSE
       * too since changing a Folder's parent on Drive is a matter of
       * simply changing "parents" property. We don't have to do any
       * recursive move. */
      if (flags & G_FILE_COPY_OVERWRITE)
        {
          if (num_same_title_files > 1)
            {
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }

          if (GDATA_IS_DOCUMENTS_FOLDER (existing_entry))
            {
              if (GDATA_IS_DOCUMENTS_FOLDER (source_entry))
                {
                  g_vfs_job_failed (G_VFS_JOB (job),
                                    G_IO_ERROR,
                                    G_IO_ERROR_WOULD_MERGE,
                                    _("Can’t copy directory over directory"));
                  goto out;
                }
              else
                {
                  g_vfs_job_failed (G_VFS_JOB (job),
                                    G_IO_ERROR,
                                    G_IO_ERROR_IS_DIRECTORY,
                                    _("Can’t copy file over directory"));
                  goto out;
                }
            }

          /* If the destination-basename matches that of some other entry, we
           * further check if the files have the same ID or not. If the source
           * file as well as the destination file have same ID, we fail the
           * operation. */
          if (g_strcmp0 (existing_entry_id, source_id) == 0)
            {
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }
          else if (g_strcmp0 (existing_entry_id, destination_basename) == 0)
            {
              if (g_strcmp0 (source_parent_id, destination_parent_id) == 0)
                {
                  g_vfs_job_failed_literal (G_VFS_JOB (job),
                                            G_IO_ERROR,
                                            G_IO_ERROR_FAILED,
                                            _("Operation not supported"));
                  goto out;
                }
              else
                will_overwrite = TRUE;
            }
          else if (g_strcmp0 (gdata_entry_get_title (existing_entry), destination_basename) == 0)
            will_overwrite = TRUE;
          else
            {
              /* Neither the ID nor the title of existing_entry matches the destination_basename.
               * This can only happen when destination_basename corresponds to the volatile entry
               * being pointed to by some other file. We simply ignore it and let the move
               * operation to happen. */
            }
        }
      else
        {
          /* We just check for two conditions here (out of a total of 3 we checked above when
           * G_FILE_COPY_OVERWRITE is specified). The first condition "num_same_title_files > 0"
           * checks if we have multiple files with same destination_name in the destination
           * directory, whereas the second condition specifically checks for the case when we have a
           * file whose ID is same as that of source file (i.e. the same file with multiple
           * parents).
           *
           * In the case our destination_name matches the volatile entry of some other file, we
           * simply ignore it and let the move operation take place. */
          if (num_same_title_files > 0 ||
              g_strcmp0 (gdata_entry_get_id (existing_entry), destination_basename) == 0)
            {
              g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
              goto out;
            }
        }
    }

  /* For the cases like `gio move id1 ./folder/`, the operation actually resolves to its longer form
   * `gio move id1 ./folder/id1`. We cater such cases differently and check if another file with the
   * same title as that of file with real ID id1 exists in the destination folder. This check allows
   * us to be as similar to POSIX filesystem operations as possible. This way, we can make
   * title-based move-overwrite operations to work in nautilus. */
  if (g_strcmp0 (destination_basename, source_id) == 0)
    {
      existing_entry = resolve_child (self, destination_parent, title, cancellable, NULL);
      if (existing_entry != NULL)
        {
          gint64 num_same_title_files;

          num_same_title_files = count_files_in_directory_with_title (self,
                                                                      title,
                                                                      destination_parent);

          g_debug ("  count of same title files: %ld\n", num_same_title_files);

          if (flags & G_FILE_COPY_OVERWRITE)
            {
              if (num_same_title_files > 1)
                {
                  /* We return G_IO_ERROR_FAILED below instead of
                   * G_IO_ERROR_NOT_SUPPORTED because _NOT_SUPPORTED implies that the
                   * operation hasn't been implemented yet, and hence GIO takes a
                   * fallback to perform the operation. So, for copy operation with
                   * overwrite, it uses a (replace + read + write) fallback which in
                   * turn overwrites the file even though we don't support overwrites.
                   *
                   * For a concrete case see this discussion:
                   * https://gitlab.gnome.org/GNOME/gvfs/merge_requests/58#note_584083
                   */
                  g_vfs_job_failed_literal (G_VFS_JOB (job),
                                            G_IO_ERROR,
                                            G_IO_ERROR_FAILED,
                                            _("Operation not supported"));
                  goto out;
                }

              if (GDATA_IS_DOCUMENTS_FOLDER (existing_entry))
                {
                  if (GDATA_IS_DOCUMENTS_FOLDER (source_entry))
                    {
                      g_vfs_job_failed (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_WOULD_MERGE,
                                        _("Can’t move directory over directory"));
                      goto out;
                    }
                  else
                    {
                      g_vfs_job_failed (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_IS_DIRECTORY,
                                        _("Can’t move file over directory"));
                      goto out;
                    }
                }

              will_overwrite = (num_same_title_files == 1);
            }
          else
            {
              /* If the title matches the volatile path of some
               * other file (given by existing_entry) and if the titles are
               * different, move operation should be allowed. In that case, we will
               * be having two different entries (each with different titles), but
               * the stored volatile path (in GDataDocumentsProperty) will be same.
               *
               * This isn't an issue since we've already handled those extra
               * entries in insert_entry_full().
               */
              if (num_same_title_files > 0)
                {
                  g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
                  goto out;
                }
            }
        }
    }

  g_debug ("  will overwrite: %d\n", will_overwrite);

  /* The internal ref count has to be increased before removing the
   * source_entry since remove_entry_full calls g_object_unref() internally */
  g_object_ref (source_entry);
  remove_entry (self, source_entry);

  source_parent_link_uri = g_strconcat (URI_PREFIX, source_parent_id, NULL);
  source_parent_link = gdata_link_new (source_parent_link_uri, GDATA_LINK_PARENT);

  destination_parent_link_uri = g_strconcat (URI_PREFIX, destination_parent_id, NULL);
  destination_parent_link = gdata_link_new (destination_parent_link_uri, GDATA_LINK_PARENT);

  if (!gdata_entry_remove_link (source_entry, source_parent_link))
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Error moving file/folder"));
      g_clear_object (&source_parent_link);
      g_object_unref (source_entry);
      goto out;
    }

  gdata_entry_add_link (source_entry, destination_parent_link);

  /* Handle the cases like `gio move id1 folder1/Foobar` and `gio move id1 Foobar`
   * where Foobar is the new title of the file to be moved.
   *
   * The catch here is that we can't support the case `gio move id1 folder1/id1`
   * because that conflicts with what nautilus does when doing a cut + paste
   * operation. This is a tradeoff between handling the common case vs handling a
   * rare case. If you explicity wish to move the file and update the title of
   * the file to its ID, do a move + rename operation. */
  if (g_strcmp0 (destination_basename, source_id) != 0)
    {
      /* When we're trying to overwrite a file using its ID, the destination_basename will be set to
       * the ID. Instead, we want to have the title to be same as that of source entry. */
      if (!(will_overwrite && existing_entry &&
            g_strcmp0 (gdata_entry_get_id (existing_entry), destination_basename) == 0))
        gdata_entry_set_title (source_entry, destination_basename);
    }

  error = NULL;
  new_entry = gdata_service_update_entry (GDATA_SERVICE (self->service),
                                          auth_domain,
                                          source_entry,
                                          cancellable,
                                          &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      g_object_unref (source_entry);
      goto out;
    }

  if (will_overwrite)
    {
      GDataDocumentsEntry *entry_to_remove = NULL;
      guint parent_ids_len;
      GList *parent_ids;
      gchar *existing_entry_path = NULL;

      /* The internal ref count has to be increased before removing the
       * existing_entry since remove_entry_full calls g_object_unref() internally */
      g_object_ref (existing_entry);
      remove_entry (self, existing_entry);

      parent_ids = get_parent_ids (self, existing_entry);
      parent_ids_len = g_list_length (parent_ids);
      if (parent_ids_len > 1 || !is_owner (self, GDATA_ENTRY (existing_entry)))
        {
          /* gdata_documents_service_remove_entry_from_folder () returns the
           * updated entry variable provided as argument with an increased ref.
           * The ref count after the next line shall be 2. */
          entry_to_remove = gdata_documents_service_remove_entry_from_folder (self->service,
                                                                              GDATA_DOCUMENTS_ENTRY (existing_entry),
                                                                              GDATA_DOCUMENTS_FOLDER (destination_parent),
                                                                              cancellable,
                                                                              &error);
        }
      else
        {
          GDataAuthorizationDomain *auth_domain;

          auth_domain = gdata_documents_service_get_primary_authorization_domain ();
          gdata_service_delete_entry (GDATA_SERVICE (self->service), auth_domain, existing_entry, cancellable, &error);
        }

      if (error != NULL)
        {
          sanitize_error (&error);
          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
          g_error_free (error);
          g_clear_object (&entry_to_remove);
          goto out;
        }

      existing_entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (existing_entry), NULL);
      g_hash_table_foreach (self->monitors, emit_delete_event, existing_entry_path);

      g_object_unref (existing_entry);
      g_clear_object (&entry_to_remove);
    }

  entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (new_entry), NULL);
  g_debug ("  new entry path: %s\n", entry_path);

  insert_entry (self, new_entry);
  g_hash_table_foreach (self->monitors, emit_create_event, entry_path);

  size = gdata_documents_entry_get_file_size (GDATA_DOCUMENTS_ENTRY (new_entry));
  g_vfs_job_progress_callback (size, size, job);
  g_vfs_job_succeeded (G_VFS_JOB (job));

  g_object_unref (source_entry);

 out:
  g_clear_object (&destination_parent_link);
  g_clear_object (&new_entry);
  g_free (source_parent_link_uri);
  g_free (destination_parent_link_uri);
  g_free (destination_basename);
  g_free (entry_path);
  g_free (parent_path);
  log_dir_entries (self);
  g_debug ("- move\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;
  gchar *entry_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ create_dir_monitor: %s, %d\n", filename, flags);

  if (flags & G_FILE_MONITOR_SEND_MOVED)
    {
      g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  error = NULL;
  entry = resolve (self, filename, cancellable, &entry_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  entry path: %s\n", entry_path);

  if (!GDATA_IS_DOCUMENTS_FOLDER (entry))
    {
      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 (entry_path), 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 (entry_path);
  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;
  GDataEntry *entry;
  GDataEntry *parent;
  GDataDocumentsEntry *new_entry = NULL;
  GError *error;
  gchar *entry_path = NULL;
  GList *parent_ids;
  guint parent_ids_len;
  gchar *id = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ delete: %s\n", filename);

  error = NULL;
  entry = resolve (self, filename, cancellable, &entry_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  /* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
  id = g_strdup (gdata_entry_get_id (entry));

  parent = resolve_dir (self, filename, cancellable, NULL, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  /* The G_IO_ERROR_NOT_EMPTY error is not intentionally returned for folders in
   * Shared with me folder, because the recursive delete would not work, or could
   * really remove the files from the original folder also for the owner... */
  if (GDATA_IS_DOCUMENTS_FOLDER (entry) && parent != self->shared_with_me_dir)
    {
      GHashTableIter iter;
      DirEntriesKey *key;

      if (!is_dir_listing_valid (self, entry))
        {
          rebuild_dir (self, 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_iter_init (&iter, self->dir_entries);
      while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL))
        {
          if (g_strcmp0 (key->parent_id, id) == 0)
            {
              g_vfs_job_failed (G_VFS_JOB (job),
                                G_IO_ERROR,
                                G_IO_ERROR_NOT_EMPTY,
                                _("Directory not empty"));
              goto out;
            }
        }
    }

  g_debug ("  entry path: %s\n", entry_path);

  if (entry == self->root || entry == self->home || entry == self->shared_with_me_dir ||
      entry == self->shared_drives_dir || parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  /* It has to be removed before the actual call to properly invalidate dir entries. */
  g_object_ref (entry);
  remove_entry (self, entry);

  error = NULL;

  parent_ids = get_parent_ids (self, entry);
  parent_ids_len = g_list_length (parent_ids);

  /* The files in Shared with me folder doesn't have a parent and also we don't
   * have enough permissions to physically delete them. But they can be removed
   * by removal of our permissions... */
  if (parent == self->shared_with_me_dir)
    {
      GDataFeed *acl_feed;

      acl_feed = gdata_access_handler_get_rules (GDATA_ACCESS_HANDLER (entry),
                                                 GDATA_SERVICE (self->service),
                                                 cancellable,
                                                 NULL, NULL, &error);
      if (error == NULL)
        {
          GDataGoaAuthorizer *goa_authorizer;
          GoaAccount *account;
          const gchar *account_identity;
          GDataAuthorizationDomain *auth_domain;
          GList *entries;

          goa_authorizer = GDATA_GOA_AUTHORIZER (gdata_service_get_authorizer (GDATA_SERVICE (self->service)));
          account = goa_object_peek_account (gdata_goa_authorizer_get_goa_object (goa_authorizer));
          account_identity = goa_account_get_identity (account);
          auth_domain = gdata_documents_service_get_primary_authorization_domain ();

          for (entries = gdata_feed_get_entries (acl_feed); entries != NULL; entries = entries->next)
            {
              const gchar *scope_value = NULL;
              GDataAccessRule *rule = GDATA_ACCESS_RULE (entries->data);

              gdata_access_rule_get_scope (rule, NULL, &scope_value);
              if (g_strcmp0 (scope_value, account_identity) == 0)
                gdata_service_delete_entry (GDATA_SERVICE (self->service), auth_domain, GDATA_ENTRY (rule), NULL, &error);
            }

          g_object_unref (acl_feed);
       }
    }
  else if (parent_ids_len > 1 || !is_owner (self, GDATA_ENTRY (entry)))
    {
      /* gdata_documents_service_remove_entry_from_folder () returns the
       * updated entry variable provided as argument with an increased ref.
       * The ref count after the next line shall be 2. */
      new_entry = gdata_documents_service_remove_entry_from_folder (self->service,
                                                                    GDATA_DOCUMENTS_ENTRY (entry),
                                                                    GDATA_DOCUMENTS_FOLDER (parent),
                                                                    cancellable,
                                                                    &error);
    }
  else
    {
      GDataAuthorizationDomain *auth_domain;

      auth_domain = gdata_documents_service_get_primary_authorization_domain ();
      gdata_service_delete_entry (GDATA_SERVICE (self->service), auth_domain, entry, cancellable, &error);
    }

  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      g_object_unref (entry);
      goto out;
    }

  /* In case of files owned by somebody else, the new entry is returned
   * even if it had just one parent before the operation. The backend
   * doesn't care about entries without parents (i.e. entries with
   * parent_ids_len = 1), so let's ignore it. */
  if (new_entry && parent_ids_len > 1)
    insert_entry (self, GDATA_ENTRY (new_entry));
  g_hash_table_foreach (self->monitors, emit_delete_event, entry_path);
  g_vfs_job_succeeded (G_VFS_JOB (job));
  g_object_unref (entry);

 out:
  g_clear_object (&new_entry);
  g_free (entry_path);
  g_free (id);
  g_debug ("- delete\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

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;
  GDataEntry *child;
  GError *error;
  GHashTableIter iter;
  char *parent_path;
  char *id = NULL;
  gboolean is_shared_with_me_dir;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ enumerate: %s\n", filename);

  error = NULL;
  entry = resolve (self, filename, cancellable, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (!GDATA_IS_DOCUMENTS_FOLDER (entry))
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY,_("The file is not a directory"));
      goto out;
    }

  if (!is_dir_listing_valid (self, entry))
    {
      rebuild_dir (self, entry, cancellable, &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));

  /* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
  id = g_strdup (gdata_entry_get_id (entry));

  is_shared_with_me_dir = (entry == self->shared_with_me_dir);

  g_hash_table_iter_init (&iter, self->entries);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &child))
    {
      DirEntriesKey *k;
      gchar *child_id;

      /* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
      child_id = g_strdup (gdata_entry_get_id (child));

      k = dir_entries_key_new (child_id, id);
      if ((is_shared_with_me_dir && is_shared_with_me (child)) ||
          (!is_shared_with_me_dir && g_hash_table_lookup (self->dir_entries, k) != NULL))
        {
          GFileInfo *info;
          gchar *entry_path;
          gchar *child_filename;

          /* Be sure that we are not matching title of volatile file */
          if (g_strcmp0 (child_id, gdata_entry_get_id (child)) != 0)
            {
              g_debug ("Skipping %s as it is volatile path for %s\n", child_id, gdata_entry_get_id (child));
              g_free (child_id);
              dir_entries_key_free (k);
              continue;
            }

          info = g_file_info_new ();
          entry_path = g_build_path ("/", parent_path, child_id, NULL);
          child_filename = g_build_filename (filename, child_id, NULL);
          build_file_info (self, child, flags, info, matcher, child_filename, entry_path, NULL);
          g_vfs_job_enumerate_add_info (job, info);
          g_object_unref (info);
          g_free (entry_path);
        }

      g_free (child_id);
      dir_entries_key_free (k);
    }

  g_vfs_job_enumerate_done (job);

 out:
  g_debug ("- enumerate\n");
  g_free (parent_path);
  g_free (id);
  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_folder = NULL;
  GDataDocumentsFolder *folder = NULL;
  GDataEntry *existing_entry;
  GDataEntry *parent;
  GDataEntry *summary_entry;
  GDataEntry *same_id_entry;
  GError *error;
  const gchar *summary;
  gchar *entry_path = NULL;
  gchar *basename = NULL;
  gchar *parent_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ make_directory: %s\n", filename);

  if (g_strcmp0 (filename, "/") == 0)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  error = NULL;
  parent = resolve_dir (self, filename, cancellable, &basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  parent path: %s\n", parent_path);

  summary_entry = g_hash_table_lookup (self->entries, basename);
  if (summary_entry == NULL)
    summary = NULL;
  else
    summary = gdata_entry_get_summary (summary_entry);

  existing_entry = resolve_child (self, parent, basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
      goto out;
    }

  folder = gdata_documents_folder_new (NULL);

  /* This case is particularly troublesome. Normally, when we call
   * make_directory, the title supplied by the user or client is used to create
   * a file with same title. But strangely, when we're trying to copy a folder
   * over to another directory (i.e. to a different parent), the copy operation
   * would return G_IO_ERROR_WOULD_RECURSE. This means that nautilus will
   * manually create the folders (using make_directory) and copy each file.
   *
   * The major issue here is that nautilus calls make_directory with the title
   * of folder set to ID of the original folder. Suppose, we had a folder "folder1"
   * which needs to be copied. Now, copy operation would return
   * G_IO_ERROR_WOULD_RECURSE (if the parents are different), but nautilus will
   * call make_directory with ID of "folder1" instead of title of "folder1".
   * Furthermore, nautilus calls query_info on the "filename" argument right
   * after it has done make_directory hence we need to create a volatile entry
   * for the folder too.
   *
   * Below fix is hack-ish and will fail if we wish to create a folder
   * whose title equals the ID of any other folder. */
  if ((same_id_entry = g_hash_table_lookup (self->entries, basename)) != NULL)
    {
      GDataDocumentsProperty *source_id_property, *parent_id_property;

      source_id_property = gdata_documents_property_new (SOURCE_ID_PROPERTY_KEY);
      gdata_documents_property_set_visibility (source_id_property, GDATA_DOCUMENTS_PROPERTY_VISIBILITY_PRIVATE);
      gdata_documents_property_set_value (source_id_property, basename);

      parent_id_property = gdata_documents_property_new (PARENT_ID_PROPERTY_KEY);
      gdata_documents_property_set_visibility (parent_id_property, GDATA_DOCUMENTS_PROPERTY_VISIBILITY_PRIVATE);
      gdata_documents_property_set_value (parent_id_property, gdata_entry_get_id (parent));

      gdata_documents_entry_add_documents_property (GDATA_DOCUMENTS_ENTRY (folder), source_id_property);
      gdata_documents_entry_add_documents_property (GDATA_DOCUMENTS_ENTRY (folder), parent_id_property);

      gdata_entry_set_title (GDATA_ENTRY (folder), gdata_entry_get_title (same_id_entry));

      g_clear_object (&source_id_property);
      g_clear_object (&parent_id_property);
    }
  else
    gdata_entry_set_title (GDATA_ENTRY (folder), basename);

  gdata_entry_set_summary (GDATA_ENTRY (folder), summary);

  error = NULL;
  new_folder = gdata_documents_service_add_entry_to_folder (self->service,
                                                            GDATA_DOCUMENTS_ENTRY (folder),
                                                            GDATA_DOCUMENTS_FOLDER (parent),
                                                            cancellable,
                                                            &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (GDATA_ENTRY (new_folder)), NULL);
  g_debug ("  new entry path: %s\n", entry_path);

  insert_entry (self, GDATA_ENTRY (new_folder));
  g_hash_table_foreach (self->monitors, emit_create_event, entry_path);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_clear_object (&folder);
  g_clear_object (&new_folder);
  g_free (basename);
  g_free (entry_path);
  g_free (parent_path);
  g_debug ("- make_directory\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static GList *
query_shared_drives (GVfsBackendGoogle  *self,
                     GCancellable       *cancellable,
                     GError            **error)
{
  GDataDocumentsDriveQuery *query;
  GList *shared_drives = NULL;

  query = gdata_documents_drive_query_new (NULL);
  while (TRUE)
    {
      GDataDocumentsFeed *feed;
      GList *entries;

      feed = gdata_documents_service_query_drives (self->service,
                                                   query,
                                                   cancellable,
                                                   NULL,
                                                   NULL,
                                                   error);
      if (feed == NULL)
        break;

      entries = gdata_feed_get_entries (GDATA_FEED (feed));
      if (entries == NULL)
        {
          g_object_unref (feed);
          break;
        }

      shared_drives = g_list_concat (shared_drives,
                                     g_list_copy_deep (entries, (GCopyFunc) g_object_ref, NULL));

      gdata_query_next_page (GDATA_QUERY (query));
      g_object_unref (feed);
    }

  g_clear_object (&query);

  return shared_drives;
}

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);
  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
  GDataAuthorizationDomain *auth_domain;
  GError *error;
  GList *accounts = NULL;
  GList *l;
  gboolean account_found = FALSE;
  const gchar *host;
  const gchar *user;

  g_debug ("+ mount\n");

  error = NULL;
  self->client = goa_client_new_sync (cancellable, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  host = g_mount_spec_get (spec, "host");
  user = g_mount_spec_get (spec, "user");
  self->account_identity = g_strconcat (user, "@", host, NULL);

  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, self->account_identity) == 0)
        {
          GDataGoaAuthorizer *authorizer;

          authorizer = gdata_goa_authorizer_new (object);
          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, _("Invalid mount spec"));
      goto out;
    }

  auth_domain = gdata_documents_service_get_primary_authorization_domain ();

  self->root = GDATA_ENTRY (gdata_documents_folder_new (ROOT_ID));
  gdata_entry_set_title (self->root, self->account_identity);

  error = NULL;
  self->home = gdata_service_query_single_entry (GDATA_SERVICE (self->service),
                                                 auth_domain,
                                                 "root",
                                                 NULL,
                                                 GDATA_TYPE_DOCUMENTS_FOLDER,
                                                 cancellable,
                                                 &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }
  insert_custom_entry (self, self->home, ROOT_ID);

  self->shared_with_me_dir = GDATA_ENTRY (gdata_documents_folder_new (SHARED_WITH_ME_ID));
  /* Translators: This is the "Shared with me" folder on https://drive.google.com. */
  gdata_entry_set_title (self->shared_with_me_dir, _("Shared with me"));
  insert_custom_entry (self, self->shared_with_me_dir, ROOT_ID);

  self->shared_drives = query_shared_drives (self, cancellable, &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (self->shared_drives)
    {
      self->shared_drives_dir = GDATA_ENTRY (gdata_documents_folder_new (SHARED_DRIVES_ID));
      /* Translators: This is the "Shared drives" folder on https://drive.google.com. */
      gdata_entry_set_title (self->shared_drives_dir, _("Shared drives"));
      insert_custom_entry (self, self->shared_drives_dir, ROOT_ID);
    }

  /* TODO: Make it work with GOA volume monitor resp. shadow mounts. */
  g_vfs_backend_set_default_location (_self, gdata_entry_get_id (self->home));

  g_vfs_backend_set_mount_spec (_self, spec);
  g_vfs_backend_set_display_name (_self, self->account_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_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, _("Error getting data from file"));
      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;
  GDataEntry *destination_parent;
  GDataEntry *existing_entry;
  GDataUploadStream *ostream = NULL;
  GError *error;
  GFile *local_file = NULL;
  GFileInputStream *istream = NULL;
  GFileInfo *info = NULL;
  gboolean needs_overwrite = FALSE;
  const gchar *content_type;
  const gchar *title;
  gchar *destination_basename = NULL;
  gchar *entry_path = NULL;
  gchar *parent_path = NULL;
  gchar *local_file_title = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ push: %s -> %s, %d\n", local_path, destination, flags);

  if (remove_source && (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE))
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  if (flags & G_FILE_COPY_BACKUP)
    {
      /* Return G_IO_ERROR_NOT_SUPPORTED instead of
       * G_IO_ERROR_CANT_CREATE_BACKUP to proceed with the GIO
       * fallback copy.
       */
      g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  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_ATTRIBUTE_STANDARD_SIZE,
                            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;
    }

  local_file_title = g_file_info_get_attribute_as_string (info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);

  error = NULL;
  destination_parent = resolve_dir (self, destination, cancellable, &destination_basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (destination_parent == self->root || destination_parent == self->shared_with_me_dir || destination_parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  existing_entry = resolve_child (self, destination_parent, destination_basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      const gchar *existing_entry_id;
      gint64 num_same_title_files;

      existing_entry_id = gdata_entry_get_id (existing_entry);
      num_same_title_files = count_files_in_directory_with_title (self,
                                                                  destination_basename,
                                                                  destination_parent);

      g_debug ("  count of files with title same as destination_basename: %ld\n", num_same_title_files);

      if (flags & G_FILE_COPY_OVERWRITE)
        {

          if (GDATA_IS_DOCUMENTS_FOLDER (existing_entry))
            {
              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_MERGE,
                                    _("Can’t copy directory over directory"));
                  goto out;
                }
              else
                {
                  g_vfs_job_failed (G_VFS_JOB (job),
                                    G_IO_ERROR,
                                    G_IO_ERROR_IS_DIRECTORY,
                                    _("Can’t copy file over directory"));
                  goto out;
                }
            }
          else if (is_native_file (existing_entry))
            {
              g_vfs_job_failed (G_VFS_JOB (job),
                                G_IO_ERROR,
                                G_IO_ERROR_NOT_REGULAR_FILE,
                                _("Target file is not a regular file"));
              goto out;
            }
          else
            {
              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;
                }
            }

          if (g_strcmp0 (local_file_title, existing_entry_id) == 0)
            {
              /* This corresponds to the operation when a local file has its title set to the
               * real ID of some file, and that the local file is being pushed with the same title.
               * We disallow such operation. */
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }

          if (num_same_title_files > 1)
            {
              g_vfs_job_failed_literal (G_VFS_JOB (job),
                                        G_IO_ERROR,
                                        G_IO_ERROR_FAILED,
                                        _("Operation not supported"));
              goto out;
            }

          if (num_same_title_files == 1 || g_strcmp0 (destination_basename, existing_entry_id) == 0)
            needs_overwrite = TRUE;
        }
      else
        {
          if (num_same_title_files > 0 ||
              g_strcmp0 (destination_basename, existing_entry_id) == 0 ||
              g_strcmp0 (local_file_title, existing_entry_id) == 0)
            {

              g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
              goto out;
            }
        }
    }
  else
    {
      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;
        }
    }

  g_debug ("  will overwrite: %d\n", needs_overwrite);

  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;
    }

  content_type = g_file_info_get_content_type (info);

  if (needs_overwrite)
    {
      document = GDATA_DOCUMENTS_DOCUMENT (g_object_ref (existing_entry));
      title = local_file_title;

      error = NULL;
      ostream = gdata_documents_service_update_document (self->service,
                                                         document,
                                                         title,
                                                         content_type,
                                                         cancellable,
                                                         &error);
    }
  else
    {
      document = gdata_documents_document_new (NULL);
      title = destination_basename;
      gdata_entry_set_title (GDATA_ENTRY (document), title);

      error = NULL;
      ostream = gdata_documents_service_upload_document (self->service,
                                                         document,
                                                         title,
                                                         content_type,
                                                         GDATA_DOCUMENTS_FOLDER (destination_parent),
                                                         cancellable,
                                                         &error);
    }

  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  error = NULL;
  gvfs_output_stream_splice (G_OUTPUT_STREAM (ostream),
                             G_INPUT_STREAM (istream),
                             G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
                             g_file_info_get_size (info),
                             progress_callback,
                             progress_callback_data,
                             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)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (GDATA_ENTRY (new_document)), NULL);
  g_debug ("  new entry path: %s\n", entry_path);

  if (needs_overwrite)
    remove_entry (self, existing_entry);

  insert_entry (self, GDATA_ENTRY (new_document));
  g_hash_table_foreach (self->monitors, emit_create_event, entry_path);

  if (remove_source)
    {
      error = NULL;
      g_file_delete (local_file, cancellable, &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_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 (destination_basename);
  g_free (local_file_title);
  g_free (entry_path);
  g_free (parent_path);
  g_debug ("- push\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
fs_info_cb (GObject      *source_object,
            GAsyncResult *res,
            gpointer      user_data)
{
  GDataDocumentsService *service = GDATA_DOCUMENTS_SERVICE (source_object);
  GVfsJobQueryFsInfo *job = G_VFS_JOB_QUERY_FS_INFO (user_data);
  GError *error = NULL;
  GDataDocumentsMetadata *metadata;
  goffset total, used;

  metadata = gdata_documents_service_get_metadata_finish (service, res, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  total = gdata_documents_metadata_get_quota_total (metadata);
  used = gdata_documents_metadata_get_quota_used (metadata);
  g_object_unref (metadata);

  if (used >= 0) /* sanity check */
    g_file_info_set_attribute_uint64 (job->file_info, G_FILE_ATTRIBUTE_FILESYSTEM_USED, used);

  if (total >= 0) /* -1 'total' means unlimited quota, just don't report size in that case */
    g_file_info_set_attribute_uint64 (job->file_info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE, total);

  if (total >= 0 && used >= 0)
    g_file_info_set_attribute_uint64 (job->file_info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, total - used);

  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_debug ("- query_fs_info\n");
}

static gboolean
g_vfs_backend_google_query_fs_info (GVfsBackend           *_self,
                                    GVfsJobQueryFsInfo    *job,
                                    const gchar           *filename,
                                    GFileInfo             *info,
                                    GFileAttributeMatcher *matcher)
{
  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_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_REMOTE, TRUE);
  g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW, G_FILESYSTEM_PREVIEW_TYPE_IF_ALWAYS);

  if (g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE) ||
      g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_FILESYSTEM_FREE) ||
      g_file_attribute_matcher_matches (matcher, G_FILE_ATTRIBUTE_FILESYSTEM_USED))
    {
      GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
      GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
      gdata_documents_service_get_metadata_async (self->service, cancellable, fs_info_cb, job);
      return TRUE;
    }

  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;
  gchar *entry_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ query_info: %s, %d\n", filename, flags);
  log_dir_entries (self);

  error = NULL;
  entry = resolve (self, filename, cancellable, &entry_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  entry path: %s\n", entry_path);

  error = NULL;
  build_file_info (self, entry, flags, info, matcher, filename, entry_path, &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 (entry_path);
  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);
  const gchar *filename;
  gchar *entry_path = NULL;

  g_debug ("+ query_info_on_read: %p\n", handle);

  entry = g_object_get_data (G_OBJECT (stream), "g-vfs-backend-google-entry");
  filename = g_object_get_data (G_OBJECT (stream), "g-vfs-backend-google-filename");
  entry_path = g_object_get_data (G_OBJECT (stream), "g-vfs-backend-google-entry-path");

  g_debug ("  entry path: %s\n", entry_path);

  error = NULL;
  build_file_info (self,
                   entry,
                   G_FILE_QUERY_INFO_NONE,
                   info,
                   matcher,
                   filename,
                   entry_path,
                   &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_debug ("- query_info_on_read\n");
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
g_vfs_backend_google_query_info_on_write (GVfsBackend           *_self,
                                          GVfsJobQueryInfoWrite *job,
                                          GVfsBackendHandle      handle,
                                          GFileInfo             *info,
                                          GFileAttributeMatcher *matcher)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
  GError *error;
  WriteHandle *wh = (WriteHandle *) handle;

  g_debug ("+ query_info_on_write: %p\n", handle);
  g_debug ("  entry path: %s\n", wh->entry_path);

  error = NULL;
  build_file_info (self,
                   wh->document,
                   G_FILE_QUERY_INFO_NONE,
                   info,
                   matcher,
                   wh->filename,
                   wh->entry_path,
                   &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_debug ("- query_info_on_write\n");
  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

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;
  gchar *content_type = NULL;
  gchar *entry_path = NULL;
  GDataAuthorizationDomain *auth_domain;
  const gchar *uri;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ open_for_read: %s\n", filename);

  error = NULL;
  entry = resolve (self, filename, cancellable, &entry_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  entry path: %s\n", entry_path);

  if (GDATA_IS_DOCUMENTS_FOLDER (entry))
    {
      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, _("Invalid reply received"));
      goto out;
    }

  if (is_native_file (entry))
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_REGULAR_FILE, _("File is not a regular file"));
      goto out;
    }

  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);
  if (stream == NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Error getting data from file"));
      goto out;
    }

  g_object_set_data_full (G_OBJECT (stream), "g-vfs-backend-google-entry", g_object_ref (entry), g_object_unref);
  g_object_set_data_full (G_OBJECT (stream), "g-vfs-backend-google-filename", g_strdup (filename), g_free);
  g_object_set_data_full (G_OBJECT (stream), "g-vfs-backend-google-entry-path", g_strdup (entry_path), g_free);
  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 (entry_path);
  g_debug ("- open_for_read\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
read_cb (GObject      *source_object,
         GAsyncResult *res,
         gpointer      user_data)
{
  GError *error = NULL;
  GInputStream *stream = G_INPUT_STREAM (source_object);
  GVfsJobRead *job = G_VFS_JOB_READ (user_data);
  gssize nread;

  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_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, job);
  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

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
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 (stream);
  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, job);

  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;
  GDataEntry *parent;
  GError *error;
  gchar *entry_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ set_display_name: %s, %s\n", filename, display_name);

  error = NULL;
  entry = resolve (self, filename, cancellable, &entry_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  entry path: %s\n", entry_path);

  parent = resolve_dir (self, filename, cancellable, NULL, NULL, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  if (entry == self->root || entry == self->home || entry == self->shared_with_me_dir || parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  /* The internal ref count has to be increased before removing the entry since
   * remove_entry_full calls g_object_unref(). */
  g_object_ref (entry);
  remove_entry (self, entry);

  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)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      g_object_unref (entry);
      goto out;
    }

  insert_entry (self, new_entry);
  g_hash_table_foreach (self->monitors, emit_attribute_changed_event, entry_path);
  g_vfs_job_set_display_name_set_new_path (job, entry_path);
  g_vfs_job_succeeded (G_VFS_JOB (job));
  g_object_unref (entry);

 out:
  g_clear_object (&new_entry);
  g_free (entry_path);
  g_debug ("- set_display_name\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_create (GVfsBackend         *_self,
                             GVfsJobOpenForWrite *job,
                             const gchar         *filename,
                             GFileCreateFlags     flags)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
  GDataDocumentsDocument *document = NULL;
  GDataDocumentsEntry *new_document = NULL;
  GDataEntry *existing_entry;
  GDataEntry *parent;
  GError *error;
  WriteHandle *handle;
  gchar *basename = NULL;
  gchar *entry_path = NULL;
  gchar *parent_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ create: %s, %d\n", filename, flags);

  if (g_strcmp0 (filename, "/") == 0)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  error = NULL;
  parent = resolve_dir (self, filename, cancellable, &basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  parent path: %s\n", parent_path);

  if (parent == self->root || parent == self->shared_with_me_dir || parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  existing_entry = resolve_child (self, parent, basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      if (flags & G_FILE_CREATE_REPLACE_DESTINATION)
        {
          g_vfs_job_failed_literal (G_VFS_JOB (job),
                                    G_IO_ERROR,
                                    G_IO_ERROR_NOT_SUPPORTED,
                                    _("Operation not supported"));
          goto out;
        }
      else
        {
          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists"));
          goto out;
        }
    }

  document = gdata_documents_document_new (NULL);
  gdata_entry_set_title (GDATA_ENTRY (document), basename);

  error = NULL;
  new_document = gdata_documents_service_add_entry_to_folder (self->service,
                                                              GDATA_DOCUMENTS_ENTRY (document),
                                                              GDATA_DOCUMENTS_FOLDER (parent),
                                                              cancellable,
                                                              &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (GDATA_ENTRY (new_document)), NULL);
  g_debug ("  new entry path: %s\n", entry_path);

  insert_entry (self, GDATA_ENTRY (new_document));
  g_hash_table_foreach (self->monitors, emit_create_event, entry_path);

  handle = write_handle_new (GDATA_ENTRY (new_document), NULL, filename, entry_path);
  g_vfs_job_open_for_write_set_handle (job, handle);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_clear_object (&document);
  g_clear_object (&new_document);
  g_free (basename);
  g_free (entry_path);
  g_free (parent_path);
  g_debug ("- create\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_replace (GVfsBackend         *_self,
                              GVfsJobOpenForWrite *job,
                              const gchar         *filename,
                              const gchar         *etag,
                              gboolean             make_backup,
                              GFileCreateFlags     flags)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
  GDataDocumentsDocument *document = NULL;
  GDataDocumentsEntry *new_document = NULL;
  GDataEntry *existing_entry;
  GDataEntry *parent;
  GDataUploadStream *stream = NULL;
  GError *error;
  WriteHandle *handle;
  gboolean needs_overwrite = FALSE;
  gchar *basename = NULL;
  gchar *content_type = NULL;
  gchar *entry_path = NULL;
  gchar *parent_path = NULL;

  g_rec_mutex_lock (&self->mutex);
  g_debug ("+ replace: %s, %s, %d, %d\n", filename, etag, make_backup, flags);

  if (make_backup)
    {
      g_vfs_job_failed (G_VFS_JOB (job),
                        G_IO_ERROR,
                        G_IO_ERROR_CANT_CREATE_BACKUP,
                        _("Backup file creation failed"));
      goto out;
    }

  if (g_strcmp0 (filename, "/") == 0)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  error = NULL;
  parent = resolve_dir (self, filename, cancellable, &basename, &parent_path, &error);
  if (error != NULL)
    {
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }

  g_debug ("  parent path: %s\n", parent_path);

  if (parent == self->root || parent == self->shared_with_me_dir || parent == self->shared_drives_dir)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported"));
      goto out;
    }

  existing_entry = resolve_child (self, parent, basename, cancellable, NULL);
  if (existing_entry != NULL)
    {
      if (GDATA_IS_DOCUMENTS_FOLDER (existing_entry))
        {
          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("Target file is a directory"));
          goto out;
        }
      else if (is_native_file (existing_entry))
        {
          g_vfs_job_failed (G_VFS_JOB (job),
                            G_IO_ERROR,
                            G_IO_ERROR_NOT_REGULAR_FILE,
                            _("Target file is not a regular file"));
          goto out;
        }

      needs_overwrite = TRUE;
    }

  g_debug ("  will overwrite: %d\n", needs_overwrite);

  if (needs_overwrite)
    {
      const gchar *title;

      entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (existing_entry), NULL);
      g_debug ("  existing entry path: %s\n", entry_path);

      title = gdata_entry_get_title (existing_entry);
      content_type = get_content_type_from_entry (existing_entry);

      error = NULL;
      stream = gdata_documents_service_update_document (self->service,
                                                        GDATA_DOCUMENTS_DOCUMENT (existing_entry),
                                                        title,
                                                        content_type,
                                                        cancellable,
                                                        &error);
      if (error != NULL)
        {
          sanitize_error (&error);
          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
          g_error_free (error);
          goto out;
        }

      handle = write_handle_new (NULL, stream, filename, entry_path);
    }
  else
    {
      document = gdata_documents_document_new (NULL);
      gdata_entry_set_title (GDATA_ENTRY (document), basename);

      error = NULL;
      new_document = gdata_documents_service_add_entry_to_folder (self->service,
                                                                  GDATA_DOCUMENTS_ENTRY (document),
                                                                  GDATA_DOCUMENTS_FOLDER (parent),
                                                                  cancellable,
                                                                  &error);
      if (error != NULL)
        {
          sanitize_error (&error);
          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
          g_error_free (error);
          goto out;
        }

      entry_path = g_build_path ("/", parent_path, gdata_entry_get_id (GDATA_ENTRY (new_document)), NULL);
      g_debug ("  new entry path: %s\n", entry_path);

      insert_entry (self, GDATA_ENTRY (new_document));
      g_hash_table_foreach (self->monitors, emit_create_event, entry_path);

      handle = write_handle_new (GDATA_ENTRY (new_document), NULL, filename, entry_path);
    }

  g_vfs_job_open_for_write_set_handle (job, handle);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_clear_object (&document);
  g_clear_object (&new_document);
  g_clear_object (&stream);
  g_free (basename);
  g_free (content_type);
  g_free (entry_path);
  g_free (parent_path);
  g_debug ("- replace\n");
  g_rec_mutex_unlock (&self->mutex);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_write (GVfsBackend       *_self,
                            GVfsJobWrite      *job,
                            GVfsBackendHandle  handle,
                            gchar             *buffer,
                            gsize              buffer_size)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
  GError *error;
  WriteHandle *wh = (WriteHandle *) handle;
  gssize nwrite;

  g_debug ("+ write: %p\n", handle);

  if (wh->stream == NULL)
    {
      const gchar *title;
      gchar *content_type = NULL;

      title = gdata_entry_get_title (wh->document);
      content_type = g_content_type_guess (title, (const guchar *) buffer, buffer_size, NULL);
      g_debug ("  content-type: %s\n", content_type);

      error = NULL;
      wh->stream = gdata_documents_service_update_document (self->service,
                                                            GDATA_DOCUMENTS_DOCUMENT (wh->document),
                                                            title,
                                                            content_type,
                                                            cancellable,
                                                            &error);
      g_free (content_type);

      if (error != NULL)
        {
          sanitize_error (&error);
          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
          g_error_free (error);
          goto out;
        }
    }

  g_debug ("  writing to stream: %p\n", wh->stream);
  g_debug ("  entry path: %s\n", wh->entry_path);

  error = NULL;
  nwrite = g_output_stream_write (G_OUTPUT_STREAM (wh->stream),
                                  buffer,
                                  buffer_size,
                                  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_foreach (self->monitors, emit_changed_event, wh->entry_path);
  g_vfs_job_write_set_written_size (job, (gsize) nwrite);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_debug ("- write\n");
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_close_write (GVfsBackend       *_self,
                                  GVfsJobCloseWrite *job,
                                  GVfsBackendHandle  handle)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);
  GCancellable *cancellable = G_VFS_JOB (job)->cancellable;
  GDataDocumentsDocument *new_document = NULL;
  GError *error;
  WriteHandle *wh = (WriteHandle *) handle;

  g_debug ("+ close_write: %p\n", handle);

  if (!g_output_stream_is_closed (G_OUTPUT_STREAM (wh->stream)))
    {
      error = NULL;
      g_output_stream_close (G_OUTPUT_STREAM (wh->stream), 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, wh->stream, &error);
  if (error != NULL)
    {
      sanitize_error (&error);
      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
      g_error_free (error);
      goto out;
    }
  else if (new_document == NULL)
    {
      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Error writing file"));
      goto out;
    }

  g_debug ("  new entry path: %s\n", wh->entry_path);

  remove_entry (self, wh->document);
  insert_entry (self, GDATA_ENTRY (new_document));
  g_hash_table_foreach (self->monitors, emit_changes_done_event, wh->entry_path);
  g_vfs_job_succeeded (G_VFS_JOB (job));

 out:
  g_clear_object (&new_document);
  write_handle_free (wh);
  g_debug ("- close_write\n");
}

/* ---------------------------------------------------------------------------------------------------- */

static void
g_vfs_backend_google_dispose (GObject *_self)
{
  GVfsBackendGoogle *self = G_VFS_BACKEND_GOOGLE (_self);

  if (self->dir_collisions != NULL)
    {
      g_list_free_full (self->dir_collisions, g_object_unref);
      self->dir_collisions = NULL;
    }

  g_clear_object (&self->service);
  g_clear_object (&self->root);
  g_clear_object (&self->home);
  g_clear_object (&self->shared_with_me_dir);
  g_clear_object (&self->shared_drives_dir);
  g_clear_list (&self->shared_drives, g_object_unref);
  g_clear_object (&self->client);
  g_clear_pointer (&self->entries, g_hash_table_unref);
  g_clear_pointer (&self->dir_entries, g_hash_table_unref);
  g_clear_pointer (&self->dir_timestamps, 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_foreach (self->monitors, remove_monitor_weak_ref, self->monitors);
  g_hash_table_unref (self->monitors);
  g_free (self->account_identity);

  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->close_write = g_vfs_backend_google_close_write;
  backend_class->copy = g_vfs_backend_google_copy;
  backend_class->move = g_vfs_backend_google_move;
  backend_class->create = g_vfs_backend_google_create;
  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->try_query_info_on_write = g_vfs_backend_google_query_info_on_write;
  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;
  backend_class->replace = g_vfs_backend_google_replace;
  backend_class->write = g_vfs_backend_google_write;
}

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->dir_entries = g_hash_table_new_full (entries_in_folder_hash,
                                             entries_in_folder_equal,
                                             dir_entries_key_free,
                                             g_object_unref);
  self->dir_timestamps = 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);
}
