[libgdata] [core] Added GDataDownloadStream to allow easy downloading of large files



commit d35e5400ca860a5ea12c734bd6b4a9f8437cd091
Author: Philip Withnall <philip tecnocode co uk>
Date:   Mon Aug 3 19:05:27 2009 +0100

    [core] Added GDataDownloadStream to allow easy downloading of large files
    
    GDataDownloadStream is a subclass of GInputStream which allows large files to
    be downloaded in a more controlled manner than by just specifying the
    file to save them to.

 docs/reference/Makefile.am                         |    8 +-
 docs/reference/gdata-docs.xml                      |    1 +
 docs/reference/gdata-sections.txt                  |   25 +
 gdata/Makefile.am                                  |    8 +-
 gdata/gdata-buffer.c                               |  289 ++++++++++
 gdata/gdata-buffer.h                               |   58 ++
 gdata/gdata-download-stream.c                      |  562 ++++++++++++++++++++
 gdata/gdata-download-stream.h                      |   75 +++
 gdata/gdata-private.h                              |    1 +
 gdata/gdata-service.c                              |   20 +-
 gdata/gdata-service.h                              |    4 +-
 gdata/gdata.h                                      |    1 +
 gdata/gdata.symbols                                |    9 +
 gdata/services/documents/gdata-documents-entry.c   |  104 ++---
 .../documents/gdata-documents-presentation.c       |   57 ++-
 .../documents/gdata-documents-presentation.h       |    1 +
 gdata/services/documents/gdata-documents-service.c |    4 +-
 .../documents/gdata-documents-spreadsheet.c        |   81 ++-
 .../documents/gdata-documents-spreadsheet.h        |    2 +
 gdata/services/documents/gdata-documents-text.c    |   59 ++-
 gdata/services/documents/gdata-documents-text.h    |    1 +
 21 files changed, 1222 insertions(+), 148 deletions(-)
---
diff --git a/docs/reference/Makefile.am b/docs/reference/Makefile.am
index cb19f14..da1e470 100644
--- a/docs/reference/Makefile.am
+++ b/docs/reference/Makefile.am
@@ -57,17 +57,13 @@ IGNORE_HFILES = \
 	gdata-marshal.h		\
 	gdata-enums.h		\
 	gdata-media-enums.h	\
-	gdata-media-group.c	\
 	gdata-media-group.h	\
-	gdata-youtube-group.c	\
 	gdata-youtube-group.h	\
-	gdata-youtube-control.c	\
 	gdata-youtube-control.h	\
 	gdata-picasaweb-enums.h	\
-	gdata-exif-tags.c	\
 	gdata-exif-tags.h	\
-	gdata-georss-where.c	\
-	gdata-georss-where.h
+	gdata-georss-where.h	\
+	gdata-buffer.h
 
 # Images to copy into HTML directory.
 # e.g. HTML_IMAGES=$(top_srcdir)/gtk/stock-icons/stock_about_24.png
diff --git a/docs/reference/gdata-docs.xml b/docs/reference/gdata-docs.xml
index 6e5be1a..88cec0d 100644
--- a/docs/reference/gdata-docs.xml
+++ b/docs/reference/gdata-docs.xml
@@ -30,6 +30,7 @@
 			<xi:include href="xml/gdata-entry.xml"/>
 			<xi:include href="xml/gdata-types.xml"/>
 			<xi:include href="xml/gdata-parsable.xml"/>
+			<xi:include href="xml/gdata-download-stream.xml"/>
 		</chapter>
 
 		<chapter>
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index eb28ccc..de50548 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -1356,6 +1356,7 @@ GDataDocumentsPresentationClass
 GDataDocumentsPresentationFormat
 gdata_documents_presentation_new
 gdata_documents_presentation_download_document
+gdata_documents_presentation_get_download_uri
 <SUBSECTION Standard>
 gdata_documents_presentation_get_type
 GDATA_DOCUMENTS_PRESENTATION
@@ -1410,6 +1411,7 @@ GDataDocumentsSpreadsheetClass
 GDataDocumentsSpreadsheetFormat
 gdata_documents_spreadsheet_new
 gdata_documents_spreadsheet_download_document
+gdata_documents_spreadsheet_get_download_uri
 <SUBSECTION Standard>
 gdata_documents_spreadsheet_get_type
 GDATA_DOCUMENTS_SPREADSHEET
@@ -1432,6 +1434,7 @@ GDataDocumentsTextClass
 GDataDocumentsTextFormat
 gdata_documents_text_new
 gdata_documents_text_download_document
+gdata_documents_text_get_download_uri
 <SUBSECTION Standard>
 gdata_documents_text_get_type
 GDATA_DOCUMENTS_TEXT
@@ -1475,3 +1478,25 @@ gdata_documents_service_error_quark
 <SUBSECTION Private>
 GDataDocumentsServicePrivate
 </SECTION>
+
+<SECTION>
+<FILE>gdata-download-stream</FILE>
+<TITLE>GDataDownloadStream</TITLE>
+GDataDownloadStream
+GDataDownloadStreamClass
+gdata_download_stream_new
+gdata_download_stream_get_service
+gdata_download_stream_get_download_uri
+gdata_download_stream_get_content_type
+gdata_download_stream_get_content_length
+<SUBSECTION Standard>
+GDATA_DOWNLOAD_STREAM
+GDATA_DOWNLOAD_STREAM_CLASS
+GDATA_DOWNLOAD_STREAM_GET_CLASS
+GDATA_IS_DOWNLOAD_STREAM
+GDATA_IS_DOWNLOAD_STREAM_CLASS
+GDATA_TYPE_DOWNLOAD_STREAM
+gdata_download_stream_get_type
+<SUBSECTION Private>
+GDataDownloadStreamPrivate
+</SECTION>
diff --git a/gdata/Makefile.am b/gdata/Makefile.am
index 231054d..be1ec52 100644
--- a/gdata/Makefile.am
+++ b/gdata/Makefile.am
@@ -49,7 +49,8 @@ gdata_headers = \
 	gdata-query.h		\
 	gdata-access-handler.h	\
 	gdata-access-rule.h	\
-	gdata-parsable.h
+	gdata-parsable.h	\
+	gdata-download-stream.h
 
 gdataincludedir = $(pkgincludedir)/gdata
 gdatainclude_HEADERS = \
@@ -69,8 +70,11 @@ libgdata_la_SOURCES = \
 	gdata-access-handler.c	\
 	gdata-access-rule.c	\
 	gdata-parsable.c	\
+	gdata-download-stream.c	\
 	gdata-private.h		\
-	gdata-parser.h
+	gdata-parser.h		\
+	gdata-buffer.c		\
+	gdata-buffer.h
 
 libgdata_la_CPPFLAGS = \
 	-I$(top_srcdir)			\
