Google Data 协议中的可续传媒体上传

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

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

简介

当前的 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-TypeX-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 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 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
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

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

返回页首