Eric Bidelman,G Suite API 团队
2010 年 2 月
简介
当前的网络标准不提供可靠的机制来支持 HTTP 上传大型文件。因此,Google 和其他网站上传的文件历来都是中等大小(例如 100 MB)。对于支持大文件上传的 YouTube 和 Google 文档列表 API 等服务,这成了主要障碍。
Google 数据可续传协议通过支持 HTTP/1.0 中的可续传 POST/PUT HTTP 请求,直接解决了上述问题。 该协议是根据 Google Gears 团队建议的 ResumableHttpRequestsProposal 建模的。
本文介绍了如何将 Google 数据的可续传上传功能整合到您的应用中。以下示例使用了 Google 文档列表数据 API。请注意,实现此协议的其他 Google API 可能有不同的要求/响应代码等。如需了解详情,请参阅该服务的文档。
可续传协议
发起可续传上传请求
如需启动可续传上传会话,请向可续传帖子链接发送 HTTP POST
请求。此链接在 Feed 一级。
DocList API 的可续传博文链接如下所示:
<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"/>
POST
请求的正文应为空或包含 Atom XML 条目,并且不得包含实际文件内容。
下面的示例创建了一个可续传请求来上传大型 PDF,并包含使用 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
X-Upload-Content-Type
和 X-Upload-Content-Length
标头应设为您最终要上传的文件的 MIME 类型和大小。如果在上传会话创建期间内容长度未知,则可以省略 X-Upload-Content-Length
标头。
下面是另一个请求上传字词文档的示例请求。这一次,Atom 元数据已包含在内,并且将应用于最终的文档条目。
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>
服务器对初始 POST
的响应是 Location
标头中的唯一上传 URI,也是空响应正文:
HTTP/1.1 200 OK
Location: <upload_uri>
系统将使用唯一的上传 URI 上传文件区块。
注意:初始 POST
请求不会在 Feed 中创建新条目。仅当整个上传操作完成时,才会出现这种情况。
注意:可续传会话 URI 的有效期为一周。
上传文件
可续传协议允许(但并不要求)以“分块”方式上传内容,因为 HTTP 对请求大小没有固有的限制。您的客户端可以自行选择块大小,也可以直接上传文件。
此示例使用唯一的上传 URI 发出可续传 PUT
。以下示例会发送 1234567 字节的 PDF 文件的前 100000 个字节:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 100000 Content-Range: bytes 0-99999/1234567 bytes 0-99999
如果 PDF 文件的大小未知,则此示例将使用 Content-Range: bytes
0-99999/*
。如需详细了解 Content-Range
标头,请点击此处。
服务器使用已存储的当前字节范围进行响应:
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-99999
客户端应继续 PUT
文件的每个分块,直到整个文件上传完毕。在上传完成之前,服务器将返回 HTTP 308 Resume Incomplete
及其在 Range
标头中知道的字节范围。客户端必须使用 Range
标头来确定从何处开始上传下一个分块。因此,请勿假定服务器已收到最初在 PUT
请求中发送的所有字节。
注意:在分块传输期间,服务器可能会在 Location
标头中发出一个新的唯一上传 URI。客户端应检查是否有更新后的 Location
,并使用该 URI 将剩余的数据块发送到服务器。
上传完成后,响应将与使用 API 的不可续传上传机制上传时的效果相同。也就是说,201 Created
将与由服务器创建的 <atom:entry>
一起返回。唯一的 PUT
的后续 URI 将返回与上传完成时返回的响应相同的响应。
一段时间后,响应将是 410 Gone
或 404 Not Found
。
恢复上传
如果您的请求在收到来自服务器的响应之前被终止,或者您收到来自服务器的 HTTP 503
响应,则可以通过对唯一的上传 URI 发出空的 PUT
请求来查询上传的当前状态。
客户端会轮询服务器以确定已接收哪些字节:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0 Content-Range: bytes */content_length
如果长度未知,请使用 *
作为 content_length。
服务器使用当前字节范围进行响应:
HTTP/1.1 308 Resume Incomplete Content-Length: 0 Range: bytes=0-42
注意:如果服务器尚未为会话提交任何字节,则会省略 Range
标头。
注意:在分块传输期间,服务器可能会在 Location
标头中发出一个新的唯一上传 URI。客户端应检查是否有更新后的 Location
,并使用该 URI 将剩余的数据块发送到服务器。
最后,客户端会从服务器中断的位置继续运行:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 57 Content-Range: 43-99/100 <bytes 43-99>
取消上传
如果您想取消上传并阻止对上传执行任何进一步的操作,请针对唯一上传 URI 发出 DELETE
请求。
DELETE upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 0
如果成功,服务器会响应会话已被取消,并为相同的 PUT
或查询状态请求使用相同的代码响应:
HTTP/1.1 499 Client Closed Request
注意:如果上传内容在未取消的情况下被放弃,则该内容会在创建一周后自然过期。
更新现有资源
与启动可续传上传会话类似,您可以利用可续传上传协议替换现有文件的内容。如需启动可续传更新请求,请使用 rel='...#resumable-edit-media
' 将 HTTP PUT
发送到条目的链接。如果 API 支持更新资源的内容,每个媒体 entry
都将包含此类链接。
例如,DocList API 中的文档条目将包含类似于以下内容的链接:
<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"/>
因此,初始请求为:
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
如需同时更新资源的元数据和内容,请添加 Atom XML 而不是空的正文。请参阅启动可续传上传请求部分中的示例。
当服务器使用唯一的上传 URI 进行响应时,随载荷一起发送 PUT
。获得唯一上传 URI 后,更新文件内容的过程与上传文件的过程相同。
以下这个特定示例可以一次性更新现有文档的内容:
PUT upload_uri HTTP/1.1 Host: docs.google.com Content-Length: 1000 Content-Range: 0-999/1000 <bytes 0-999>
客户端库示例
以下是使用 Google 数据客户端库将电影文件上传到 Google 文档(使用可续传上传协议)的示例。请注意,并非所有库目前都支持可续传功能。
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
如需查看完整的示例和源代码参考,请参阅以下资源: