gvfs/daemon/gvfsbackendgoogle.c

4272 lines
150 KiB
C
Raw Normal View History

2022-06-29 16:07:13 +08:00
/* -*- 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"
2023-04-07 11:45:59 +08:00
#include "gvfsdaemonutils.h"
2022-06-29 16:07:13 +08:00
struct _GVfsBackendGoogle
{
GVfsBackend parent;
GDataDocumentsService *service;
GDataEntry *root;
2023-04-07 11:45:59 +08:00
GDataEntry *home;
GDataEntry *shared_with_me_dir;
GDataEntry *shared_drives_dir;
2022-06-29 16:07:13 +08:00
GHashTable *entries; /* gchar *entry_id -> GDataEntry */
GHashTable *dir_entries; /* DirEntriesKey -> GDataEntry */
GHashTable *dir_timestamps; /* gchar *entry_id -> gint64 *timestamp */
GHashTable *monitors;
GList *dir_collisions;
2023-04-07 11:45:59 +08:00
GList *shared_drives;
2022-06-29 16:07:13 +08:00
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"
2023-04-07 11:45:59 +08:00
#define ROOT_ID "GVfsRoot"
#define SHARED_WITH_ME_ID "GVfsSharedWithMe"
#define SHARED_DRIVES_ID "GVfsSharedDrives"
2022-06-29 16:07:13 +08:00
/* ---------------------------------------------------------------------------------------------------- */
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 doesnt 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;
}
2023-04-07 11:45:59 +08:00
static gboolean
is_shared_with_me (GDataEntry *entry)
{
return gdata_documents_entry_get_shared_with_me_date (GDATA_DOCUMENTS_ENTRY (entry)) > 0;
}
2022-06-29 16:07:13 +08:00
/* ---------------------------------------------------------------------------------------------------- */
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);
}
2023-04-07 11:45:59 +08:00
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));
}
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
if (is_shared_with_me (entry))
g_hash_table_remove (self->dir_timestamps, SHARED_WITH_ME_ID);
2022-06-29 16:07:13 +08:00
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");
2023-04-07 11:45:59 +08:00
if (timestamp == NULL)
return TRUE;
2022-06-29 16:07:13 +08:00
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;
2023-04-07 11:45:59 +08:00
if (parent == self->root)
return TRUE;
2022-06-29 16:07:13 +08:00
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;
}
2023-04-07 11:45:59 +08:00
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);
}
2022-06-29 16:07:13 +08:00
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;
2023-04-07 11:45:59 +08:00
if (parent == self->shared_drives_dir)
{
rebuild_shared_drives_dir (self, cancellable, error);
return;
}
2022-06-29 16:07:13 +08:00
/* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
parent_id = g_strdup (gdata_entry_get_id (parent));
2023-04-07 11:45:59 +08:00
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);
2022-06-29 16:07:13 +08:00
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;
2023-04-07 11:45:59 +08:00
gboolean is_shared_with_me_dir = (parent == self->shared_with_me_dir);
2022-06-29 16:07:13 +08:00
parent_id = gdata_entry_get_id (parent);
k = dir_entries_key_new (basename, parent_id);
2023-04-07 11:45:59 +08:00
if (is_shared_with_me_dir)
entry = g_hash_table_lookup (self->entries, basename);
else
entry = g_hash_table_lookup (self->dir_entries, k);
2022-06-29 16:07:13 +08:00
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;
}
2023-04-07 11:45:59 +08:00
if (is_shared_with_me_dir)
entry = g_hash_table_lookup (self->entries, basename);
else
entry = g_hash_table_lookup (self->dir_entries, k);
2022-06-29 16:07:13 +08:00
}
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)
{
2023-04-07 11:45:59 +08:00
/* 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;
}
2022-06-29 16:07:13 +08:00
}
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 char *
get_extension_offset (const char *title)
{
gchar *end;
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;
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);
*extension_offset = '\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;
2023-04-07 11:45:59 +08:00
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;
2022-06-29 16:07:13 +08:00
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);
}
2023-04-07 11:45:59 +08:00
/* 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);
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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);
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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);
}
2022-06-29 16:07:13 +08:00
2023-04-07 11:45:59 +08:00
g_file_info_set_icon (info, icon);
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
if (is_root)
name = "/";
else if (is_symlink)
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
if (is_root || is_home || is_shared_with_me_dir || is_shared_drives_dir)
goto out;
2022-06-29 16:07:13 +08:00
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;
}
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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,
_("Cant copy directory over directory"));
goto out;
}
else
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Cant 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,
_("Cant 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;
}
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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,
_("Cant copy directory over directory"));
goto out;
}
else
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Cant 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,
_("Cant move directory over directory"));
goto out;
}
else
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Cant 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));
2023-04-07 11:45:59 +08:00
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)
2022-06-29 16:07:13 +08:00
{
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);
2023-04-07 11:45:59 +08:00
if (entry == self->root || entry == self->home || entry == self->shared_with_me_dir ||
entry == self->shared_drives_dir || parent == self->shared_drives_dir)
2022-06-29 16:07:13 +08:00
{
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);
2023-04-07 11:45:59 +08:00
/* 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)))
2022-06-29 16:07:13 +08:00
{
/* 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;
2023-04-07 11:45:59 +08:00
GDataEntry *child;
2022-06-29 16:07:13 +08:00
GError *error;
GHashTableIter iter;
char *parent_path;
char *id = NULL;
2023-04-07 11:45:59 +08:00
gboolean is_shared_with_me_dir;
2022-06-29 16:07:13 +08:00
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));
2023-04-07 11:45:59 +08:00
is_shared_with_me_dir = (entry == self->shared_with_me_dir);
2022-06-29 16:07:13 +08:00
g_hash_table_iter_init (&iter, self->entries);
2023-04-07 11:45:59 +08:00
while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &child))
2022-06-29 16:07:13 +08:00
{
DirEntriesKey *k;
gchar *child_id;
/* g_strdup() is necessary to prevent segfault because gdata_entry_get_id() calls g_free() */
2023-04-07 11:45:59 +08:00
child_id = g_strdup (gdata_entry_get_id (child));
2022-06-29 16:07:13 +08:00
k = dir_entries_key_new (child_id, id);
2023-04-07 11:45:59 +08:00
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))
2022-06-29 16:07:13 +08:00
{
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);
2023-04-07 11:45:59 +08:00
build_file_info (self, child, flags, info, matcher, child_filename, entry_path, NULL);
2022-06-29 16:07:13 +08:00
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);
}
/* ---------------------------------------------------------------------------------------------------- */
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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 ();
2023-04-07 11:45:59 +08:00
self->root = GDATA_ENTRY (gdata_documents_folder_new (ROOT_ID));
gdata_entry_set_title (self->root, self->account_identity);
2022-06-29 16:07:13 +08:00
error = NULL;
2023-04-07 11:45:59 +08:00
self->home = gdata_service_query_single_entry (GDATA_SERVICE (self->service),
2022-06-29 16:07:13 +08:00
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;
}
2023-04-07 11:45:59 +08:00
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));
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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","
2023-04-07 11:45:59 +08:00
G_FILE_ATTRIBUTE_STANDARD_TYPE","
G_FILE_ATTRIBUTE_STANDARD_SIZE,
2022-06-29 16:07:13 +08:00
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;
}
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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,
_("Cant copy directory over directory"));
goto out;
}
else
{
g_vfs_job_failed (G_VFS_JOB (job),
G_IO_ERROR,
G_IO_ERROR_IS_DIRECTORY,
_("Cant 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,
_("Cant 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,
_("Cant 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;
2023-04-07 11:45:59 +08:00
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);
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW, G_FILESYSTEM_PREVIEW_TYPE_IF_ALWAYS);
2022-06-29 16:07:13 +08:00
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, _("Cant 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;
2023-04-07 11:45:59 +08:00
GDataEntry *parent;
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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)
2022-06-29 16:07:13 +08:00
{
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);
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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;
}
2022-06-29 16:07:13 +08:00
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);
2023-04-07 11:45:59 +08:00
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);
2022-06-29 16:07:13 +08:00
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);
}