diff --git a/gdata/gdata-buffer.c b/gdata/gdata-buffer.c
new file mode 100644
index 0000000..f5de476
--- /dev/null
+++ b/gdata/gdata-buffer.c
@@ -0,0 +1,289 @@
+/* -*- 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-buffer
+ * @short_description: GData buffer to allow threadsafe buffering
+ * @stability: Unstable
+ * @include: gdata/gdata-buffer.h
+ *
+ * #GDataBuffer is a simple object which allows threadsafe buffering of data meaning, for example, data can be received from
+ * the network in a "push" fashion, buffered, then sent out to an output stream in a "pull" fashion.
+ **/
+
+#include <config.h>
+#include <glib.h>
+#include <string.h>
+
+#include "gdata-buffer.h"
+
+struct _GDataBufferChunk {
+	/*< private >*/
+	guint8 *data;
+	gsize length;
+	GDataBufferChunk *next;
+	/* Note: the data is actually allocated in the same memory block, so it's inside this comment right now.
+	 * We simply set chunk->data to point to chunk + sizeof (GDataBufferChunk). */
+};
+
+/**
+ * gdata_buffer_new:
+ *
+ * Creates a new empty #GDataBuffer.
+ *
+ * Return value: a new #GDataBuffer; free with gdata_buffer_free()
+ *
+ * Since: 0.5.0
+ **/
+GDataBuffer *
+gdata_buffer_new (void)
+{
+	GDataBuffer *buffer = g_slice_new0 (GDataBuffer);
+	g_static_mutex_init (&(buffer->mutex));
+	buffer->cond = g_cond_new ();
+
+	return buffer;
+}
+
+/**
+ * gdata_buffer_free:
+ *
+ * Frees a #GDataBuffer. The function isn't threadsafe, so should only be called once
+ * use of the buffer has been reduced to only one thread (the reading thread, after
+ * the EOF has been reached).
+ *
+ * Since: 0.5.0
+ **/
+void
+gdata_buffer_free (GDataBuffer *self)
+{
+	GDataBufferChunk *chunk, *next_chunk;
+
+	for (chunk = self->head; chunk != NULL; chunk = next_chunk) {
+		next_chunk = chunk->next;
+		g_free (chunk);
+	}
+
+	g_cond_free (self->cond);
+	g_static_mutex_free (&(self->mutex));
+	g_slice_free (GDataBuffer, self);
+}
+
+/**
+ * gdata_buffer_push_data:
+ * @self: a #GDataBuffer
+ * @data: the data to push onto the buffer
+ * @length: the length of @data
+ *
+ * Pushes @length bytes of @data onto the buffer, taking a copy of the data. If @data is %NULL and @length is %0,
+ * the buffer will be marked as having reached the EOF, and subsequent calls to gdata_buffer_push_data()
+ * will fail and return %FALSE.
+ *
+ * Assuming the buffer hasn't reached EOF, this operation is guaranteed to succeed (unless memory allocation fails).
+ *
+ * This function holds the lock on the #GDataBuffer, and signals any waiting calls to gdata_buffer_pop_data() once
+ * the new data has been pushed onto the buffer. This function is threadsafe.
+ *
+ * Return value: %TRUE on success, %FALSE otherwise
+ *
+ * Since: 0.5.0
+ **/
+gboolean
+gdata_buffer_push_data (GDataBuffer *self, const guint8 *data, gsize length)
+{
+	GDataBufferChunk *chunk;
+
+	g_static_mutex_lock (&(self->mutex));
+
+	if (G_UNLIKELY (self->reached_eof == TRUE)) {
+		/* If we're marked as having reached EOF, don't accept any more data */
+		g_static_mutex_unlock (&(self->mutex));
+		return FALSE;
+	} else if (G_UNLIKELY (data == NULL && length == 0)) {
+		/* If @data is NULL and @length is 0, mark the buffer as having reached EOF,
+		 * and signal any waiting threads. */
+		self->reached_eof = TRUE;
+		g_cond_signal (self->cond);
+		g_static_mutex_unlock (&(self->mutex));
+		return FALSE;
+	}
+
+	/* Create the chunk */
+	chunk = g_malloc (sizeof (GDataBufferChunk) + length);
+	chunk->data = (gpointer) chunk + (gsize) sizeof (GDataBufferChunk);
+	chunk->length = length;
+	chunk->next = NULL;
+
+	/* Copy the data to the chunk */
+	memcpy (chunk->data, data, length);
+
+	/* Add it to the buffer's tail */
+	if (self->tail != NULL)
+		*(self->tail) = chunk;
+	else
+		self->head = chunk;
+	self->tail = &(chunk->next);
+	self->total_length += length;
+
+	/* Signal any threads waiting to pop that data is available */
+	g_cond_signal (self->cond);
+
+	g_static_mutex_unlock (&(self->mutex));
+
+	return TRUE;
+}
+
+typedef struct {
+	GDataBuffer *buffer;
+	gboolean *cancelled;
+} CancelledData;
+
+static void
+pop_cancelled_cb (GCancellable *cancellable, CancelledData *data)
+{
+	/* Signal the pop_data function that it should stop blocking and cancel */
+	*(data->cancelled) = TRUE;
+	g_cond_signal (data->buffer->cond);
+}
+
+/**
+ * gdata_buffer_pop_data:
+ * @self: a #GDataBuffer
+ * @data: return location for the popped data
+ * @length_requested: the number of bytes of data requested
+ * @cancellable: a #GCancellable, or %NULL
+ *
+ * Pops up to @length_requested bytes off the head of the buffer and copies them to @data, which must be allocated by
+ * the caller and have enough space to store at most @length_requested bytes of output.
+ *
+ * If the buffer contains enough data to satisfy @length_requested, this function returns immediately.
+ * Otherwise, this function blocks until data is pushed onto the head of the buffer with gdata_buffer_pop_data(). If
+ * the buffer is marked as having reached the EOF, this function will not block, and will instead return the
+ * remaining data in the buffer.
+ *
+ * This function holds the lock on the #GDataBuffer, and will automatically be signalled of new data pushed onto the
+ * buffer if it's blocking.
+ *
+ * If @cancellable is provided, calling g_cancellable_cancel() on it from another thread will cause the call to
+ * gdata_buffer_pop_data() to return immediately with whatever data it can find.
+ *
+ * Return value: the number of bytes returned in @data
+ *
+ * Since: 0.5.0
+ **/
+gsize
+gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested, GCancellable *cancellable)
+{
+	GDataBufferChunk *chunk;
+	gsize return_length = 0, length_remaining;
+
+	/* In the case:
+	 *  - length_requested < amount available: return length_requested
+	 *  - length_requested > amount available: block until more is available, return length_requested
+	 *  - length_requested > amount available and we've reached EOF: don't block, return all remaining data
+	 *  - length_requested is a whole number of chunks: remove those chunks, return length_requested
+	 *  - length_requested is less than one chunk: remove no chunks, return length_requested, set head_read_offset
+	 *  - length_requested is a fraction of multiple chunks: remove whole chunks, return length_requested, set head_read_offset for remaining fraction
+	 */
+
+	g_static_mutex_lock (&(self->mutex));
+
+	if (self->reached_eof == TRUE && length_requested > self->total_length) {
+		/* Return data up to the EOF */
+		return_length = self->total_length;
+	} else if (length_requested > self->total_length) {
+		gulong cancelled_signal = 0;
+		gboolean cancelled = FALSE;
+
+		/* Set up a handler so we can stop if we're cancelled */
+		if (cancellable != NULL) {
+			CancelledData cancelled_data;
+
+			cancelled_data.buffer = self;
+			cancelled_data.cancelled = &cancelled;
+
+			cancelled_signal = g_signal_connect (cancellable, "cancelled", (GCallback) pop_cancelled_cb, &cancelled_data);
+		}
+
+		/* Block until more data is available */
+		while (length_requested > self->total_length) {
+			g_cond_wait (self->cond, g_static_mutex_get_mutex (&(self->mutex)));
+
+			/* If the g_cond_wait() returned because it was signalled from the GCancellable callback (rather than from
+			 * data being pushed into the buffer), stop blocking for data and make do with what we have so far. */
+			if (cancelled == TRUE || self->reached_eof == TRUE) {
+				return_length = MIN (length_requested, self->total_length);
+				break;
+			} else {
+				return_length = length_requested;
+			}
+		}
+
+		/* Disconnect from the cancelled signal */
+		if (cancellable != NULL)
+			g_signal_handler_disconnect (cancellable, cancelled_signal);
+	} else {
+		return_length = 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)
+		return 0;
+
+	/* Otherwise, get on with things */
+	length_remaining = return_length;
+
+	/* We can't assume we'll have enough data, since we may have reached EOF */
+	chunk = self->head;
+	while (chunk != NULL && length_remaining >= chunk->length) {
+		GDataBufferChunk *next_chunk;
+		gsize chunk_length = chunk->length - self->head_read_offset;
+
+		/* Copy the data to the output */
+		length_remaining -= chunk_length;
+		memcpy (data, chunk->data + self->head_read_offset, chunk_length);
+		data += chunk_length;
+
+		/* Free the chunk and move on */
+		next_chunk = chunk->next;
+		g_free (chunk);
+		chunk = next_chunk;
+
+		/* Reset the head read offset, since we've processed at least the first chunk now */
+		self->head_read_offset = 0;
+	}
+
+	/* If the requested length is still > 0, it must be < chunk->length, and chunk must != NULL (if it does, the cached total_length has
+	 * been corrupted somewhere). */
+	if (G_LIKELY (length_remaining > 0)) {
+		/* Copy the requested data to the output */
+		memcpy (data, chunk->data, length_remaining);
+		data += length_remaining;
+		self->head_read_offset = length_remaining;
+	}
+
+	self->head = chunk;
+	if (self->head == NULL)
+		self->tail = NULL;
+	self->total_length -= return_length;
+
+	g_static_mutex_unlock (&(self->mutex));
+
+	return return_length;
+}
diff --git a/gdata/gdata-buffer.h b/gdata/gdata-buffer.h
new file mode 100644
index 0000000..ec807b5
--- /dev/null
+++ b/gdata/gdata-buffer.h
@@ -0,0 +1,58 @@
+/* -*- 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_BUFFER_H
+#define GDATA_BUFFER_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GDataBufferChunk GDataBufferChunk;
+
+/**
+ * GDataBuffer:
+ *
+ * All the fields in the #GDataBuffer structure are private and should never be accessed directly.
+ *
+ * Since: 0.5.0
+ **/
+typedef struct {
+	/*< private >*/
+	GDataBufferChunk *head;
+	gsize head_read_offset; /* number of bytes which have already been popped from the head chunk */
+	gsize total_length; /* total length of all the chunks available to read (i.e. head_read_offset is already subtracted) */
+	gboolean reached_eof; /* set to TRUE only once we've reached EOF */
+	GDataBufferChunk **tail; /* pointer to the GDataBufferChunk->next field of the current tail chunk */
+
+	GStaticMutex mutex; /* mutex protecting the entire structure on push and pop */
+	GCond *cond; /* a GCond to allow a popping thread to block on data being pushed into the buffer */
+} GDataBuffer;
+
+GDataBuffer *gdata_buffer_new (void) G_GNUC_WARN_UNUSED_RESULT;
+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);
+
+G_END_DECLS
+
+#endif /* !GDATA_BUFFER_H */
diff --git a/gdata/gdata-download-stream.c b/gdata/gdata-download-stream.c
new file mode 100644
index 0000000..bfd3b58
--- /dev/null
+++ b/gdata/gdata-download-stream.c
@@ -0,0 +1,562 @@
+/* -*- 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-download-stream
+ * @short_description: GData download stream object
+ * @stability: Unstable
+ * @include: gdata/gdata-download-stream.h
+ *
+ * #GDataDownloadStream is a #GInputStream subclass to allow downloading of files from GData services with authentication from a #GDataService.
+ *
+ * Once a #GDataDownloadStream is instantiated with gdata_download_stream_new(), the standard #GInputStream API can be used on the stream to download
+ * the file. Network communication may not actually begin until the first call to g_input_stream_read(), so having a #GDataDownloadStream around is no
+ * guarantee that the file is being downloaded.
+ *
+ * The content type and length of the file being downloaded are made available through #GDataDownloadStream:content-type and #GDataDownloadStream:content-length
+ * as soon as the appropriate data is received from the server. Connect to #GDataDownloadStream::notify::content-type and
+ * #GDataDownloadStream::notify::content-length to be notified as soon as the data is available.
+ *
+ * Since: 0.5.0
+ **/
+
+#include <config.h>
+#include <glib.h>
+
+#include "gdata-download-stream.h"
+#include "gdata-buffer.h"
+#include "gdata-private.h"
+
+static void gdata_download_stream_seekable_iface_init (GSeekableIface *seekable_iface);
+static void gdata_download_stream_dispose (GObject *object);
+static void gdata_download_stream_finalize (GObject *object);
+static void gdata_download_stream_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec);
+static void gdata_download_stream_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec);
+
+static gssize gdata_download_stream_read (GInputStream *stream, void *buffer, gsize count, GCancellable *cancellable, GError **error);
+static gboolean gdata_download_stream_close (GInputStream *stream, GCancellable *cancellable, GError **error);
+
+static goffset gdata_download_stream_tell (GSeekable *seekable);
+static gboolean gdata_download_stream_can_seek (GSeekable *seekable);
+static gboolean gdata_download_stream_seek (GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error);
+static gboolean gdata_download_stream_can_truncate (GSeekable *seekable);
+static gboolean gdata_download_stream_truncate (GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error);
+
+static void create_network_thread (GDataDownloadStream *self, GError **error);
+
+struct _GDataDownloadStreamPrivate {
+	gchar *download_uri;
+	GDataService *service;
+	SoupSession *session;
+	SoupMessage *message;
+	GDataBuffer *buffer;
+	gboolean finished;
+	goffset offset; /* seek offset */
+	GThread *network_thread;
+
+	/* Cached data from the SoupMessage */
+	gchar *content_type;
+	gssize content_length;
+	GStaticMutex content_mutex; /* mutex to protect them */
+};
+
+enum {
+	PROP_SERVICE = 1,
+	PROP_DOWNLOAD_URI,
+	PROP_CONTENT_TYPE,
+	PROP_CONTENT_LENGTH
+};
+
+G_DEFINE_TYPE_WITH_CODE (GDataDownloadStream, gdata_download_stream, G_TYPE_INPUT_STREAM,
+			 G_IMPLEMENT_INTERFACE (G_TYPE_SEEKABLE, gdata_download_stream_seekable_iface_init))
+#define GDATA_DOWNLOAD_STREAM_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStreamPrivate))
+
+static void
+gdata_download_stream_class_init (GDataDownloadStreamClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+	GInputStreamClass *stream_class = G_INPUT_STREAM_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (GDataDownloadStreamPrivate));
+
+	gobject_class->dispose = gdata_download_stream_dispose;
+	gobject_class->finalize = gdata_download_stream_finalize;
+	gobject_class->get_property = gdata_download_stream_get_property;
+	gobject_class->set_property = gdata_download_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->read_fn = gdata_download_stream_read;
+	stream_class->close_fn = gdata_download_stream_close;
+
+	/**
+	 * GDataDownloadStream:service:
+	 *
+	 * The service which is used to authenticate the download, and to which the download 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 download.",
+							      GDATA_TYPE_SERVICE,
+							      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataDownloadStream:download-uri:
+	 *
+	 * The URI of the file to download.
+	 *
+	 * Since: 0.5.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_DOWNLOAD_URI,
+					 g_param_spec_string ("download-uri",
+							      "Download URI", "The URI of the file to download.",
+							      NULL,
+							      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataDownloadStream:content-type:
+	 *
+	 * The content type of the file being downloaded.
+	 *
+	 * 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 downloaded.",
+							      NULL,
+							      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+	/**
+	 * GDataDownloadStream:content-length:
+	 *
+	 * The length (in bytes) of the file being downloaded.
+	 *
+	 * Since: 0.5.0
+	 **/
+	g_object_class_install_property (gobject_class, PROP_CONTENT_LENGTH,
+					 g_param_spec_long ("content-length",
+							    "Content length", "The length (in bytes) of the file being downloaded.",
+							    -1, G_MAXSSIZE, -1,
+							    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gdata_download_stream_seekable_iface_init (GSeekableIface *seekable_iface)
+{
+	seekable_iface->tell = gdata_download_stream_tell;
+	seekable_iface->can_seek = gdata_download_stream_can_seek;
+	seekable_iface->seek = gdata_download_stream_seek;
+	seekable_iface->can_truncate = gdata_download_stream_can_truncate;
+	seekable_iface->truncate_fn = gdata_download_stream_truncate;
+}
+
+static void
+gdata_download_stream_init (GDataDownloadStream *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStreamPrivate);
+	self->priv->content_length = -1;
+	self->priv->buffer = gdata_buffer_new ();
+	g_static_mutex_init (&(self->priv->content_mutex));
+}
+
+static void
+gdata_download_stream_dispose (GObject *object)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_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;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_download_stream_parent_class)->dispose (object);
+}
+
+static void
+gdata_download_stream_finalize (GObject *object)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM_GET_PRIVATE (object);
+
+	g_thread_join (priv->network_thread);
+	g_static_mutex_free (&(priv->content_mutex));
+	gdata_buffer_free (priv->buffer);
+	g_free (priv->download_uri);
+	g_free (priv->content_type);
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (gdata_download_stream_parent_class)->finalize (object);
+}
+
+static void
+gdata_download_stream_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM_GET_PRIVATE (object);
+
+	switch (property_id) {
+		case PROP_SERVICE:
+			g_value_set_object (value, priv->service);
+			break;
+		case PROP_DOWNLOAD_URI:
+			g_value_set_string (value, priv->download_uri);
+			break;
+		case PROP_CONTENT_TYPE:
+			g_static_mutex_lock (&(priv->content_mutex));
+			g_value_set_string (value, priv->content_type);
+			g_static_mutex_unlock (&(priv->content_mutex));
+			break;
+		case PROP_CONTENT_LENGTH:
+			g_static_mutex_lock (&(priv->content_mutex));
+			g_value_set_long (value, priv->content_length);
+			g_static_mutex_unlock (&(priv->content_mutex));
+			break;
+		default:
+			/* We don't have any other property... */
+			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+			break;
+	}
+}
+
+static void
+gdata_download_stream_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_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_DOWNLOAD_URI:
+			priv->download_uri = 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 gssize
+gdata_download_stream_read (GInputStream *stream, void *buffer, gsize count, GCancellable *cancellable, GError **error)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM (stream)->priv;
+	gssize length_read;
+
+	/* We're lazy about starting the network operation so we don't end up with a massive buffer */
+	if (priv->network_thread == NULL) {
+		create_network_thread (GDATA_DOWNLOAD_STREAM (stream), error);
+		if (priv->network_thread == NULL)
+			return 0;
+	}
+
+	/* Read the data off the buffer */
+	length_read = (gssize) gdata_buffer_pop_data (priv->buffer, buffer, count, cancellable);
+
+	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
+		/* Handle cancellation */
+		return length_read;
+	} else if (SOUP_STATUS_IS_SUCCESSFUL (priv->message->status_code) == FALSE) {
+		GDataServiceClass *klass = GDATA_SERVICE_GET_CLASS (priv->service);
+
+		/* Set an appropriate error */
+		g_assert (klass->parse_error_response != NULL);
+		klass->parse_error_response (priv->service, GDATA_SERVICE_ERROR_WITH_DOWNLOAD, priv->message->status_code, priv->message->reason_phrase,
+					     NULL, 0, error);
+		return 0;
+	}
+
+	return length_read;
+}
+
+static gboolean
+gdata_download_stream_close (GInputStream *stream, GCancellable *cancellable, GError **error)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM (stream)->priv;
+
+	if (priv->finished == FALSE)
+		soup_session_cancel_message (priv->session, priv->message, SOUP_STATUS_CANCELLED);
+
+	return TRUE;
+}
+
+static goffset
+gdata_download_stream_tell (GSeekable *seekable)
+{
+	return GDATA_DOWNLOAD_STREAM (seekable)->priv->offset;
+}
+
+static gboolean
+gdata_download_stream_can_seek (GSeekable *seekable)
+{
+	return TRUE;
+}
+
+extern void soup_message_io_cleanup (SoupMessage *msg);
+
+/* Copied from SoupInputStream */
+static gboolean
+gdata_download_stream_seek (GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error)
+{
+	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM (seekable)->priv;
+	gchar *range = NULL;
+
+	if (type == G_SEEK_END) {
+		/* FIXME: we could send "bytes=-offset", but unless we know the
+		 * Content-Length, we wouldn't be able to answer a tell() properly.
+		 * We could find the Content-Length by doing a HEAD...
+		*/
+		g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "G_SEEK_END not currently supported");
+		return FALSE;
+	}
+
+	if (g_input_stream_set_pending (G_INPUT_STREAM (seekable), error) == FALSE)
+		return FALSE;
+
+	soup_session_cancel_message (priv->session, priv->message, SOUP_STATUS_CANCELLED);
+	soup_message_io_cleanup (priv->message);
+
+	switch (type) {
+		case G_SEEK_CUR:
+			offset += priv->offset;
+			/* fall through */
+		case G_SEEK_SET:
+			range = g_strdup_printf ("bytes=%" G_GUINT64_FORMAT "-", (guint64) offset);
+			priv->offset = offset;
+			break;
+		case G_SEEK_END:
+		default:
+			g_assert_not_reached ();
+	}
+
+	/* Change the Range header and re-send the message */
+	soup_message_headers_remove (priv->message->request_headers, "Range");
+	soup_message_headers_append (priv->message->request_headers, "Range", range);
+	g_free (range);
+
+	/* Wait for the thread to quit, then launch another one with the modified message */
+	g_thread_join (priv->network_thread);
+	create_network_thread (GDATA_DOWNLOAD_STREAM (seekable), error);
+	if (priv->network_thread == NULL)
+		return FALSE;
+
+	g_input_stream_clear_pending (G_INPUT_STREAM (seekable));
+
+	return TRUE;
+}
+
+static gboolean
+gdata_download_stream_can_truncate (GSeekable *seekable)
+{
+	return FALSE;
+}
+
+static gboolean
+gdata_download_stream_truncate (GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error)
+{
+	g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Truncate not allowed on input stream");
+	return FALSE;
+}
+
+static gboolean
+notify_content_data_cb (GObject *download_stream)
+{
+	g_object_freeze_notify (download_stream);
+	g_object_notify (download_stream, "content-length");
+	g_object_notify (download_stream, "content-type");
+	g_object_thaw_notify (download_stream);
+
+	g_object_unref (download_stream);
+
+	return FALSE;
+}
+
+static void
+got_headers_cb (SoupMessage *message, GDataDownloadStream *self)
+{
+	/* Don't get the client's hopes up by setting the Content-Type or -Length if the response
+	 * is actually unsuccessful. */
+	if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code) == FALSE)
+		return;
+
+	g_static_mutex_lock (&(self->priv->content_mutex));
+	self->priv->content_type = g_strdup (soup_message_headers_get_content_type (message->response_headers, NULL));
+	self->priv->content_length = soup_message_headers_get_content_length (message->response_headers);
+	g_static_mutex_unlock (&(self->priv->content_mutex));
+
+	/* Emit the notifications for the Content-Length and -Type properties in the main thread */
+	g_idle_add ((GSourceFunc) notify_content_data_cb, g_object_ref (self));
+}
+
+static void
+got_chunk_cb (SoupMessage *message, SoupBuffer *buffer, GDataDownloadStream *self)
+{
+	/* Ignore the chunk if the response is unsuccessful or it has zero length */
+	if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code) == FALSE || buffer->length == 0)
+		return;
+
+	/* Push the data onto the buffer immediately */
+	gdata_buffer_push_data (self->priv->buffer, g_memdup (buffer->data, buffer->length), buffer->length);
+}
+
+static void
+finished_cb (SoupMessage *message, GDataDownloadStream *self)
+{
+	self->priv->finished = TRUE;
+
+	/* Mark the buffer as having reached EOF */
+	gdata_buffer_push_data (self->priv->buffer, NULL, 0);
+}
+
+static gpointer
+download_thread (GDataDownloadStream *self)
+{
+	/* Connect to the got-headers signal so we can notify clients of the values of content-type and content-length */
+	g_signal_connect (self->priv->message, "got-headers", (GCallback) got_headers_cb, self);
+	g_signal_connect (self->priv->message, "got-chunk", (GCallback) got_chunk_cb, self);
+	g_signal_connect (self->priv->message, "finished", (GCallback) finished_cb, self);
+
+	soup_session_send_message (self->priv->session, self->priv->message);
+
+	return NULL;
+}
+
+static void
+create_network_thread (GDataDownloadStream *self, GError **error)
+{
+	self->priv->network_thread = g_thread_create ((GThreadFunc) download_thread, self, TRUE, error);
+}
+
+/**
+ * gdata_download_stream_new:
+ * @service: a #GDataService
+ * @download_uri: the URI to download
+ *
+ * Creates a new #GDataDownloadStream, allowing a file to be downloaded from a GData service using standard #GInputStream API.
+ *
+ * As well as the standard GIO errors, calls to the #GInputStream API on a #GDataDownloadStream can also return any relevant specific error from
+ * #GDataServiceError, or %GDATA_SERVICE_ERROR_WITH_DOWNLOAD in the general case.
+ *
+ * Return value: a new #GInputStream, or %NULL; unref with g_object_unref()
+ *
+ * Since: 0.5.0
+ **/
+GInputStream *
+gdata_download_stream_new (GDataService *service, const gchar *download_uri)
+{
+	GDataServiceClass *klass;
+	GDataDownloadStream *download_stream;
+	SoupMessage *message;
+
+	g_return_val_if_fail (GDATA_IS_SERVICE (service), NULL);
+	g_return_val_if_fail (download_uri != NULL, NULL);
+
+	/* Build the message */
+	message = soup_message_new (SOUP_METHOD_GET, download_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);
+
+	/* We don't want to accumulate chunks */
+	soup_message_body_set_accumulate (message->request_body, FALSE);
+
+	download_stream = g_object_new (GDATA_TYPE_DOWNLOAD_STREAM, "download-uri", download_uri, "service", service, NULL);
+	download_stream->priv->message = message;
+
+	/* Downloading doesn't actually start until the first call to read() */
+
+	return G_INPUT_STREAM (download_stream);
+}
+
+/**
+ * gdata_download_stream_get_service:
+ * @self: a #GDataDownloadStream
+ *
+ * Gets the service used to authenticate the download, as passed to gdata_download_stream_new().
+ *
+ * Return value: the #GDataService used to authenticate the download
+ *
+ * Since: 0.5.0
+ **/
+GDataService *
+gdata_download_stream_get_service (GDataDownloadStream *self)
+{
+	g_return_val_if_fail (GDATA_IS_DOWNLOAD_STREAM (self), NULL);
+	return self->priv->service;
+}
+
+/**
+ * gdata_download_stream_get_download_uri:
+ * @self: a #GDataDownloadStream
+ *
+ * Gets the URI of the file being downloaded, as passed to gdata_download_stream_new().
+ *
+ * Return value: the URI of the file being downloaded
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_download_stream_get_download_uri (GDataDownloadStream *self)
+{
+	g_return_val_if_fail (GDATA_IS_DOWNLOAD_STREAM (self), NULL);
+	return self->priv->download_uri;
+}
+
+/**
+ * gdata_download_stream_get_content_type:
+ * @self: a #GDataDownloadStream
+ *
+ * Gets the content type of the file being downloaded. If the <literal>Content-Type</literal> header has not yet
+ * been received, %NULL will be returned.
+ *
+ * Return value: the content type of the file being downloaded, or %NULL
+ *
+ * Since: 0.5.0
+ **/
+const gchar *
+gdata_download_stream_get_content_type (GDataDownloadStream *self)
+{
+	g_return_val_if_fail (GDATA_IS_DOWNLOAD_STREAM (self), NULL);
+	return self->priv->content_type;
+}
+
+/**
+ * gdata_download_stream_get_content_length:
+ * @self: a #GDataDownloadStream
+ *
+ * Gets the length (in bytes) of the file being downloaded. If the <literal>Content-Length</literal> header has not yet
+ * been received from the server, %-1 will be returned.
+ *
+ * Return value: the content length of the file being downloaded, or %-1
+ *
+ * Since: 0.5.0
+ **/
+gssize
+gdata_download_stream_get_content_length (GDataDownloadStream *self)
+{
+	g_return_val_if_fail (GDATA_IS_DOWNLOAD_STREAM (self), -1);
+	return self->priv->content_length;
+}
diff --git a/gdata/gdata-download-stream.h b/gdata/gdata-download-stream.h
new file mode 100644
index 0000000..5ee4da9
--- /dev/null
+++ b/gdata/gdata-download-stream.h
@@ -0,0 +1,75 @@
+/* -*- 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_DOWNLOAD_STREAM_H
+#define GDATA_DOWNLOAD_STREAM_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include <gdata/gdata-service.h>
+
+G_BEGIN_DECLS
+
+#define GDATA_TYPE_DOWNLOAD_STREAM		(gdata_download_stream_get_type ())
+#define GDATA_DOWNLOAD_STREAM(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStream))
+#define GDATA_DOWNLOAD_STREAM_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStreamClass))
+#define GDATA_IS_DOWNLOAD_STREAM(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), GDATA_TYPE_DOWNLOAD_STREAM))
+#define GDATA_IS_DOWNLOAD_STREAM_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_DOWNLOAD_STREAM))
+#define GDATA_DOWNLOAD_STREAM_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStreamClass))
+
+typedef struct _GDataDownloadStreamPrivate	GDataDownloadStreamPrivate;
+
+/**
+ * GDataDownloadStream:
+ *
+ * All the fields in the #GDataDownloadStream structure are private and should never be accessed directly.
+ *
+ * Since: 0.5.0
+ **/
+typedef struct {
+	GInputStream parent;
+	GDataDownloadStreamPrivate *priv;
+} GDataDownloadStream;
+
+/**
+ * GDataDownloadStreamClass:
+ *
+ * All the fields in the #GDataDownloadStreamClass structure are private and should never be accessed directly.
+ *
+ * Since: 0.5.0
+ **/
+typedef struct {
+	/*< private >*/
+	GInputStreamClass parent;
+} GDataDownloadStreamClass;
+
+GType gdata_download_stream_get_type (void) G_GNUC_CONST;
+
+GInputStream *gdata_download_stream_new (GDataService *service, const gchar *download_uri) G_GNUC_WARN_UNUSED_RESULT;
+
+GDataService *gdata_download_stream_get_service (GDataDownloadStream *self);
+const gchar *gdata_download_stream_get_download_uri (GDataDownloadStream *self);
+const gchar *gdata_download_stream_get_content_type (GDataDownloadStream *self);
+gssize gdata_download_stream_get_content_length (GDataDownloadStream *self);
+
+G_END_DECLS
+
+#endif /* !GDATA_DOWNLOAD_STREAM_H */
diff --git a/gdata/gdata-private.h b/gdata/gdata-private.h
index 53729b4..c8d007e 100644
--- a/gdata/gdata-private.h
+++ b/gdata/gdata-private.h
@@ -29,6 +29,7 @@
 G_BEGIN_DECLS
 
 #include "gdata-service.h"
