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 个字节:
PUTupload_uri HTTP/1.1 Host: docs.google.com Content-Length: 100000 Content-Range: bytes 0-99999/1234567bytes 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
请求来查询上传的当前状态。
客户端会轮询服务器以确定已接收哪些字节:
PUTupload_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 将剩余的数据块发送到服务器。
最后,客户端会从服务器中断的位置继续运行:
PUTupload_uri HTTP/1.1 Host: docs.google.com Content-Length: 57 Content-Range: 43-99/100 <bytes 43-99>
取消上传
如果您想取消上传并阻止对上传执行任何进一步的操作,请针对唯一上传 URI 发出 DELETE
请求。
DELETEupload_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 后,更新文件内容的过程与上传文件的过程相同。
以下这个特定示例可以一次性更新现有文档的内容:
PUTupload_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
List
// 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
如需查看完整的示例和源代码参考,请参阅以下资源: