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

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

  1. Introdução
  2. O protocolo retomável
    1. Como iniciar uma solicitação de upload retomável
    2. Como fazer upload de um arquivo
    3. Como retomar um upload
    4. Como cancelar um upload
    5. Como atualizar um recurso atual
  3. Exemplos de biblioteca de cliente

Introdução

Os padrões atuais da Web não oferecem um mecanismo confiável para facilitar o upload por HTTP de arquivos grandes. Como resultado, os uploads de arquivos no Google e em outros sites costumam ser limitados a tamanhos moderados (por exemplo, 100 MB). Para serviços como o YouTube e as APIs da Lista de documentos do Google, que suportam uploads de grandes arquivos, isso apresenta um grande obstáculo.

O protocolo retomável de dados do Google aborda os problemas mencionados acima com suporte às solicitações HTTP/POST HTTP retomáveis em HTTP/1.0. O protocolo foi modelado após o ResumableHttpRequestsProposal, sugerido pela equipe do Google Gear.

Este documento descreve como incorporar o recurso de upload retomável dos dados do Google em 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 um pouco diferentes etc. Consulte a documentação do serviço para ver as especificações.

O protocolo retomável

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

Para iniciar uma sessão de upload retomável, envie uma solicitação POST HTTP ao link de postagem retomável. Esse link está disponível 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 documento futuro 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 a duração do conteúdo for desconhecida na criação da sessão de upload, o cabeçalho X-Upload-Content-Length poderá ser omitido.

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

A resposta do servidor a partir 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: um URI de sessão retomável expira após uma semana.

Como enviar um arquivo

O protocolo retomável permite, mas não exige, o upload do conteúdo em "blocos", porque não há restrições inerentes em HTTP nos tamanhos de solicitações. O cliente pode escolher o tamanho do fragmento ou apenas fazer upload do arquivo como um todo. 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 1234.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 for desconhecido, este exemplo usará 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 a usar PUT em cada fragmento do arquivo até que tenha sido concluído Até que o upload seja concluído, o servidor responderá com um 308 Resume Incomplete HTTP e o intervalo de bytes conhecido no cabeçalho Range. Os clientes precisam usar o cabeçalho Range para determinar onde iniciar a próxima parte. Portanto, não presuma que o servidor recebeu todos os bytes originalmente enviados na solicitação PUT.

Observação: o servidor pode emitir um novo URI de upload exclusivo no cabeçalho Location durante uma parte. Seu cliente precisa verificar um Location atualizado e usar esse URI para enviar os blocos restantes para o 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 junto com o <atom:entry>, conforme criado pelo servidor. As PUTs subsequentes ao URI de upload exclusivo retornarão a mesma resposta que foi retornada quando o upload foi concluído. Após 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, envie uma solicitação PUT vazia no URI exclusivo do upload.

O cliente pesquisa 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 bytes para a sessão, ele omitirá o cabeçalho Range.

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

Por fim, o cliente retoma de onde 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 for bem-sucedido, o servidor vai responder que a sessão foi cancelada e responder com o mesmo código para mais PUTs ou solicitações de status de 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 atual

Semelhante à inicialização de uma sessão de upload retomável, é possível utilizar 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 PUT HTTP para o link da entrada com rel='...#resumable-edit-media'. Cada entry de mídia conterá esse link se a API for compatível com a atualização do conteúdo do recurso.

Por exemplo, a entrada de um documento na API DocList conterá um link semelhante a:

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

Assim, 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 o Atom XML em vez de um corpo vazio. Veja 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 o payload. Com o URI de upload exclusivo, o processo de atualização do conteúdo do arquivo é o mesmo que o do upload de um arquivo.

Este exemplo específico atualizará o conteúdo do documento 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 biblioteca de cliente

Veja abaixo exemplos de upload de um arquivo de filme para o Documentos Google (usando o protocolo de upload retomável) nas bibliotecas de cliente do Google Data. Observe que nem todas as bibliotecas são compatíveis com o recurso retomável 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 ver exemplos completos e referências de código-fonte, consulte os seguintes recursos:

Voltar ao início