Eric Bidelman, equipo de API de G Suite
Febrero de 2010
Introducción
Los estándares web actuales no proporcionan un mecanismo confiable para facilitar la carga HTTP de archivos grandes. Como resultado, las cargas de archivos en Google y otros sitios solían limitarse a tamaños moderados (p.ej., 100 MB). Para servicios como YouTube y las API de lista de documentos de Google que admiten archivos de gran tamaño, esto representa un obstáculo importante.
El protocolo reanudable de datos de Google soluciona directamente los problemas mencionados anteriormente, ya que admite solicitudes HTTP POST/PUT reanudables en HTTP/1.0. El protocolo se modeló en función de la ResumableHttpRequestsProposal sugerida por el equipo de Google Gears.
En este documento, se describe cómo incorporar la función de carga reanudable de Datos de Google en tus aplicaciones. Los ejemplos siguientes usan la API de datos de lista de documentos de Google. Ten en cuenta que las API adicionales de Google que implementan este protocolo pueden tener requisitos, códigos de respuesta, etc. ligeramente diferentes. Consulta la documentación del servicio para obtener información específica.
El protocolo reanudable
Inicia una solicitud de carga reanudable
Para iniciar una sesión de carga reanudable, envía una solicitud POST
HTTP al vínculo de publicación reanudable. Este vínculo se encuentra a nivel del feed.
El vínculo de publicación reanudable de la API de DocList tiene el siguiente aspecto:
<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 la solicitud POST
debe estar vacío o contener una entrada XML de Atom y no debe incluir el contenido del archivo.
En el siguiente ejemplo, se crea una solicitud reanudable a fin de subir un PDF grande y se incluye un título para el documento futuro 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 configurarse en el tipo MIME y el tamaño del archivo que subirás en el futuro. Si se desconoce 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
.
Esta es otra solicitud de ejemplo que sube un documento de Word. Esta vez, se incluyen los metadatos de Atom y se aplicarán a la entrada final del documento.
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 del 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 utilizará para subir los fragmentos de archivo.
Nota: La solicitud inicial POST
no crea una entrada nueva en el feed.
Esto solo sucede una vez completada la operación de carga.
Nota: Un URI de sesión reanudable vence después de una semana.
Carga un archivo
El protocolo reanudable permite, pero no requiere que se suba contenido en "fragmentos", ya que no hay restricciones inherentes en HTTP en los tamaños de solicitud. Su cliente puede elegir el tamaño del bloque o simplemente cargar el archivo en su totalidad.
En este ejemplo, se usa el URI de carga único para emitir un PUT
reanudable. En el siguiente ejemplo, se envía el primer archivo PDF de 1234567 bytes de 100,000 bytes:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 100000 Content-Range: bytes 0-99999/1234567 bytes 0-99999
Si el tamaño del archivo PDF fuera desconocido, 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 continuar con PUT
cada parte del archivo hasta que este 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 la siguiente parte.
Por lo tanto, no supongas que el servidor recibió todos los bytes que se enviaron originalmente en la solicitud PUT
.
Nota: El servidor puede emitir un nuevo URI de carga único en el encabezado Location
durante un fragmento. Tu cliente debe buscar una Location
actualizada y usar ese URI para enviar los fragmentos restantes al servidor.
Cuando se complete la carga, la respuesta será la misma que si se hubiera realizado con el mecanismo de carga no reanudable de la API. Es decir, se mostrará un 201 Created
junto con el <atom:entry>
, tal como lo creó el servidor. Los elementos PUT
subsiguientes al URI de carga único mostrarán la misma respuesta que la que se mostró cuando se completó la carga.
Después de un tiempo, la respuesta será 410 Gone
o 404 Not Found
.
Reanuda una carga
Si se finaliza tu solicitud antes de recibir una respuesta del servidor o si recibes una respuesta HTTP 503
del servidor, puedes consultar el estado actual de la carga mediante una solicitud PUT
vacía en el URI de carga único.
El cliente sondea el servidor para determinar qué bytes ha recibido:
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: El servidor puede emitir un nuevo URI de carga único en el encabezado Location
durante un fragmento. Tu cliente debe buscar una Location
actualizada y usar ese URI para enviar los fragmentos restantes al servidor.
Por último, el cliente reanuda donde se detuvo el servidor:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 57 Content-Range: 43-99/100 <bytes 43-99>
Cómo cancelar 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 correctamente, el servidor responde que la sesión está cancelada y responde con el mismo código para otras PUT
o solicitudes de estado de consulta:
HTTP/1.1 499 Client Closed Request
Nota: Si se abandona una carga sin cancelación, esta vencerá de forma natural una semana después de su creación.
Actualiza un recurso existente
De manera similar a cómo iniciar una sesión de carga reanudable, puedes usar el protocolo de carga reanudable para reemplazar el contenido de un archivo existente. Para iniciar una solicitud de actualización reanudable, envía un PUT
HTTP al vínculo de la entrada con rel='...#resumable-edit-media
'. Cada entry
de medios contendrá este vínculo 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 a este:
<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 los metadatos y el contenido de un recurso al mismo tiempo, incluye Atom XML 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 tienes el URI de carga único, el proceso para actualizar el contenido del archivo es el mismo que para subir un archivo.
Este ejemplo en particular actualizará el contenido del documento existente en una toma:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 1000 Content-Range: 0-999/1000 <bytes 0-999>
Ejemplos de biblioteca cliente
A continuación, se muestran ejemplos de cómo subir un archivo de película a los documentos de Google (mediante el protocolo de carga reanudable) en las bibliotecas cliente de Google Data. Ten en cuenta que no todas las bibliotecas admiten la función reanudable en este momento.
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 obtener muestras completas y referencias de código fuente, consulta los siguientes recursos:
- App de ejemplo y fuente de la biblioteca de Java
- App de ejemplo de la biblioteca de Objective-C
- Fuente de la biblioteca .NET