[libgdata] documents: Add support for resumable uploads and updates



commit 6e8499dfbdb6029b7797ae31957b87f688a88482
Author: Philip Withnall <philip tecnocode co uk>
Date:   Mon Apr 2 15:12:35 2012 +0100

    documents: Add support for resumable uploads and updates
    
    This adds support for resumable uploads and updates to the Documents API. In
    doing so, it was necessary to wade through a sea of bugs and documentation
    inadequacies in the GData protocol (thanks, Google). In the end, it all seems
    to work (ignoring the pile of hacks to handle differences between
    non-resumable and resumable uploadsâ handling of metadata; and the bugs in
    content-only uploads).
    
    Itâs worth noting that this is only a basic implementation of resumable
    upload/update. It does not, for example, automatically retry chunks of
    uploads which fail. That can come later.
    
    A new, expanded set of test cases for uploads/updates is included.
    
    New API:
     â gdata_documents_service_upload_document_resumable()
     â gdata_documents_service_update_document_resumable()
    
    Helps: https://bugzilla.gnome.org/show_bug.cgi?id=607272
    Helps: https://bugzilla.gnome.org/show_bug.cgi?id=593537

 docs/reference/gdata-sections.txt                  |    2 +
 gdata/gdata-upload-stream.c                        |   26 +-
 gdata/gdata.symbols                                |    2 +
 gdata/services/documents/gdata-documents-service.c |  289 +++++++-
 gdata/services/documents/gdata-documents-service.h |    8 +
 gdata/tests/documents.c                            |  807 +++++++++++---------
 6 files changed, 742 insertions(+), 392 deletions(-)
---
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index d671218..14f0f45 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -1750,7 +1750,9 @@ gdata_documents_service_get_spreadsheet_authorization_domain
 gdata_documents_service_query_documents
 gdata_documents_service_query_documents_async
 gdata_documents_service_upload_document
+gdata_documents_service_upload_document_resumable
 gdata_documents_service_update_document
+gdata_documents_service_update_document_resumable
 gdata_documents_service_finish_upload
 gdata_documents_service_add_entry_to_folder
 gdata_documents_service_add_entry_to_folder_async
