Eric Bidelman,G Suite API 团队
2010 年 2 月
简介
当前的 Web 标准未提供可靠的机制来帮助通过 HTTP 上传大型文件。因此,Google 和其他网站上的文件上传大小一直受到限制(例如 100 MB)。对于支持上传大型文件的服务(例如 YouTube 和 Google Documents List API),这是一个主要障碍。
Google Data 可恢复协议通过在 HTTP/1.0 中支持可恢复的 POST/PUT HTTP 请求,直接解决了上述问题。该协议是以 Google Gears 团队建议的 ResumableHttpRequestsProposal 为模型设计的。
本文档介绍了如何将 Google Data 的可恢复上传功能纳入您的应用中。以下示例使用 Google Documents List Data API。请注意,实现此协议的其他 Google API 可能有略微不同的要求/响应代码/等。请参阅相应服务的文档了解具体信息。
可恢复协议
启动可续传上传请求
如需启动可续传上传会话,请向 resumable-post 链接发送 HTTP POST 请求。此链接位于 Feed 级。
DocList API 的可恢复 POST 链接如下所示:
<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 标头。
下面是另一个请求示例,该示例会上传 Word 文档。这次,系统会包含 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>。后续对唯一上传 URI 的 PUT 将返回与上传完成时返回的响应相同的响应。
过一段时间后,响应将为 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 Data 客户端库将电影文件上传到 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
如需查看完整的示例和源代码参考,请参阅以下资源: