Uploads de mídia retomáveis no protocolo de dados do Google

Eric Bidelman, equipe das APIs do G Suite
fevereiro de 2010

  1. Introdução
  2. O protocolo de retomada
    1. Como iniciar uma solicitação de upload retomável
    2. Fazer upload de um arquivo
    3. Retomar um upload
    4. Como cancelar um upload
    5. Atualizar um recurso atual
  3. Exemplos de bibliotecas de cliente

Introdução

Os padrões da Web atuais não oferecem um mecanismo confiável para facilitar o upload HTTP de arquivos grandes. Por isso, os uploads de arquivos no Google e em outros sites sempre foram limitados a tamanhos moderados (por exemplo, 100 MB). Para serviços como as APIs YouTube e Google Documents List, que aceitam uploads de arquivos grandes, isso representa um grande obstáculo.

O protocolo retomável de dados do Google resolve diretamente os problemas mencionados acima ao oferecer suporte a solicitações HTTP POST/PUT retomáveis no HTTP/1.0. O protocolo foi modelado de acordo com a ResumableHttpRequestsProposal sugerida pela equipe do Google Gears.

Este documento descreve como incorporar o recurso de upload retomável do Google Data aos seus aplicativos. Os exemplos abaixo usam a API Google Documents List Data. Outras APIs do Google que implementam esse protocolo podem ter requisitos/códigos de resposta/etc. ligeiramente diferentes. Consulte a documentação do serviço para saber os detalhes.

O protocolo resumível

Como iniciar uma solicitação de upload retomável

Para iniciar uma sessão de upload retomável, envie uma solicitação HTTP POST ao link resumable-post. Esse link é encontrado no nível do feed. O link de postagem retomável da API DocList tem esta aparência:

<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"/>

O corpo da solicitação POST precisa estar vazio ou conter uma entrada XML Atom e não pode incluir o conteúdo real do arquivo. O exemplo abaixo cria uma solicitação retomável para fazer upload de um PDF grande e inclui um título para o futuro documento usando o cabeçalho 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

Os cabeçalhos X-Upload-Content-Type e X-Upload-Content-Length precisam ser definidos como o tipo MIME e o tamanho do arquivo que você vai enviar. Se o tamanho do conteúdo for desconhecido no momento da criação da sessão de upload, o cabeçalho X-Upload-Content-Length poderá ser omitido.

Confira outro exemplo de solicitação que faz upload de um documento do Word. Desta vez, os metadados do Atom são incluídos e serão aplicados à entrada do 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>

A resposta do servidor do POST inicial é um URI de upload exclusivo no cabeçalho Location e um corpo de resposta vazio:

HTTP/1.1 200 OK
Location: <upload_uri>

O URI de upload exclusivo será usado para fazer upload dos blocos de arquivos.

Observação: a solicitação POST inicial não cria uma nova entrada no feed. Isso só acontece quando toda a operação de upload é concluída.

Observação: o URI de uma sessão retomável expira após uma semana.

Como enviar um arquivo

O protocolo retomável permite, mas não exige, que o conteúdo seja enviado em "pedaços", porque não há restrições inerentes no HTTP sobre tamanhos de solicitação. O cliente pode escolher o tamanho do bloco ou fazer upload do arquivo inteiro. Este exemplo usa o URI de upload exclusivo para emitir um PUT retomável. O exemplo a seguir envia os primeiros 100.000 bytes de um arquivo 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