diff --git a/gdata/gdata-upload-stream.c b/gdata/gdata-upload-stream.c
index 04fe783..920712e 100644
--- a/gdata/gdata-upload-stream.c
+++ b/gdata/gdata-upload-stream.c
@@ -408,21 +408,11 @@ gdata_upload_stream_init (GDataUploadStream *self)
 static SoupMessage *
 build_message (GDataUploadStream *self, const gchar *method, const gchar *upload_uri)
 {
-	GDataUploadStreamPrivate *priv;
-	GDataServiceClass *klass;
 	SoupMessage *new_message;
 
-	priv = self->priv;
-
 	/* Build the message */
 	new_message = soup_message_new (method, upload_uri);
 
-	/* Make sure the headers are set */
-	klass = GDATA_SERVICE_GET_CLASS (priv->service);
-	if (klass->append_query_headers != NULL) {
-		klass->append_query_headers (priv->service, priv->authorization_domain, new_message);
-	}
-
 	/* We don't want to accumulate chunks */
 	soup_message_body_set_accumulate (new_message->request_body, FALSE);
 
@@ -433,6 +423,7 @@ static GObject *
 gdata_upload_stream_constructor (GType type, guint n_construct_params, GObjectConstructParam *construct_params)
 {
 	GDataUploadStreamPrivate *priv;
+	GDataServiceClass *klass;
 	GObject *object;
 
 	/* Chain up to the parent class */
@@ -509,6 +500,13 @@ gdata_upload_stream_constructor (GType type, guint n_construct_params, GObjectCo
 		priv->chunk_size = MIN (priv->content_length, MAX_RESUMABLE_CHUNK_SIZE);
 	}
 
+	/* Make sure the headers are set. HACK: This should actually be in build_message(), but we have to work around
+	 * http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3033 in GDataDocumentsService's append_query_headers(). */
+	klass = GDATA_SERVICE_GET_CLASS (priv->service);
+	if (klass->append_query_headers != NULL) {
+		klass->append_query_headers (priv->service, priv->authorization_domain, priv->message);
+	}
+
 	/* If the entry exists and has an ETag, we assume we're updating the entry, so we can set the If-Match header */
 	if (priv->entry != NULL && gdata_entry_get_etag (priv->entry) != NULL)
 		soup_message_headers_append (priv->message->request_headers, "If-Match", gdata_entry_get_etag (priv->entry));
@@ -1107,6 +1105,7 @@ upload_thread (GDataUploadStream *self)
 	g_assert (priv->cancellable != NULL);
 
 	while (TRUE) {
+		GDataServiceClass *klass;
 		gulong wrote_headers_signal, wrote_body_data_signal;
 		gchar *new_uri;
 		SoupMessage *new_message;
@@ -1200,6 +1199,13 @@ upload_thread (GDataUploadStream *self)
 		soup_message_headers_set_content_range (new_message->request_headers, priv->total_network_bytes_written,
 		                                        priv->total_network_bytes_written + next_chunk_length - 1, priv->content_length);
 
+		/* Make sure the headers are set. HACK: This should actually be in build_message(), but we have to work around
+		 * http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3033 in GDataDocumentsService's append_query_headers(). */
+		klass = GDATA_SERVICE_GET_CLASS (priv->service);
+		if (klass->append_query_headers != NULL) {
+			klass->append_query_headers (priv->service, priv->authorization_domain, new_message);
+		}
+
 		g_signal_handler_disconnect (priv->message, wrote_body_data_signal);
 		g_signal_handler_disconnect (priv->message, wrote_headers_signal);
 
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index e75fa49..0bfbb3a 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -939,3 +939,5 @@ gdata_contacts_contact_get_file_as
 gdata_contacts_contact_set_file_as
 gdata_upload_stream_new_resumable
 gdata_upload_stream_get_content_length
+gdata_documents_service_upload_document_resumable
+gdata_documents_service_update_document_resumable
diff --git a/gdata/services/documents/gdata-documents-service.c b/gdata/services/documents/gdata-documents-service.c
index 1879186..919ad58 100644
--- a/gdata/services/documents/gdata-documents-service.c
+++ b/gdata/services/documents/gdata-documents-service.c
@@ -269,8 +269,11 @@ gdata_documents_service_error_quark (void)
 	return g_quark_from_static_string ("gdata-documents-service-error-quark");
 }
 
+static void append_query_headers (GDataService *self, GDataAuthorizationDomain *domain, SoupMessage *message);
 static GList *get_authorization_domains (void);
 
+static gchar *_build_v2_upload_uri (GDataDocumentsFolder *folder) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
 _GDATA_DEFINE_AUTHORIZATION_DOMAIN (documents, "writely", "https://docs.google.com/feeds/";)
 _GDATA_DEFINE_AUTHORIZATION_DOMAIN (spreadsheets, "wise", "https://spreadsheets.google.com/feeds/";)
 G_DEFINE_TYPE_WITH_CODE (GDataDocumentsService, gdata_documents_service, GDATA_TYPE_SERVICE, G_IMPLEMENT_INTERFACE (GDATA_TYPE_BATCHABLE, NULL))
@@ -280,6 +283,8 @@ gdata_documents_service_class_init (GDataDocumentsServiceClass *klass)
 {
 	GDataServiceClass *service_class = GDATA_SERVICE_CLASS (klass);
 	service_class->feed_type = GDATA_TYPE_DOCUMENTS_FEED;
+
+	service_class->append_query_headers = append_query_headers;
 	service_class->get_authorization_domains = get_authorization_domains;
 
 	service_class->api_version = "3";
@@ -291,6 +296,49 @@ gdata_documents_service_init (GDataDocumentsService *self)
 	/* Nothing to see here */
 }
 
+static void
+append_query_headers (GDataService *self, GDataAuthorizationDomain *domain, SoupMessage *message)
+{
+	g_assert (message != NULL);
+
+	if (message->method == SOUP_METHOD_POST && soup_message_headers_get_one (message->request_headers, "X-Upload-Content-Length") == NULL) {
+		gchar *upload_uri;
+		const gchar *v3_pos;
+
+		upload_uri = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+		v3_pos = strstr (upload_uri, "://docs.google.com/feeds/upload/create-session/default/private/full");
+
+		if (v3_pos != NULL) {
+			gchar *v2_upload_uri;
+			SoupURI *_v2_upload_uri;
+
+			/* Content length header for resumable uploads. Only set it if this looks like the initial request of a resumable upload, and
+			 * if no content length has been set previously.
+			 * This allows methods like gdata_service_insert_entry() (which aren't resumable-upload-aware) to continue working for creating
+			 * documents with metadata only, by simulating the initial request of a resumable upload as described here:
+			 * https://developers.google.com/google-apps/documents-list/#creating_a_new_document_or_file_with_metadata_only */
+			soup_message_headers_replace (message->request_headers, "X-Upload-Content-Length", "0");
+
+			/* Also set the encoding to be content length encoding. */
+			soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CONTENT_LENGTH);
+
+			/* HACK: Work around http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3033 by changing the upload URI
+			 * to the v2 API's upload URI. Grrr. */
+			v2_upload_uri = g_strconcat (_gdata_service_get_scheme (), "://docs.google.com/feeds/default/private/full",
+			                             v3_pos + strlen ("://docs.google.com/feeds/upload/create-session/default/private/full"), NULL);
+			_v2_upload_uri = soup_uri_new (v2_upload_uri);
+			soup_message_set_uri (message, _v2_upload_uri);
+			soup_uri_free (_v2_upload_uri);
+			g_free (v2_upload_uri);
+		}
+
+		g_free (upload_uri);
+	}
+
+	/* Chain up to the parent class */
+	GDATA_SERVICE_CLASS (gdata_documents_service_parent_class)->append_query_headers (self, domain, message);
+}
+
 static GList *
 get_authorization_domains (void)
 {
@@ -302,6 +350,12 @@ get_authorization_domains (void)
 	return authorization_domains;
 }
 
+static gchar *
+escape_for_uri (const gchar *unescaped_component)
+{
+	return g_uri_escape_string (unescaped_component, NULL, TRUE);
+}
+
 /**
  * gdata_documents_service_new:
  * @authorizer: (allow-none): a #GDataAuthorizer to authorize the service's requests, or %NULL
@@ -470,9 +524,9 @@ gdata_documents_service_query_documents_async (GDataDocumentsService *self, GDat
 
 static GDataUploadStream *
 upload_update_document (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug, const gchar *content_type,
-                        const gchar *method, const gchar *upload_uri, GCancellable *cancellable)
+                        goffset content_length, const gchar *method, const gchar *upload_uri, GCancellable *cancellable)
 {
-	/* Corrects a bug on spreadsheet content types handling
+	/* HACK: Corrects a bug on spreadsheet content types handling
 	 * The content type for ODF spreadsheets 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.
@@ -481,8 +535,35 @@ upload_update_document (GDataDocumentsService *self, GDataDocumentsDocument *doc
 		content_type = "application/x-vnd.oasis.opendocument.spreadsheet";
 
 	/* We need streaming file I/O: GDataUploadStream */
-	return GDATA_UPLOAD_STREAM (gdata_upload_stream_new (GDATA_SERVICE (self), get_documents_authorization_domain (), method, upload_uri,
-	                                                     GDATA_ENTRY (document), slug, content_type, cancellable));
+	if (content_length == -1) {
+		/* Non-resumable upload. */
+		return GDATA_UPLOAD_STREAM (gdata_upload_stream_new (GDATA_SERVICE (self), get_documents_authorization_domain (), method, upload_uri,
+	                                                             GDATA_ENTRY (document), slug, content_type, cancellable));
+	} else {
+		/* Resumable upload. */
+		return GDATA_UPLOAD_STREAM (gdata_upload_stream_new_resumable (GDATA_SERVICE (self), get_documents_authorization_domain (), method,
+		                                                               upload_uri, GDATA_ENTRY (document), slug, content_type, content_length,
+		                                                               cancellable));
+	}
+}
+
+static gboolean
+_upload_checks (GDataDocumentsService *self, GDataDocumentsDocument *document, GError **error)
+{
+	if (gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
+	                                               get_documents_authorization_domain ()) == FALSE) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+		                     _("You must be authenticated to upload documents."));
+		return FALSE;
+	}
+
+	if (document != NULL && gdata_entry_is_inserted (GDATA_ENTRY (document)) == TRUE) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_ENTRY_ALREADY_INSERTED,
+		                     _("The document has already been uploaded."));
+		return FALSE;
+	}
+
+	return TRUE;
 }
 
 /**
@@ -499,6 +580,10 @@ upload_update_document (GDataDocumentsService *self, GDataDocumentsDocument *doc
  * the document data does not need to be provided at the moment, just the metadata, use gdata_service_insert_entry() instead (e.g. in the case of
  * creating a new, empty file to be edited at a later date).
  *
+ * This performs a non-resumable upload, unlike gdata_documents_service_upload_document(). This means that errors during transmission will cause the
+ * upload to fail, and the entire document will have to be re-uploaded. It is recommended that gdata_documents_service_upload_document_resumable()
+ * be used instead.
+ *
  * If @document is %NULL, only the document data will be uploaded. The new document entry will be named using @slug, and will have default metadata.
  *
  * The stream returned by this function should be written to using the standard #GOutputStream methods, asychronously or synchronously. Once the stream
@@ -530,26 +615,94 @@ gdata_documents_service_upload_document (GDataDocumentsService *self, GDataDocum
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
 	g_return_val_if_fail (error == NULL || *error == NULL, NULL);
 
-	if (gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
-	                                               get_documents_authorization_domain ()) == FALSE) {
-		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
-		                     _("You must be authenticated to upload documents."));
+	if (_upload_checks (self, document, error) == FALSE) {
 		return NULL;
 	}
 
-	if (document != NULL && gdata_entry_is_inserted (GDATA_ENTRY (document)) == TRUE) {
-		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_ENTRY_ALREADY_INSERTED,
-		                     _("The document has already been uploaded."));
+	/* HACK: Since we're using non-resumable upload, we have to use the v2 API upload URI to work around
+	 * http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3033 */
+	upload_uri = _build_v2_upload_uri (folder);
+	upload_stream = upload_update_document (self, document, slug, content_type, -1, SOUP_METHOD_POST, upload_uri, cancellable);
+	g_free (upload_uri);
+
+	return upload_stream;
+}
+
+/**
+ * gdata_documents_service_upload_document_resumable:
+ * @self: an authenticated #GDataDocumentsService
+ * @document: (allow-none): the #GDataDocumentsDocument to insert, or %NULL
+ * @slug: the filename to give to the uploaded document
+ * @content_type: the content type of the uploaded data
+ * @content_length: the size (in bytes) of the file being uploaded
+ * @folder: (allow-none): the folder to which the document should be uploaded, or %NULL
+ * @cancellable: (allow-none): a #GCancellable for the entire upload stream, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Uploads a document to Google Documents, using the properties from @document and the document data written to the resulting #GDataUploadStream. If
+ * the document data does not need to be provided at the moment, just the metadata, use gdata_service_insert_entry() instead (e.g. in the case of
+ * creating a new, empty file to be edited at a later date).
+ *
+ * Unlike gdata_documents_service_upload_document(), this method performs a
+ * <ulink type="http" url="http://code.google.com/apis/gdata/docs/resumable_upload.html";>resumable upload</ulink> which allows for correction of
+ * transmission errors without re-uploading the entire file. Use of this method is preferred over gdata_documents_service_upload_document().
+ *
+ * If @document is %NULL, only the document data will be uploaded. The new document entry will be named using @slug, and will have default metadata.
+ *
+ * The stream returned by this function should be written to using the standard #GOutputStream methods, asychronously or synchronously. Once the stream
+ * is closed (using g_output_stream_close()), gdata_documents_service_finish_upload() should be called on it to parse and return the updated
+ * #GDataDocumentsDocument for the document. This must be done, as @document isn't updated in-place.
+ *
+ * In order to cancel the upload, a #GCancellable passed in to @cancellable must be cancelled using g_cancellable_cancel(). Cancelling the individual
+ * #GOutputStream operations on the #GDataUploadStream will not cancel the entire upload; merely the write or close operation in question. See the
+ * #GDataUploadStream:cancellable for more details.
+ *
+ * Any upload errors will be thrown by the stream methods, and may come from the #GDataServiceError domain.
+ *
+ * Return value: (transfer full): a #GDataUploadStream to write the document data to, or %NULL; unref with g_object_unref()
+ *
+ * Since: 0.11.2
+ */
+GDataUploadStream *
+gdata_documents_service_upload_document_resumable (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
+                                                   const gchar *content_type, goffset content_length, GDataDocumentsFolder *folder,
+                                                   GCancellable *cancellable, GError **error)
+{
+	GDataUploadStream *upload_stream;
+	gchar *upload_uri;
+
+	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (self), NULL);
+	g_return_val_if_fail (document == NULL || GDATA_IS_DOCUMENTS_DOCUMENT (document), NULL);
+	g_return_val_if_fail (slug != NULL && *slug != '\0', NULL);
+	g_return_val_if_fail (content_type != NULL && *content_type != '\0', NULL);
+	g_return_val_if_fail (folder == NULL || GDATA_IS_DOCUMENTS_FOLDER (folder), NULL);
+	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
+	g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+	if (_upload_checks (self, document, error) == FALSE) {
 		return NULL;
 	}
 
 	upload_uri = gdata_documents_service_get_upload_uri (folder);
