Eric Bidelman, equipe das APIs do G Suite
fevereiro de 2010
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>
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 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 exemplos completos e referência de código-fonte, consulte os seguintes recursos:
- Exemplo de app e fonte da biblioteca Java
- App de exemplo da biblioteca Objective-C
- Fonte da biblioteca .NET