+SoupSession *_gdata_service_get_session (GDataService *self);
 void _gdata_service_set_authenticated (GDataService *self, gboolean authenticated);
 guint _gdata_service_send_message (GDataService *self, SoupMessage *message, GError **error);
 SoupMessage *_gdata_service_query (GDataService *self, const gchar *feed_uri, GDataQuery *query, GCancellable *cancellable,
diff --git a/gdata/gdata-service.c b/gdata/gdata-service.c
index 6f44529..74aa76c 100644
--- a/gdata/gdata-service.c
+++ b/gdata/gdata-service.c
@@ -332,7 +332,6 @@ real_parse_error_response (GDataService *self, GDataServiceError error_type, gui
 			   gint length, GError **error)
 {
 	/* See: http://code.google.com/apis/gdata/docs/2.0/reference.html#HTTPStatusCodes */
-
 	switch (status) {
 		case 400:
 			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
@@ -365,24 +364,29 @@ real_parse_error_response (GDataService *self, GDataServiceError error_type, gui
 	switch (error_type) {
 		case GDATA_SERVICE_ERROR_WITH_INSERTION:
 			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_INSERTION,
-				     /* Translators: the first parameter is a HTTP status, and the second is an error message returned by the server. */
+				     /* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
 				     _("Error code %u when inserting an entry: %s"), status, response_body);
 			break;
 		case GDATA_SERVICE_ERROR_WITH_UPDATE:
 			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_UPDATE,
-				     /* Translators: the first parameter is a HTTP status, and the second is an error message returned by the server. */
+				     /* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
 				     _("Error code %u when updating an entry: %s"), status, response_body);
 			break;
 		case GDATA_SERVICE_ERROR_WITH_DELETION:
 			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_DELETION,
-				     /* Translators: the first parameter is a HTTP status, and the second is an error message returned by the server. */
+				     /* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
 				     _("Error code %u when deleting an entry: %s"), status, response_body);
 			break;
 		case GDATA_SERVICE_ERROR_WITH_QUERY:
 			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_QUERY,
-				     /* Translators: the first parameter is a HTTP status, and the second is an error message returned by the server. */
+				     /* Translators: the first parameter is an HTTP status, and the second is an error message returned by the server. */
 				     _("Error code %u when querying: %s"), status, response_body);
 			break;
+		case GDATA_SERVICE_ERROR_WITH_DOWNLOAD:
+			g_set_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_WITH_DOWNLOAD,
+				     /* 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;
 		default:
 			/* We should not be called with anything other than the above four generic error types */
 			g_assert_not_reached ();
@@ -1720,3 +1724,9 @@ gdata_service_get_password (GDataService *self)
 	g_return_val_if_fail (GDATA_IS_SERVICE (self), NULL);
 	return self->priv->password;
 }
+
+SoupSession *
+_gdata_service_get_session (GDataService *self)
+{
+	return self->priv->session;
+}
diff --git a/gdata/gdata-service.h b/gdata/gdata-service.h
index c61f88b..ddb9fc3 100644
--- a/gdata/gdata-service.h
+++ b/gdata/gdata-service.h
@@ -43,6 +43,7 @@ G_BEGIN_DECLS
  * @GDATA_SERVICE_ERROR_CONFLICT: There was a conflict when updating an entry on the server; the server-side copy was modified inbetween downloading
  * 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).
  *
  * Error codes for #GDataService operations.
  **/
@@ -57,7 +58,8 @@ typedef enum {
 	GDATA_SERVICE_ERROR_WITH_DELETION,
 	GDATA_SERVICE_ERROR_NOT_FOUND,
 	GDATA_SERVICE_ERROR_CONFLICT,
-	GDATA_SERVICE_ERROR_FORBIDDEN
+	GDATA_SERVICE_ERROR_FORBIDDEN,
+	GDATA_SERVICE_ERROR_WITH_DOWNLOAD
 } GDataServiceError;
 
 /**
diff --git a/gdata/gdata.h b/gdata/gdata.h
index d02c3c0..c68e7c6 100644
--- a/gdata/gdata.h
+++ b/gdata/gdata.h
@@ -30,6 +30,7 @@
 #include <gdata/gdata-access-handler.h>
 #include <gdata/gdata-access-rule.h>
 #include <gdata/gdata-parsable.h>
+#include <gdata/gdata-download-stream.h>
 
 /* Namespaces */
 
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index b658280..d2c24ac 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -589,12 +589,15 @@ gdata_picasaweb_visibility_get_type
 gdata_documents_presentation_get_type
 gdata_documents_presentation_new
 gdata_documents_presentation_download_document
+gdata_documents_presentation_get_download_uri
 gdata_documents_text_get_type
 gdata_documents_text_new
 gdata_documents_text_download_document
+gdata_documents_text_get_download_uri
 gdata_documents_spreadsheet_get_type
 gdata_documents_spreadsheet_new
 gdata_documents_spreadsheet_download_document
+gdata_documents_spreadsheet_get_download_uri
 gdata_documents_folder_get_type
 gdata_documents_folder_new
 gdata_documents_service_get_type
@@ -636,3 +639,9 @@ gdata_documents_service_error_get_type
 gdata_documents_text_format_get_type
 gdata_documents_presentation_format_get_type
 gdata_documents_spreadsheet_format_get_type
+gdata_download_stream_get_type
+gdata_download_stream_new
+gdata_download_stream_get_service
+gdata_download_stream_get_download_uri
+gdata_download_stream_get_content_type
+gdata_download_stream_get_content_length
diff --git a/gdata/services/documents/gdata-documents-entry.c b/gdata/services/documents/gdata-documents-entry.c
index aabb2b2..5d4bbf9 100644
--- a/gdata/services/documents/gdata-documents-entry.c
+++ b/gdata/services/documents/gdata-documents-entry.c
@@ -43,6 +43,7 @@
 #include "gdata-types.h"
 #include "gdata-private.h"
 #include "gdata-access-handler.h"
+#include "gdata-download-stream.h"
 
 static void gdata_documents_entry_access_handler_init (GDataAccessHandlerIface *iface);
 static void gdata_documents_entry_finalize (GObject *object);
@@ -550,9 +551,9 @@ gdata_documents_entry_is_deleted (GDataDocumentsEntry *self)
 }
 
 static void
-got_chunk_cb (SoupMessage *message, SoupBuffer *chunk, GOutputStream *output_stream)
+notify_content_type_cb (GDataDownloadStream *download_stream, GParamSpec *pspec, gchar **content_type)
 {
-	g_output_stream_write (output_stream, (void*) chunk->data, chunk->length, NULL, NULL);
+	*content_type = g_strdup (gdata_download_stream_get_content_type (download_stream));
 }
 
 /*
@@ -592,10 +593,10 @@ _gdata_documents_entry_download_document (GDataDocumentsEntry *self, GDataServic
 					  GFile *destination_file, const gchar *file_extension, gboolean replace_file_if_exists,
 					  GCancellable *cancellable, GError **error)
 {
-	GDataServiceClass *klass;
+	GError *child_error = NULL;
+	GFile *output_file;
 	GFileOutputStream *file_stream;
-	SoupMessage *message;
-	guint status;
+	GInputStream *download_stream;
 
 	/* TODO: async version */
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_ENTRY (self), NULL);
@@ -613,74 +614,47 @@ _gdata_documents_entry_download_document (GDataDocumentsEntry *self, GDataServic
 	}
 
 	/* Create a new file */