Se o tamanho do arquivo PDF fosse desconhecido, este exemplo usaria Content-Range: bytes 0-99999/*. Leia mais informações sobre o cabeçalho Content-Range aqui.

O servidor responde com o intervalo de bytes atual que foi armazenado:

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: bytes=0-99999

O cliente precisa continuar usando PUT em cada parte do arquivo até que ele seja totalmente enviado. Até que o upload seja concluído, o servidor vai responder com um HTTP 308 Resume Incomplete e o intervalo de bytes que ele conhece no cabeçalho Range. Os clientes precisam usar o cabeçalho Range para determinar onde iniciar a próxima parte. Portanto, não suponha que o servidor recebeu todos os bytes enviados originalmente na solicitação PUT.

Observação: o servidor pode emitir um novo URI de upload exclusivo no cabeçalho Location durante um bloco. O cliente precisa verificar um Location atualizado e usar esse URI para enviar os blocos restantes ao servidor.

Quando o upload for concluído, a resposta será a mesma que se o upload tivesse sido feito usando o mecanismo de upload não retomável da API. Ou seja, um 201 Created será retornado com o <atom:entry>, conforme criado pelo servidor. Os PUTs subsequentes ao URI de upload exclusivo vão retornar a mesma resposta que foi retornada quando o upload foi concluído. Depois de um período, a resposta será 410 Gone ou 404 Not Found.

Como retomar um upload

Se a solicitação for encerrada antes de receber uma resposta do servidor ou se você receber uma resposta HTTP 503 do servidor, será possível consultar o status atual do upload emitindo uma solicitação PUT vazia no URI de upload exclusivo.

O cliente consulta o servidor para determinar quais bytes ele recebeu:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 0
Content-Range: bytes */content_length

Use * como content_length se o comprimento não for conhecido.

O servidor responde com o intervalo de bytes atual:

HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: bytes=0-42

Observação: se o servidor não tiver confirmado nenhum byte para a sessão, ele vai omitir o cabeçalho Range.

Observação: o servidor pode emitir um novo URI de upload exclusivo no cabeçalho Location durante um bloco. O cliente precisa verificar um Location atualizado e usar esse URI para enviar os blocos restantes ao servidor.

Por fim, o cliente retoma a migração do ponto em que o servidor parou:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 57
Content-Range: 43-99/100

<bytes 43-99>

Como cancelar um upload

Se você quiser cancelar o upload e impedir qualquer ação futura nele, emita uma solicitação DELETE no URI exclusivo do upload.

DELETE upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 0

Se a operação for bem-sucedida, o servidor vai responder que a sessão foi cancelada e vai retornar o mesmo código para outras PUTs ou solicitações de status da consulta:

HTTP/1.1 499 Client Closed Request

Observação: se um upload for abandonado sem cancelamento, ele vai expirar naturalmente uma semana após a criação.

Como atualizar um recurso

Assim como iniciar uma sessão de upload retomável, você pode usar o protocolo de upload retomável para substituir o conteúdo de um arquivo existente. Para iniciar uma solicitação de atualização retomável, envie um HTTP PUT para o link da entrada com rel='...#resumable-edit-media'. Cada entry de mídia vai conter um link desse tipo se a API aceitar a atualização do conteúdo do recurso.

Por exemplo, uma entrada de documento na API DocList vai conter um link semelhante 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"/>

Portanto, a solicitação inicial seria:

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 atualizar os metadados e o conteúdo de um recurso ao mesmo tempo, inclua Atom XML em vez de um corpo vazio. Consulte o exemplo na seção Como iniciar uma solicitação de upload retomável.

Quando o servidor responder com o URI de upload exclusivo, envie um PUT com seu payload. Depois de ter o URI de upload exclusivo, o processo para atualizar o conteúdo do arquivo é o mesmo de fazer upload de um arquivo.

Este exemplo específico vai atualizar o conteúdo do documento atual de uma só vez:

PUT upload_uri HTTP/1.1
Host: docs.google.com
Content-Length: 1000
Content-Range: 0-999/1000

<bytes 0-999>

Voltar ao início

Exemplos de bibliotecas de cliente

Confira abaixo exemplos de como fazer upload de um arquivo de filme para o Google Docs (usando o protocolo de upload retomável) nas bibliotecas de cliente do Google Data. Nem todas as bibliotecas são compatíveis com o recurso de retomada no 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 exemplos completos e referência de código-fonte, consulte os seguintes recursos:

Voltar ao início