[libgdata] [core] Added GDataUploadStream to allow easy uploading of large files
- From: Philip Withnall <pwithnall src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [libgdata] [core] Added GDataUploadStream to allow easy uploading of large files
- Date: Wed, 5 Aug 2009 19:15:59 +0000 (UTC)
commit 4369021250a06320c56654616e8ef20bb6da4e2e
Author: Philip Withnall <philip tecnocode co uk>
Date: Wed Aug 5 20:14:45 2009 +0100
[core] Added GDataUploadStream to allow easy uploading of large files
GDataUploadStream is a subclass of GOutputStream designed to allow easy
uploading of files (with or without an associated entry) to a service using
streaming I/O.
docs/reference/gdata-docs.xml | 1 +
docs/reference/gdata-sections.txt | 24 +
gdata/Makefile.am | 4 +-
gdata/gdata-buffer.c | 24 +-
gdata/gdata-buffer.h | 1 +
gdata/gdata-service.c | 6 +
gdata/gdata-service.h | 4 +-
gdata/gdata-upload-stream.c | 748 ++++++++++++++++++++
gdata/gdata-upload-stream.h | 80 +++
gdata/gdata.h | 1 +
gdata/gdata.symbols | 7 +
gdata/services/documents/gdata-documents-service.c | 255 +++-----
12 files changed, 981 insertions(+), 174 deletions(-)
---
diff --git a/docs/reference/gdata-docs.xml b/docs/reference/gdata-docs.xml
index 88cec0d..89e42a5 100644
--- a/docs/reference/gdata-docs.xml
+++ b/docs/reference/gdata-docs.xml
@@ -31,6 +31,7 @@
<xi:include href="xml/gdata-types.xml"/>
<xi:include href="xml/gdata-parsable.xml"/>
<xi:include href="xml/gdata-download-stream.xml"/>
+ <xi:include href="xml/gdata-upload-stream.xml"/>
</chapter>
<chapter>
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index de50548..599b849 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -1500,3 +1500,27 @@ gdata_download_stream_get_type
<SUBSECTION Private>
GDataDownloadStreamPrivate
</SECTION>
+
+<SECTION>
+<FILE>gdata-upload-stream</FILE>
+<TITLE>GDataUploadStream</TITLE>
+GDataUploadStream
+GDataUploadStreamClass
+gdata_upload_stream_new
+gdata_upload_stream_get_response
+gdata_upload_stream_get_service
+gdata_upload_stream_get_upload_uri
+gdata_upload_stream_get_entry
+gdata_upload_stream_get_slug
+gdata_upload_stream_get_content_type
+<SUBSECTION Standard>
+gdata_upload_stream_get_type
+GDATA_UPLOAD_STREAM
+GDATA_UPLOAD_STREAM_CLASS
+GDATA_UPLOAD_STREAM_GET_CLASS
+GDATA_IS_UPLOAD_STREAM
+GDATA_IS_UPLOAD_STREAM_CLASS
+GDATA_TYPE_UPLOAD_STREAM
+<SUBSECTION Private>
+GDataUploadStreamPrivate
+</SECTION>
diff --git a/gdata/Makefile.am b/gdata/Makefile.am
index be1ec52..8fa159a 100644
--- a/gdata/Makefile.am
+++ b/gdata/Makefile.am
@@ -50,7 +50,8 @@ gdata_headers = \
gdata-access-handler.h \
gdata-access-rule.h \
gdata-parsable.h \
- gdata-download-stream.h
+ gdata-download-stream.h \
+ gdata-upload-stream.h
gdataincludedir = $(pkgincludedir)/gdata
gdatainclude_HEADERS = \
@@ -71,6 +72,7 @@ libgdata_la_SOURCES = \
gdata-access-rule.c \
gdata-parsable.c \
gdata-download-stream.c \
+ gdata-upload-stream.c \
gdata-private.h \
gdata-parser.h \
gdata-buffer.c \
diff --git a/gdata/gdata-buffer.c b/gdata/gdata-buffer.c
index f5de476..8bb1e2f 100644
--- a/gdata/gdata-buffer.c
+++ b/gdata/gdata-buffer.c
@@ -243,8 +243,10 @@ gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested,
}
/* Return if we haven't got any data to pop (i.e. if we were cancelled before even one chunk arrived) */
- if (return_length == 0)
+ if (return_length == 0) {
+ g_static_mutex_unlock (&(self->mutex));
return 0;
+ }
/* Otherwise, get on with things */
length_remaining = return_length;
@@ -287,3 +289,23 @@ gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested,
return return_length;
}
+
+/**
+ * gdata_buffer_pop_all_data:
+ * @self: a #GDataBuffer
+ * @data: return location for the popped data
+ *
+ * Pops as much data as possible off the #GDataBuffer, up to a limit of @maxium_length bytes. If fewer bytes exist
+ * in the buffer, fewer bytes will be returned. If more bytes exist in the buffer, @maximum_length bytes will be returned.
+ *
+ * This function will never block.
+ *
+ * Return value: the number of bytes returned in @data
+ *
+ * Since: 0.5.0
+ **/
+gsize
+gdata_buffer_pop_data_limited (GDataBuffer *self, guint8 *data, gsize maximum_length)
+{
+ return gdata_buffer_pop_data (self, data, MIN (maximum_length, self->total_length), NULL);
+}
diff --git a/gdata/gdata-buffer.h b/gdata/gdata-buffer.h
index ec807b5..be0c622 100644
--- a/gdata/gdata-buffer.h
+++ b/gdata/gdata-buffer.h
@@ -52,6 +52,7 @@ void gdata_buffer_free (GDataBuffer *self);
gboolean gdata_buffer_push_data (GDataBuffer *self, const guint8 *data, gsize length);
gsize gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested, GCancellable *cancellable);
+gsize gdata_buffer_pop_data_limited (GDataBuffer *self, guint8 *data, gsize maximum_length);
G_END_DECLS
diff --git a/gdata/gdata-service.c b/gdata/gdata-service.c
index 74aa76c..4c3b8a8 100644
--- a/gdata/gdata-service.c
+++ b/gdata/gdata-service.c
@@ -331,6 +331,7 @@ static void
real_parse_error_response (GDataService *self, GDataServiceError error_type, guint status, const gchar *reason_phrase, const gchar *response_body,
gint length, GError **error)
{
+ g_message ("*** parse_error_response: %u, %s, %s, %i", status, reason_phrase, response_body, length);
/* See: http://code.google.com/apis/gdata/docs/2.0/reference.html#HTTPStatusCodes */
switch (status) {
case 400:
@@ -387,6 +388,11 @@ real_parse_error_response (GDataService *self, GDataServiceError error_type, gui
/* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
_("Error code %u when downloading: %s"), status, response_body);
break;
+ case GDATA_SERVICE_ERROR_WITH_UPLOAD:
+ g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_UPLOAD,
+ /* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
+ _("Error code %u when uploading: %s"), status, response_body);
+ break;
default:
/* We should not be called with anything other than the above four generic error types */
g_assert_not_reached ();
diff --git a/gdata/gdata-service.h b/gdata/gdata-service.h
index ddb9fc3..cbadcfe 100644
--- a/gdata/gdata-service.h
+++ b/gdata/gdata-service.h
@@ -44,6 +44,7 @@ G_BEGIN_DECLS
* and uploading the modified entry
* @GDATA_SERVICE_ERROR_FORBIDDEN: Generic error for a forbidden action (not due to having insufficient permissions)
* @GDATA_SERVICE_ERROR_WITH_DOWNLOAD: Generic error when downloading a file (rather than querying for an entry).
+ * @GDATA_SERVICE_ERROR_WITH_UPLOAD: Generic error when uploading a file (either inserting or updating an entry).
*
* Error codes for #GDataService operations.
**/
@@ -59,7 +60,8 @@ typedef enum {
GDATA_SERVICE_ERROR_NOT_FOUND,
GDATA_SERVICE_ERROR_CONFLICT,
GDATA_SERVICE_ERROR_FORBIDDEN,
- GDATA_SERVICE_ERROR_WITH_DOWNLOAD
+ GDATA_SERVICE_ERROR_WITH_DOWNLOAD,
+ GDATA_SERVICE_ERROR_WITH_UPLOAD
} GDataServiceError;
/**
diff --git a/gdata/gdata-upload-stream.c b/gdata/gdata-upload-stream.c
new file mode 100644
index 0000000..bbc0a7d
--- /dev/null
+++ b/gdata/gdata-upload-stream.c
@@ -0,0 +1,748 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ *
+ * GData Client 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.1 of the License, or (at your option) any later version.
+ *
+ * GData Client 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 GData Client. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-upload-stream
+ * @short_description: GData upload stream object
+ * @stability: Unstable
+ * @include: gdata/gdata-upload-stream.h
+ *
+ * #GDataUploadStream is a #GOutputStream subclass to allow uploading of files from GData services with authentication from a #GDataService.
+ *
+ * Once a #GDataUploadStream is instantiated with gdata_upload_stream_new(), the standard #GOutputStream API can be used on the stream to upload
+ * the file. Network communication may not actually begin until the first call to g_output_stream_write(), so having a #GDataUploadStream around is no
+ * guarantee that the file is being uploaded.
+ *
+ * Uploads of a file, or a file with associated metadata (a #GDataEntry) may take place, but if you want to simply upload a single #GDataEntry,
+ * use gdata_service_insert_entry() instead. #GDataUploadStream is for large streaming uploads.
+ *
+ * Once an upload is complete, the server's response can be retrieved from the #GDataUploadStream using gdata_upload_stream_get_response(). In order
+ * for network communication to be guaranteed to have stopped (and thus the response definitely available), g_output_stream_close() must be called
+ * on the #GDataUploadStream first. Otherwise, gdata_upload_stream_get_response() may return saying that the operation is still in progress.
+ *
+ * Since: 0.5.0
+ **/
+
+/*
+ * We have a network thread which does all the uploading work. We send the message encoded as chunks, but cannot use the SoupMessageBody as a
+ * data buffer, since it can only ever be touched by the network thread. Instead, we pass data to the network thread through a GDataBuffer, with
+ * the main thread pushing it on as and when write() is called. The network thread cannot block on popping data off the buffer, as it requests fixed-
+ * size chunks, and there's no way to notify it that we've reached EOF; so when it gets to popping the last chunk off the buffer, which may well be
+ * smaller than its chunk size, it would block for more data and therefore hang. Consequently, the network thread instead pops as much data as it can
+ * off the buffer, up to its chunk size, which is a non-blocking operation.
+ *
+ * The write() and close() operations on the output stream are synchronised with the network thread, so that the write() call only returns once the
+ * network thread has written a chunk (although we don't guarantee that it's the same chunk which was passed to the write() function), and the
+ * close() call only returns once all network activity has finished (including receiving the response from the server). Async versions of these calls
+ * are provided by GOutputStream.
+ *
+ * Mutex locking order:
+ * 1. response_mutex
+ * 2. write_mutex
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <string.h>
+
+#include "gdata-upload-stream.h"
+#include "gdata-buffer.h"
+#include "gdata-private.h"
+
+#define BOUNDARY_STRING "0003Z5W789deadbeefRTE456KlemsnoZV"
+
+static void gdata_upload_stream_dispose (GObject *object);
+static void gdata_upload_stream_finalize (GObject *object);
+static void gdata_upload_stream_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_upload_stream_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+
+static gssize gdata_upload_stream_write (GOutputStream *stream, const void *buffer, gsize count, GCancellable *cancellable, GError **error);
+static gboolean gdata_upload_stream_close (GOutputStream *stream, GCancellable *cancellable, GError **error);
+
+static void create_network_thread (GDataUploadStream *self, GError **error);
+
+struct _GDataUploadStreamPrivate {
+ gchar *upload_uri;
+ GDataService *service;
+ GDataEntry *entry;
+ gchar *slug;
+ gchar *content_type;
+ SoupSession *session;
+ SoupMessage *message;
+ GDataBuffer *buffer;
+
+ GThread *network_thread;
+
+ GStaticMutex write_mutex; /* mutex for write operations (specifically, write_finished) */
+ gboolean write_finished; /* set when the network thread has finished writing a chunk (before it signals write_cond) */
+ GCond *write_cond; /* signalled when a chunk has been written (protected by write_mutex) */
+
+ guint response_status; /* set once we finish receiving the response (0 otherwise) (protected by response_mutex) */
+ GCond *finished_cond; /* signalled when sending the message (and receiving the response) is finished (protected by response_mutex) */
+ GError *response_error; /* error asynchronously set by the network thread, and picked up by the main thread when appropriate*/
+ GStaticMutex response_mutex; /* mutex for ->response_error, ->response_status and ->finished_cond */
+};
+
+enum {
+ PROP_SERVICE = 1,
+ PROP_UPLOAD_URI,
+ PROP_ENTRY,
+ PROP_SLUG,
+ PROP_CONTENT_TYPE
+};
+
+G_DEFINE_TYPE (GDataUploadStream, gdata_upload_stream, G_TYPE_OUTPUT_STREAM)
+#define GDATA_UPLOAD_STREAM_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_UPLOAD_STREAM, GDataUploadStreamPrivate))
+
+static void
+gdata_upload_stream_class_init (GDataUploadStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GOutputStreamClass *stream_class = G_OUTPUT_STREAM_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (GDataUploadStreamPrivate));
+
+ gobject_class->dispose = gdata_upload_stream_dispose;
+ gobject_class->finalize = gdata_upload_stream_finalize;
+ gobject_class->get_property = gdata_upload_stream_get_property;
+ gobject_class->set_property = gdata_upload_stream_set_property;
+
+ /* We use the default implementations of the async functions, which just run
+ * our implementation of the sync function in a thread. */
+ stream_class->write_fn = gdata_upload_stream_write;
+ stream_class->close_fn = gdata_upload_stream_close;
+
+ /**
+ * GDataUploadStream:service:
+ *
+ * The service which is used to authenticate the upload, and to which the upload relates.
+ *
+ * Since: 0.5.0
+ **/
+ g_object_class_install_property (gobject_class, PROP_SERVICE,
+ g_param_spec_object ("service",
+ "Service", "The service which is used to authenticate the upload.",
+ GDATA_TYPE_SERVICE,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GDataUploadStream:upload-uri:
+ *
+ * The URI of the file to upload.
+ *
+ * Since: 0.5.0
+ **/
+ g_object_class_install_property (gobject_class, PROP_UPLOAD_URI,
+ g_param_spec_string ("upload-uri",
+ "Upload URI", "The URI of the file to upload.",
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GDataUploadStream:entry:
+ *
+ * The entry used for metadata to upload.
+ *
+ * Since: 0.5.0
+ **/
+ g_object_class_install_property (gobject_class, PROP_ENTRY,
+ g_param_spec_object ("entry",
+ "Entry", "The entry used for metadata to upload.",
+ GDATA_TYPE_ENTRY,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GDataUploadStream:slug:
+ *
+ * The slug of the file being uploaded.
+ *
+ * Since: 0.5.0
+ **/
+ g_object_class_install_property (gobject_class, PROP_SLUG,
+ g_param_spec_string ("slug",
+ "Slug", "The slug of the file being uploaded.",
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GDataUploadStream:content-type:
+ *
+ * The content type of the file being uploaded.
+ *
+ * Since: 0.5.0
+ **/
+ g_object_class_install_property (gobject_class, PROP_CONTENT_TYPE,
+ g_param_spec_string ("content-type",
+ "Content type", "The content type of the file being uploaded.",
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gdata_upload_stream_init (GDataUploadStream *self)
+{
+ self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_UPLOAD_STREAM, GDataUploadStreamPrivate);
+ self->priv->buffer = gdata_buffer_new ();
+ g_static_mutex_init (&(self->priv->write_mutex));
+ self->priv->write_cond = g_cond_new ();
+ self->priv->finished_cond = g_cond_new ();
+ g_static_mutex_init (&(self->priv->response_mutex));
+}
+
+static void
+gdata_upload_stream_dispose (GObject *object)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM_GET_PRIVATE (object);
+
+ if (priv->service != NULL)
+ g_object_unref (priv->service);
+ priv->service = NULL;
+
+ if (priv->message != NULL)
+ g_object_unref (priv->message);
+ priv->message = NULL;
+
+ if (priv->entry != NULL)
+ g_object_unref (priv->entry);
+ priv->entry = NULL;
+
+ /* Chain up to the parent class */
+ G_OBJECT_CLASS (gdata_upload_stream_parent_class)->dispose (object);
+}
+
+static void
+gdata_upload_stream_finalize (GObject *object)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM_GET_PRIVATE (object);
+
+ g_thread_join (priv->network_thread);
+ g_static_mutex_free (&(priv->response_mutex));
+ g_cond_free (priv->finished_cond);
+ g_cond_free (priv->write_cond);
+ g_static_mutex_free (&(priv->write_mutex));
+ gdata_buffer_free (priv->buffer);
+ g_free (priv->upload_uri);
+ g_free (priv->slug);
+ g_free (priv->content_type);
+
+ /* Chain up to the parent class */
+ G_OBJECT_CLASS (gdata_upload_stream_parent_class)->finalize (object);
+}
+
+static void
+gdata_upload_stream_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM_GET_PRIVATE (object);
+
+ switch (property_id) {
+ case PROP_SERVICE:
+ g_value_set_object (value, priv->service);
+ break;
+ case PROP_UPLOAD_URI:
+ g_value_set_string (value, priv->upload_uri);
+ break;
+ case PROP_ENTRY:
+ g_value_set_object (value, priv->entry);
+ break;
+ case PROP_SLUG:
+ g_value_set_string (value, priv->slug);
+ break;
+ case PROP_CONTENT_TYPE:
+ g_value_set_string (value, priv->content_type);
+ break;
+ default:
+ /* We don't have any other property... */
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gdata_upload_stream_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM (object)->priv;
+
+ switch (property_id) {
+ case PROP_SERVICE:
+ priv->service = g_value_dup_object (value);
+ priv->session = _gdata_service_get_session (priv->service);
+ break;
+ case PROP_UPLOAD_URI:
+ priv->upload_uri = g_value_dup_string (value);
+ break;
+ case PROP_ENTRY:
+ priv->entry = g_value_dup_object (value);
+ break;
+ case PROP_SLUG:
+ priv->slug = g_value_dup_string (value);
+ break;
+ case PROP_CONTENT_TYPE:
+ priv->content_type = g_value_dup_string (value);
+ break;
+ default:
+ /* We don't have any other property... */
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+write_cancelled_cb (GCancellable *cancellable, GDataUploadStream *self)
+{
+ GDataUploadStreamPrivate *priv = self->priv;
+
+ /* Set the error and signal that the write operation has finished */
+ g_static_mutex_lock (&(priv->write_mutex));
+
+ g_static_mutex_lock (&(priv->response_mutex));
+ g_cancellable_set_error_if_cancelled (cancellable, &(priv->response_error));
+ g_static_mutex_unlock (&(priv->response_mutex));
+
+ priv->write_finished = TRUE;
+ g_cond_signal (priv->write_cond);
+
+ g_static_mutex_unlock (&(priv->write_mutex));
+}
+
+static gssize
+gdata_upload_stream_write (GOutputStream *stream, const void *buffer, gsize count, GCancellable *cancellable, GError **error)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM (stream)->priv;
+ gsize length_written = count;
+ gulong cancelled_signal = 0;
+
+ /* Listen for cancellation events */
+ if (cancellable != NULL)
+ cancelled_signal = g_signal_connect (cancellable, "cancelled", (GCallback) write_cancelled_cb, GDATA_UPLOAD_STREAM (stream));
+
+ /* Set write_finished so we know if the write operation has finished before we reach write_cond */
+ priv->write_finished = FALSE;
+
+ /* Handle the more common case of the network thread already having been created first */
+ if (priv->network_thread != NULL) {
+ /* Push the new data into the buffer */
+ gdata_buffer_push_data (priv->buffer, buffer, count);
+ goto write;
+ }
+
+ /* We're lazy about starting the network operation so we don't time out before we've even started */
+ if (priv->entry != NULL) {
+ /* Start by writing out the entry; then the thread has something to write to the network when it's created */
+ const gchar *first_part_header;
+ gchar *entry_xml, *second_part_header;
+
+ first_part_header = "--" BOUNDARY_STRING "\nContent-Type: application/atom+xml; charset=UTF-8\n\n<?xml version='1.0'?>";
+ entry_xml = gdata_parsable_get_xml (GDATA_PARSABLE (priv->entry));
+ second_part_header = g_strdup_printf ("\n--" BOUNDARY_STRING "\nContent-Type: %s\nContent-Transfer-Encoding: binary\n\n",
+ priv->content_type);
+
+ /* Push the message parts onto the message body; we can skip the buffer, since the network thread hasn't yet been created,
+ * so we're the sole thread accessing the SoupMessage. */
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_STATIC, first_part_header, strlen (first_part_header));
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_TAKE, entry_xml, strlen (entry_xml));
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_TAKE, second_part_header, strlen (second_part_header));
+ }
+
+ /* Also write out the first chunk of data, so there's guaranteed to be something in the request body */
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_COPY, buffer, count);
+
+ /* Create the thread and let the writing commence! */
+ create_network_thread (GDATA_UPLOAD_STREAM (stream), error);
+ if (priv->network_thread == NULL) {
+ if (cancellable != NULL)
+ g_signal_handler_disconnect (cancellable, cancelled_signal);
+ return -1;
+ }
+
+write:
+ /* Wait for it to be written */
+ g_static_mutex_lock (&(priv->write_mutex));
+ if (priv->write_finished == FALSE)
+ g_cond_wait (priv->write_cond, g_static_mutex_get_mutex (&(priv->write_mutex)));
+ g_static_mutex_unlock (&(priv->write_mutex));
+
+ g_static_mutex_lock (&(priv->response_mutex));
+
+ /* Disconnect from the cancelled signal so we can't receive any more cancel events before we handle errors */
+ if (cancellable != NULL)
+ g_signal_handler_disconnect (cancellable, cancelled_signal);
+
+ /* Check for an error and return if necessary */
+ if (priv->response_error != NULL) {
+ g_propagate_error (error, priv->response_error);
+ priv->response_error = NULL;
+ length_written = -1;
+ }
+
+ g_static_mutex_unlock (&(priv->response_mutex));
+
+ return length_written;
+}
+
+static void
+close_cancelled_cb (GCancellable *cancellable, GDataUploadStream *self)
+{
+ GDataUploadStreamPrivate *priv = self->priv;
+
+ /* Set the error and signal that the close operation has finished */
+ g_static_mutex_lock (&(priv->response_mutex));
+ g_cancellable_set_error_if_cancelled (cancellable, &(priv->response_error));
+ g_cond_signal (priv->finished_cond);
+ g_static_mutex_unlock (&(priv->response_mutex));
+}
+
+/* It's guaranteed that we set the response_status and are done with *all* network activity before this returns.
+ * This means that it's safe to call gdata_upload_stream_get_response() once a call to close() has returned. */
+static gboolean
+gdata_upload_stream_close (GOutputStream *stream, GCancellable *cancellable, GError **error)
+{
+ GDataUploadStreamPrivate *priv = GDATA_UPLOAD_STREAM (stream)->priv;
+ gboolean success = TRUE;
+
+ g_static_mutex_lock (&(priv->response_mutex));
+
+ if (priv->response_status == 0) {
+ gulong cancelled_signal = 0;
+
+ /* Mark the buffer as having reached EOF, and the write operation will close in its own time */
+ gdata_buffer_push_data (priv->buffer, NULL, 0);
+
+ /* Allow cancellation */
+ if (cancellable != NULL)
+ cancelled_signal = g_signal_connect (cancellable, "cancelled", (GCallback) close_cancelled_cb, GDATA_UPLOAD_STREAM (stream));
+
+ /* Wait for the signal that we've finished */
+ g_cond_wait (priv->finished_cond, g_static_mutex_get_mutex (&(priv->response_mutex)));
+
+ /* Disconnect from the signal handler so we can't receive any more cancellation events before we handle errors*/
+ if (cancellable != NULL)
+ g_signal_handler_disconnect (cancellable, cancelled_signal);
+ }
+
+ /* Report any errors which have been set by the network thread */
+ if (priv->response_error != NULL) {
+ g_propagate_error (error, priv->response_error);
+ priv->response_error = NULL;
+ success = FALSE;
+ }
+
+ g_static_mutex_unlock (&(priv->response_mutex));
+
+ return success;
+}
+
+/* In the network thread context, called after a chunk has been written.
+ * We don't let it return until we've finished pushing all the data into the buffer.
+ * This is due to http://bugzilla.gnome.org/show_bug.cgi?id=522147, which means that
+ * we can't use soup_session_(un)?pause_message() with a SoupSessionSync.
+ * If we don't return from this signal handler, the message is never paused, and thus
+ * Bad Things don't happen (due to the bug, messages can be paused, but not unpaused).
+ * Obviously this means that our memory usage will increase, and we'll eventually end
+ * up storing the entire request body in memory, but that's unavoidable at this point.
+ * The additions to work around the bug are commented as "bgo#522147". */
+static void
+wrote_chunk_cb (SoupMessage *message, GDataUploadStream *self)
+{
+ #define CHUNK_SIZE 8192 /* 8KiB */
+
+ GDataUploadStreamPrivate *priv = self->priv;
+ gsize length;
+ guint8 buffer[CHUNK_SIZE];
+
+ /* Signal the main thread that the chunk has been written */
+ g_static_mutex_lock (&(priv->write_mutex));
+ priv->write_finished = TRUE;
+ g_cond_signal (priv->write_cond);
+ g_static_mutex_unlock (&(priv->write_mutex));
+
+ /* We've written one chunk; now let's append another to the body so it can join in the fun.
+ * Note that this call isn't blocking, and can return less than the CHUNK_SIZE. This is because
+ * we could deadlock if we block on getting CHUNK_SIZE bytes at the end of the stream. write() could
+ * easily be called with fewer bytes, but has no way to notify us that we've reached the end of the
+ * stream, so we'd happily block on receiving more bytes which weren't forthcoming. */
+ length = gdata_buffer_pop_data_limited (priv->buffer, buffer, CHUNK_SIZE);
+ if (length == 0) {
+ /* We've reached the end of the stream, so append the footer (if appropriate) and stop */
+ if (priv->entry != NULL) {
+ const gchar *footer = "\n--" BOUNDARY_STRING "--";
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_STATIC, footer, strlen (footer));
+ }
+
+ soup_message_body_complete (priv->message->request_body);
+ } else {
+ soup_message_body_append (priv->message->request_body, SOUP_MEMORY_COPY, buffer, length);
+ }
+
+ soup_session_unpause_message (priv->session, priv->message);
+}
+
+static gpointer
+upload_thread (GDataUploadStream *self)
+{
+ GDataUploadStreamPrivate *priv = self->priv;
+ guint status;
+
+ /* Connect to the wrote-* signals so we can prepare the next chunk for transmission */
+ g_signal_connect (priv->message, "wrote-headers", (GCallback) wrote_chunk_cb, self);
+ g_signal_connect (priv->message, "wrote-chunk", (GCallback) wrote_chunk_cb, self);
+
+ status = soup_session_send_message (priv->session, priv->message);
+
+ /* Signal write_cond, just in case we errored out and finished sending in the middle of a write */
+ g_static_mutex_lock (&(priv->response_mutex));
+ g_static_mutex_lock (&(priv->write_mutex));
+ priv->write_finished = TRUE;
+ g_cond_signal (priv->write_cond);
+ g_static_mutex_unlock (&(priv->write_mutex));
+
+ /* Deal with the response if it was unsuccessful */
+ if (SOUP_STATUS_IS_SUCCESSFUL (status) == FALSE) {
+ GDataServiceClass *klass = GDATA_SERVICE_GET_CLASS (priv->service);
+
+ /* Error! Store it in the structure, and it'll be returned by the next function in the main thread
+ * which can give an error response.*/
+ g_assert (klass->parse_error_response != NULL);
+ klass->parse_error_response (priv->service, GDATA_SERVICE_ERROR_WITH_UPLOAD, status, priv->message->reason_phrase,
+ priv->message->response_body->data, priv->message->response_body->length, &(priv->response_error));
+ }
+
+ /* Signal the main thread that the response is ready (good or bad) */
+ priv->response_status = status;
+ g_cond_signal (priv->finished_cond);
+
+ g_static_mutex_unlock (&(priv->response_mutex));
+
+ return NULL;
+}
+
+static void
+create_network_thread (GDataUploadStream *self, GError **error)
+{
+ self->priv->network_thread = g_thread_create ((GThreadFunc) upload_thread, self, TRUE, error);
+}
+
+/**
+ * gdata_upload_stream_new:
+ * @service: a #GDataService
+ * @upload_uri: the URI to upload
+ * @entry: the entry to upload as metadata, or %NULL
+ * @slug: the file's slug (filename)
+ * @content_type: the content type of the file being uploaded
+ *
+ * Creates a new #GDataUploadStream, allowing a file to be uploaded from a GData service using standard #GOutputStream API.
+ *
+ * If @entry is specified, it will be attached to the upload as the entry to which the file being uploaded belongs. Otherwise, just the file
+ * written to the stream will be uploaded, and given a default entry as determined by the server.
+ *
+ * @slug and @content_type must be specified before the upload begins, as they describe the file being streamed. @slug is the filename given to the
+ * file, which will typically be stored on the server and made available when downloading the file again. @content_type must be the correct
+ * content type for the file, and should be in the service's list of acceptable content types.
+ *
+ * As well as the standard GIO errors, calls to the #GOutputStream API on a #GDataUploadStream can also return any relevant specific error from
+ * #GDataServiceError, or %GDATA_SERVICE_ERROR_WITH_UPLOAD in the general case.
+ *
+ * Note that network communication won't begin until the first call to g_output_stream_write() on the #GDataUploadStream.
+ *
+ * Return value: a new #GOutputStream, or %NULL; unref with g_object_unref()
+ *
+ * Since: 0.5.0
+ **/
+GOutputStream *
+gdata_upload_stream_new (GDataService *service, const gchar *upload_uri, GDataEntry *entry, const gchar *slug, const gchar *content_type)
+{
+ GDataServiceClass *klass;
+ GDataUploadStream *upload_stream;
+ SoupMessage *message;
+
+ g_return_val_if_fail (GDATA_IS_SERVICE (service), NULL);
+ g_return_val_if_fail (upload_uri != NULL, NULL);
+ g_return_val_if_fail (entry == NULL || GDATA_IS_ENTRY (entry), NULL);
+ g_return_val_if_fail (slug != NULL, NULL);
+ g_return_val_if_fail (content_type != NULL, NULL);
+
+ /* Build the message */
+ message = soup_message_new (SOUP_METHOD_POST, upload_uri);
+
+ /* Make sure the headers are set */
+ klass = GDATA_SERVICE_GET_CLASS (service);
+ if (klass->append_query_headers != NULL)
+ klass->append_query_headers (GDATA_SERVICE (service), message);
+
+ if (slug != NULL)
+ soup_message_headers_append (message->request_headers, "Slug", slug);
+
+ /* We don't want to accumulate chunks */
+ soup_message_body_set_accumulate (message->request_body, FALSE);
+ soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CHUNKED);
+
+ /* The Content-Type should be multipart/related if we're also uploading the metadata (entry != NULL),
+ * and the given content_type otherwise.
+ * Note that the Content-Length header is set when we first start writing to the network. */
+ if (entry != NULL)
+ soup_message_headers_set_content_type (message->request_headers, "multipart/related; boundary=" BOUNDARY_STRING, NULL);
+ else
+ soup_message_headers_set_content_type (message->request_headers, content_type, NULL);
+
+ /* If the entry exists and has an ETag, we assume we're updating the entry, so we can set the If-Match header */
+ if (entry != NULL && gdata_entry_get_etag (entry) != NULL)
+ soup_message_headers_append (message->request_headers, "If-Match", gdata_entry_get_etag (entry));
+
+ /* Create the upload stream */
+ upload_stream = g_object_new (GDATA_TYPE_UPLOAD_STREAM, "upload-uri", upload_uri, "service", service, "entry", entry,
+ "slug", slug, "content-type", content_type, NULL);
+ upload_stream->priv->message = message;
+
+ /* Uploading doesn't actually start until the first call to write() */
+
+ return G_OUTPUT_STREAM (upload_stream);
+}
+
+/**
+ * gdata_upload_stream_get_response:
+ * @self: a #GDataUploadStream
+ * @length: return location for the length of the response, or %NULL
+ *
+ * Returns the server's response to the upload operation performed by the #GDataUploadStream. If the operation
+ * is still underway, or the server's response hasn't been received yet, %NULL is returned and @length is set to %-1.
+ *
+ * If there was an error during the upload operation (but it is complete), %NULL is returned, and @length is set to %0.
+ *
+ * While it is safe to call this function from any thread at any time during the network operation, the only way to
+ * guarantee that the response has been set before calling this function is to have closed the #GDataUploadStream. Once
+ * the stream has been closed, all network communication is guaranteed to have finished.
+ *
+ * Return value: the server's response to the upload, or %NULL
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_upload_stream_get_response (GDataUploadStream *self, gssize *length)
+{
+ gssize _length;
+ const gchar *_response;
+
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+
+ g_static_mutex_lock (&(self->priv->response_mutex));
+
+ if (self->priv->response_status == 0) {
+ /* We can't touch the message until the network thread has finished using it, since it isn't threadsafe */
+ _length = -1;
+ _response = NULL;
+ } else if (SOUP_STATUS_IS_SUCCESSFUL (self->priv->response_status) == FALSE) {
+ /* The response has been received, and was unsuccessful */
+ _length = 0;
+ _response = NULL;
+ } else {
+ /* The response has been received, and was successful */
+ _length = self->priv->message->response_body->length;
+ _response = self->priv->message->response_body->data;
+ }
+
+ g_static_mutex_unlock (&(self->priv->response_mutex));
+
+ if (length != NULL)
+ *length = _length;
+ return _response;
+}
+
+/**
+ * gdata_upload_stream_get_service:
+ * @self: a #GDataUploadStream
+ *
+ * Gets the service used to authenticate the upload, as passed to gdata_upload_stream_new().
+ *
+ * Return value: the #GDataService used to authenticate the upload
+ *
+ * Since: 0.5.0
+ **/
+GDataService *
+gdata_upload_stream_get_service (GDataUploadStream *self)
+{
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+ return self->priv->service;
+}
+
+/**
+ * gdata_upload_stream_get_upload_uri:
+ * @self: a #GDataUploadStream
+ *
+ * Gets the URI the file is being uploaded to, as passed to gdata_upload_stream_new().
+ *
+ * Return value: the URI which the file is being uploaded to
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_upload_stream_get_upload_uri (GDataUploadStream *self)
+{
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+ return self->priv->upload_uri;
+}
+
+/**
+ * gdata_upload_stream_get_entry:
+ * @self: a #GDataUploadStream
+ *
+ * Gets the entry being used to upload metadata, if one was passed to gdata_upload_stream_new().
+ *
+ * Return value: the entry used for metadata, or %NULL
+ *
+ * Since: 0.5.0
+ **/
+GDataEntry *
+gdata_upload_stream_get_entry (GDataUploadStream *self)
+{
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+ return self->priv->entry;
+}
+
+/**
+ * gdata_upload_stream_get_slug:
+ * @self: a #GDataUploadStream
+ *
+ * Gets the slug (filename) of the file being uploaded.
+ *
+ * Return value: the slug of the file being uploaded
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_upload_stream_get_slug (GDataUploadStream *self)
+{
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+ return self->priv->slug;
+}
+
+/**
+ * gdata_upload_stream_get_content_type:
+ * @self: a #GDataUploadStream
+ *
+ * Gets the content type of the file being uploaded.
+ *
+ * Return value: the content type of the file being uploaded
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_upload_stream_get_content_type (GDataUploadStream *self)
+{
+ g_return_val_if_fail (GDATA_IS_UPLOAD_STREAM (self), NULL);
+ return self->priv->content_type;
+}
diff --git a/gdata/gdata-upload-stream.h b/gdata/gdata-upload-stream.h
new file mode 100644
index 0000000..d15dae6
--- /dev/null
+++ b/gdata/gdata-upload-stream.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2009 <philip tecnocode co uk>
+ *
+ * GData Client 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.1 of the License, or (at your option) any later version.
+ *
+ * GData Client 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 GData Client. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_UPLOAD_STREAM_H
+#define GDATA_UPLOAD_STREAM_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include <gdata/gdata-service.h>
+#include <gdata/gdata-entry.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_UPLOAD_STREAM (gdata_upload_stream_get_type ())
+#define GDATA_UPLOAD_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_UPLOAD_STREAM, GDataUploadStream))
+#define GDATA_UPLOAD_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_UPLOAD_STREAM, GDataUploadStreamClass))
+#define GDATA_IS_UPLOAD_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_UPLOAD_STREAM))
+#define GDATA_IS_UPLOAD_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_UPLOAD_STREAM))
+#define GDATA_UPLOAD_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_UPLOAD_STREAM, GDataUploadStreamClass))
+
+typedef struct _GDataUploadStreamPrivate GDataUploadStreamPrivate;
+
+/**
+ * GDataUploadStream:
+ *
+ * All the fields in the #GDataUploadStream structure are private and should never be accessed directly.
+ *
+ * Since: 0.5.0
+ **/
+typedef struct {
+ GOutputStream parent;
+ GDataUploadStreamPrivate *priv;
+} GDataUploadStream;
+
+/**
+ * GDataUploadStreamClass:
+ *
+ * All the fields in the #GDataUploadStreamClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.5.0
+ **/
+typedef struct {
+ /*< private >*/
+ GOutputStreamClass parent;
+} GDataUploadStreamClass;
+
+GType gdata_upload_stream_get_type (void) G_GNUC_CONST;
+
+GOutputStream *gdata_upload_stream_new (GDataService *service, const gchar *upload_uri, GDataEntry *entry,
+ const gchar *slug, const gchar *content_type) G_GNUC_WARN_UNUSED_RESULT;
+
+const gchar *gdata_upload_stream_get_response (GDataUploadStream *self, gssize *length);
+
+GDataService *gdata_upload_stream_get_service (GDataUploadStream *self);
+const gchar *gdata_upload_stream_get_upload_uri (GDataUploadStream *self);
+GDataEntry *gdata_upload_stream_get_entry (GDataUploadStream *self);
+const gchar *gdata_upload_stream_get_slug (GDataUploadStream *self);
+const gchar *gdata_upload_stream_get_content_type (GDataUploadStream *self);
+
+G_END_DECLS
+
+#endif /* !GDATA_UPLOAD_STREAM_H */
diff --git a/gdata/gdata.h b/gdata/gdata.h
index c68e7c6..2bb1f49 100644
--- a/gdata/gdata.h
+++ b/gdata/gdata.h
@@ -31,6 +31,7 @@
#include <gdata/gdata-access-rule.h>
#include <gdata/gdata-parsable.h>
#include <gdata/gdata-download-stream.h>
+#include <gdata/gdata-upload-stream.h>
/* Namespaces */
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index d2c24ac..1ab37fd 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -645,3 +645,10 @@ gdata_download_stream_get_service
gdata_download_stream_get_download_uri
gdata_download_stream_get_content_type
gdata_download_stream_get_content_length
+gdata_upload_stream_get_type
+gdata_upload_stream_new
+gdata_upload_stream_get_service
+gdata_upload_stream_get_upload_uri
+gdata_upload_stream_get_entry
+gdata_upload_stream_get_slug
+gdata_upload_stream_get_content_type
diff --git a/gdata/services/documents/gdata-documents-service.c b/gdata/services/documents/gdata-documents-service.c
index 25d546b..7988c4d 100644
--- a/gdata/services/documents/gdata-documents-service.c
+++ b/gdata/services/documents/gdata-documents-service.c
@@ -46,6 +46,7 @@
#include "gdata-documents-presentation.h"
#include "gdata-service.h"
#include "gdata-private.h"
+#include "gdata-upload-stream.h"
GQuark
gdata_documents_service_error_quark (void)
@@ -58,8 +59,7 @@ static void gdata_documents_service_get_property (GObject *object, guint propert
static void notify_authenticated_cb (GObject *service, GParamSpec *pspec, GObject *self);
static void notify_proxy_uri_cb (GObject *service, GParamSpec *pspec, GObject *self);
GDataDocumentsEntry *upload_update_document (GDataDocumentsService *self, GDataDocumentsEntry *document, GFile *document_file,
- SoupMessage *message, guint good_status_code,
- GCancellable *cancellable, GError **error);
+ const gchar *upload_uri, GCancellable *cancellable, GError **error);
struct _GDataDocumentsServicePrivate {
GDataService *spreadsheet_service;
@@ -327,177 +327,102 @@ notify_proxy_uri_cb (GObject *service, GParamSpec *pspec, GObject *self)
}
GDataDocumentsEntry *
-upload_update_document (GDataDocumentsService *self, GDataDocumentsEntry *document, GFile *document_file, SoupMessage *message,
- guint good_status_code, GCancellable *cancellable, GError **error)
+upload_update_document (GDataDocumentsService *self, GDataDocumentsEntry *document, GFile *document_file, const gchar *upload_uri,
+ GCancellable *cancellable, GError **error)
{
- /* TODO: Async variant */
- #define BOUNDARY_STRING "0003Z5W789RTE456KlemsnoZV"
-
- GDataServiceClass *klass;
- GType new_document_type = GDATA_TYPE_DOCUMENTS_ENTRY; /* If the document type is wrong we get an error from the server anyway */
- gchar *entry_xml, *second_chunk_header, *upload_data, *document_contents = NULL, *i;
- const gchar *first_chunk_header, *footer;
- GFileInfo *document_file_info = NULL;
- guint status;
- gsize content_length, first_chunk_header_length, second_chunk_header_length, entry_xml_length, document_length, footer_length;
-
- g_return_val_if_fail (document != NULL || document_file != NULL, NULL);
- g_return_val_if_fail (message != NULL, NULL);
-
- /* Make sure subclasses set their headers */
- klass = GDATA_SERVICE_GET_CLASS (self);
- if (klass->append_query_headers != NULL)
- klass->append_query_headers (GDATA_SERVICE (self), message);
-
- /* Gets document file information */
+ GDataDocumentsEntry *new_entry;
+ GOutputStream *output_stream;
+ GInputStream *input_stream;
+ const gchar *slug = NULL, *content_type = NULL, *response_body;
+ gssize response_length;
+ GFileInfo *file_info = NULL;
+ GType new_document_type;
+
+ /* Get some information about the file we're uploading */
if (document_file != NULL) {
- /* Get the data early so we can calculate the content length */
- if (g_file_load_contents (document_file, NULL, &document_contents, &document_length, NULL, error) == FALSE)
+ /* Get the slug and content type */
+ file_info = g_file_query_info (document_file, "standard::display-name,standard::content-type", G_FILE_QUERY_INFO_NONE, NULL, error);
+ if (file_info == NULL)
return NULL;
- document_file_info = g_file_query_info (document_file, "standard::display-name,standard::content-type", G_FILE_QUERY_INFO_NONE, NULL, error);
- if (document_file_info == NULL) {
- g_free (document_contents);
- return NULL;
- }
+ slug = g_file_info_get_display_name (file_info);
+ content_type = g_file_info_get_content_type (file_info);
- /* Check for cancellation */
- if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
- g_free (document_contents);
- g_object_unref (document_file_info);
- return NULL;
- }
-
- /* Add document-upload--specific headers */
- soup_message_headers_append (message->request_headers, "Slug", g_file_info_get_display_name (document_file_info));
-
- if (document == NULL) {
- const gchar *content_type = g_file_info_get_content_type (document_file_info);
-
- /* Corrects a bug on spreadsheet content types handling
- * The content type for ODF spreasheets is "application/vnd.oasis.opendocument.spreadsheet" for my ODF spreadsheet;
- * but Google Documents' spreadsheet service is waiting for "application/x-vnd.oasis.opendocument.spreadsheet"
- * and nothing else.
- * Bug filed with Google: http://code.google.com/p/gdata-issues/issues/detail?id=1127 */
- if (strcmp (content_type, "application/vnd.oasis.opendocument.spreadsheet") == 0)
- content_type = "application/x-vnd.oasis.opendocument.spreadsheet";
- soup_message_set_request (message, content_type, SOUP_MEMORY_TAKE, document_contents, document_length);
-
- /* We get the document type of the document which is being uploaded */
- if (strcmp (content_type, "application/x-vnd.oasis.opendocument.spreadsheet") == 0 ||
- strcmp (content_type, "text/tab-separated-values") == 0 ||
- strcmp (content_type, "application/x-vnd.oasis.opendocument.spreadsheet") == 0 ||
- strcmp (content_type, "application/vnd.ms-excel") == 0) {
- new_document_type = GDATA_TYPE_DOCUMENTS_SPREADSHEET;
- } else if (strcmp (content_type, "application/msword") == 0 ||
- strcmp (content_type, "application/vnd.oasis.opendocument.text") == 0 ||
- strcmp (content_type, "application/rtf") == 0 ||
- strcmp (content_type, "text/html") == 0 ||
- strcmp (content_type, "application/vnd.sun.xml.writer") == 0 ||
- strcmp (content_type, "text/plain") == 0) {
- new_document_type = GDATA_TYPE_DOCUMENTS_TEXT;
- } else if (strcmp (content_type, "application/vnd.ms-powerpoint") == 0) {
- new_document_type = GDATA_TYPE_DOCUMENTS_PRESENTATION;
- } else {
- g_set_error_literal (error, GDATA_DOCUMENTS_SERVICE_ERROR, GDATA_DOCUMENTS_SERVICE_ERROR_INVALID_CONTENT_TYPE,
- _("The supplied document had an invalid content type."));
- g_free (document_contents);
- g_object_unref (document_file_info);
- return NULL;
- }
- }
+ /* Corrects a bug on spreadsheet content types handling
+ * The content type for ODF spreasheets is "application/vnd.oasis.opendocument.spreadsheet" for my ODF spreadsheet;
+ * but Google Documents' spreadsheet service is waiting for "application/x-vnd.oasis.opendocument.spreadsheet"
+ * and nothing else.
+ * Bug filed with Google: http://code.google.com/p/gdata-issues/issues/detail?id=1127 */
+ if (strcmp (content_type, "application/vnd.oasis.opendocument.spreadsheet") == 0)
+ content_type = "application/x-vnd.oasis.opendocument.spreadsheet";
}
- /* Prepare XML file to upload metadata */
- if (document != NULL) {
- /* Test on the document entry */
- new_document_type = G_OBJECT_TYPE (document);
-
- /* Get the XML content */
- entry_xml = gdata_parsable_get_xml (GDATA_PARSABLE (document));
-
- /* Check for cancellation */
- if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
- g_free (entry_xml);
- g_free (document_contents);
- if (document_file_info != NULL)
- g_object_unref (document_file_info);
- return NULL;
- }
-
- /* Prepare the data to upload containg both metadata and document content */
- if (document_file != NULL) {
- const gchar *content_type = g_file_info_get_content_type (document_file_info);
-
- first_chunk_header = "--" BOUNDARY_STRING "\nContent-Type: application/atom+xml; charset=UTF-8\n\n<?xml version='1.0'?>";
- /* Corrects a bug on spreadsheet content types handling (see above) */
- if (strcmp (content_type, "application/vnd.oasis.opendocument.spreadsheet") == 0)
- content_type = "application/x-vnd.oasis.opendocument.spreadsheet";
-
- second_chunk_header = g_strdup_printf ("\n--" BOUNDARY_STRING "\nContent-Type: %s\n\n", content_type);
- footer = "\n--" BOUNDARY_STRING "--";
-
- first_chunk_header_length = strlen (first_chunk_header);
- second_chunk_header_length = strlen (second_chunk_header);
- entry_xml_length = strlen (entry_xml);
- footer_length = strlen (footer);
- content_length = first_chunk_header_length + entry_xml_length + second_chunk_header_length + document_length + footer_length;
-
- /* Build the upload data */
- upload_data = i = g_malloc (content_length);
-
- memcpy (upload_data, first_chunk_header, first_chunk_header_length);
- i += first_chunk_header_length;
-
- memcpy (i, entry_xml, entry_xml_length);
- i += entry_xml_length;
- g_free (entry_xml);
-
- memcpy (i, second_chunk_header, second_chunk_header_length);
- g_free (second_chunk_header);
- i += second_chunk_header_length;
-
- memcpy (i, document_contents, document_length);
- g_free (document_contents);
- i += document_length;
-
- memcpy (i, footer, footer_length);
-
- /* Append the data */
- soup_message_set_request (message, "multipart/related; boundary=" BOUNDARY_STRING, SOUP_MEMORY_TAKE, upload_data, content_length);
+ /* Determine the type of the document we're uploading */
+ if (document == NULL) {
+ /* We get the document type of the document which is being uploaded */
+ if (strcmp (content_type, "application/x-vnd.oasis.opendocument.spreadsheet") == 0 ||
+ strcmp (content_type, "text/tab-separated-values") == 0 ||
+ strcmp (content_type, "application/x-vnd.oasis.opendocument.spreadsheet") == 0 ||
+ strcmp (content_type, "application/vnd.ms-excel") == 0) {
+ new_document_type = GDATA_TYPE_DOCUMENTS_SPREADSHEET;
+ } else if (strcmp (content_type, "application/msword") == 0 ||
+ strcmp (content_type, "application/vnd.oasis.opendocument.text") == 0 ||
+ strcmp (content_type, "application/rtf") == 0 ||
+ strcmp (content_type, "text/html") == 0 ||
+ strcmp (content_type, "application/vnd.sun.xml.writer") == 0 ||
+ strcmp (content_type, "text/plain") == 0) {
+ new_document_type = GDATA_TYPE_DOCUMENTS_TEXT;
+ } else if (strcmp (content_type, "application/vnd.ms-powerpoint") == 0) {
+ new_document_type = GDATA_TYPE_DOCUMENTS_PRESENTATION;
} else {
- /* Send only metadata */
- upload_data = g_strconcat ("<?xml version='1.0' encoding='UTF-8'?>", entry_xml, NULL);
- g_free (entry_xml);
- soup_message_set_request (message, "application/atom+xml", SOUP_MEMORY_TAKE, upload_data, strlen (upload_data));
+ g_set_error_literal (error, GDATA_DOCUMENTS_SERVICE_ERROR, GDATA_DOCUMENTS_SERVICE_ERROR_INVALID_CONTENT_TYPE,
+ _("The supplied document had an invalid content type."));
+ if (file_info != NULL)
+ g_object_unref (file_info);
+ return NULL;
}
+ } else {
+ new_document_type = G_OBJECT_TYPE (document);
+ }
- if (document_file_info != NULL)
- g_object_unref (document_file_info);
+ /* If we're not uploading a file, simply use gdata_service_insert_entry() */
+ if (document_file == NULL) {
+ return GDATA_DOCUMENTS_ENTRY (gdata_service_insert_entry (GDATA_SERVICE (self), upload_uri, GDATA_ENTRY (document),
+ cancellable, error));
}
- /* Send the message */
- status = _gdata_service_send_message (GDATA_SERVICE (self), message, error);
- if (status == SOUP_STATUS_NONE)
+ /* Otherwise, we need streaming file I/O: GDataUploadStream */
+ output_stream = gdata_upload_stream_new (GDATA_SERVICE (self), upload_uri, GDATA_ENTRY (document), slug, content_type);
+
+ if (file_info != NULL)
+ g_object_unref (file_info);
+ if (output_stream == NULL)
return NULL;
- /* Check for cancellation */
- if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE)
+ /* Open the document file for reading and pipe it to the upload stream */
+ input_stream = G_INPUT_STREAM (g_file_read (document_file, cancellable, error));
+ if (input_stream == NULL) {
+ g_object_unref (output_stream);
return NULL;
+ }
- if (status != good_status_code) {
- /* Error */
- g_assert (klass->parse_error_response != NULL);
- klass->parse_error_response (GDATA_SERVICE (self), GDATA_SERVICE_ERROR_WITH_INSERTION, status, message->reason_phrase, message->response_body->data,
- message->response_body->length, error);
+ g_output_stream_splice (output_stream, input_stream, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+ cancellable, error);
+
+ g_object_unref (input_stream);
+ if (error != NULL && *error != NULL) {
+ g_object_unref (output_stream);
return NULL;
}
- /* Build the updated entry */
- g_assert (message->response_body->data != NULL);
+ /* Get and parse the response from the server */
+ response_body = gdata_upload_stream_get_response (GDATA_UPLOAD_STREAM (output_stream), &response_length);
+ g_assert (response_body != NULL && response_length > 0);
+
+ new_entry = GDATA_DOCUMENTS_ENTRY (gdata_parsable_new_from_xml (new_document_type, response_body, (gint) response_length, error));
+ g_object_unref (output_stream);
- /* Parse the XML; create and return a new GDataEntry of the same type as @entry */
- return GDATA_DOCUMENTS_ENTRY (gdata_parsable_new_from_xml (new_document_type, message->response_body->data, (gint) message->response_body->length, error));
+ return new_entry;
}
/**
@@ -531,7 +456,6 @@ gdata_documents_service_upload_document (GDataDocumentsService *self, GDataDocum
GCancellable *cancellable, GError **error)
{
GDataDocumentsEntry *new_document;
- SoupMessage *message;
gchar *upload_uri;
g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (self), NULL);
@@ -562,12 +486,9 @@ gdata_documents_service_upload_document (GDataDocumentsService *self, GDataDocum
upload_uri = g_strdup ("http://docs.google.com/feeds/documents/private/full");
}
- message = soup_message_new (SOUP_METHOD_POST, upload_uri);
+ new_document = upload_update_document (self, document, document_file, upload_uri, cancellable, error);
g_free (upload_uri);
- new_document = upload_update_document (self, document, document_file, message, 201, cancellable, error);
- g_object_unref (message);
-
return new_document;
}
@@ -594,9 +515,7 @@ GDataDocumentsEntry *
gdata_documents_service_update_document (GDataDocumentsService *self, GDataDocumentsEntry *document, GFile *document_file,
GCancellable *cancellable, GError **error)
{
- GDataDocumentsEntry *updated_document;
- SoupMessage *message;
- GDataLink *update_uri;
+ GDataLink *update_link;
g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (self), NULL);
g_return_val_if_fail (GDATA_IS_DOCUMENTS_ENTRY (document), NULL);
@@ -610,18 +529,12 @@ gdata_documents_service_update_document (GDataDocumentsService *self, GDataDocum
}
if (document_file == NULL)
- update_uri = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_EDIT);
+ update_link = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_EDIT);
else
- update_uri = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_EDIT_MEDIA);
- g_assert (update_uri != NULL);
-
- message = soup_message_new (SOUP_METHOD_PUT, gdata_link_get_uri (update_uri));
- soup_message_headers_append (message->request_headers, "If-Match", gdata_entry_get_etag (GDATA_ENTRY (document)));
-
- updated_document = upload_update_document (self, document, document_file, message, 200, cancellable, error);
- g_object_unref (message);
+ update_link = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_EDIT_MEDIA);
+ g_assert (update_link != NULL);
- return updated_document;
+ return upload_update_document (self, document, document_file, gdata_link_get_uri (update_link), cancellable, error);
}
/**
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]