-	file_stream = g_file_create (destination_file, G_FILE_CREATE_NONE, cancellable, error);
-	if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
-		/* Replace a pre-existing file */
+	file_stream = g_file_create (destination_file, G_FILE_CREATE_NONE, cancellable, &child_error);
+	if (g_error_matches (child_error, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
 		if (replace_file_if_exists == TRUE) {
-			g_clear_error (error);
-			file_stream = g_file_replace (destination_file, NULL, TRUE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, error);
-		}
+			g_error_free (child_error);
+			child_error = NULL;
 
-		if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY)) {
-			GFile *new_destination_file = NULL;
-			const gchar *document_title;
-			gchar *filename;
+			/* Replace a pre-existing file */
+			file_stream = g_file_replace (destination_file, NULL, TRUE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, &child_error);
 
-			g_clear_error (error);
+			if (g_error_matches (child_error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY)) {
+				GFile *new_destination_file;
+				const gchar *document_title;
+				gchar *filename;
 
-			/* Prepare the GFile */
-			document_title = gdata_entry_get_title (GDATA_ENTRY (self));
-			filename = g_strdup_printf ("%s.%s", document_title, file_extension);
-			new_destination_file = g_file_get_child (destination_file, filename);
-			g_free (filename);
+				g_error_free (child_error);
 
-			return _gdata_documents_entry_download_document (self, service, content_type, download_uri, new_destination_file, 
-									 file_extension, replace_file_if_exists, cancellable, error);
-		}
+				/* Prepare a new GFile */
+				document_title = gdata_entry_get_title (GDATA_ENTRY (self));
+				filename = g_strdup_printf ("%s.%s", document_title, file_extension);
+				new_destination_file = g_file_get_child (destination_file, filename);
+				g_free (filename);
 
-		return NULL;
+				file_stream = g_file_replace (new_destination_file, NULL, TRUE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, error);
+				output_file = new_destination_file;
+			} else {
+				output_file = g_object_ref (destination_file);
+			}
+		} else {
+			g_propagate_error (error, child_error);
+			return NULL;
+		}
+	} else {
+		output_file = g_object_ref (destination_file);
 	}
 
