Eric Bidelman, equipo de las APIs de G Suite
Febrero de 2010
Introducción
Los estándares web actuales no proporcionan un mecanismo confiable para facilitar la carga de archivos grandes por HTTP. Como resultado, las cargas de archivos en Google y otros sitios se limitaron tradicionalmente a tamaños moderados (p.ej., 100 MB). Para los servicios como las APIs de YouTube y de la lista de documentos de Google, que admiten la carga de archivos grandes, esto representa un gran obstáculo.
El protocolo reanudable de Google Data aborda directamente los problemas mencionados anteriormente, ya que admite solicitudes HTTP POST/PUT reanudables en HTTP/1.0. El protocolo se diseñó según la ResumableHttpRequestsProposal sugerida por el equipo de Google Gears.
En este documento, se describe cómo incorporar la función de carga reanudable de Google Data en tus aplicaciones. En los siguientes ejemplos, se usa la API de Google Documents List Data. Ten en cuenta que las APIs de Google adicionales que implementan este protocolo pueden tener requisitos, códigos de respuesta, etc., ligeramente diferentes. Consulta la documentación del servicio para obtener detalles específicos.
El protocolo de reanudación
Cómo iniciar una solicitud de carga reanudable
Para iniciar una sesión de carga reanudable, envía una solicitud POST HTTP al vínculo resumable-post. Este vínculo se encuentra a nivel del feed.
El vínculo de publicación reanudable de la API de DocList se ve de la siguiente manera:
<link rel="http://schemas.google.com/g/2005#resumable-create-media" type="application/atom+xml" href="https://docs.google.com/feeds/upload/create-session/default/private/full"/>
El cuerpo de tu solicitud POST debe estar vacío o contener una entrada XML de Atom, y no debe incluir el contenido real del archivo.
En el siguiente ejemplo, se crea una solicitud reanudable para subir un PDF grande y se incluye un título para el futuro documento con el encabezado Slug.
POST /feeds/upload/create-session/default/private/full HTTP/1.1 Host: docs.google.com GData-Version: version_number Authorization: authorization Content-Length: 0 Slug: MyTitle X-Upload-Content-Type: content_type X-Upload-Content-Length: content_length empty body
Los encabezados X-Upload-Content-Type y X-Upload-Content-Length deben establecerse en el tipo de MIME y el tamaño del archivo que subirás. Si no se conoce la longitud del contenido en el momento de la creación de la sesión de carga, se puede omitir el encabezado X-Upload-Content-Length.
Este es otro ejemplo de solicitud que, en cambio, sube un documento de Word. Esta vez, se incluyen los metadatos de Atom, que se aplicarán a la entrada del documento final.
POST /feeds/upload/create-session/default/private/full?convert=false HTTP/1.1
Host: docs.google.com
GData-Version: version_number
Authorization: authorization
Content-Length: atom_metadata_content_length
Content-Type: application/atom+xml
X-Upload-Content-Type: application/msword
X-Upload-Content-Length: 7654321
<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
<category scheme="http://schemas.google.com/g/2005#kind"
term="http://schemas.google.com/docs/2007#document"/>
<title>MyTitle</title>
<docs:writersCanInvite value="false"/>
</entry>
La respuesta del servidor de la POST inicial es un URI de carga único en el encabezado Location y un cuerpo de respuesta vacío:
HTTP/1.1 200 OK
Location: <upload_uri>
El URI de carga único se usará para subir los fragmentos del archivo.
Nota: La solicitud POST inicial no crea una entrada nueva en el feed.
Esto solo ocurre cuando se completa toda la operación de carga.
Nota: El URI de sesión reanudable vence después de una semana.
Carga un archivo
El protocolo resumible permite, pero no requiere, que el contenido se suba en "fragmentos", ya que no hay restricciones inherentes en HTTP sobre los tamaños de las solicitudes. Tu cliente puede elegir el tamaño de fragmento o simplemente subir el archivo completo.
En este ejemplo, se usa el URI de carga único para emitir un PUT reanudable. En el siguiente ejemplo, se envían los primeros 100,000 bytes de un archivo PDF de 1,234,567 bytes:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 100000 Content-Range: bytes 0-99999/1234567 bytes 0-99999
Si se desconociera el tamaño del archivo PDF, en este ejemplo se usaría Content-Range: bytes
0-99999/*. Obtén más información sobre el encabezado Content-Range aquí.
El servidor responde con el rango de bytes actual que se almacenó:
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-99999
Tu cliente debe seguir ejecutando solicitudes PUT para cada fragmento del archivo hasta que se haya subido por completo.
Hasta que se complete la carga, el servidor responderá con un 308 Resume Incomplete HTTP y el rango de bytes que conoce en el encabezado Range. Los clientes deben usar el encabezado Range para determinar dónde comenzar el siguiente fragmento.
Por lo tanto, no supongas que el servidor recibió todos los bytes que se enviaron originalmente en la solicitud PUT.
Nota: Es posible que el servidor emita un nuevo URI de carga único en el encabezado Location durante un fragmento. Tu cliente debe verificar si hay un Location actualizado y usar ese URI para enviar los fragmentos restantes al servidor.
Cuando se complete la carga, la respuesta será la misma que si la carga se hubiera realizado con el mecanismo de carga no reanudable de la API. Es decir, se devolverá un 201 Created junto con el <atom:entry>, tal como lo creó el servidor. Las llamadas PUT posteriores al URI de carga único devolverán la misma respuesta que se devolvió cuando se completó la carga.
Después de un período, la respuesta será 410 Gone o 404 Not Found.
Cómo reanudar una carga
Si tu solicitud se finaliza antes de que recibas una respuesta del servidor o si recibes una respuesta HTTP 503 del servidor, puedes consultar el estado actual de la carga emitiendo una solicitud PUT vacía en el URI de carga único.
El cliente sondea el servidor para determinar qué bytes recibió:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0 Content-Range: bytes */content_length
Usa * como content_length si no se conoce la longitud.
El servidor responde con el rango de bytes actual:
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-42
Nota: Si el servidor no confirmó ningún byte para la sesión, omitirá el encabezado Range.
Nota: Es posible que el servidor emita un nuevo URI de carga único en el encabezado Location durante un fragmento. Tu cliente debe verificar si hay un Location actualizado y usar ese URI para enviar los fragmentos restantes al servidor.
Por último, el cliente reanuda la operación donde la dejó el servidor:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 57 Content-Range: 43-99/100 <bytes 43-99>
Cancela una carga
Si deseas cancelar la carga y evitar cualquier otra acción sobre esta, envía una solicitud DELETE en el URI de carga único.
DELETE upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0
Si se ejecuta de forma correcta, el servidor responde que se canceló la sesión y responde con el mismo código para las siguientes PUT o solicitudes de estado de la consulta:
HTTP/1.1 499 Client Closed Request
Nota: Si se abandona una carga sin cancelarla, esta vencerá naturalmente una semana después de su creación.
Actualiza un recurso existente
Al igual que cuando inicias una sesión de carga reanudable, puedes utilizar el protocolo de carga reanudable para reemplazar el contenido de un archivo existente. Para iniciar una solicitud de actualización reanudable, envía una solicitud HTTP PUT al vínculo de la entrada con rel='...#resumable-edit-media'. Cada entry de medios contendrá un vínculo de este tipo si la API admite la actualización del contenido del recurso.
Por ejemplo, una entrada de documento en la API de DocList contendrá un vínculo similar al siguiente:
<link rel="http://schemas.google.com/g/2005#resumable-edit-media" type="application/atom+xml" href="https://docs.google.com/feeds/upload/create-session/default/private/full/document%3A12345"/>
Por lo tanto, la solicitud inicial sería la siguiente:
PUT /feeds/upload/create-session/default/private/full/document%3A12345 HTTP/1.1 Host: docs.google.com GData-Version: version_number Authorization: authorization If-Match: ETag | * Content-Length: 0 X-Upload-Content-Length: content_length X-Upload-Content-Type: content_type empty body
Para actualizar el contenido y los metadatos de un recurso al mismo tiempo, incluye XML de Atom en lugar de un cuerpo vacío. Consulta el ejemplo en la sección Inicia una solicitud de carga reanudable.
Cuando el servidor responda con el URI de carga único, envía un PUT con tu carga útil. Una vez que tengas el URI de carga único, el proceso para actualizar el contenido del archivo será el mismo que el de subir un archivo.
En este ejemplo en particular, se actualizará el contenido del documento existente de una sola vez:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 1000 Content-Range: 0-999/1000 <bytes 0-999>
Ejemplos de bibliotecas cliente
A continuación, se muestran ejemplos de cómo subir un archivo de película a Documentos de Google (con el protocolo de carga reanudable) en las bibliotecas cliente de Google Data. Ten en cuenta que, por el momento, no todas las bibliotecas admiten la función de reanudación.
int MAX_CONCURRENT_UPLOADS = 10; int PROGRESS_UPDATE_INTERVAL = 1000; int DEFAULT_CHUNK_SIZE = 10485760; DocsService client = new DocsService("yourCompany-yourAppName-v1"); client.setUserCredentials("user@gmail.com", "pa$$word"); // Create a listener FileUploadProgressListener listener = new FileUploadProgressListener(); // See the sample for details on this class. // Pool for handling concurrent upload tasks ExecutorService executor = Executors.newFixedThreadPool(MAX_CONCURRENT_UPLOADS); // Create {@link ResumableGDataFileUploader} for each file to upload Listuploaders = Lists.newArrayList(); File file = new File("test.mpg"); String contentType = DocumentListEntry.MediaType.fromFileName(file.getName()).getMimeType(); MediaFileSource mediaFile = new MediaFileSource(file, contentType); URL createUploadUrl = new URL("https://docs.google.com/feeds/upload/create-session/default/private/full"); ResumableGDataFileUploader uploader = new ResumableGDataFileUploader(createUploadUrl, mediaFile, client, DEFAULT_CHUNK_SIZE, executor, listener, PROGRESS_UPDATE_INTERVAL); uploaders.add(uploader); listener.listenTo(uploaders); // attach the listener to list of uploaders // Start the upload(s) for (ResumableGDataFileUploader uploader : uploaders) { uploader.start(); } // wait for uploads to complete while(!listener.isDone()) { try { Thread.sleep(100); } catch (InterruptedException ie) { listener.printResults(); throw ie; // rethrow }
// Chunk size in MB int CHUNK_SIZE = 1; ClientLoginAuthenticator cla = new ClientLoginAuthenticator( "yourCompany-yourAppName-v1", ServiceNames.Documents, "user@gmail.com", "pa$$word"); // Set up resumable uploader and notifications ResumableUploader ru = new ResumableUploader(CHUNK_SIZE); ru.AsyncOperationCompleted += new AsyncOperationCompletedEventHandler(this.OnDone); ru.AsyncOperationProgress += new AsyncOperationProgressEventHandler(this.OnProgress); // Set metadata for our upload. Document entry = new Document() entry.Title = "My Video"; entry.MediaSource = new MediaFileSource("c:\\test.mpg", "video/mpeg"); // Add the upload uri to document entry. Uri createUploadUrl = new Uri("https://docs.google.com/feeds/upload/create-session/default/private/full"); AtomLink link = new AtomLink(createUploadUrl.AbsoluteUri); link.Rel = ResumableUploader.CreateMediaRelation; entry.DocumentEntry.Links.Add(link); ru.InsertAsync(cla, entry.DocumentEntry, userObject);
- (void)uploadAFile { NSString *filePath = @"~/test.mpg"; NSString *fileName = [filePath lastPathComponent]; // get the file's data NSData *data = [NSData dataWithContentsOfMappedFile:filePath]; // create an entry to upload GDataEntryDocBase *newEntry = [GDataEntryStandardDoc documentEntry]; [newEntry setTitleWithString:fileName]; [newEntry setUploadData:data]; [newEntry setUploadMIMEType:@"video/mpeg"]; [newEntry setUploadSlug:fileName]; // to upload, we need the entry, our service object, the upload URL, // and the callback for when upload has finished GDataServiceGoogleDocs *service = [self docsService]; NSURL *uploadURL = [GDataServiceGoogleDocs docsUploadURL]; SEL finishedSel = @selector(uploadTicket:finishedWithEntry:error:); // now start the upload GDataServiceTicket *ticket = [service fetchEntryByInsertingEntry:newEntry forFeedURL:uploadURL delegate:self didFinishSelector:finishedSel]; // progress monitoring is done by specifying a callback, like this SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:); [ticket setUploadProgressSelector:progressSel]; } // callback for when uploading has finished - (void)uploadTicket:(GDataServiceTicket *)ticket finishedWithEntry:(GDataEntryDocBase *)entry error:(NSError *)error { if (error == nil) { // upload succeeded } } - (void)pauseOrResumeUploadForTicket:(GDataServiceTicket *)ticket { if ([ticket isUploadPaused]) { [ticket resumeUpload]; } else { [ticket pauseUpload]; } }
import os.path import atom.data import gdata.client import gdata.docs.client import gdata.docs.data CHUNK_SIZE = 10485760 client = gdata.docs.client.DocsClient(source='yourCompany-yourAppName-v1') client.ClientLogin('user@gmail.com', 'pa$$word', client.source); f = open('test.mpg') file_size = os.path.getsize(f.name) uploader = gdata.client.ResumableUploader( client, f, 'video/mpeg', file_size, chunk_size=CHUNK_SIZE, desired_class=gdata.docs.data.DocsEntry) # Set metadata for our upload. entry = gdata.docs.data.DocsEntry(title=atom.data.Title(text='My Video')) new_entry = uploader.UploadFile('/feeds/upload/create-session/default/private/full', entry=entry) print 'Document uploaded: ' + new_entry.title.text print 'Quota used: %s' % new_entry.quota_bytes_used.text
Para ver ejemplos completos y referencias de código fuente, consulta los siguientes recursos:
- App de ejemplo y código fuente de la biblioteca de Java
- App de ejemplo de la biblioteca de Objective-C
- Fuente de la biblioteca de .NET