[libgdata] [core] Added GDataUploadStream to allow easy uploading of large files



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]