-	/* Get the document URI */
-	message = soup_message_new (SOUP_METHOD_GET, download_uri);
-
-	/* We copy the data to disk as it comes through the network pipe */
-	soup_message_body_set_accumulate (message->response_body, FALSE);
-	g_signal_connect (message, "got-chunk", (GCallback) got_chunk_cb, file_stream);
-
-	/* 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);
-
-	/* Send the message */
-	status = _gdata_service_send_message (GDATA_SERVICE (service), message, error);
+	download_stream = gdata_download_stream_new (GDATA_SERVICE (service), download_uri);
+	g_signal_connect (download_stream, "notify::content-type", (GCallback) notify_content_type_cb, content_type);
+	g_output_stream_splice (G_OUTPUT_STREAM (file_stream), download_stream, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+				cancellable, error);
+	g_object_unref (download_stream);
 	g_object_unref (file_stream);
-	if (status == SOUP_STATUS_NONE) {
-		g_object_unref (message);
-		return NULL;
-	}
-
-	/* Check for cancellation */
-	if (g_cancellable_set_error_if_cancelled (cancellable, error) == TRUE) {
-		g_object_unref (message);
-		return NULL;
-	}
-
-	if (status != 200) {
-		/* Error */
-		g_assert (klass->parse_error_response != NULL);
-		klass->parse_error_response (GDATA_SERVICE (service), GDATA_SERVICE_ERROR_WITH_QUERY, status, message->reason_phrase,
-					     message->response_body->data, message->response_body->length, error);
-		g_object_unref (message);
-		return NULL;
-	}
-
-	/* Sort out the return values */
-	if (content_type != NULL)
-		*content_type = g_strdup (soup_message_headers_get_content_type (message->response_headers, NULL));
-
-	g_object_unref (message);
 