-	upload_stream = upload_update_document (self, document, slug, content_type, SOUP_METHOD_POST, upload_uri, cancellable);
+	upload_stream = upload_update_document (self, document, slug, content_type, content_length, SOUP_METHOD_POST, upload_uri, cancellable);
 	g_free (upload_uri);
 
 	return upload_stream;
 }
 
+static gboolean
+_update_checks (GDataDocumentsService *self, GError **error)
+{
+	if (gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
+	                                               get_documents_authorization_domain ()) == FALSE) {
+		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
+		                     _("You must be authenticated to update documents."));
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
 /**
  * gdata_documents_service_update_document:
  * @self: a #GDataDocumentsService
@@ -562,6 +715,10 @@ gdata_documents_service_upload_document (GDataDocumentsService *self, GDataDocum
  * Update the document using the properties from @document and the document data written to the resulting #GDataUploadStream. If the document data does
  * not need to be changed, just the metadata, use gdata_service_update_entry() instead.
  *
+ * This performs a non-resumable upload, unlike gdata_documents_service_update_document(). This means that errors during transmission will cause the
+ * upload to fail, and the entire document will have to be re-uploaded. It is recommended that gdata_documents_service_update_document_resumable()
+ * be used instead.
+ *
  * The stream returned by this function should be written to using the standard #GOutputStream methods, asychronously or synchronously. Once the stream
  * is closed (using g_output_stream_close()), gdata_documents_service_finish_upload() should be called on it to parse and return the updated
  * #GDataDocumentsDocument for the document. This must be done, as @document isn't updated in-place.
@@ -591,17 +748,72 @@ gdata_documents_service_update_document (GDataDocumentsService *self, GDataDocum
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
 	g_return_val_if_fail (error == NULL || *error == NULL, NULL);
 
-	if (gdata_authorizer_is_authorized_for_domain (gdata_service_get_authorizer (GDATA_SERVICE (self)),
-	                                               get_documents_authorization_domain ()) == FALSE) {
-		g_set_error_literal (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED,
-		                     _("You must be authenticated to update documents."));
+	if (_update_checks (self, error) == FALSE) {
 		return NULL;
 	}
 
 	update_link = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_EDIT_MEDIA);
 	g_assert (update_link != NULL);
 
-	return upload_update_document (self, document, slug, content_type, SOUP_METHOD_PUT, gdata_link_get_uri (update_link), cancellable);
+	return upload_update_document (self, document, slug, content_type, -1, SOUP_METHOD_PUT, gdata_link_get_uri (update_link),
+	                               cancellable);
+}
+
+/**
+ * gdata_documents_service_update_document_resumable:
+ * @self: a #GDataDocumentsService
+ * @document: the #GDataDocumentsDocument to update
+ * @slug: the filename to give to the uploaded document
+ * @content_type: the content type of the uploaded data
+ * @content_length: the size (in bytes) of the file being uploaded
+ * @cancellable: (allow-none): a #GCancellable for the entire upload stream, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Update the document using the properties from @document and the document data written to the resulting #GDataUploadStream. If the document data does
+ * not need to be changed, just the metadata, use gdata_service_update_entry() instead.
+ *
+ * Unlike gdata_documents_service_update_document(), this method performs a
+ * <ulink type="http" url="http://code.google.com/apis/gdata/docs/resumable_upload.html";>resumable upload</ulink> which allows for correction of
+ * transmission errors without re-uploading the entire file. Use of this method is preferred over gdata_documents_service_update_document().
+ *
+ * The stream returned by this function should be written to using the standard #GOutputStream methods, asychronously or synchronously. Once the stream
+ * is closed (using g_output_stream_close()), gdata_documents_service_finish_upload() should be called on it to parse and return the updated
+ * #GDataDocumentsDocument for the document. This must be done, as @document isn't updated in-place.
+ *
+ * In order to cancel the update, a #GCancellable passed in to @cancellable must be cancelled using g_cancellable_cancel(). Cancelling the individual
+ * #GOutputStream operations on the #GDataUploadStream will not cancel the entire update; merely the write or close operation in question. See the
+ * #GDataUploadStream:cancellable for more details.
+ *
+ * Any upload errors will be thrown by the stream methods, and may come from the #GDataServiceError domain.
+ *
+ * For more information, see gdata_service_update_entry().
+ *
+ * Return value: (transfer full): a #GDataUploadStream to write the document data to; unref with g_object_unref()
+ *
+ * Since: 0.11.2
+ */
+GDataUploadStream *
+gdata_documents_service_update_document_resumable (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
+                                                   const gchar *content_type, goffset content_length, GCancellable *cancellable, GError **error)
+{
+	GDataLink *update_link;
+
+	g_return_val_if_fail (GDATA_IS_DOCUMENTS_SERVICE (self), NULL);
+	g_return_val_if_fail (GDATA_IS_DOCUMENTS_DOCUMENT (document), NULL);
+	g_return_val_if_fail (slug != NULL && *slug != '\0', NULL);
+	g_return_val_if_fail (content_type != NULL && *content_type != '\0', NULL);
+	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL);
+	g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+	if (_update_checks (self, error) == FALSE) {
+		return NULL;
+	}
+
+	update_link = gdata_entry_look_up_link (GDATA_ENTRY (document), GDATA_LINK_RESUMABLE_EDIT_MEDIA);
+	g_assert (update_link != NULL);
+
+	return upload_update_document (self, document, slug, content_type, content_length, SOUP_METHOD_PUT, gdata_link_get_uri (update_link),
+	                               cancellable);
 }
 
 /**
@@ -1042,6 +1254,28 @@ gdata_documents_service_remove_entry_from_folder_finish (GDataDocumentsService *
 	g_assert_not_reached ();
 }
 
+/* HACK: Work around http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3033 by also using the upload URI for the v2 API. Grrr. */
+static gchar *
+_build_v2_upload_uri (GDataDocumentsFolder *folder)
+{
+	g_return_val_if_fail (folder == NULL || GDATA_IS_DOCUMENTS_FOLDER (folder), NULL);
+
+	/* If we have a folder, return the folder's upload URI */
+	if (folder != NULL) {
+		gchar *upload_uri, *escaped_resource_id;
+
+		escaped_resource_id = escape_for_uri (gdata_documents_entry_get_resource_id (GDATA_DOCUMENTS_ENTRY (folder)));
+		upload_uri = g_strconcat (_gdata_service_get_scheme (), "://docs.google.com/feeds/default/private/full/", escaped_resource_id,
+		                          "/contents", NULL);
+		g_free (escaped_resource_id);
+
+		return upload_uri;
+	}
+
+	/* Otherwise return the default upload URI */
+	return g_strconcat (_gdata_service_get_scheme (), "://docs.google.com/feeds/default/private/full", NULL);
+}
+
 /**
  * gdata_documents_service_get_upload_uri:
  * @folder: (allow-none): the #GDataDocumentsFolder into which to upload the document, or %NULL
@@ -1059,6 +1293,27 @@ gdata_documents_service_get_upload_uri (GDataDocumentsFolder *folder)
 {
 	g_return_val_if_fail (folder == NULL || GDATA_IS_DOCUMENTS_FOLDER (folder), NULL);
 
+	if (folder != NULL) {
+		GDataLink *upload_link;
+
+		/* Get the folder's upload URI. */
+		upload_link = gdata_entry_look_up_link (GDATA_ENTRY (folder), GDATA_LINK_RESUMABLE_CREATE_MEDIA);
+
+		if (upload_link == NULL) {
+			gchar *upload_uri, *escaped_resource_id;
+
+			/* Fall back to building a URI manually. */
+			escaped_resource_id = escape_for_uri (gdata_documents_entry_get_resource_id (GDATA_DOCUMENTS_ENTRY (folder)));
+			upload_uri = g_strconcat (_gdata_service_get_scheme (), "://docs.google.com/feeds/upload/create-session/default/private/full/",
+			                          escaped_resource_id, "/contents", NULL);
+			g_free (escaped_resource_id);
+
+			return upload_uri;
+		}
+
+		return g_strdup (gdata_link_get_uri (upload_link));
+	}
+
 	/* Use resumable upload. */
 	return g_strconcat (_gdata_service_get_scheme (), "://docs.google.com/feeds/upload/create-session/default/private/full", NULL);
 }
