Cargas de medios reanudables en el protocolo de datos de Google

Eric Bidelman, equipo de API de G Suite
Febrero de 2010

  1. Introducción
  2. El protocolo reanudable
    1. Inicia una solicitud de carga reanudable
    2. Cómo cargar un archivo
    3. Reanuda una carga
    4. Cómo cancelar una carga
    5. Actualiza un recurso existente
  3. Ejemplos de biblioteca cliente

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>

Volver al principio

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
List uploaders = 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:

Volver al principio