-	return g_object_ref (destination_file);
+	return output_file;
 }
diff --git a/gdata/services/documents/gdata-documents-presentation.c b/gdata/services/documents/gdata-documents-presentation.c
index 8d966d8..cfd8b6b 100644
--- a/gdata/services/documents/gdata-documents-presentation.c
+++ b/gdata/services/documents/gdata-documents-presentation.c
@@ -43,6 +43,14 @@
 
 static void get_xml (GDataParsable *parsable, GString *xml_string);
 
+static const gchar *export_formats[] = {
+	"pdf", /* GDATA_DOCUMENTS_PRESENTATION_PDF */
+	"png", /* GDATA_DOCUMENTS_PRESENTATION_PNG */
+	"ppt", /* GDATA_DOCUMENTS_PRESENTATION_PPT */
+	"swf", /* GDATA_DOCUMENTS_PRESENTATION_SWF */
+	"txt" /* GDATA_DOCUMENTS_PRESENTATION_TXT */
+};
+
 G_DEFINE_TYPE (GDataDocumentsPresentation, gdata_documents_presentation, GDATA_TYPE_DOCUMENTS_ENTRY)
 #define GDATA_DOCUMENTS_PRESENTATION_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_DOCUMENTS_PRESENTATION, GDataDocumentsPresentationPrivate))
 
@@ -119,34 +127,45 @@ gdata_documents_presentation_download_document (GDataDocumentsPresentation *self
 						GDataDocumentsPresentationFormat export_format, GFile *destination_file,
 						gboolean replace_file_if_exists, GCancellable *cancellable, GError **error)
 {
-	const gchar *document_id;
 	gchar *link_href;
 
-	const gchar *export_formats[] = {
-		"pdf", /* GDATA_DOCUMENTS_PRESENTATION_PDF */
-		"png", /* GDATA_DOCUMENTS_PRESENTATION_PNG */
-		"ppt", /* GDATA_DOCUMENTS_PRESENTATION_PPT */
-		"swf", /* GDATA_DOCUMENTS_PRESENTATION_SWF */
-		"txt" /* GDATA_DOCUMENTS_PRESENTATION_TXT */
-	};
-
-	/* TODO: async version */
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_PRESENTATION (self), NULL);
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (service), NULL);
 	g_return_val_if_fail (G_IS_FILE (destination_file), NULL);
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
-	g_return_val_if_fail (export_format >= 0 && export_format < G_N_ELEMENTS (export_formats), NULL);
-
-	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
-	g_assert (document_id != NULL);
-
-	link_href = g_strdup_printf ("http://docs.google.com/feeds/download/presentations/Export?exportFormat=%s&docID=%s";,
-				     export_formats[export_format], document_id);
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
 
 	/* Call the common download method on the parent class */