diff --git a/gdata/services/documents/gdata-documents-service.h b/gdata/services/documents/gdata-documents-service.h
index 0a3a809..f68a6e0 100644
--- a/gdata/services/documents/gdata-documents-service.h
+++ b/gdata/services/documents/gdata-documents-service.h
@@ -99,9 +99,17 @@ void gdata_documents_service_query_documents_async (GDataDocumentsService *self,
 GDataUploadStream *gdata_documents_service_upload_document (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
                                                             const gchar *content_type, GDataDocumentsFolder *folder,
                                                             GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+GDataUploadStream *gdata_documents_service_upload_document_resumable (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
+                                                                      const gchar *content_type, goffset content_length, GDataDocumentsFolder *folder,
+                                                                      GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
 GDataUploadStream *gdata_documents_service_update_document (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
                                                             const gchar *content_type, GCancellable *cancellable,
                                                             GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+GDataUploadStream *gdata_documents_service_update_document_resumable (GDataDocumentsService *self, GDataDocumentsDocument *document, const gchar *slug,
+                                                                      const gchar *content_type, goffset content_length, GCancellable *cancellable,
+                                                                      GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
 GDataDocumentsDocument *gdata_documents_service_finish_upload (GDataDocumentsService *self, GDataUploadStream *upload_stream,
                                                                GError **error) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
 
diff --git a/gdata/tests/documents.c b/gdata/tests/documents.c
index 7c57800..741baec 100644
--- a/gdata/tests/documents.c
+++ b/gdata/tests/documents.c
@@ -28,14 +28,19 @@
 static gboolean
 check_document_is_in_folder (GDataDocumentsDocument *document, GDataDocumentsFolder *folder)
 {
-	GList *categories;
+	GList *links;
 	gboolean found_folder_category = FALSE;
+	GDataLink *folder_self_link;
 
-	for (categories = gdata_entry_get_categories (GDATA_ENTRY (document)); categories != NULL; categories = categories->next) {
-		GDataCategory *category = GDATA_CATEGORY (categories->data);
+	folder_self_link = gdata_entry_look_up_link (GDATA_ENTRY (folder), GDATA_LINK_SELF);
+	g_assert (folder_self_link != NULL);
 
-		if (strcmp (gdata_category_get_scheme (category), "http://schemas.google.com/docs/2007/folders/"; DOCUMENTS_USERNAME) == 0 &&
-		    strcmp (gdata_category_get_term (category), gdata_entry_get_title (GDATA_ENTRY (folder))) == 0) {
+	for (links = gdata_entry_look_up_links (GDATA_ENTRY (document), "http://schemas.google.com/docs/2007#parent";);
+	     links != NULL; links = links->next) {
+		GDataLink *_link = GDATA_LINK (links->data);
+
+		if (strcmp (gdata_link_get_uri (_link), gdata_link_get_uri (folder_self_link)) == 0 &&
+		    strcmp (gdata_link_get_title (_link), gdata_entry_get_title (GDATA_ENTRY (folder))) == 0) {
 			g_assert (found_folder_category == FALSE);
 			found_folder_category = TRUE;
 		}
@@ -44,6 +49,19 @@ check_document_is_in_folder (GDataDocumentsDocument *document, GDataDocumentsFol
 	return found_folder_category;
 }
 
+static gboolean
+check_document_is_in_root_folder (GDataDocumentsDocument *document)
+{
+	GList *links;
+	gboolean is_in_root_folder;
+
+	links = gdata_entry_look_up_links (GDATA_ENTRY (document), "http://schemas.google.com/docs/2007#parent";);
+	is_in_root_folder = (links == NULL) ? TRUE : FALSE;
+	g_list_free (links);
+
+	return is_in_root_folder;
+}
+
 static void
 delete_entry (GDataDocumentsEntry *entry, GDataService *service)
 {
@@ -60,6 +78,27 @@ delete_entry (GDataDocumentsEntry *entry, GDataService *service)
 	g_object_unref (new_entry);
 }
 
+static GDataDocumentsFolder *
+create_folder (GDataDocumentsService *service, const gchar *title)
+{
+	GDataDocumentsFolder *folder, *new_folder;
+	gchar *upload_uri;
+
+	folder = gdata_documents_folder_new (NULL);
+	gdata_entry_set_title (GDATA_ENTRY (folder), title);
+
+	/* Insert the folder */
+	upload_uri = gdata_documents_service_get_upload_uri (NULL);
+	new_folder = GDATA_DOCUMENTS_FOLDER (gdata_service_insert_entry (GDATA_SERVICE (service),
+	                                                                 gdata_documents_service_get_primary_authorization_domain (),
+	                                                                 upload_uri, GDATA_ENTRY (folder), NULL, NULL));
+	g_assert (GDATA_IS_DOCUMENTS_FOLDER (new_folder));
+	g_free (upload_uri);
+	g_object_unref (folder);
+
+	return new_folder;
+}
+
 static void
 test_authentication (void)
 {
@@ -212,34 +251,6 @@ set_up_temp_document_spreadsheet (TempDocumentData *data, gconstpointer service)
 }
 
 static void
-set_up_temp_document_text (TempDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsText *document;
-
-	/* Create a document */
-	document = gdata_documents_text_new (NULL);
-	gdata_entry_set_title (GDATA_ENTRY (document), "Temporary Document (Text)");
-
-	data->document = _set_up_temp_document (GDATA_DOCUMENTS_ENTRY (document), GDATA_SERVICE (service));
-
-	g_object_unref (document);
-}
-
-static void
-set_up_temp_document_presentation (TempDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsPresentation *document;
-
-	/* Create a document */
-	document = gdata_documents_presentation_new (NULL);
-	gdata_entry_set_title (GDATA_ENTRY (document), "Temporary Document (Presentation)");
-
-	data->document = _set_up_temp_document (GDATA_DOCUMENTS_ENTRY (document), GDATA_SERVICE (service));
-
-	g_object_unref (document);
-}
-
-static void
 tear_down_temp_document (TempDocumentData *data, gconstpointer service)
 {
 	if (data->document != NULL) {
@@ -457,188 +468,400 @@ test_query_all_documents_async_progress_closure (TempDocumentsData *documents_da
 	g_slice_free (GDataAsyncProgressClosure, data);
 }
 
+typedef enum {
+	UPLOAD_METADATA_ONLY,
+	UPLOAD_CONTENT_ONLY,
+	UPLOAD_CONTENT_AND_METADATA,
+} PayloadType;
+#define UPLOAD_PAYLOAD_TYPE_MAX UPLOAD_CONTENT_AND_METADATA
+
+const gchar *payload_type_names[] = {
+	"metadata-only",
+	"content-only",
+	"content-and-metadata",
+};
+
+typedef enum {
+	UPLOAD_IN_FOLDER,
+	UPLOAD_ROOT_FOLDER,
+} FolderType;
+#define UPLOAD_FOLDER_TYPE_MAX UPLOAD_ROOT_FOLDER
+
+const gchar *folder_type_names[] = {
+	"in-folder",
+	"root-folder",
+};
+
+typedef enum {
+	UPLOAD_RESUMABLE,
+	UPLOAD_NON_RESUMABLE,
+} ResumableType;
+#define UPLOAD_RESUMABLE_TYPE_MAX UPLOAD_NON_RESUMABLE
+
+const gchar *resumable_type_names[] = {
+	"resumable",
+	"non-resumable",
+};
+
+typedef struct {
+	PayloadType payload_type;
+	FolderType folder_type;
+	ResumableType resumable_type;
+	gchar *test_name;
+
+	GDataDocumentsService *service;
+} UploadDocumentTestParams;
+
 typedef struct {
 	GDataDocumentsFolder *folder;
 	GDataDocumentsDocument *new_document;
 } UploadDocumentData;
 
 static void
-set_up_upload_document (UploadDocumentData *data, gconstpointer service)
-{
-	data->folder = NULL;
-	data->new_document = NULL;
-}
-
-static void
-set_up_upload_document_with_folder (UploadDocumentData *data, gconstpointer service)
+set_up_upload_document (UploadDocumentData *data, gconstpointer _test_params)
 {
-	GDataDocumentsFolder *folder;
-	gchar *upload_uri;
+	const UploadDocumentTestParams *test_params = _test_params;
 
-	/* Set up the structure */
-	set_up_upload_document (data, service);
-
-	/* Create a folder */
-	folder = gdata_documents_folder_new (NULL);
-	gdata_entry_set_title (GDATA_ENTRY (folder), "Temporary Folder for Uploading Documents");
+	data->new_document = NULL;
 
-	/* Insert the folder */
-	upload_uri = gdata_documents_service_get_upload_uri (NULL);
-	data->folder = GDATA_DOCUMENTS_FOLDER (gdata_service_insert_entry (GDATA_SERVICE (service),
-	                                                                   gdata_documents_service_get_primary_authorization_domain (),
-	                                                                   upload_uri, GDATA_ENTRY (folder), NULL, NULL));
-	g_assert (GDATA_IS_DOCUMENTS_FOLDER (data->folder));
-	g_free (upload_uri);
-	g_object_unref (folder);
+	switch (test_params->folder_type) {
+		case UPLOAD_IN_FOLDER:
+			data->folder = create_folder (test_params->service, "Temporary Folder for Uploading Documents");
+			break;
+		case UPLOAD_ROOT_FOLDER:
+			data->folder = NULL;
+			break;
+		default:
+			g_assert_not_reached ();
+	}
 }
 
 static void
-tear_down_upload_document (UploadDocumentData *data, gconstpointer service)
+tear_down_upload_document (UploadDocumentData *data, gconstpointer _test_params)
 {
+	const UploadDocumentTestParams *test_params = _test_params;
+
 	/* Delete the new file */
 	if (data->new_document != NULL) {
 		/* HACK: Query for the new document, as Google's servers appear to modify it behind our back if we don't upload both metadata and data
 		 * when creating the document: http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=2337. We have to wait a few
 		 * seconds before trying this to allow the various Google servers to catch up with each other. */
 		g_usleep (5 * G_USEC_PER_SEC);
-		delete_entry (GDATA_DOCUMENTS_ENTRY (data->new_document), GDATA_SERVICE (service));
+		delete_entry (GDATA_DOCUMENTS_ENTRY (data->new_document), GDATA_SERVICE (test_params->service));
 		g_object_unref (data->new_document);
 	}
 
 	/* Delete the folder */
 	if (data->folder != NULL) {
-		delete_entry (GDATA_DOCUMENTS_ENTRY (data->folder), GDATA_SERVICE (service));
+		delete_entry (GDATA_DOCUMENTS_ENTRY (data->folder), GDATA_SERVICE (test_params->service));
 		g_object_unref (data->folder);
 	}
 }
 
 static void
-test_upload_metadata (UploadDocumentData *data, gconstpointer service)
+test_upload (UploadDocumentData *data, gconstpointer _test_params)
 {
-	GDataDocumentsEntry *document;
+	const UploadDocumentTestParams *test_params = _test_params;
+
+	GDataDocumentsDocument *document = NULL;
+	GFile *document_file = NULL;
+	GFileInfo *file_info = NULL;
 	GError *error = NULL;
-	gchar *upload_uri;
 
-	document = GDATA_DOCUMENTS_ENTRY (gdata_documents_spreadsheet_new (NULL));
-	gdata_entry_set_title (GDATA_ENTRY (document), "myNewSpreadsheet");
+	/* Upload content? */
+	switch (test_params->payload_type) {
+		case UPLOAD_METADATA_ONLY:
+			document_file = NULL;
+			file_info = NULL;
+			break;
+		case UPLOAD_CONTENT_ONLY:
+		case UPLOAD_CONTENT_AND_METADATA:
+			document_file = g_file_new_for_path (TEST_FILE_DIR "test.odt");
+			file_info = g_file_query_info (document_file,
+			                               G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
+			                               G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE, NULL, &error);
+			g_assert_no_error (error);
+			break;
+		default:
+			g_assert_not_reached ();
+	}
 
-	/* Insert the document */
-	upload_uri = gdata_documents_service_get_upload_uri (NULL);
-	data->new_document = GDATA_DOCUMENTS_DOCUMENT (gdata_service_insert_entry (GDATA_SERVICE (service),
-	                                                                           gdata_documents_service_get_primary_authorization_domain (),
-	                                                                           upload_uri, GDATA_ENTRY (document), NULL, &error));
-	g_free (upload_uri);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_SPREADSHEET (data->new_document));
+	/* Upload metadata? */
+	switch (test_params->payload_type) {
+		case UPLOAD_CONTENT_ONLY:
+			document = NULL;
+			break;
+		case UPLOAD_METADATA_ONLY:
+		case UPLOAD_CONTENT_AND_METADATA: {
+			gchar *title;
 
-	g_clear_error (&error);
-	g_object_unref (document);
-}
+			document = GDATA_DOCUMENTS_DOCUMENT (gdata_documents_text_new (NULL));
 
-static void
-test_upload_metadata_file (UploadDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsDocument *document;
-	GFile *document_file;
-	GFileInfo *file_info;
-	GDataUploadStream *upload_stream;
-	GFileInputStream *file_stream;
-	GError *error = NULL;
+			/* Build a title including the test details. */
+			title = g_strdup_printf ("Test Upload file (%s)", test_params->test_name);
+			gdata_entry_set_title (GDATA_ENTRY (document), title);
+			g_free (title);
 
-	document_file = g_file_new_for_path (TEST_FILE_DIR "test.odt");
-	file_info = g_file_query_info (document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-	                               G_FILE_QUERY_INFO_NONE, NULL, &error);
-	g_assert_no_error (error);
+			break;
+		}
+		default:
+			g_assert_not_reached ();
+	}
 
-	document = GDATA_DOCUMENTS_DOCUMENT (gdata_documents_text_new (NULL));
-	gdata_entry_set_title (GDATA_ENTRY (document), "upload_metadata_file");
+	if (test_params->payload_type == UPLOAD_METADATA_ONLY) {
+		gchar *upload_uri;
 
-	/* Prepare the upload stream */
-	upload_stream = gdata_documents_service_upload_document (GDATA_DOCUMENTS_SERVICE (service), document, g_file_info_get_display_name (file_info),
-	                                                         g_file_info_get_content_type (file_info), NULL, NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
+		/* Insert the document */
+		upload_uri = gdata_documents_service_get_upload_uri (data->folder);
+		data->new_document = GDATA_DOCUMENTS_DOCUMENT (gdata_service_insert_entry (GDATA_SERVICE (test_params->service),
+		                                                                           gdata_documents_service_get_primary_authorization_domain (),
+		                                                                           upload_uri, GDATA_ENTRY (document), NULL, &error));
+		g_free (upload_uri);
 
-	g_object_unref (file_info);
+		g_assert_no_error (error);
+	} else {
+		GDataUploadStream *upload_stream;
+		GFileInputStream *file_stream;
+
+		/* Prepare the upload stream */
+		switch (test_params->resumable_type) {
+			case UPLOAD_NON_RESUMABLE:
+				upload_stream = gdata_documents_service_upload_document (test_params->service, document,
+				                                                         g_file_info_get_display_name (file_info),
+				                                                         g_file_info_get_content_type (file_info), data->folder,
+				                                                         NULL, &error);
+				break;
+			case UPLOAD_RESUMABLE:
+				upload_stream = gdata_documents_service_upload_document_resumable (test_params->service, document,
+				                                                                   g_file_info_get_display_name (file_info),
+				                                                                   g_file_info_get_content_type (file_info),
+				                                                                   g_file_info_get_size (file_info), data->folder,
+				                                                                   NULL, &error);
+				break;
+			default:
+				g_assert_not_reached ();
+		}
 
-	/* Open the file */
-	file_stream = g_file_read (document_file, NULL, &error);
-	g_assert_no_error (error);
+		g_assert_no_error (error);
+		g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
 
-	/* Upload the document */
-	g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
-	                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
-	g_assert_no_error (error);
+		g_object_unref (file_info);
+
+		/* Open the file */
+		file_stream = g_file_read (document_file, NULL, &error);
+		g_assert_no_error (error);
+
+		/* Upload the document */
+		g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
+		                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
+		g_assert_no_error (error);
+
+		/* Finish the upload */
+		data->new_document = gdata_documents_service_finish_upload (test_params->service, upload_stream, &error);
+		g_assert_no_error (error);
+
+		g_object_unref (upload_stream);
+		g_object_unref (file_stream);
+	}
 
-	/* Finish the upload */
-	data->new_document = gdata_documents_service_finish_upload (GDATA_DOCUMENTS_SERVICE (service), upload_stream, &error);
-	g_assert_no_error (error);
 	g_assert (GDATA_IS_DOCUMENTS_TEXT (data->new_document));
 
 	/* Verify the uploaded document is the same as the original */
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (document)), ==, gdata_entry_get_title (GDATA_ENTRY (data->new_document)));
+	switch (test_params->payload_type) {
+		case UPLOAD_METADATA_ONLY:
+		case UPLOAD_CONTENT_AND_METADATA:
+			g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (data->new_document)), ==, gdata_entry_get_title (GDATA_ENTRY (document)));
+			break;
+		case UPLOAD_CONTENT_ONLY:
+			/* HACK: The title returned by the server varies depending on how we uploaded the document. */
+			if (test_params->resumable_type == UPLOAD_NON_RESUMABLE) {
+				g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (data->new_document)), ==, "test.odt");
+			} else {
+				g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (data->new_document)), ==, "Untitled");
+			}
+
+			break;
+		default:
+			g_assert_not_reached ();
+	}
+
+	/* Check it's in the right folder. */
+	switch (test_params->folder_type) {
+		case UPLOAD_IN_FOLDER:
+			/* HACK: When uploading content-only to a folder using the folder's resumable-create-media link, Google decides that it's
+			 * not useful to list the folder in the returned entry XML for the new document (i.e. the server pretends the document's
+			 * not in the folder you've just uploaded it to). Joy. */
+			g_assert (test_params->payload_type == UPLOAD_CONTENT_ONLY ||
+			          check_document_is_in_folder (data->new_document, data->folder) == TRUE);
+			break;
+		case UPLOAD_ROOT_FOLDER:
+			/* Check root folder. */
+			g_assert (check_document_is_in_root_folder (data->new_document) == TRUE);
+			break;
+		default:
+			g_assert_not_reached ();
+	}
 
 	g_clear_error (&error);
-	g_object_unref (upload_stream);
-	g_object_unref (file_stream);
-	g_object_unref (document_file);
+	g_clear_object (&document_file);
+	g_clear_object (&document);
+}
+
+typedef struct {
+	PayloadType payload_type;
+	ResumableType resumable_type;
+	gchar *test_name;
+
+	GDataDocumentsService *service;
+} UpdateDocumentTestParams;
+
+typedef struct {
+	GDataDocumentsDocument *document;
+} UpdateDocumentData;
+
+static void
+set_up_update_document (UpdateDocumentData *data, gconstpointer _test_params)
+{
+	const UpdateDocumentTestParams *test_params = _test_params;
+	GDataDocumentsText *document;
+	gchar *title;
+
+	/* Create a document */
+	document = gdata_documents_text_new (NULL);
+	title = g_strdup_printf ("Test Update file (%s)", test_params->test_name);
+	gdata_entry_set_title (GDATA_ENTRY (document), title);
+	g_free (title);
+
+	data->document = _set_up_temp_document (GDATA_DOCUMENTS_ENTRY (document), GDATA_SERVICE (test_params->service));
+
 	g_object_unref (document);
 }
 
 static void
-test_upload_file_get_entry (UploadDocumentData *data, gconstpointer service)
+tear_down_update_document (UpdateDocumentData *data, gconstpointer _test_params)
 {
-	GDataEntry *new_presentation;
-	GDataUploadStream *upload_stream;
-	GFileInputStream *file_stream;
-	GFile *document_file;
-	GFileInfo *file_info;
+	const UpdateDocumentTestParams *test_params = _test_params;
+
+	/* Delete the new file */
+	if (data->document != NULL) {
+		/* HACK: Query for the new document, as Google's servers appear to modify it behind our back if we don't update both metadata and data
+		 * when creating the document: http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=2337. We have to wait a few
+		 * seconds before trying this to allow the various Google servers to catch up with each other. */
+		g_usleep (5 * G_USEC_PER_SEC);
+		delete_entry (GDATA_DOCUMENTS_ENTRY (data->document), GDATA_SERVICE (test_params->service));
+		g_object_unref (data->document);
+	}
+}
+
+static void
+test_update (UpdateDocumentData *data, gconstpointer _test_params)
+{
+	const UpdateDocumentTestParams *test_params = _test_params;
+
+	GDataDocumentsDocument *updated_document;
+	gchar *original_title;
 	GError *error = NULL;
 
-	document_file = g_file_new_for_path (TEST_FILE_DIR "test.ppt");
-	file_info = g_file_query_info (document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-	                               G_FILE_QUERY_INFO_NONE, NULL, &error);
-	g_assert_no_error (error);
+	switch (test_params->payload_type) {
+		case UPLOAD_METADATA_ONLY:
+		case UPLOAD_CONTENT_AND_METADATA: {
+			gchar *new_title;
 
-	/* Prepare the upload stream */
-	upload_stream = gdata_documents_service_upload_document (GDATA_DOCUMENTS_SERVICE (service), NULL, g_file_info_get_display_name (file_info),
-	                                                         g_file_info_get_content_type (file_info), NULL, NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
+			/* Change the title of the document */
+			original_title = g_strdup (gdata_entry_get_title (GDATA_ENTRY (data->document)));
+			new_title = g_strdup_printf ("Updated Test Update file (%s)", test_params->test_name);
+			gdata_entry_set_title (GDATA_ENTRY (data->document), new_title);
+			g_free (new_title);
 
-	g_object_unref (file_info);
+			break;
+		}
+		case UPLOAD_CONTENT_ONLY:
+			original_title = NULL;
+			break;
+		default:
+			g_assert_not_reached ();
+	}
 
-	/* Open the file */
-	file_stream = g_file_read (document_file, NULL, &error);
-	g_assert_no_error (error);
+	if (test_params->payload_type == UPLOAD_METADATA_ONLY) {
+		/* Update the document */
+		updated_document = GDATA_DOCUMENTS_DOCUMENT (gdata_service_update_entry (GDATA_SERVICE (test_params->service),
+		                                                                         gdata_documents_service_get_primary_authorization_domain (),
+		                                                                         GDATA_ENTRY (data->document), NULL, &error));
+		g_assert_no_error (error);
+	} else {
+		GDataUploadStream *upload_stream;
+		GFileInputStream *file_stream;
+		GFile *updated_document_file;
+		GFileInfo *file_info;
+
+		/* Prepare the updated file */
+		updated_document_file = g_file_new_for_path (TEST_FILE_DIR "test_updated.odt");
+
+		file_info = g_file_query_info (updated_document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+		                               G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NONE,
+		                               NULL, &error);
+		g_assert_no_error (error);
+
+		/* Prepare the upload stream */
+		switch (test_params->resumable_type) {
+			case UPLOAD_NON_RESUMABLE:
+				upload_stream = gdata_documents_service_update_document (test_params->service, data->document,
+				                                                         g_file_info_get_display_name (file_info),
+				                                                         g_file_info_get_content_type (file_info),
+				                                                         NULL, &error);
+				break;
+			case UPLOAD_RESUMABLE:
+				upload_stream = gdata_documents_service_update_document_resumable (test_params->service, data->document,
+				                                                                   g_file_info_get_display_name (file_info),
+				                                                                   g_file_info_get_content_type (file_info),
+				                                                                   g_file_info_get_size (file_info), NULL, &error);
+				break;
+			default:
+				g_assert_not_reached ();
+		}
 
-	g_object_unref (document_file);
+		g_assert_no_error (error);
+		g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
 
-	/* Upload the document */
-	g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
-	                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
-	g_assert_no_error (error);
+		g_object_unref (file_info);
 
-	/* Finish the upload */
-	data->new_document = gdata_documents_service_finish_upload (GDATA_DOCUMENTS_SERVICE (service), upload_stream, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_PRESENTATION (data->new_document));
+		/* Open the file */
+		file_stream = g_file_read (updated_document_file, NULL, &error);
+		g_assert_no_error (error);
 
-	g_object_unref (file_stream);
-	g_object_unref (upload_stream);
+		g_object_unref (updated_document_file);
 
-	/* Get the entry on the server */
-	new_presentation = gdata_service_query_single_entry (GDATA_SERVICE (service), gdata_documents_service_get_primary_authorization_domain (),
-	                                                     gdata_entry_get_id (GDATA_ENTRY (data->new_document)), NULL,
-	                                                     GDATA_TYPE_DOCUMENTS_PRESENTATION, NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_PRESENTATION (new_presentation));
+		/* Upload the updated document */
+		g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
+		                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
+		g_assert_no_error (error);
 
-	/* Verify that the entry is correct (mangled version of the file's display name) */
-	g_assert_cmpstr (gdata_entry_get_title (new_presentation), ==, "test");
+		/* Finish the upload */
+		updated_document = gdata_documents_service_finish_upload (test_params->service, upload_stream, &error);
+		g_assert_no_error (error);
 
-	g_clear_error (&error);
-	g_object_unref (new_presentation);
+		g_object_unref (upload_stream);
+		g_object_unref (file_stream);
+	}
+
+	g_assert (GDATA_IS_DOCUMENTS_TEXT (updated_document));
+
+	/* Check for success */
+	switch (test_params->payload_type) {
+		case UPLOAD_METADATA_ONLY:
+		case UPLOAD_CONTENT_AND_METADATA:
+			g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), !=, original_title);
+			/* Fall through */
+		case UPLOAD_CONTENT_ONLY:
+			g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), ==,
+			                 gdata_entry_get_title (GDATA_ENTRY (data->document)));
+			break;
+		default:
+			g_assert_not_reached ();
+	}
+
+	g_free (original_title);
+	g_object_unref (updated_document);
 }
 
 typedef struct {
@@ -844,206 +1067,6 @@ G_STMT_START {
 } G_STMT_END);
 
 static void
