Google 数据协议中的可续传媒体内容

Eric Bidelman,G Suite API 团队
2010 年 2 月

  1. 简介
  2. 可续传协议
    1. 发起可续传上传请求
    2. 上传文件
    3. 恢复上传
    4. 取消上传
    5. 更新现有资源
  3. 客户端库示例

简介

当前的网络标准不提供可靠的机制来支持 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-TypeX-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 Gone404 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
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

如需查看完整的示例和源代码参考,请参阅以下资源:

返回页首