-	destination_file = _gdata_documents_entry_download_document (GDATA_DOCUMENTS_ENTRY (self), GDATA_SERVICE (service), content_type,
-								     link_href, destination_file, export_formats[export_format], replace_file_if_exists, cancellable, error);
+	link_href = gdata_documents_presentation_get_download_uri (self, export_format);
+	destination_file = _gdata_documents_entry_download_document (GDATA_DOCUMENTS_ENTRY (self), GDATA_SERVICE (service), content_type, link_href,
+								     destination_file, export_formats[export_format], replace_file_if_exists, cancellable, error);
 	g_free (link_href);
 
 	return destination_file;
 }
+
+/**
+ * gdata_documents_presentation_get_download_uri:
+ * @self: a #GDataDocumentsPresentation
+ * @export_format: the format in which the presentation should be exported when downloaded
+ *
+ * Builds and returns the download URI for the given #GDataDocumentsPresentation in the desired format. Note that directly downloading
+ * the document using this URI isn't possible, as authentication is required. You should instead use gdata_download_stream_new() with
+ * the URI, and use the resulting #GInputStream.
+ *
+ * Return value: the download URI; free with g_free()
+ *
+ * Since: 0.5.0
+ **/
+gchar *
+gdata_documents_presentation_get_download_uri (GDataDocumentsPresentation *self, GDataDocumentsPresentationFormat export_format)
+{
+	const gchar *document_id;
+
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
+
+	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
+	g_assert (document_id != NULL);
+
+	return g_strdup_printf ("http://docs.google.com/feeds/download/presentations/Export?exportFormat=%s&docID=%s";, export_formats[export_format], document_id);
+}
diff --git a/gdata/services/documents/gdata-documents-presentation.h b/gdata/services/documents/gdata-documents-presentation.h
index 211a459..99fda36 100644
--- a/gdata/services/documents/gdata-documents-presentation.h
+++ b/gdata/services/documents/gdata-documents-presentation.h
@@ -90,6 +90,7 @@ GFile *gdata_documents_presentation_download_document (GDataDocumentsPresentatio
 						       GDataDocumentsPresentationFormat export_format, GFile *destination_file,
 						       gboolean replace_file_if_exists, GCancellable *cancellable,
 						       GError **error) G_GNUC_WARN_UNUSED_RESULT;
+gchar *gdata_documents_presentation_get_download_uri (GDataDocumentsPresentation *self, GDataDocumentsPresentationFormat export_format) G_GNUC_WARN_UNUSED_RESULT;
 
 G_END_DECLS
 
diff --git a/gdata/services/documents/gdata-documents-service.c b/gdata/services/documents/gdata-documents-service.c
index dfa486d..25d546b 100644
--- a/gdata/services/documents/gdata-documents-service.c
+++ b/gdata/services/documents/gdata-documents-service.c
@@ -293,10 +293,10 @@ gdata_documents_service_query_documents_async (GDataDocumentsService *self, GDat
 				   GDATA_TYPE_DOCUMENTS_ENTRY, cancellable, progress_callback, progress_user_data, callback, user_data);
 }
 
-/**
+/*
  * To upload spreasheet documents, another token is needed since the service for it is "wise" as apposed to "writely" for other operations.
  * This callback aims to authenticate to this service as a private property (@priv->spreadsheet_service) of #GDataDocumentsService.
- * */
+ */
 static void
 notify_authenticated_cb (GObject *service, GParamSpec *pspec, GObject *self)
 {
diff --git a/gdata/services/documents/gdata-documents-spreadsheet.c b/gdata/services/documents/gdata-documents-spreadsheet.c
index a7c305a..99ede51 100644
--- a/gdata/services/documents/gdata-documents-spreadsheet.c
+++ b/gdata/services/documents/gdata-documents-spreadsheet.c
@@ -42,6 +42,15 @@
 
 static void get_xml (GDataParsable *parsable, GString *xml_string);
 
+static const struct { const gchar *extension; const gchar *fmcmd; } export_formats[] = {
+	{ "xls", "4" }, /* GDATA_DOCUMENTS_SPREADSHEET_XLS */
+	{ "csv", "5" }, /* GDATA_DOCUMENTS_SPREADSHEET_CSV */
+	{ "pdf", "12" }, /* GDATA_DOCUMENTS_SPREADSHEET_PDF */
+	{ "ods", "13" }, /* GDATA_DOCUMENTS_SPREADSHEET_ODS */
+	{ "tsv", "23" }, /* GDATA_DOCUMENTS_SPREADSHEET_TSV */
+	{ "html", "102" } /* GDATA_DOCUMENTS_SPREADSHEET_HTML */
+};
+
 G_DEFINE_TYPE (GDataDocumentsSpreadsheet, gdata_documents_spreadsheet, GDATA_TYPE_DOCUMENTS_ENTRY)
 #define GDATA_DOCUMENTS_SPREADSHEET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_DOCUMENTS_SPREADSHEET, GDataDocumentsSpreadsheetPrivate))
 
@@ -106,8 +115,8 @@ gdata_documents_spreadsheet_new (const gchar *id)
  * If the operation was cancelled, the error %G_IO_ERROR_CANCELLED will be returned.
  *
  * When requesting a %GDATA_DOCUMENTS_SPREADSHEET_CSV or %GDATA_DOCUMENTS_SPREADSHEET_TSV file you must specify an additional
- * parameter called @gid which indicates which grid, or sheet, you wish to get (the index is %0-based, so gid %1 actually refers
- * to the second sheet sheet on a given spreadsheet).
+ * parameter called @gid which indicates which grid, or sheet, you wish to get (the index is %0-based, so GID %1 actually refers
+ * to the second sheet on a given spreadsheet).
  *
  * If @destination_file is a directory, then the file will be downloaded in this directory with the #GDataEntry:title with 
  * the apropriate extension as name.
@@ -124,47 +133,25 @@ gdata_documents_spreadsheet_download_document (GDataDocumentsSpreadsheet *self,
 					       gboolean replace_file_if_exists, GCancellable *cancellable, GError **error)
 {
 	gchar *link_href;
-	const gchar *document_id, *extension, *fmcmd;
+	const gchar *extension;
 	GDataService *spreadsheet_service;
 
-	const struct { const gchar *extension; const gchar *fmcmd; } export_formats[] = {
-		{ "xls", "4" },
-		{ "csv", "5" },
-		{ "pdf", "12" },
-		{ "ods", "13" },
-		{ "tsv", "23" },
-		{ "html", "102" }
-	};
-
 	/* TODO: async version */
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SPREADSHEET (self), NULL);
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (service), NULL);
-	g_return_val_if_fail (export_format >= 0 && export_format < G_N_ELEMENTS (export_formats), NULL);
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
 	g_return_val_if_fail (gid >= -1, NULL);
-	g_return_val_if_fail ((export_format != GDATA_DOCUMENTS_SPREADSHEET_CSV && export_format != GDATA_DOCUMENTS_SPREADSHEET_TSV) ||
-			      gid != -1, NULL);
+	g_return_val_if_fail ((export_format != GDATA_DOCUMENTS_SPREADSHEET_CSV && export_format != GDATA_DOCUMENTS_SPREADSHEET_TSV) || gid != -1, NULL);
 	g_return_val_if_fail (G_IS_FILE (destination_file), NULL);
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
 
-	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
-	g_assert (document_id != NULL);
-
 	extension = export_formats[export_format].extension;
-	fmcmd = export_formats[export_format].fmcmd;
-
-	/* Build the download URI */
-	if (gid != -1) {
-		link_href = g_strdup_printf ("http://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=%s&fmcmd=%s&gid=%d";,
-					     document_id, fmcmd, gid);
-	} else {
-		link_href = g_strdup_printf ("http://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=%s&fmcmd=%s";,
-					     document_id, fmcmd);
-	}
 
 	/* Get the spreadsheet service */
 	spreadsheet_service = _gdata_documents_service_get_spreadsheet_service (service);
 
 	/* Download the document */
+	link_href = gdata_documents_spreadsheet_get_download_uri (self, export_format, gid);
 	destination_file = _gdata_documents_entry_download_document (GDATA_DOCUMENTS_ENTRY (self), spreadsheet_service, content_type,
 								     link_href, destination_file, extension, replace_file_if_exists,
 								     cancellable, error);