-test_upload_file_metadata_in_new_folder (UploadDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsDocument *document;
-	GDataUploadStream *upload_stream;
-	GFileInputStream *file_stream;
-	GFile *document_file;
-	GFileInfo *file_info;
-	GError *error = NULL;
-
-	/* Prepare the file */
-	document_file = g_file_new_for_path (TEST_FILE_DIR "test.odt");
-	document = GDATA_DOCUMENTS_DOCUMENT (gdata_documents_text_new (NULL));
-	gdata_entry_set_title (GDATA_ENTRY (document), "upload_file_metadata_in_new_folder_text");
-
-	file_info = g_file_query_info (document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-	                               G_FILE_QUERY_INFO_NONE, NULL, &error);
-	g_assert_no_error (error);
-
-	/* Prepare the upload stream */
-	upload_stream = gdata_documents_service_upload_document (GDATA_DOCUMENTS_SERVICE (service), document, g_file_info_get_display_name (file_info),
-	                                                         g_file_info_get_content_type (file_info), data->folder, NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
-
-	g_object_unref (file_info);
-
-	/* Open the file */
-	file_stream = g_file_read (document_file, NULL, &error);
-	g_assert_no_error (error);
-
-	g_object_unref (document_file);
-
-	/* Upload the document into the new folder */
-	g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
-	                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
-	g_assert_no_error (error);
-
-	/* Finish the upload */
-	data->new_document = gdata_documents_service_finish_upload (GDATA_DOCUMENTS_SERVICE (service), upload_stream, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_TEXT (data->new_document));
-
-	g_object_unref (upload_stream);
-	g_object_unref (file_stream);
-
-	/* Check for success */
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (data->new_document)), ==, gdata_entry_get_title (GDATA_ENTRY (document)));
-	g_assert (check_document_is_in_folder (data->new_document, data->folder) == TRUE);
-
-	g_clear_error (&error);
-	g_object_unref (document);
-}
-
-static void
-test_update_metadata (TempDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsEntry *updated_document;
-	gchar *original_title;
-	GError *error = NULL;
-
-	/* Change the document title */
-	original_title = g_strdup (gdata_entry_get_title (GDATA_ENTRY (data->document)));
-	gdata_entry_set_title (GDATA_ENTRY (data->document), "Updated Title for Metadata Only");
-
-	/* Update the document */
-	updated_document = GDATA_DOCUMENTS_ENTRY (gdata_service_update_entry (GDATA_SERVICE (service),
-	                                                                      gdata_documents_service_get_primary_authorization_domain (),
-	                                                                      GDATA_ENTRY (data->document), NULL, &error));
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_TEXT (updated_document));
-	g_clear_error (&error);
-
-	/* Check for success */
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), ==, gdata_entry_get_title (GDATA_ENTRY (data->document)));
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), !=, original_title);
-
-	g_free (original_title);
-	g_object_unref (updated_document);
-}
-
-static void
-test_update_metadata_file (TempDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsDocument *updated_document;
-	GDataUploadStream *upload_stream;
-	GFileInputStream *file_stream;
-	GFile *updated_document_file;
-	GFileInfo *file_info;
-	gchar *original_title;
-	GError *error = NULL;
-
-	/* Change the title of the document */
-	original_title = g_strdup (gdata_entry_get_title (GDATA_ENTRY (data->document)));
-	gdata_entry_set_title (GDATA_ENTRY (data->document), "Updated Title for Metadata and File");
-
-	/* Prepare the updated file */
-	updated_document_file = g_file_new_for_path (TEST_FILE_DIR "test_updated.odt");
-
-	file_info = g_file_query_info (updated_document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-	                               G_FILE_QUERY_INFO_NONE, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	/* Prepare the upload stream */
-	upload_stream = gdata_documents_service_update_document (GDATA_DOCUMENTS_SERVICE (service), data->document,
-	                                                         g_file_info_get_display_name (file_info), g_file_info_get_content_type (file_info),
-	                                                         NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
-	g_clear_error (&error);
-
-	g_object_unref (file_info);
-
-	/* Open the file */
-	file_stream = g_file_read (updated_document_file, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	g_object_unref (updated_document_file);
-
-	/* Upload the updated document */
-	g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
-	                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	/* Finish the upload */
-	updated_document = gdata_documents_service_finish_upload (GDATA_DOCUMENTS_SERVICE (service), upload_stream, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_TEXT (updated_document));
-	g_clear_error (&error);
-
-	g_object_unref (upload_stream);
-	g_object_unref (file_stream);
-
-	/* Check for success */
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), ==, gdata_entry_get_title (GDATA_ENTRY (data->document)));
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), !=, original_title);
-
-	g_free (original_title);
-	g_object_unref (updated_document);
-}
-
-static void
-test_update_file (TempDocumentData *data, gconstpointer service)
-{
-	GDataDocumentsDocument *updated_document;
-	GDataUploadStream *upload_stream;
-	GFileInputStream *file_stream;
-	GFile *document_file;
-	GFileInfo *file_info;
-	GError *error = NULL;
-
-	/* Get the file info for the updated document */
-	document_file = g_file_new_for_path (TEST_FILE_DIR "test_updated_file.ppt");
-	file_info = g_file_query_info (document_file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
-	                               G_FILE_QUERY_INFO_NONE, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	/* Prepare the upload stream */
-	upload_stream = gdata_documents_service_update_document (GDATA_DOCUMENTS_SERVICE (service), data->document,
-	                                                         g_file_info_get_display_name (file_info), g_file_info_get_content_type (file_info),
-	                                                         NULL, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_UPLOAD_STREAM (upload_stream));
-	g_clear_error (&error);
-
-	g_object_unref (file_info);
-
-	/* Open the file */
-	file_stream = g_file_read (document_file, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	g_object_unref (document_file);
-
-	/* Upload the document */
-	g_output_stream_splice (G_OUTPUT_STREAM (upload_stream), G_INPUT_STREAM (file_stream),
-	                        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, &error);
-	g_assert_no_error (error);
-	g_clear_error (&error);
-
-	g_object_unref (file_stream);
-
-	/* Finish the upload */
-	updated_document = gdata_documents_service_finish_upload (GDATA_DOCUMENTS_SERVICE (service), upload_stream, &error);
-	g_assert_no_error (error);
-	g_assert (GDATA_IS_DOCUMENTS_PRESENTATION (updated_document));
-	g_clear_error (&error);
-
-	g_object_unref (upload_stream);
-
-	/* Check for success */
-	g_assert_cmpstr (gdata_entry_get_title (GDATA_ENTRY (updated_document)), ==, gdata_entry_get_title (GDATA_ENTRY (data->document)));
-
-	g_object_unref (updated_document);
-}
-
-static void
 _test_download_document (GDataDocumentsDocument *document, GDataService *service)
 {
 	GDataDownloadStream *download_stream;
@@ -1458,24 +1481,78 @@ main (int argc, char *argv[])
 		            tear_down_temp_document);
 		g_test_add ("/documents/delete/folder", TempFolderData, service, set_up_temp_folder, test_delete_folder, tear_down_temp_folder);
 
-		g_test_add ("/documents/upload/only_file_get_entry", UploadDocumentData, service, set_up_upload_document, test_upload_file_get_entry,
-		            tear_down_upload_document);
-		g_test_add ("/documents/upload/metadata_file", UploadDocumentData, service, set_up_upload_document, test_upload_metadata_file,
-		            tear_down_upload_document);
-		g_test_add ("/documents/upload/only_metadata", UploadDocumentData, service, set_up_upload_document, test_upload_metadata,
-		            tear_down_upload_document);
-		g_test_add ("/documents/upload/metadata_file_in_new_folder", UploadDocumentData, service, set_up_upload_document_with_folder,
-		            test_upload_file_metadata_in_new_folder, tear_down_upload_document);
+		/* Test all possible combinations of conditions for resumable uploads. */
+		{
+			PayloadType i;
+			FolderType j;
+			ResumableType k;
+
+			for (i = 0; i < UPLOAD_PAYLOAD_TYPE_MAX + 1; i++) {
+				for (j = 0; j < UPLOAD_FOLDER_TYPE_MAX + 1; j++) {
+					for (k = 0; k < UPLOAD_RESUMABLE_TYPE_MAX + 1; k++) {
+						UploadDocumentTestParams *test_params;
+						gchar *test_name;
+
+						/* Resumable metadata-only uploads don't make sense. */
+						if (i == UPLOAD_METADATA_ONLY && k == UPLOAD_RESUMABLE) {
+							continue;
+						}
+
+						test_name = g_strdup_printf ("/documents/upload/%s/%s/%s",
+						                             payload_type_names[i], folder_type_names[j],
+						                             resumable_type_names[k]);
+
+						/* Allocate a new struct. We leak this. */
+						test_params = g_slice_new0 (UploadDocumentTestParams);
+						test_params->payload_type = i;
+						test_params->folder_type = j;
+						test_params->resumable_type = k;
+						test_params->test_name = g_strdup (test_name);
+						test_params->service = GDATA_DOCUMENTS_SERVICE (service);
+
+						g_test_add (test_name, UploadDocumentData, test_params, set_up_upload_document, test_upload,
+						            tear_down_upload_document);
+
+						g_free (test_name);
+					}
+				}
+			}
+		}
 
 		g_test_add ("/documents/download/document", TempDocumentsData, service, set_up_temp_documents, test_download_document,
 		            tear_down_temp_documents);
 
-		g_test_add ("/documents/update/only_metadata", TempDocumentData, service, set_up_temp_document_text, test_update_metadata,
-		            tear_down_temp_document);
-		g_test_add ("/documents/update/only_file", TempDocumentData, service, set_up_temp_document_presentation, test_update_file,
-		            tear_down_temp_document);
-		g_test_add ("/documents/update/metadata_file", TempDocumentData, service, set_up_temp_document_text, test_update_metadata_file,
-		            tear_down_temp_document);
+		/* Test all possible combinations of conditions for resumable updates. */
+		{
+			PayloadType i;
+			ResumableType j;
+
+			for (i = 0; i < UPLOAD_PAYLOAD_TYPE_MAX + 1; i++) {
+				for (j = 0; j < UPLOAD_RESUMABLE_TYPE_MAX + 1; j++) {
+					UpdateDocumentTestParams *test_params;
+					gchar *test_name;
+
+					/* Resumable metadata-only updates don't make sense. */
+					if (i == UPLOAD_METADATA_ONLY && j == UPLOAD_RESUMABLE) {
+						continue;
+					}
+
+					test_name = g_strdup_printf ("/documents/update/%s/%s", payload_type_names[i], resumable_type_names[j]);
+
+					/* Allocate a new struct. We leak this. */
+					test_params = g_slice_new0 (UpdateDocumentTestParams);
+					test_params->payload_type = i;
+					test_params->resumable_type = j;
+					test_params->test_name = g_strdup (test_name);
+					test_params->service = GDATA_DOCUMENTS_SERVICE (service);
+
+					g_test_add (test_name, UpdateDocumentData, test_params, set_up_update_document, test_update,
+					            tear_down_update_document);
+
+					g_free (test_name);
+				}
+			}
+		}
 
 		g_test_add ("/documents/access-rule/insert", TempDocumentData, service, set_up_temp_document_spreadsheet, test_access_rule_insert,
 		            tear_down_temp_document);



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