@@ -172,3 +159,41 @@ gdata_documents_spreadsheet_download_document (GDataDocumentsSpreadsheet *self,
 
 	return destination_file;
 }
+
+/**
+ * gdata_documents_spreadsheet_get_download_uri:
+ * @self: a #GDataDocumentsSpreadsheet
+ * @export_format: the format in which the spreadsheet should be exported when downloaded
+ * @gid: the %0-based sheet ID to download, or %-1
+ *
+ * Builds and returns the download URI for the given #GDataDocumentsSpreadsheet in the desired format. Note that directly downloading
+ * the document using this URI isn't possible, as authentication is required. You should instead use gdata_download_stream_new() with
+ * the URI, and use the resulting #GInputStream.
+ *
+ * When requesting a %GDATA_DOCUMENTS_SPREADSHEET_CSV or %GDATA_DOCUMENTS_SPREADSHEET_TSV file you must specify an additional
+ * parameter called @gid which indicates which grid, or sheet, you wish to get (the index is %0-based, so GID %1 actually refers
+ * to the second sheet on a given spreadsheet).
+ *
+ * Return value: the download URI; free with g_free()
+ *
+ * Since: 0.5.0
+ **/
+gchar *
+gdata_documents_spreadsheet_get_download_uri (GDataDocumentsSpreadsheet *self, GDataDocumentsSpreadsheetFormat export_format, gint gid)
+{
+	const gchar *document_id, *fmcmd;
+
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
+	g_return_val_if_fail (gid >= -1, NULL);
+	g_return_val_if_fail ((export_format != GDATA_DOCUMENTS_SPREADSHEET_CSV && export_format != GDATA_DOCUMENTS_SPREADSHEET_TSV) || gid != -1, NULL);
+
+	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
+	g_assert (document_id != NULL);
+
+	fmcmd = export_formats[export_format].fmcmd;
+
+	if (gid != -1)
+		return g_strdup_printf ("http://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=%s&fmcmd=%s&gid=%d";, document_id, fmcmd, gid);
+	else
+		return g_strdup_printf ("http://spreadsheets.google.com/feeds/download/spreadsheets/Export?key=%s&fmcmd=%s";, document_id, fmcmd);
+}
diff --git a/gdata/services/documents/gdata-documents-spreadsheet.h b/gdata/services/documents/gdata-documents-spreadsheet.h
index 30b2045..42ef751 100644
--- a/gdata/services/documents/gdata-documents-spreadsheet.h
+++ b/gdata/services/documents/gdata-documents-spreadsheet.h
@@ -93,6 +93,8 @@ GFile *gdata_documents_spreadsheet_download_document (GDataDocumentsSpreadsheet
 						      GDataDocumentsSpreadsheetFormat export_format, gint gid, GFile *destination_file,
 						      gboolean replace_file_if_exists, GCancellable *cancellable,
 						      GError **error) G_GNUC_WARN_UNUSED_RESULT;
+gchar *gdata_documents_spreadsheet_get_download_uri (GDataDocumentsSpreadsheet *self, GDataDocumentsSpreadsheetFormat export_format,
+						     gint gid) G_GNUC_WARN_UNUSED_RESULT;
 
 G_END_DECLS
 
diff --git a/gdata/services/documents/gdata-documents-text.c b/gdata/services/documents/gdata-documents-text.c
index 933059a..8ae323f 100644
--- a/gdata/services/documents/gdata-documents-text.c
+++ b/gdata/services/documents/gdata-documents-text.c
@@ -42,6 +42,17 @@
 
 static void get_xml (GDataParsable *parsable, GString *xml_string);
 
+static const gchar *export_formats[] = {
+	"doc", /* GDATA_DOCUMENTS_TEXT_DOC */
+	"html", /* GDATA_DOCUMENTS_TEXT_HTML */
+	"odt", /* GDATA_DOCUMENTS_TEXT_ODT */
+	"pdf", /* GDATA_DOCUMENTS_TEXT_PDF */
+	"png", /* GDATA_DOCUMENTS_TEXT_PNG */
+	"rtf", /* GDATA_DOCUMENTS_TEXT_RTF */
+	"txt", /* GDATA_DOCUMENTS_TEXT_TXT */
+	"zip" /* GDATA_DOCUMENTS_TEXT_ZIP */
+};
+
 G_DEFINE_TYPE (GDataDocumentsText, gdata_documents_text, GDATA_TYPE_DOCUMENTS_ENTRY)
 #define GDATA_DOCUMENTS_TEXT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GDATA_TYPE_DOCUMENTS_TEXT, GDataDocumentsTextClass))
 
@@ -118,34 +129,16 @@ gdata_documents_text_download_document (GDataDocumentsText *self, GDataDocuments
 					GDataDocumentsTextFormat export_format, GFile *destination_file,
 					gboolean replace_file_if_exists, GCancellable *cancellable, GError **error)
 {
-	const gchar *document_id;
 	gchar *link_href;
 
-	const gchar *export_formats[] = {
-		"doc", /* GDATA_DOCUMENTS_TEXT_DOC */
-		"html", /* GDATA_DOCUMENTS_TEXT_HTML */
-		"odt", /* GDATA_DOCUMENTS_TEXT_ODT */
-		"pdf", /* GDATA_DOCUMENTS_TEXT_PDF */
-		"png", /* GDATA_DOCUMENTS_TEXT_PNG */
-		"rtf", /* GDATA_DOCUMENTS_TEXT_RTF */
-		"txt", /* GDATA_DOCUMENTS_TEXT_TXT */
-		"zip" /* GDATA_DOCUMENTS_TEXT_ZIP */
-	};
-
-	/* TODO: async version */
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_TEXT (self), NULL);
 	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (service), NULL);
-	g_return_val_if_fail (export_format >= 0 && export_format < G_N_ELEMENTS (export_formats), NULL);
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
 	g_return_val_if_fail (G_IS_FILE (destination_file), NULL);
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
 
-	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
-	g_assert (document_id != NULL);
-
-	link_href = g_strdup_printf ("http://docs.google.com/feeds/download/presentations/Export?exportFormat=%s&docID=%s";,
-				     export_formats[export_format], document_id);
-
 	/* Download the file */
+	link_href = gdata_documents_text_get_download_uri (self, export_format);
 	destination_file = _gdata_documents_entry_download_document (GDATA_DOCUMENTS_ENTRY (self), GDATA_SERVICE (service),
 								     content_type, link_href, destination_file, export_formats[export_format], replace_file_if_exists,
 								     cancellable, error);
@@ -153,3 +146,29 @@ gdata_documents_text_download_document (GDataDocumentsText *self, GDataDocuments
 
 	return destination_file;
 }
+
+/**
+ * gdata_documents_text_get_download_uri:
+ * @self: a #GDataDocumentsText
+ * @export_format: the format in which the document should be exported when downloaded
+ *
+ * Builds and returns the download URI for the given #GDataDocumentsText in the desired format. Note that directly downloading
+ * the document using this URI isn't possible, as authentication is required. You should instead use gdata_download_stream_new() with
+ * the URI, and use the resulting #GInputStream.
+ *
+ * Return value: the download URI; free with g_free()
+ *
+ * Since: 0.5.0
+ **/
+gchar *
+gdata_documents_text_get_download_uri (GDataDocumentsText *self, GDataDocumentsTextFormat export_format)
+{
+	const gchar *document_id;
+
+	g_return_val_if_fail (export_format < G_N_ELEMENTS (export_formats), NULL);
+
+	document_id = gdata_documents_entry_get_document_id (GDATA_DOCUMENTS_ENTRY (self));
+	g_assert (document_id != NULL);
+
+	return g_strdup_printf ("http://docs.google.com/feeds/download/documents/Export?exportFormat=%s&docID=%s";, export_formats[export_format], document_id);
+}
diff --git a/gdata/services/documents/gdata-documents-text.h b/gdata/services/documents/gdata-documents-text.h
index f8526df..393dd98 100644
--- a/gdata/services/documents/gdata-documents-text.h
+++ b/gdata/services/documents/gdata-documents-text.h
@@ -97,6 +97,7 @@ GFile *gdata_documents_text_download_document (GDataDocumentsText *self, GDataDo
 					       GDataDocumentsTextFormat export_format, GFile *destination_file,
 					       gboolean replace_file_if_exists, GCancellable *cancellable,
 					       GError **error) G_GNUC_WARN_UNUSED_RESULT;
+gchar *gdata_documents_text_get_download_uri (GDataDocumentsText *self, GDataDocumentsTextFormat export_format) G_GNUC_WARN_UNUSED_RESULT;
 
 G_END_DECLS
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]